Another interim commit
Home page and some post stuff written
This commit is contained in:
parent
b86ba7b6f6
commit
3656bb384c
@ -1,9 +1,47 @@
|
||||
namespace myWebLog.Data
|
||||
|
||||
open RethinkDb.Driver
|
||||
open RethinkDb.Driver.Net
|
||||
open Newtonsoft.Json
|
||||
|
||||
/// Data configuration
|
||||
type DataConfig = {
|
||||
/// The hostname for the RethinkDB server
|
||||
hostname : string
|
||||
/// The port for the RethinkDB server
|
||||
port : int
|
||||
/// The authorization key to use when connecting to the server
|
||||
authKey : string
|
||||
/// How long an attempt to connect to the server should wait before giving up
|
||||
timeout : int
|
||||
/// The name of the default database to use on the connection
|
||||
database : string
|
||||
/// A connection to the RethinkDB server using the configuration in this object
|
||||
conn : IConnection
|
||||
}
|
||||
|
||||
with
|
||||
/// Create a data configuration from JSON
|
||||
static member fromJson json =
|
||||
let mutable cfg = JsonConvert.DeserializeObject<DataConfig> json
|
||||
cfg <- match cfg.hostname with
|
||||
| null -> { cfg with hostname = RethinkDBConstants.DefaultHostname }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.port with
|
||||
| 0 -> { cfg with port = RethinkDBConstants.DefaultPort }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.authKey with
|
||||
| null -> { cfg with authKey = RethinkDBConstants.DefaultAuthkey }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.timeout with
|
||||
| 0 -> { cfg with timeout = RethinkDBConstants.DefaultTimeout }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.database with
|
||||
| null -> { cfg with database = RethinkDBConstants.DefaultDbName }
|
||||
| _ -> cfg
|
||||
{ cfg with conn = RethinkDB.R.Connection()
|
||||
.Hostname(cfg.hostname)
|
||||
.Port(cfg.port)
|
||||
.AuthKey(cfg.authKey)
|
||||
.Db(cfg.database)
|
||||
.Timeout(cfg.timeout)
|
||||
.Connect() }
|
||||
|
14
src/myWebLog.Data/Page.fs
Normal file
14
src/myWebLog.Data/Page.fs
Normal file
@ -0,0 +1,14 @@
|
||||
module myWebLog.Data.Page
|
||||
|
||||
open myWebLog.Entities
|
||||
open Rethink
|
||||
|
||||
/// Get a page by its Id
|
||||
let tryFindPage conn webLogId pageId : Page option =
|
||||
match table Table.Page
|
||||
|> get pageId
|
||||
|> filter (fun p -> upcast p.["webLogId"].Eq(webLogId))
|
||||
|> runAtomAsync<Page> conn
|
||||
|> box with
|
||||
| null -> None
|
||||
| page -> Some <| unbox page
|
49
src/myWebLog.Data/Post.fs
Normal file
49
src/myWebLog.Data/Post.fs
Normal file
@ -0,0 +1,49 @@
|
||||
module myWebLog.Data.Post
|
||||
|
||||
open myWebLog.Entities
|
||||
open Rethink
|
||||
open RethinkDb.Driver
|
||||
|
||||
let private r = RethinkDB.R
|
||||
|
||||
/// Get a page of published posts
|
||||
let findPageOfPublishedPosts conn webLogId pageNbr nbrPerPage =
|
||||
table Table.Post
|
||||
|> getAll [| webLogId, PostStatus.Published |]
|
||||
|> optArg "index" "webLogAndStatus"
|
||||
|> orderBy (fun p -> upcast r.Desc(p.["publishedOn"]))
|
||||
|> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage)
|
||||
|> runCursorAsync<Post> conn
|
||||
|> Seq.toList
|
||||
|
||||
/// Try to get the next newest post from the given post
|
||||
let tryFindNewerPost conn post =
|
||||
table Table.Post
|
||||
|> getAll [| post.webLogId, PostStatus.Published |]
|
||||
|> optArg "index" "webLogAndStatus"
|
||||
|> filter (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn))
|
||||
|> orderBy (fun p -> upcast p.["publishedOn"])
|
||||
|> limit 1
|
||||
|> runCursorAsync<Post> conn
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Try to get the next oldest post from the given post
|
||||
let tryFindOlderPost conn post =
|
||||
table Table.Post
|
||||
|> getAll [| post.webLogId, PostStatus.Published |]
|
||||
|> optArg "index" "webLogAndStatus"
|
||||
|> filter (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn))
|
||||
|> orderBy (fun p -> upcast r.Desc(p.["publishedOn"]))
|
||||
|> limit 1
|
||||
|> runCursorAsync<Post> conn
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Get a page of all posts in all statuses
|
||||
let findPageOfAllPosts conn webLogId pageNbr nbrPerPage =
|
||||
table Table.Post
|
||||
|> getAll [| webLogId |]
|
||||
|> optArg "index" "webLogId"
|
||||
|> orderBy (fun p -> upcast r.Desc(r.Branch(p.["publishedOn"].Eq(int64 0), p.["lastUpdatedOn"], p.["publishedOn"])))
|
||||
|> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage)
|
||||
|> runCursorAsync<Post> conn
|
||||
|> Seq.toList
|
@ -7,17 +7,20 @@ let private r = RethinkDb.Driver.RethinkDB.R
|
||||
let private await task = task |> Async.AwaitTask |> Async.RunSynchronously
|
||||
|
||||
let delete (expr : ReqlExpr) = expr.Delete ()
|
||||
let filter (expr : ReqlExpr -> ReqlExpr) (table : ReqlExpr) = table.Filter expr
|
||||
let get (expr : obj) (table : Table) = table.Get expr
|
||||
let getAll (exprs : obj[]) (table : Table) = table.GetAll exprs
|
||||
let insert (expr : obj) (table : Table) = table.Insert expr
|
||||
let limit (expr : obj) (table : ReqlExpr) = table.Limit expr
|
||||
let optArg key (value : obj) (expr : GetAll) = expr.OptArg (key, value)
|
||||
let orderBy (exprA : obj) (expr : ReqlExpr) = expr.OrderBy exprA
|
||||
let orderBy (exprA : ReqlExpr -> ReqlExpr) (expr : ReqlExpr) = expr.OrderBy exprA
|
||||
let replace (exprA : obj) (expr : ReqlExpr) = expr.Replace exprA
|
||||
let runAtomAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<'T> conn |> await
|
||||
let runCursorAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunCursorAsync<'T> conn |> await
|
||||
let runListAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<System.Collections.Generic.List<'T>> conn
|
||||
|> await
|
||||
let runResultAsync (conn : IConnection) (ast : ReqlAst) = ast.RunResultAsync conn |> await
|
||||
let slice (exprA : obj) (exprB : obj) (ast : ReqlExpr) = ast.Slice (exprA, exprB)
|
||||
let table (expr : obj) = r.Table expr
|
||||
let update (exprA : obj) (expr : ReqlExpr) = expr.Update exprA
|
||||
let without (exprs : obj[]) (expr : ReqlExpr) = expr.Without exprs
|
@ -3,18 +3,19 @@
|
||||
open myWebLog.Entities
|
||||
open Rethink
|
||||
open RethinkDb.Driver
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
let private r = RethinkDB.R
|
||||
|
||||
type PageList = { pageList : Ast.CoerceTo }
|
||||
|
||||
/// Detemine the web log by the URL base
|
||||
let tryFindWebLogByUrlBase (cfg : DataConfig) (urlBase : string) =
|
||||
let tryFindWebLogByUrlBase (conn : IConnection) (urlBase : string) =
|
||||
r.Table(Table.WebLog).GetAll([| urlBase |]).OptArg("index", "urlBase")
|
||||
.Merge(fun webLog -> { pageList = r.Table(Table.Page)
|
||||
.GetAll([| webLog.["id"], true |]).OptArg("index", "pageList")
|
||||
.OrderBy("title")
|
||||
.Pluck([| "title", "permalink" |])
|
||||
.CoerceTo("array") })
|
||||
|> runCursorAsync<WebLog> cfg.conn
|
||||
|> runCursorAsync<WebLog> conn
|
||||
|> Seq.tryHead
|
||||
|
@ -56,6 +56,8 @@
|
||||
<Compile Include="DataConfig.fs" />
|
||||
<Compile Include="Rethink.fs" />
|
||||
<Compile Include="SetUp.fs" />
|
||||
<Compile Include="Page.fs" />
|
||||
<Compile Include="Post.fs" />
|
||||
<Compile Include="WebLog.fs" />
|
||||
<Content Include="packages.config" />
|
||||
</ItemGroup>
|
||||
|
@ -11,37 +11,32 @@ open Nancy.Bootstrapper
|
||||
open Nancy.Cryptography
|
||||
open Nancy.Owin
|
||||
open Nancy.Security
|
||||
open Nancy.Session
|
||||
open Nancy.Session.Persistable
|
||||
open Nancy.Session.RethinkDb
|
||||
open Nancy.TinyIoc
|
||||
open Nancy.ViewEngines.SuperSimpleViewEngine
|
||||
open RethinkDb.Driver
|
||||
open RethinkDb.Driver.Net
|
||||
open Suave
|
||||
open Suave.Owin
|
||||
open System
|
||||
open System.Text.RegularExpressions
|
||||
|
||||
/// Set up a database connection
|
||||
let cfg =
|
||||
{ database = "myWebLog"
|
||||
conn = RethinkDB.R.Connection()
|
||||
.Hostname(RethinkDBConstants.DefaultHostname)
|
||||
.Port(RethinkDBConstants.DefaultPort)
|
||||
.AuthKey(RethinkDBConstants.DefaultAuthkey)
|
||||
.Db("myWebLog")
|
||||
.Timeout(RethinkDBConstants.DefaultTimeout)
|
||||
.Connect() }
|
||||
let cfg = try DataConfig.fromJson (System.IO.File.ReadAllText "data-config.json")
|
||||
with ex -> ApplicationException("Could not convert data-config.json to RethinkDB connection", ex)
|
||||
|> raise
|
||||
|
||||
do
|
||||
startUpCheck cfg
|
||||
|
||||
|
||||
/// Support RESX lookup via the @Translate SSVE alias
|
||||
type TranslateTokenViewEngineMatcher() =
|
||||
static let regex = Regex("@Translate\.(?<TranslationKey>[a-zA-Z0-9-_]+);?", RegexOptions.Compiled)
|
||||
interface ISuperSimpleViewEngineMatcher with
|
||||
member this.Invoke (content, model, host) =
|
||||
regex.Replace(content, fun m -> let key = m.Groups.["TranslationKey"].Value
|
||||
match Resources.ResourceManager.GetString key with
|
||||
match myWebLog.Resources.ResourceManager.GetString key with
|
||||
| null -> key
|
||||
| xlat -> xlat)
|
||||
|
||||
@ -51,8 +46,8 @@ type MyWebLogUser(name, claims) =
|
||||
interface IUserIdentity with
|
||||
member this.UserName with get() = name
|
||||
member this.Claims with get() = claims
|
||||
member this.UserName with get() = (this :> IUserIdentity).UserName
|
||||
member this.Claims with get() = (this :> IUserIdentity).Claims
|
||||
(*member this.UserName with get() = (this :> IUserIdentity).UserName
|
||||
member this.Claims with get() = (this :> IUserIdentity).Claims -- do we need these? *)
|
||||
|
||||
type MyWebLogUserMapper(container : TinyIoCContainer) =
|
||||
|
||||
@ -63,25 +58,30 @@ type MyWebLogUserMapper(container : TinyIoCContainer) =
|
||||
| _ -> null
|
||||
|
||||
|
||||
/// Set up the RethinkDB connection instance to be used by the IoC container
|
||||
type ApplicationBootstrapper() =
|
||||
/// Set up the application environment
|
||||
type MyWebLogBootstrapper() =
|
||||
inherit DefaultNancyBootstrapper()
|
||||
|
||||
override this.ConfigureRequestContainer (container, context) =
|
||||
base.ConfigureRequestContainer (container, context)
|
||||
/// User mapper for forms authentication
|
||||
container.Register<IUserMapper, MyWebLogUserMapper>()
|
||||
|> ignore
|
||||
|
||||
override this.ApplicationStartup (container, pipelines) =
|
||||
base.ApplicationStartup (container, pipelines)
|
||||
// Data configuration
|
||||
// Data configuration (both config and the connection; Nancy modules just need the connection)
|
||||
container.Register<DataConfig>(cfg)
|
||||
|> ignore
|
||||
container.Register<IConnection>(cfg.conn)
|
||||
|> ignore
|
||||
// I18N in SSVE
|
||||
container.Register<seq<ISuperSimpleViewEngineMatcher>>(fun _ _ ->
|
||||
Seq.singleton (TranslateTokenViewEngineMatcher() :> ISuperSimpleViewEngineMatcher))
|
||||
|> ignore
|
||||
// Forms authentication configuration
|
||||
let salt = (System.Text.ASCIIEncoding()).GetBytes "NoneOfYourBeesWax"
|
||||
let auth =
|
||||
let auth =
|
||||
FormsAuthenticationConfiguration(
|
||||
CryptographyConfiguration = CryptographyConfiguration
|
||||
(RijndaelEncryptionProvider(PassphraseKeyGenerator("Secrets", salt)),
|
||||
@ -99,7 +99,7 @@ type ApplicationBootstrapper() =
|
||||
|
||||
|
||||
let version =
|
||||
let v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version
|
||||
let v = Reflection.Assembly.GetExecutingAssembly().GetName().Version
|
||||
match v.Build with
|
||||
| 0 -> match v.Minor with
|
||||
| 0 -> string v.Major
|
||||
@ -112,13 +112,13 @@ type RequestEnvironment() =
|
||||
interface IRequestStartup with
|
||||
member this.Initialize (pipelines, context) =
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline
|
||||
(fun ctx -> ctx.Items.["requestStart"] <- System.DateTime.Now.Ticks
|
||||
match tryFindWebLogByUrlBase cfg ctx.Request.Url.HostName with
|
||||
| Some webLog -> ctx.Items.["webLog"] <- webLog
|
||||
| None -> System.ApplicationException
|
||||
(fun ctx -> ctx.Items.[Keys.RequestStart] <- DateTime.Now.Ticks
|
||||
match tryFindWebLogByUrlBase cfg.conn ctx.Request.Url.HostName with
|
||||
| Some webLog -> ctx.Items.[Keys.WebLog] <- webLog
|
||||
| None -> ApplicationException
|
||||
(sprintf "%s is not properly configured for myWebLog" ctx.Request.Url.HostName)
|
||||
|> raise
|
||||
ctx.Items.["version"] <- version
|
||||
ctx.Items.[Keys.Version] <- version
|
||||
null)
|
||||
|
||||
|
||||
|
@ -1,3 +1,11 @@
|
||||
module myWebLog.Keys
|
||||
|
||||
let User = "user"
|
||||
let Messages = "messages"
|
||||
|
||||
let RequestStart = "request-start"
|
||||
|
||||
let User = "user"
|
||||
|
||||
let Version = "version"
|
||||
|
||||
let WebLog = "web-log"
|
24
src/myWebLog.Web/MyWebLogModule.fs
Normal file
24
src/myWebLog.Web/MyWebLogModule.fs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace myWebLog
|
||||
|
||||
open myWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.Security
|
||||
|
||||
/// Parent class for all myWebLog Nancy modules
|
||||
[<AbstractClass>]
|
||||
type MyWebLogModule() =
|
||||
inherit NancyModule()
|
||||
|
||||
/// Strongly-typed access to the web log for the current request
|
||||
member this.WebLog = this.Context.Items.[Keys.WebLog] :?> WebLog
|
||||
|
||||
/// Display a view using the theme specified for the web log
|
||||
member this.ThemedRender view model = this.View.[(sprintf "%s/%s" this.WebLog.themePath view), model]
|
||||
|
||||
/// Return a 404
|
||||
member this.NotFound () = this.Negotiate.WithStatusCode 404
|
||||
|
||||
/// Require a specific level of access for the current web log
|
||||
member this.RequiresAccessLevel level =
|
||||
this.RequiresAuthentication()
|
||||
this.RequiresClaims [| sprintf "%s|%s" this.WebLog.id level |]
|
67
src/myWebLog.Web/PostModule.fs
Normal file
67
src/myWebLog.Web/PostModule.fs
Normal file
@ -0,0 +1,67 @@
|
||||
namespace myWebLog
|
||||
|
||||
open myWebLog.Data.Page
|
||||
open myWebLog.Data.Post
|
||||
open Nancy
|
||||
open Nancy.Authentication.Forms
|
||||
open Nancy.Security
|
||||
open RethinkDb.Driver.Net
|
||||
open myWebLog.Entities
|
||||
|
||||
type PostModule(conn : IConnection) as this =
|
||||
inherit MyWebLogModule()
|
||||
|
||||
do
|
||||
this.Get.["/"] <- fun _ -> upcast this.HomePage ()
|
||||
this.Get.["/posts/page/{page:int}"] <- fun parms -> upcast this.GetPageOfPublishedPosts (downcast parms)
|
||||
|
||||
this.Get.["/posts/list"] <- fun _ -> upcast this.PostList 1
|
||||
this.Get.["/posts/list/page/{page:int"] <- fun parms -> upcast this.PostList
|
||||
((parms :?> DynamicDictionary).["page"] :?> int)
|
||||
|
||||
// ---- Display posts to users ----
|
||||
|
||||
/// Display a page of published posts
|
||||
member private this.DisplayPageOfPublishedPosts pageNbr =
|
||||
let model = PostsModel(this.Context, this.WebLog)
|
||||
model.pageNbr <- pageNbr
|
||||
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10
|
||||
model.hasNewer <- match List.isEmpty model.posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.posts)
|
||||
model.hasOlder <- match List.isEmpty model.posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.posts)
|
||||
model.urlPrefix <- "/posts"
|
||||
model.pageTitle <- match pageNbr with
|
||||
| 1 -> ""
|
||||
| _ -> sprintf "Page #%i" pageNbr
|
||||
this.ThemedRender "posts" model
|
||||
|
||||
/// Display either the newest posts or the configured home page
|
||||
member this.HomePage () =
|
||||
match this.WebLog.defaultPage with
|
||||
| "posts" -> this.DisplayPageOfPublishedPosts 1
|
||||
| page -> match tryFindPage conn this.WebLog.id page with
|
||||
| Some page -> let model = PageModel(this.Context, this.WebLog, page)
|
||||
model.pageTitle <- page.title
|
||||
this.ThemedRender "page" model
|
||||
| None -> this.Negotiate.WithStatusCode 404
|
||||
|
||||
/// Get a page of public posts (other than the first one if the home page is a page of posts)
|
||||
member this.GetPageOfPublishedPosts (parameters : DynamicDictionary) =
|
||||
this.DisplayPageOfPublishedPosts (parameters.["page"] :?> int)
|
||||
|
||||
// ---- Administer posts ----
|
||||
|
||||
/// Display a page of posts in the admin area
|
||||
member this.PostList pageNbr =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let model = PostsModel(this.Context, this.WebLog)
|
||||
model.pageNbr <- pageNbr
|
||||
model.posts <- findPageOfAllPosts conn this.WebLog.id pageNbr 25
|
||||
model.hasNewer <- pageNbr > 1
|
||||
model.hasOlder <- 25 > List.length model.posts
|
||||
model.urlPrefix <- "/post/list"
|
||||
model.pageTitle <- "Posts"
|
||||
this.View.["admin/post/list", model]
|
95
src/myWebLog.Web/ViewModels.fs
Normal file
95
src/myWebLog.Web/ViewModels.fs
Normal file
@ -0,0 +1,95 @@
|
||||
namespace myWebLog
|
||||
|
||||
open myWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.Session.Persistable
|
||||
|
||||
/// Levels for a user message
|
||||
module Level =
|
||||
/// An informational message
|
||||
let Info = "Info"
|
||||
/// A message regarding a non-fatal but non-optimal condition
|
||||
let Warning = "WARNING"
|
||||
/// A message regarding a failure of the expected result
|
||||
let Error = "ERROR"
|
||||
|
||||
|
||||
/// A message for the user
|
||||
type UserMessage = {
|
||||
/// The level of the message (use Level module constants)
|
||||
level : string
|
||||
/// The text of the message
|
||||
message : string
|
||||
/// Further details regarding the message
|
||||
details : string option
|
||||
}
|
||||
with
|
||||
/// An empty message
|
||||
static member empty =
|
||||
{ level = Level.Info
|
||||
message = ""
|
||||
details = None }
|
||||
|
||||
|
||||
/// Parent view model for all myWebLog views
|
||||
type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
|
||||
|
||||
/// Get the messages from the session
|
||||
let getMessages () =
|
||||
let msg = ctx.Request.PersistableSession.GetOrDefault<UserMessage list>(Keys.Messages, List.empty)
|
||||
match List.length msg with
|
||||
| 0 -> ()
|
||||
| _ -> ctx.Request.Session.Delete Keys.Messages
|
||||
msg
|
||||
|
||||
/// The web log for this request
|
||||
member this.webLog = webLog
|
||||
/// User messages
|
||||
member val messages = getMessages () with get, set
|
||||
/// The currently logged in user
|
||||
member this.user = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.empty)
|
||||
/// The title of the page
|
||||
member val pageTitle = "" with get, set
|
||||
/// The request start time
|
||||
member this.requestStart = ctx.Items.[Keys.RequestStart] :?> int64
|
||||
/// Is a user authenticated for this request?
|
||||
member this.isAuthenticated = "" <> this.user.id
|
||||
/// Add a message to the output
|
||||
member this.addMessage message = this.messages <- message :: this.messages
|
||||
|
||||
|
||||
/// Model for all page-of-posts pages
|
||||
type PostsModel(ctx, webLog) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
|
||||
/// The posts to display
|
||||
member val posts = List.empty<Post> with get, set
|
||||
|
||||
/// The page number of the post list
|
||||
member val pageNbr = 0 with get, set
|
||||
|
||||
/// Whether there is a newer page of posts for the list
|
||||
member val hasNewer = false with get, set
|
||||
|
||||
/// Whether there is an older page of posts for the list
|
||||
member val hasOlder = true with get, set
|
||||
|
||||
/// The prefix for the next/prior links
|
||||
member val urlPrefix = "" with get, set
|
||||
|
||||
/// The link for the next newer page of posts
|
||||
member this.newerLink =
|
||||
match this.urlPrefix = "/posts" && this.pageNbr = 2 && this.webLog.defaultPage = "posts" with
|
||||
| true -> "/"
|
||||
| _ -> sprintf "%s/page/%i" this.urlPrefix (this.pageNbr - 1)
|
||||
|
||||
/// The link for the prior (older) page of posts
|
||||
member this.olderLink = sprintf "%s/page/%i" this.urlPrefix (this.pageNbr + 1)
|
||||
|
||||
|
||||
/// Model for page display
|
||||
type PageModel(ctx, webLog, page) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
|
||||
/// The page to be displayed
|
||||
member this.page : Page = page
|
@ -52,6 +52,9 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="AssemblyInfo.fs" />
|
||||
<Compile Include="Keys.fs" />
|
||||
<Compile Include="ViewModels.fs" />
|
||||
<Compile Include="MyWebLogModule.fs" />
|
||||
<Compile Include="PostModule.fs" />
|
||||
<Compile Include="App.fs" />
|
||||
<Content Include="packages.config" />
|
||||
</ItemGroup>
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("myWebLog")]
|
||||
[assembly: AssemblyDescription("A lightweight blogging platform built on Nancy and RethinkDB")]
|
||||
[assembly: AssemblyDescription("A lightweight blogging platform built on Suave, Nancy, and RethinkDB")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("myWebLog")]
|
||||
|
5
src/myWebLog/content/styles/admin.css
Normal file
5
src/myWebLog/content/styles/admin.css
Normal file
@ -0,0 +1,5 @@
|
||||
footer {
|
||||
background-color: #808080;
|
||||
border-top: solid 1px black;
|
||||
color: white;
|
||||
}
|
3
src/myWebLog/data-config.json
Normal file
3
src/myWebLog/data-config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"database": "myWebLog"
|
||||
}
|
@ -48,6 +48,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="data-config.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\myWebLog.Data\myWebLog.Data.fsproj">
|
||||
@ -65,9 +66,14 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<Content Include="content\styles\admin.css" />
|
||||
<Content Include="views\admin\admin-layout.html" />
|
||||
<Content Include="views\admin\post\list.html" />
|
||||
<Content Include="views\default\index-content.html" />
|
||||
<Content Include="views\default\index.html" />
|
||||
<Content Include="views\default\layout.html" />
|
||||
<Content Include="views\default\page-content.html" />
|
||||
<Content Include="views\default\page.html" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
50
src/myWebLog/views/admin/admin-layout.html
Normal file
50
src/myWebLog/views/admin/admin-layout.html
Normal file
@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>@Model.pageTitle | @Translate.Admin | @Model.webLog.name</title>
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.css" />
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.4/cosmo/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/content/styles/admin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">@Model.webLog.name</a>
|
||||
</div>
|
||||
<div class="navbar-left">
|
||||
<p class="navbar-text">@Model.pageTitle</p>
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
@If.isAuthenticated
|
||||
<li><a href="/admin">@Translate.Dashboard</a></li>
|
||||
<li><a href="/user/logoff">@Translate.LogOff</a></li>
|
||||
@EndIf
|
||||
@IfNot.isAuthenticated
|
||||
<li><a href="/user/logon">@Translate.LogOn</a></li>
|
||||
@EndIf
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container">
|
||||
<!-- Partial['admin/messages', Model] // TODO -->
|
||||
@Section['Content'];
|
||||
</div>
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-right">@Model.generator</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="text/javascript" src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.3.min.js"></script>
|
||||
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="//tinymce.cachefly.net/4.1/tinymce.min.js"></script>
|
||||
@Section['Scripts'];
|
||||
</body>
|
||||
</html>
|
60
src/myWebLog/views/admin/post/list.html
Normal file
60
src/myWebLog/views/admin/post/list.html
Normal file
@ -0,0 +1,60 @@
|
||||
@Master['admin/admin-layout']
|
||||
|
||||
@Section['Content']
|
||||
<div class="row">
|
||||
<p>
|
||||
<a class="btn btn-primary" href="/post/new/edit">
|
||||
<i class="fa fa-plus"></i> | @Translate.AddNew
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>@Translate.Date</th>
|
||||
<th>@Translate.Title</th>
|
||||
<th>@Translate.Status</th>
|
||||
<th>@Translate.Tags</th>
|
||||
</tr>
|
||||
@Each.posts
|
||||
<!-- - var theDate = moment(post.publishedDate ? post.publishedDate : post.lastUpdatedDate)
|
||||
- var tags = (post.tag || []).sort() -->
|
||||
<tr>
|
||||
<td>
|
||||
<!-- =theDate.format('MMM D, YYYY')
|
||||
br
|
||||
#{__("at")} #{theDate.format('h:mma')} -->
|
||||
// TODO format date
|
||||
</td>
|
||||
<td>
|
||||
@Current.title<br />
|
||||
<a href="/@Current.permalink">@Translate.View</a> |
|
||||
<a href="/post/@Current.id/edit">@Translate.Edit</a> |
|
||||
<a href="/post/@Current.id/delete">@Translate.Delete</a>
|
||||
</td>
|
||||
<td>@Current.status</td>
|
||||
<td>
|
||||
<!-- if 5 > tags.length
|
||||
=tags.join(' | ')
|
||||
else
|
||||
=tags.slice(0, 3).join(' | ') + ' | '
|
||||
span(title=tags.slice(3).join(' | '))= __("and %d more...", tags.slice(3).length) -->
|
||||
// TODO fix tags
|
||||
</td>
|
||||
</tr>
|
||||
@EndEach
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3 col-xs-offset-2">
|
||||
@If.hasNewer
|
||||
<p><a class="btn btn-default" href="@Model.newerLink">« @Translate.NewerPosts</a></p>
|
||||
@EndIf
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-1 text-right">
|
||||
@If.hasOlder
|
||||
<p><a class="btn btn-default" href="@Model.olderLink">@Translate.OlderPosts »</a></p>
|
||||
@EndIf
|
||||
</div>
|
||||
</div>
|
||||
@EndSection
|
@ -23,16 +23,17 @@
|
||||
@EndEach
|
||||
<div class="row">
|
||||
<div class="col-xs-3 col-xs-offset-3">
|
||||
// TODO: stopped here
|
||||
if hasNext
|
||||
- var nextLink = (2 < page) ? '/page/' + (page - 1) : '/'
|
||||
if '/posts' == extraUrl && 2 == page
|
||||
//- Leave the "next" link at '/'
|
||||
else
|
||||
- nextLink = extraUrl + nextLink
|
||||
p: a.btn.btn-primary(href="#{nextLink}")= __("Newer Posts")
|
||||
</div>
|
||||
.col-xs-3.text-right
|
||||
if hasPrior
|
||||
p: a.btn.btn-primary(href=extraUrl + '/page/' + (page + 1))= __("Older Posts")
|
||||
</div>
|
||||
@If.hasNewer
|
||||
<p>
|
||||
<a class="btn btn-primary" href="@Model.newerLink">@Translate.NewerPosts</a>
|
||||
</p>
|
||||
@EndIf
|
||||
</div>
|
||||
<div class="col-xs-3 text-right">
|
||||
@If.hasOlder
|
||||
<p>
|
||||
<a class="btn btn-primary" href="@Model.olderLink">@Translate.OlderPosts</a>
|
||||
</p>
|
||||
@EndIf
|
||||
</div>
|
||||
</div>
|
4
src/myWebLog/views/default/page-content.html
Normal file
4
src/myWebLog/views/default/page-content.html
Normal file
@ -0,0 +1,4 @@
|
||||
<article>
|
||||
<h1>@Model.page.title</h1>
|
||||
@Model.page.text
|
||||
</article>
|
5
src/myWebLog/views/default/page.html
Normal file
5
src/myWebLog/views/default/page.html
Normal file
@ -0,0 +1,5 @@
|
||||
@Master['default/layout']
|
||||
|
||||
@Section['Content']
|
||||
@Partial['default/page-content', Model]
|
||||
@EndSection
|
Loading…
Reference in New Issue
Block a user