PascalCase record members

Contrary to some examples, the official design guidelines for F# state
that these should be PascalCase rather than camelCase.  Also, while the
projects are still "myWebLog", the namespaces and DLLs are now
"MyWebLog". (never intended it to be the other way, really...)
This commit is contained in:
Daniel J. Summers 2016-07-26 23:17:13 -05:00
parent 2574501ccd
commit ac8fa084d1
44 changed files with 950 additions and 994 deletions

5
src/Settings.FSharpLint Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FSharpLintSettings xmlns="https://github.com/fsprojects/FSharpLint/blob/master/ConfigurationSchema.xsd">
<IgnoreFiles Update="Overwrite"><![CDATA[assemblyinfo.*]]></IgnoreFiles>
<Analysers />
</FSharpLintSettings>

View File

@ -4,11 +4,11 @@ open System.Reflection
open System.Runtime.CompilerServices
open System.Runtime.InteropServices
[<assembly: AssemblyTitle("myWebLog.Data")>]
[<assembly: AssemblyTitle("MyWebLog.Data")>]
[<assembly: AssemblyDescription("Data access for myWebLog")>]
[<assembly: AssemblyConfiguration("")>]
[<assembly: AssemblyCompany("DJS Consulting")>]
[<assembly: AssemblyProduct("myWebLog.Data")>]
[<assembly: AssemblyProduct("MyWebLog.Data")>]
[<assembly: AssemblyCopyright("Copyright © 2016")>]
[<assembly: AssemblyTrademark("")>]
[<assembly: AssemblyCulture("")>]

View File

@ -1,8 +1,9 @@
module myWebLog.Data.Category
module MyWebLog.Data.Category
open FSharp.Interop.Dynamic
open myWebLog.Entities
open MyWebLog.Entities
open Rethink
open RethinkDb.Driver.Ast
open System.Dynamic
let private r = RethinkDb.Driver.RethinkDB.R
@ -11,18 +12,18 @@ let private r = RethinkDb.Driver.RethinkDB.R
let private category (webLogId : string) (catId : string) =
r.Table(Table.Category)
.Get(catId)
.Filter(fun c -> c.["webLogId"].Eq(webLogId))
.Filter(fun c -> c.["WebLogId"].Eq(webLogId))
/// Sort categories by their name, with their children sorted below them, including an indent level
let sortCategories categories =
let rec getChildren (cat : Category) indent =
seq {
yield cat, indent
for child in categories |> List.filter (fun c -> c.parentId = Some cat.id) do
for child in categories |> List.filter (fun c -> c.ParentId = Some cat.Id) do
yield! getChildren child (indent + 1)
}
categories
|> List.filter (fun c -> c.parentId.IsNone)
|> List.filter (fun c -> c.ParentId.IsNone)
|> List.map (fun c -> getChildren c 0)
|> Seq.collect id
|> Seq.toList
@ -30,8 +31,8 @@ let sortCategories categories =
/// Get all categories for a web log
let getAllCategories conn (webLogId : string) =
r.Table(Table.Category)
.GetAll(webLogId).OptArg("index", "webLogId")
.OrderBy("name")
.GetAll(webLogId).OptArg("index", "WebLogId")
.OrderBy("Name")
.RunListAsync<Category>(conn)
|> await
|> Seq.toList
@ -46,28 +47,28 @@ let tryFindCategory conn webLogId catId : Category option =
/// Save a category
let saveCategory conn webLogId (cat : Category) =
match cat.id with
| "new" -> let newCat = { cat with id = string <| System.Guid.NewGuid()
webLogId = webLogId }
match cat.Id with
| "new" -> let newCat = { cat with Id = string <| System.Guid.NewGuid()
WebLogId = webLogId }
r.Table(Table.Category)
.Insert(newCat)
.RunResultAsync(conn) |> await |> ignore
newCat.id
newCat.Id
| _ -> let upd8 = ExpandoObject()
upd8?name <- cat.name
upd8?slug <- cat.slug
upd8?description <- cat.description
upd8?parentId <- cat.parentId
(category webLogId cat.id)
upd8?Name <- cat.Name
upd8?Slug <- cat.Slug
upd8?Description <- cat.Description
upd8?ParentId <- cat.ParentId
(category webLogId cat.Id)
.Update(upd8)
.RunResultAsync(conn) |> await |> ignore
cat.id
cat.Id
/// Remove a category from a given parent
let removeCategoryFromParent conn webLogId parentId catId =
match tryFindCategory conn webLogId parentId with
| Some parent -> let upd8 = ExpandoObject()
upd8?children <- parent.children
upd8?Children <- parent.Children
|> List.filter (fun childId -> childId <> catId)
(category webLogId parentId)
.Update(upd8)
@ -78,7 +79,7 @@ let removeCategoryFromParent conn webLogId parentId catId =
let addCategoryToParent conn webLogId parentId catId =
match tryFindCategory conn webLogId parentId with
| Some parent -> let upd8 = ExpandoObject()
upd8?children <- catId :: parent.children
upd8?Children <- catId :: parent.Children
(category webLogId parentId)
.Update(upd8)
.RunResultAsync(conn) |> await |> ignore
@ -87,40 +88,40 @@ let addCategoryToParent conn webLogId parentId catId =
/// Delete a category
let deleteCategory conn cat =
// Remove the category from its parent
match cat.parentId with
| Some parentId -> removeCategoryFromParent conn cat.webLogId parentId cat.id
match cat.ParentId with
| Some parentId -> removeCategoryFromParent conn cat.WebLogId parentId cat.Id
| None -> ()
// Move this category's children to its parent
let newParent = ExpandoObject()
newParent?parentId <- cat.parentId
cat.children
|> List.iter (fun childId -> (category cat.webLogId childId)
newParent?ParentId <- cat.ParentId
cat.Children
|> List.iter (fun childId -> (category cat.WebLogId childId)
.Update(newParent)
.RunResultAsync(conn) |> await |> ignore)
// Remove the category from posts where it is assigned
r.Table(Table.Post)
.GetAll(cat.webLogId).OptArg("index", "webLogId")
.Filter(fun p -> p.["categoryIds"].Contains(cat.id))
.GetAll(cat.WebLogId).OptArg("index", "WebLogId")
.Filter(ReqlFunction1(fun p -> upcast p.["CategoryIds"].Contains(cat.Id)))
.RunCursorAsync<Post>(conn)
|> await
|> Seq.toList
|> List.iter (fun post -> let newCats = ExpandoObject()
newCats?categoryIds <- post.categoryIds
|> List.filter (fun c -> c <> cat.id)
newCats?CategoryIds <- post.CategoryIds
|> List.filter (fun c -> c <> cat.Id)
r.Table(Table.Post)
.Get(post.id)
.Get(post.Id)
.Update(newCats)
.RunResultAsync(conn) |> await |> ignore)
// Now, delete the category
r.Table(Table.Category)
.Get(cat.id)
.Get(cat.Id)
.Delete()
.RunResultAsync(conn) |> await |> ignore
/// Get a category by its slug
let tryFindCategoryBySlug conn (webLogId : string) (slug : string) =
r.Table(Table.Category)
.GetAll(r.Array(webLogId, slug)).OptArg("index", "slug")
.GetAll(r.Array(webLogId, slug)).OptArg("index", "Slug")
.RunCursorAsync<Category>(conn)
|> await
|> Seq.tryHead

View File

@ -1,47 +1,59 @@
namespace myWebLog.Data
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
}
type DataConfig =
{ /// The hostname for the RethinkDB server
[<JsonProperty("hostname")>]
Hostname : string
/// The port for the RethinkDB server
[<JsonProperty("port")>]
Port : int
/// The authorization key to use when connecting to the server
[<JsonProperty("authKey")>]
AuthKey : string
/// How long an attempt to connect to the server should wait before giving up
[<JsonProperty("timeout")>]
Timeout : int
/// The name of the default database to use on the connection
[<JsonProperty("database")>]
Database : string
/// A connection to the RethinkDB server using the configuration in this object
[<JsonIgnore>]
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() }
static member FromJson json =
let ensureHostname cfg = match cfg.Hostname with
| null -> { cfg with Hostname = RethinkDBConstants.DefaultHostname }
| _ -> cfg
let ensurePort cfg = match cfg.Port with
| 0 -> { cfg with Port = RethinkDBConstants.DefaultPort }
| _ -> cfg
let ensureAuthKey cfg = match cfg.AuthKey with
| null -> { cfg with AuthKey = RethinkDBConstants.DefaultAuthkey }
| _ -> cfg
let ensureTimeout cfg = match cfg.Timeout with
| 0 -> { cfg with Timeout = RethinkDBConstants.DefaultTimeout }
| _ -> cfg
let ensureDatabase cfg = match cfg.Database with
| null -> { cfg with Database = RethinkDBConstants.DefaultDbName }
| _ -> cfg
let connect cfg = { cfg with Conn = RethinkDB.R.Connection()
.Hostname(cfg.Hostname)
.Port(cfg.Port)
.AuthKey(cfg.AuthKey)
.Db(cfg.Database)
.Timeout(cfg.Timeout)
.Connect() }
JsonConvert.DeserializeObject<DataConfig> json
|> ensureHostname
|> ensurePort
|> ensureAuthKey
|> ensureTimeout
|> ensureDatabase
|> connect

View File

@ -1,4 +1,4 @@
namespace myWebLog.Entities
namespace MyWebLog.Entities
open Newtonsoft.Json
@ -37,261 +37,244 @@ module CommentStatus =
// ---- Entities ----
/// A revision of a post or page
type Revision = {
/// The instant which this revision was saved
asOf : int64
/// The source language
sourceType : string
/// The text
text : string
}
type Revision =
{ /// The instant which this revision was saved
AsOf : int64
/// The source language
SourceType : string
/// The text
Text : string }
with
/// An empty revision
static member empty =
{ asOf = int64 0
sourceType = RevisionSource.HTML
text = "" }
static member Empty =
{ AsOf = int64 0
SourceType = RevisionSource.HTML
Text = "" }
/// A page with static content
type Page = {
/// The Id
id : string
/// The Id of the web log to which this page belongs
webLogId : string
/// The Id of the author of this page
authorId : string
/// The title of the page
title : string
/// The link at which this page is displayed
permalink : string
/// The instant this page was published
publishedOn : int64
/// The instant this page was last updated
updatedOn : int64
/// Whether this page shows as part of the web log's navigation
showInPageList : bool
/// The current text of the page
text : string
/// Revisions of this page
revisions : Revision list
}
type Page =
{ /// The Id
Id : string
/// The Id of the web log to which this page belongs
WebLogId : string
/// The Id of the author of this page
AuthorId : string
/// The title of the page
Title : string
/// The link at which this page is displayed
Permalink : string
/// The instant this page was published
PublishedOn : int64
/// The instant this page was last updated
UpdatedOn : int64
/// Whether this page shows as part of the web log's navigation
ShowInPageList : bool
/// The current text of the page
Text : string
/// Revisions of this page
Revisions : Revision list }
with
static member empty =
{ id = ""
webLogId = ""
authorId = ""
title = ""
permalink = ""
publishedOn = int64 0
updatedOn = int64 0
showInPageList = false
text = ""
revisions = List.empty
static member Empty =
{ Id = ""
WebLogId = ""
AuthorId = ""
Title = ""
Permalink = ""
PublishedOn = int64 0
UpdatedOn = int64 0
ShowInPageList = false
Text = ""
Revisions = List.empty
}
/// An entry in the list of pages displayed as part of the web log (derived via query)
type PageListEntry = {
permalink : string
title : string
}
type PageListEntry =
{ Permalink : string
Title : string }
/// A web log
type WebLog = {
/// The Id
id : string
/// The name
name : string
/// The subtitle
subtitle : string option
/// The default page ("posts" or a page Id)
defaultPage : string
/// The path of the theme (within /views/themes)
themePath : string
/// The URL base
urlBase : string
/// The time zone in which dates/times should be displayed
timeZone : string
/// A list of pages to be rendered as part of the site navigation
[<JsonIgnore>]
pageList : PageListEntry list
}
type WebLog =
{ /// The Id
Id : string
/// The name
Name : string
/// The subtitle
Subtitle : string option
/// The default page ("posts" or a page Id)
DefaultPage : string
/// The path of the theme (within /views/themes)
ThemePath : string
/// The URL base
UrlBase : string
/// The time zone in which dates/times should be displayed
TimeZone : string
/// A list of pages to be rendered as part of the site navigation (not stored)
PageList : PageListEntry list }
with
/// An empty web log
static member empty =
{ id = ""
name = ""
subtitle = None
defaultPage = ""
themePath = "default"
urlBase = ""
timeZone = "America/New_York"
pageList = List.empty
}
static member Empty =
{ Id = ""
Name = ""
Subtitle = None
DefaultPage = ""
ThemePath = "default"
UrlBase = ""
TimeZone = "America/New_York"
PageList = List.empty }
/// An authorization between a user and a web log
type Authorization = {
/// The Id of the web log to which this authorization grants access
webLogId : string
/// The level of access granted by this authorization
level : string
}
type Authorization =
{ /// The Id of the web log to which this authorization grants access
WebLogId : string
/// The level of access granted by this authorization
Level : string }
/// A user of myWebLog
type User = {
/// The Id
id : string
/// The user name (e-mail address)
userName : string
/// The first name
firstName : string
/// The last name
lastName : string
/// The user's preferred name
preferredName : string
/// The hash of the user's password
passwordHash : string
/// The URL of the user's personal site
url : string option
/// The user's authorizations
authorizations : Authorization list
}
type User =
{ /// The Id
Id : string
/// The user name (e-mail address)
UserName : string
/// The first name
FirstName : string
/// The last name
LastName : string
/// The user's preferred name
PreferredName : string
/// The hash of the user's password
PasswordHash : string
/// The URL of the user's personal site
Url : string option
/// The user's authorizations
Authorizations : Authorization list }
with
/// An empty user
static member empty =
{ id = ""
userName = ""
firstName = ""
lastName = ""
preferredName = ""
passwordHash = ""
url = None
authorizations = List.empty
}
static member Empty =
{ Id = ""
UserName = ""
FirstName = ""
LastName = ""
PreferredName = ""
PasswordHash = ""
Url = None
Authorizations = List.empty }
/// Claims for this user
[<JsonIgnore>]
member this.claims = this.authorizations
|> List.map (fun auth -> sprintf "%s|%s" auth.webLogId auth.level)
member this.Claims = this.Authorizations
|> List.map (fun auth -> sprintf "%s|%s" auth.WebLogId auth.Level)
/// A category to which posts may be assigned
type Category = {
/// The Id
id : string
/// The Id of the web log to which this category belongs
webLogId : string
/// The displayed name
name : string
/// The slug (used in category URLs)
slug : string
/// A longer description of the category
description : string option
/// The parent Id of this category (if a subcategory)
parentId : string option
/// The categories for which this category is the parent
children : string list
}
type Category =
{ /// The Id
Id : string
/// The Id of the web log to which this category belongs
WebLogId : string
/// The displayed name
Name : string
/// The slug (used in category URLs)
Slug : string
/// A longer description of the category
Description : string option
/// The parent Id of this category (if a subcategory)
ParentId : string option
/// The categories for which this category is the parent
Children : string list }
with
/// An empty category
static member empty =
{ id = "new"
webLogId = ""
name = ""
slug = ""
description = None
parentId = None
children = List.empty
}
{ Id = "new"
WebLogId = ""
Name = ""
Slug = ""
Description = None
ParentId = None
Children = List.empty }
/// A comment (applies to a post)
type Comment = {
/// The Id
id : string
/// The Id of the post to which this comment applies
postId : string
/// The Id of the comment to which this comment is a reply
inReplyToId : string option
/// The name of the commentor
name : string
/// The e-mail address of the commentor
email : string
/// The URL of the commentor's personal website
url : string option
/// The status of the comment
status : string
/// The instant the comment was posted
postedOn : int64
/// The text of the comment
text : string
}
type Comment =
{ /// The Id
Id : string
/// The Id of the post to which this comment applies
PostId : string
/// The Id of the comment to which this comment is a reply
InReplyToId : string option
/// The name of the commentor
Name : string
/// The e-mail address of the commentor
Email : string
/// The URL of the commentor's personal website
Url : string option
/// The status of the comment
Status : string
/// The instant the comment was posted
PostedOn : int64
/// The text of the comment
Text : string }
with
static member empty =
{ id = ""
postId = ""
inReplyToId = None
name = ""
email = ""
url = None
status = CommentStatus.Pending
postedOn = int64 0
text = ""
}
static member Empty =
{ Id = ""
PostId = ""
InReplyToId = None
Name = ""
Email = ""
Url = None
Status = CommentStatus.Pending
PostedOn = int64 0
Text = "" }
/// A post
type Post = {
/// The Id
id : string
/// The Id of the web log to which this post belongs
webLogId : string
/// The Id of the author of this post
authorId : string
/// The status
status : string
/// The title
title : string
/// The link at which the post resides
permalink : string
/// The instant on which the post was originally published
publishedOn : int64
/// The instant on which the post was last updated
updatedOn : int64
/// The text of the post
text : string
/// The Ids of the categories to which this is assigned
categoryIds : string list
/// The tags for the post
tags : string list
/// The permalinks at which this post may have once resided
priorPermalinks : string list
/// Revisions of this post
revisions : Revision list
/// The categories to which this is assigned
[<JsonIgnore>]
categories : Category list
/// The comments
[<JsonIgnore>]
comments : Comment list
}
type Post =
{ /// The Id
Id : string
/// The Id of the web log to which this post belongs
WebLogId : string
/// The Id of the author of this post
AuthorId : string
/// The status
Status : string
/// The title
Title : string
/// The link at which the post resides
Permalink : string
/// The instant on which the post was originally published
PublishedOn : int64
/// The instant on which the post was last updated
UpdatedOn : int64
/// The text of the post
Text : string
/// The Ids of the categories to which this is assigned
CategoryIds : string list
/// The tags for the post
Tags : string list
/// The permalinks at which this post may have once resided
PriorPermalinks : string list
/// Revisions of this post
Revisions : Revision list
/// The categories to which this is assigned (not stored in database)
Categories : Category list
/// The comments (not stored in database)
Comments : Comment list }
with
static member empty =
{ id = "new"
webLogId = ""
authorId = ""
status = PostStatus.Draft
title = ""
permalink = ""
publishedOn = int64 0
updatedOn = int64 0
text = ""
categoryIds = List.empty
tags = List.empty
priorPermalinks = List.empty
revisions = List.empty
categories = List.empty
comments = List.empty
}
static member Empty =
{ Id = "new"
WebLogId = ""
AuthorId = ""
Status = PostStatus.Draft
Title = ""
Permalink = ""
PublishedOn = int64 0
UpdatedOn = int64 0
Text = ""
CategoryIds = List.empty
Tags = List.empty
PriorPermalinks = List.empty
Revisions = List.empty
Categories = List.empty
Comments = List.empty }

View File

@ -1,7 +1,7 @@
module myWebLog.Data.Page
module MyWebLog.Data.Page
open FSharp.Interop.Dynamic
open myWebLog.Entities
open MyWebLog.Entities
open Rethink
open RethinkDb.Driver.Ast
open System.Dynamic
@ -12,7 +12,7 @@ let private r = RethinkDb.Driver.RethinkDB.R
let private page (webLogId : string) (pageId : string) =
r.Table(Table.Page)
.Get(pageId)
.Filter(ReqlFunction1(fun p -> upcast p.["webLogId"].Eq(webLogId)))
.Filter(ReqlFunction1(fun p -> upcast p.["WebLogId"].Eq(webLogId)))
/// Get a page by its Id
let tryFindPage conn webLogId pageId =
@ -21,14 +21,14 @@ let tryFindPage conn webLogId pageId =
.RunAtomAsync<Page>(conn) |> await |> box with
| null -> None
| page -> let pg : Page = unbox page
match pg.webLogId = webLogId with
match pg.WebLogId = webLogId with
| true -> Some pg
| _ -> None
/// Get a page by its Id (excluding revisions)
let tryFindPageWithoutRevisions conn webLogId pageId : Page option =
match (page webLogId pageId)
.Without("revisions")
.Without("Revisions")
.RunAtomAsync<Page>(conn) |> await |> box with
| null -> None
| page -> Some <| unbox page
@ -36,8 +36,8 @@ let tryFindPageWithoutRevisions conn webLogId pageId : Page option =
/// Find a page by its permalink
let tryFindPageByPermalink conn (webLogId : string) (permalink : string) =
r.Table(Table.Page)
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "permalink")
.Without("revisions")
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "Permalink")
.Without("Revisions")
.RunCursorAsync<Page>(conn)
|> await
|> Seq.tryHead
@ -45,32 +45,32 @@ let tryFindPageByPermalink conn (webLogId : string) (permalink : string) =
/// Get a list of all pages (excludes page text and revisions)
let findAllPages conn (webLogId : string) =
r.Table(Table.Page)
.GetAll(webLogId).OptArg("index", "webLogId")
.OrderBy("title")
.Without("text", "revisions")
.GetAll(webLogId).OptArg("index", "WebLogId")
.OrderBy("Title")
.Without("Text", "Revisions")
.RunListAsync<Page>(conn)
|> await
|> Seq.toList
/// Save a page
let savePage conn (pg : Page) =
match pg.id with
| "new" -> let newPage = { pg with id = string <| System.Guid.NewGuid() }
match pg.Id with
| "new" -> let newPage = { pg with Id = string <| System.Guid.NewGuid() }
r.Table(Table.Page)
.Insert(page)
.RunResultAsync(conn) |> await |> ignore
newPage.id
newPage.Id
| _ -> let upd8 = ExpandoObject()
upd8?title <- pg.title
upd8?permalink <- pg.permalink
upd8?publishedOn <- pg.publishedOn
upd8?updatedOn <- pg.updatedOn
upd8?text <- pg.text
upd8?revisions <- pg.revisions
(page pg.webLogId pg.id)
upd8?Title <- pg.Title
upd8?Permalink <- pg.Permalink
upd8?PublishedOn <- pg.PublishedOn
upd8?UpdatedOn <- pg.UpdatedOn
upd8?Text <- pg.Text
upd8?Revisions <- pg.Revisions
(page pg.WebLogId pg.Id)
.Update(upd8)
.RunResultAsync(conn) |> await |> ignore
pg.id
pg.Id
/// Delete a page
let deletePage conn webLogId pageId =

View File

@ -1,7 +1,7 @@
module myWebLog.Data.Post
module MyWebLog.Data.Post
open FSharp.Interop.Dynamic
open myWebLog.Entities
open MyWebLog.Entities
open Rethink
open RethinkDb.Driver.Ast
open System.Dynamic
@ -11,22 +11,20 @@ let private r = RethinkDb.Driver.RethinkDB.R
/// Shorthand to select all published posts for a web log
let private publishedPosts (webLogId : string)=
r.Table(Table.Post)
.GetAll(r.Array(webLogId, PostStatus.Published)).OptArg("index", "webLogAndStatus")
.GetAll(r.Array(webLogId, PostStatus.Published)).OptArg("index", "WebLogAndStatus")
/// Shorthand to sort posts by published date, slice for the given page, and return a list
let private toPostList conn pageNbr nbrPerPage (filter : ReqlExpr) =
filter
.OrderBy(r.Desc("publishedOn"))
.OrderBy(r.Desc("PublishedOn"))
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
.RunListAsync<Post>(conn)
|> await
|> Seq.toList
/// Shorthand to get a newer or older post
// TODO: older posts need to sort by published on DESC
//let private adjacentPost conn post (theFilter : ReqlExpr -> ReqlExpr) (sort :ReqlExpr) : Post option =
let private adjacentPost conn post (theFilter : ReqlExpr -> obj) (sort : obj) : Post option =
(publishedPosts post.webLogId)
let private adjacentPost conn post (theFilter : ReqlExpr -> obj) (sort : obj) =
(publishedPosts post.WebLogId)
.Filter(theFilter)
.OrderBy(sort)
.Limit(1)
@ -48,45 +46,45 @@ let findPageOfPublishedPosts conn webLogId pageNbr nbrPerPage =
/// Get a page of published posts assigned to a given category
let findPageOfCategorizedPosts conn webLogId (categoryId : string) pageNbr nbrPerPage =
(publishedPosts webLogId)
.Filter(ReqlFunction1(fun p -> upcast p.["categoryIds"].Contains(categoryId)))
.Filter(ReqlFunction1(fun p -> upcast p.["CategoryIds"].Contains(categoryId)))
|> toPostList conn pageNbr nbrPerPage
/// Get a page of published posts tagged with a given tag
let findPageOfTaggedPosts conn webLogId (tag : string) pageNbr nbrPerPage =
(publishedPosts webLogId)
.Filter(ReqlFunction1(fun p -> upcast p.["tags"].Contains(tag)))
.Filter(ReqlFunction1(fun p -> upcast p.["Tags"].Contains(tag)))
|> toPostList conn pageNbr nbrPerPage
/// Try to get the next newest post from the given post
let tryFindNewerPost conn post = newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn))
let tryFindNewerPost conn post = newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn))
/// Try to get the next newest post assigned to the given category
let tryFindNewerCategorizedPost conn (categoryId : string) post =
newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn)
.And(p.["categoryIds"].Contains(categoryId)))
newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn)
.And(p.["CategoryIds"].Contains(categoryId)))
/// Try to get the next newest tagged post from the given tagged post
let tryFindNewerTaggedPost conn (tag : string) post =
newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn).And(p.["tags"].Contains(tag)))
newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn).And(p.["Tags"].Contains(tag)))
/// Try to get the next oldest post from the given post
let tryFindOlderPost conn post = olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn))
let tryFindOlderPost conn post = olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn))
/// Try to get the next oldest post assigned to the given category
let tryFindOlderCategorizedPost conn (categoryId : string) post =
olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn)
.And(p.["categoryIds"].Contains(categoryId)))
olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn)
.And(p.["CategoryIds"].Contains(categoryId)))
/// Try to get the next oldest tagged post from the given tagged post
let tryFindOlderTaggedPost conn (tag : string) post =
olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn).And(p.["tags"].Contains(tag)))
olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn).And(p.["Tags"].Contains(tag)))
/// Get a page of all posts in all statuses
let findPageOfAllPosts conn (webLogId : string) pageNbr nbrPerPage =
// FIXME: sort unpublished posts by their last updated date
r.Table(Table.Post)
.GetAll(webLogId).OptArg("index", "webLogId")
.OrderBy(r.Desc("publishedOn"))
.GetAll(webLogId).OptArg("index", "WebLogId")
.OrderBy(r.Desc("PublishedOn"))
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
.RunListAsync<Post>(conn)
|> await
@ -96,7 +94,7 @@ let findPageOfAllPosts conn (webLogId : string) pageNbr nbrPerPage =
let tryFindPost conn webLogId postId : Post option =
match r.Table(Table.Post)
.Get(postId)
.Filter(fun p -> p.["webLogId"].Eq(webLogId))
.Filter(ReqlFunction1(fun p -> upcast p.["WebLogId"].Eq(webLogId)))
.RunAtomAsync<Post>(conn)
|> box with
| null -> None
@ -106,27 +104,22 @@ let tryFindPost conn webLogId postId : Post option =
// TODO: see if we can make .Merge work for page list even though the attribute is ignored
// (needs to be ignored for serialization, but included for deserialization)
let tryFindPostByPermalink conn webLogId permalink =
match r.Table(Table.Post)
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "permalink")
.Filter(fun p -> p.["status"].Eq(PostStatus.Published))
.Without("revisions")
.RunCursorAsync<Post>(conn)
|> await
|> Seq.tryHead with
| Some p -> Some { p with categories = r.Table(Table.Category)
.GetAll(p.categoryIds |> List.toArray)
.Without("children")
.OrderBy("name")
.RunListAsync<Category>(conn)
|> await
|> Seq.toList
comments = r.Table(Table.Comment)
.GetAll(p.id).OptArg("index", "postId")
.OrderBy("postedOn")
.RunListAsync<Comment>(conn)
|> await
|> Seq.toList }
| None -> None
r.Table(Table.Post)
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "Permalink")
.Filter(fun p -> p.["Status"].Eq(PostStatus.Published))
.Without("Revisions")
.Merge(fun p -> r.HashMap("Categories", r.Table(Table.Category)
.GetAll(p.["CategoryIds"])
.Without("Children")
.OrderBy("Name")
.CoerceTo("array")))
.Merge(fun p -> r.HashMap("Comments", r.Table(Table.Comment)
.GetAll(p.["Id"]).OptArg("index", "PostId")
.OrderBy("PostedOn")
.CoerceTo("array")))
.RunCursorAsync<Post>(conn)
|> await
|> Seq.tryHead
/// Try to find a post by its prior permalink
let tryFindPostByPriorPermalink conn (webLogId : string) (permalink : string) =
@ -140,34 +133,34 @@ let tryFindPostByPriorPermalink conn (webLogId : string) (permalink : string) =
/// Get a set of posts for RSS
let findFeedPosts conn webLogId nbr : (Post * User option) list =
findPageOfPublishedPosts conn webLogId 1 nbr
|> List.map (fun post -> { post with categories = r.Table(Table.Category)
.GetAll(post.categoryIds |> List.toArray)
.OrderBy("name")
.Pluck("id", "name")
.RunListAsync<Category>(conn)
|> await
|> Seq.toList },
(match r.Table(Table.User)
.Get(post.authorId)
.RunAtomAsync<User>(conn)
|> await
|> box with
| null -> None
| user -> Some <| unbox user))
(publishedPosts webLogId)
.Merge(fun post -> r.HashMap("Categories", r.Table(Table.Category)
.GetAll(post.["CategoryIds"])
.OrderBy("Name")
.Pluck("Id", "Name")
.CoerceTo("array")))
|> toPostList conn 1 nbr
|> List.map (fun post -> post, match r.Table(Table.User)
.Get(post.AuthorId)
.RunAtomAsync<User>(conn)
|> await
|> box with
| null -> None
| user -> Some <| unbox user)
/// Save a post
let savePost conn post =
match post.id with
| "new" -> let newPost = { post with id = string <| System.Guid.NewGuid() }
match post.Id with
| "new" -> let newPost = { post with Id = string <| System.Guid.NewGuid() }
r.Table(Table.Post)
.Insert(newPost)
.RunResultAsync(conn)
|> ignore
newPost.id
newPost.Id
| _ -> r.Table(Table.Post)
.Get(post.id)
.Replace(post)
.Get(post.Id)
.Replace( { post with Categories = List.empty
Comments = List.empty } )
.RunResultAsync(conn)
|> ignore
post.id
post.Id

View File

@ -1,4 +1,4 @@
module myWebLog.Data.Rethink
module MyWebLog.Data.Rethink
open RethinkDb.Driver.Ast
open RethinkDb.Driver.Net

View File

@ -1,4 +1,4 @@
module myWebLog.Data.SetUp
module MyWebLog.Data.SetUp
open Rethink
open RethinkDb.Driver.Ast
@ -12,17 +12,17 @@ let private logStepDone () = Console.Out.WriteLine (" done.")
/// Ensure the myWebLog database exists
let checkDatabase (cfg : DataConfig) =
logStep "|> Checking database"
let dbs = r.DbList().RunListAsync<string>(cfg.conn) |> await
match dbs.Contains cfg.database with
let dbs = r.DbList().RunListAsync<string>(cfg.Conn) |> await
match dbs.Contains cfg.Database with
| true -> ()
| _ -> logStepStart (sprintf " %s database not found - creating" cfg.database)
r.DbCreate(cfg.database).RunResultAsync(cfg.conn) |> await |> ignore
| _ -> logStepStart (sprintf " %s database not found - creating" cfg.Database)
r.DbCreate(cfg.Database).RunResultAsync(cfg.Conn) |> await |> ignore
logStepDone ()
/// Ensure all required tables exist
let checkTables cfg =
logStep "|> Checking tables"
let tables = r.Db(cfg.database).TableList().RunListAsync<string>(cfg.conn) |> await
let tables = r.Db(cfg.Database).TableList().RunListAsync<string>(cfg.Conn) |> await
[ Table.Category; Table.Comment; Table.Page; Table.Post; Table.User; Table.WebLog ]
|> List.map (fun tbl -> match tables.Contains tbl with
| true -> None
@ -30,27 +30,27 @@ let checkTables cfg =
|> List.filter (fun create -> create.IsSome)
|> List.map (fun create -> create.Value)
|> List.iter (fun (tbl, create) -> logStepStart (sprintf " Creating table %s" tbl)
create.RunResultAsync(cfg.conn) |> await |> ignore
create.RunResultAsync(cfg.Conn) |> await |> ignore
logStepDone ())
/// Shorthand to get the table
let tbl cfg table = r.Db(cfg.database).Table(table)
let tbl cfg table = r.Db(cfg.Database).Table(table)
/// Create the given index
let createIndex cfg table (index : string * (ReqlExpr -> obj) option) =
let idxName, idxFunc = index
logStepStart (sprintf """ Creating index "%s" on table %s""" idxName table)
match idxFunc with
| Some f -> (tbl cfg table).IndexCreate(idxName, f).RunResultAsync(cfg.conn)
| None -> (tbl cfg table).IndexCreate(idxName ).RunResultAsync(cfg.conn)
| Some f -> (tbl cfg table).IndexCreate(idxName, f).RunResultAsync(cfg.Conn)
| None -> (tbl cfg table).IndexCreate(idxName ).RunResultAsync(cfg.Conn)
|> await |> ignore
(tbl cfg table).IndexWait(idxName).RunAtomAsync(cfg.conn) |> await |> ignore
(tbl cfg table).IndexWait(idxName).RunAtomAsync(cfg.Conn) |> await |> ignore
logStepDone ()
/// Ensure that the given indexes exist, and create them if required
let ensureIndexes cfg (indexes : (string * (string * (ReqlExpr -> obj) option) list) list) =
let ensureForTable tabl =
let idx = (tbl cfg (fst tabl)).IndexList().RunListAsync<string>(cfg.conn) |> await
let idx = (tbl cfg (fst tabl)).IndexList().RunListAsync<string>(cfg.Conn) |> await
snd tabl
|> List.iter (fun index -> match idx.Contains (fst index) with
| true -> ()
@ -68,21 +68,21 @@ let webLogField (name : string) : (ReqlExpr -> obj) option =
/// Ensure all the required indexes exist
let checkIndexes cfg =
logStep "|> Checking indexes"
[ Table.Category, [ "webLogId", None
"slug", webLogField "slug"
[ Table.Category, [ "WebLogId", None
"Slug", webLogField "Slug"
]
Table.Comment, [ "postId", None
Table.Comment, [ "PostId", None
]
Table.Page, [ "webLogId", None
"permalink", webLogField "permalink"
Table.Page, [ "WebLogId", None
"Permalink", webLogField "Permalink"
]
Table.Post, [ "webLogId", None
"webLogAndStatus", webLogField "status"
"permalink", webLogField "permalink"
Table.Post, [ "WebLogId", None
"WebLogAndStatus", webLogField "Status"
"Permalink", webLogField "Permalink"
]
Table.User, [ "userName", None
Table.User, [ "UserName", None
]
Table.WebLog, [ "urlBase", None
Table.WebLog, [ "UrlBase", None
]
]
|> ensureIndexes cfg

View File

@ -1,4 +1,4 @@
module myWebLog.Data.Table
module MyWebLog.Data.Table
/// The Category table
let Category = "Category"

View File

@ -1,6 +1,6 @@
module myWebLog.Data.User
module MyWebLog.Data.User
open myWebLog.Entities
open MyWebLog.Entities
open Rethink
let private r = RethinkDb.Driver.RethinkDB.R
@ -11,8 +11,8 @@ let private r = RethinkDb.Driver.RethinkDB.R
// http://rethinkdb.com/docs/secondary-indexes/java/ for more information.
let tryUserLogOn conn (email : string) (passwordHash : string) =
r.Table(Table.User)
.GetAll(email).OptArg("index", "userName")
.Filter(fun u -> u.["passwordHash"].Eq(passwordHash))
.GetAll(email).OptArg("index", "UserName")
.Filter(fun u -> u.["PasswordHash"].Eq(passwordHash))
.RunCursorAsync<User>(conn)
|> await
|> Seq.tryHead

View File

@ -1,43 +1,39 @@
module myWebLog.Data.WebLog
module MyWebLog.Data.WebLog
open myWebLog.Entities
open MyWebLog.Entities
open Rethink
open RethinkDb.Driver.Ast
let private r = RethinkDb.Driver.RethinkDB.R
/// Counts of items displayed on the admin dashboard
type DashboardCounts = {
/// The number of pages for the web log
pages : int
/// The number of pages for the web log
posts : int
/// The number of categories for the web log
categories : int
}
type DashboardCounts =
{ /// The number of pages for the web log
Pages : int
/// The number of pages for the web log
Posts : int
/// The number of categories for the web log
Categories : int }
/// Detemine the web log by the URL base
// TODO: see if we can make .Merge work for page list even though the attribute is ignored
// (needs to be ignored for serialization, but included for deserialization)
let tryFindWebLogByUrlBase conn (urlBase : string) =
let webLog = r.Table(Table.WebLog)
.GetAll(urlBase).OptArg("index", "urlBase")
.RunCursorAsync<WebLog>(conn)
|> await
|> Seq.tryHead
match webLog with
| Some w -> Some { w with pageList = r.Table(Table.Page)
.GetAll(w.id).OptArg("index", "webLogId")
.Filter(fun pg -> pg.["showInPageList"].Eq(true))
.OrderBy("title")
.Pluck("title", "permalink")
.RunListAsync<PageListEntry>(conn) |> await |> Seq.toList }
| None -> None
r.Table(Table.WebLog)
.GetAll(urlBase).OptArg("index", "urlBase")
.Merge(fun w -> r.HashMap("PageList", r.Table(Table.Page)
.GetAll(w.["Id"]).OptArg("index", "WebLogId")
.Filter(ReqlFunction1(fun pg -> upcast pg.["ShowInPageList"].Eq(true)))
.OrderBy("Title")
.Pluck("Title", "Permalink")
.CoerceTo("array")))
.RunCursorAsync<WebLog>(conn)
|> await
|> Seq.tryHead
/// Get counts for the admin dashboard
let findDashboardCounts conn (webLogId : string) =
r.Expr( r.HashMap("pages", r.Table(Table.Page ).GetAll(webLogId).OptArg("index", "webLogId").Count()))
.Merge(r.HashMap("posts", r.Table(Table.Post ).GetAll(webLogId).OptArg("index", "webLogId").Count()))
.Merge(r.HashMap("categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "webLogId").Count()))
r.Expr( r.HashMap("Pages", r.Table(Table.Page ).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
.Merge(r.HashMap("Posts", r.Table(Table.Post ).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
.Merge(r.HashMap("Categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
.RunAtomAsync<DashboardCounts>(conn)
|> await

View File

@ -8,7 +8,7 @@
<ProjectGuid>1fba0b84-b09e-4b16-b9b6-5730dea27192</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>myWebLog.Data</RootNamespace>
<AssemblyName>myWebLog.Data</AssemblyName>
<AssemblyName>MyWebLog.Data</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
@ -22,7 +22,7 @@
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<WarningLevel>3</WarningLevel>
<DocumentationFile>bin\Debug\myWebLog.Data.xml</DocumentationFile>
<DocumentationFile>bin\Debug\MyWebLog.Data.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>

View File

@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace myWebLog {
namespace MyWebLog {
using System;
@ -39,7 +39,7 @@ namespace myWebLog {
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("myWebLog.Resources", typeof(Resources).Assembly);
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MyWebLog.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;

View File

@ -7,8 +7,8 @@
<ProjectGuid>{A12EA8DA-88BC-4447-90CB-A0E2DCC37523}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>myWebLog</RootNamespace>
<AssemblyName>myWebLog.Resources</AssemblyName>
<RootNamespace>MyWebLog</RootNamespace>
<AssemblyName>MyWebLog.Resources</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
namespace myWebLog
namespace MyWebLog
open myWebLog.Data.WebLog
open myWebLog.Entities
open MyWebLog.Data.WebLog
open MyWebLog.Entities
open Nancy
open RethinkDb.Driver.Net
@ -15,6 +15,6 @@ type AdminModule(conn : IConnection) as this =
/// Admin dashboard
member this.Dashboard () =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let model = DashboardModel(this.Context, this.WebLog, findDashboardCounts conn this.WebLog.id)
model.pageTitle <- Resources.Dashboard
let model = DashboardModel(this.Context, this.WebLog, findDashboardCounts conn this.WebLog.Id)
model.PageTitle <- Resources.Dashboard
upcast this.View.["admin/dashboard", model]

View File

@ -1,10 +1,10 @@
module myWebLog.App
module MyWebLog.App
open myWebLog
open myWebLog.Data
open myWebLog.Data.SetUp
open myWebLog.Data.WebLog
open myWebLog.Entities
open MyWebLog
open MyWebLog.Data
open MyWebLog.Data.SetUp
open MyWebLog.Data.WebLog
open MyWebLog.Entities
open Nancy
open Nancy.Authentication.Forms
open Nancy.Bootstrapper
@ -25,7 +25,7 @@ open System.IO
open System.Text.RegularExpressions
/// Set up a database connection
let cfg = try DataConfig.fromJson (System.IO.File.ReadAllText "data-config.json")
let cfg = try DataConfig.FromJson (System.IO.File.ReadAllText "data-config.json")
with ex -> raise <| ApplicationException(Resources.ErrDataConfig, ex)
do
@ -37,7 +37,7 @@ type TranslateTokenViewEngineMatcher() =
interface ISuperSimpleViewEngineMatcher with
member this.Invoke (content, model, host) =
regex.Replace(content, fun m -> let key = m.Groups.["TranslationKey"].Value
match myWebLog.Resources.ResourceManager.GetString key with
match MyWebLog.Resources.ResourceManager.GetString key with
| null -> key
| xlat -> xlat)
@ -54,8 +54,8 @@ type MyWebLogUserMapper(container : TinyIoCContainer) =
interface IUserMapper with
member this.GetUserFromIdentifier (identifier, context) =
match context.Request.PersistableSession.GetOrDefault(Keys.User, User.empty) with
| user when user.id = string identifier -> upcast MyWebLogUser(user.preferredName, user.claims)
match context.Request.PersistableSession.GetOrDefault(Keys.User, User.Empty) with
| user when user.Id = string identifier -> upcast MyWebLogUser(user.PreferredName, user.Claims)
| _ -> null
@ -87,7 +87,7 @@ type MyWebLogBootstrapper() =
// Data configuration (both config and the connection; Nancy modules just need the connection)
container.Register<DataConfig>(cfg)
|> ignore
container.Register<IConnection>(cfg.conn)
container.Register<IConnection>(cfg.Conn)
|> ignore
// NodaTime
container.Register<IClock>(SystemClock.Instance)
@ -109,8 +109,8 @@ type MyWebLogBootstrapper() =
// CSRF
Csrf.Enable pipelines
// Sessions
let sessions = RethinkDbSessionConfiguration(cfg.conn)
sessions.Database <- cfg.database
let sessions = RethinkDbSessionConfiguration(cfg.Conn)
sessions.Database <- cfg.Database
PersistableSessions.Enable (pipelines, sessions)
()
@ -128,17 +128,18 @@ let version =
type RequestEnvironment() =
interface IRequestStartup with
member this.Initialize (pipelines, context) =
pipelines.BeforeRequest.AddItemToStartOfPipeline
(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 %s" ctx.Request.Url.HostName Resources.ErrNotConfigured)
|> raise
ctx.Items.[Keys.Version] <- version
null)
let establishEnv (ctx : NancyContext) =
ctx.Items.[Keys.RequestStart] <- DateTime.Now.Ticks
match tryFindWebLogByUrlBase cfg.Conn ctx.Request.Url.HostName with
| Some webLog -> ctx.Items.[Keys.WebLog] <- webLog
| None -> // TODO: redirect to domain set up page
ApplicationException (sprintf "%s %s" ctx.Request.Url.HostName Resources.ErrNotConfigured)
|> raise
ctx.Items.[Keys.Version] <- version
null
pipelines.BeforeRequest.AddItemToStartOfPipeline establishEnv
let app = OwinApp.ofMidFunc "/" (NancyMiddleware.UseNancy (NancyOptions()))
let run () = startWebServer defaultConfig app // webPart
let Run () = startWebServer defaultConfig app // webPart

View File

@ -4,11 +4,11 @@ open System.Reflection
open System.Runtime.CompilerServices
open System.Runtime.InteropServices
[<assembly: AssemblyTitle("myWebLog.Web")>]
[<assembly: AssemblyTitle("MyWebLog.Web")>]
[<assembly: AssemblyDescription("Main Nancy assembly for myWebLog")>]
[<assembly: AssemblyConfiguration("")>]
[<assembly: AssemblyCompany("DJS Consulting")>]
[<assembly: AssemblyProduct("myWebLog.Web")>]
[<assembly: AssemblyProduct("MyWebLog.Web")>]
[<assembly: AssemblyCopyright("Copyright © 2016")>]
[<assembly: AssemblyTrademark("")>]
[<assembly: AssemblyCulture("")>]

View File

@ -1,7 +1,7 @@
namespace myWebLog
namespace MyWebLog
open myWebLog.Data.Category
open myWebLog.Entities
open MyWebLog.Data.Category
open MyWebLog.Entities
open Nancy
open Nancy.ModelBinding
open Nancy.Security
@ -21,24 +21,21 @@ type CategoryModule(conn : IConnection) as this =
member this.CategoryList () =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let model = CategoryListModel(this.Context, this.WebLog,
(getAllCategories conn this.WebLog.id
|> List.map (fun cat -> IndentedCategory.create cat (fun _ -> false))))
(getAllCategories conn this.WebLog.Id
|> List.map (fun cat -> IndentedCategory.Create cat (fun _ -> false))))
upcast this.View.["/admin/category/list", model]
/// Edit a category
member this.EditCategory (parameters : DynamicDictionary) =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let catId : string = downcast parameters.["id"]
let catId = parameters.["id"].ToString ()
match (match catId with
| "new" -> Some Category.empty
| _ -> tryFindCategory conn this.WebLog.id catId) with
| _ -> tryFindCategory conn this.WebLog.Id catId) with
| Some cat -> let model = CategoryEditModel(this.Context, this.WebLog, cat)
let cats = getAllCategories conn this.WebLog.id
|> List.map (fun cat -> IndentedCategory.create cat
(fun c -> c = defaultArg (fst cat).parentId ""))
model.categories <- getAllCategories conn this.WebLog.id
|> List.map (fun cat -> IndentedCategory.create cat
(fun c -> c = defaultArg (fst cat).parentId ""))
model.Categories <- getAllCategories conn this.WebLog.Id
|> List.map (fun cat -> IndentedCategory.Create cat
(fun c -> c = defaultArg (fst cat).ParentId ""))
upcast this.View.["admin/category/edit", model]
| None -> this.NotFound ()
@ -46,32 +43,32 @@ type CategoryModule(conn : IConnection) as this =
member this.SaveCategory (parameters : DynamicDictionary) =
this.ValidateCsrfToken ()
this.RequiresAccessLevel AuthorizationLevel.Administrator
let catId : string = downcast parameters.["id"]
let catId = parameters.["id"].ToString ()
let form = this.Bind<CategoryForm> ()
let oldCat = match catId with
| "new" -> Some Category.empty
| _ -> tryFindCategory conn this.WebLog.id catId
| _ -> tryFindCategory conn this.WebLog.Id catId
match oldCat with
| Some old -> let cat = { old with name = form.name
slug = form.slug
description = match form.description with | "" -> None | d -> Some d
parentId = match form.parentId with | "" -> None | p -> Some p }
let newCatId = saveCategory conn this.WebLog.id cat
match old.parentId = cat.parentId with
| Some old -> let cat = { old with Name = form.Name
Slug = form.Slug
Description = match form.Description with "" -> None | d -> Some d
ParentId = match form.ParentId with "" -> None | p -> Some p }
let newCatId = saveCategory conn this.WebLog.Id cat
match old.ParentId = cat.ParentId with
| true -> ()
| _ -> match old.parentId with
| Some parentId -> removeCategoryFromParent conn this.WebLog.id parentId newCatId
| _ -> match old.ParentId with
| Some parentId -> removeCategoryFromParent conn this.WebLog.Id parentId newCatId
| None -> ()
match cat.parentId with
| Some parentId -> addCategoryToParent conn this.WebLog.id parentId newCatId
match cat.ParentId with
| Some parentId -> addCategoryToParent conn this.WebLog.Id parentId newCatId
| None -> ()
let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info
message = System.String.Format
(Resources.MsgCategoryEditSuccess,
(match catId with | "new" -> Resources.Added | _ -> Resources.Updated))
details = None }
|> model.addMessage
{ UserMessage.Empty with
Level = Level.Info
Message = System.String.Format
(Resources.MsgCategoryEditSuccess,
(match catId with | "new" -> Resources.Added | _ -> Resources.Updated)) }
|> model.AddMessage
this.Redirect (sprintf "/category/%s/edit" newCatId) model
| None -> this.NotFound ()
@ -79,13 +76,12 @@ type CategoryModule(conn : IConnection) as this =
member this.DeleteCategory (parameters : DynamicDictionary) =
this.ValidateCsrfToken ()
this.RequiresAccessLevel AuthorizationLevel.Administrator
let catId : string = downcast parameters.["id"]
match tryFindCategory conn this.WebLog.id catId with
let catId = parameters.["id"].ToString ()
match tryFindCategory conn this.WebLog.Id catId with
| Some cat -> deleteCategory conn cat
let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info
message = System.String.Format(Resources.MsgCategoryDeleted, cat.name)
details = None }
|> model.addMessage
{ UserMessage.Empty with Level = Level.Info
Message = System.String.Format(Resources.MsgCategoryDeleted, cat.Name) }
|> model.AddMessage
this.Redirect "/categories" model
| None -> this.NotFound ()

View File

@ -1,4 +1,4 @@
module myWebLog.Keys
module MyWebLog.Keys
let Messages = "messages"

View File

@ -1,8 +1,7 @@
[<AutoOpen>]
module myWebLog.ModuleExtensions
module MyWebLog.ModuleExtensions
open myWebLog
open myWebLog.Entities
open MyWebLog.Entities
open Nancy
open Nancy.Security
@ -14,19 +13,19 @@ type NancyModule with
/// Display a view using the theme specified for the web log
member this.ThemedView view (model : MyWebLogModel) : obj =
upcast this.View.[(sprintf "themes/%s/%s" this.WebLog.themePath view), model]
upcast this.View.[(sprintf "themes/%s/%s" this.WebLog.ThemePath view), model]
/// Return a 404
member this.NotFound () : obj = upcast HttpStatusCode.NotFound
/// Redirect a request, storing messages in the session if they exist
member this.Redirect url (model : MyWebLogModel) : obj =
match List.length model.messages with
match List.length model.Messages with
| 0 -> ()
| _ -> this.Session.[Keys.Messages] <- model.messages
| _ -> this.Session.[Keys.Messages] <- model.Messages
upcast this.Response.AsRedirect(url).WithStatusCode HttpStatusCode.TemporaryRedirect
/// 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 |]
this.RequiresClaims [| sprintf "%s|%s" this.WebLog.Id level |]

View File

@ -1,8 +1,8 @@
namespace myWebLog
namespace MyWebLog
open FSharp.Markdown
open myWebLog.Data.Page
open myWebLog.Entities
open MyWebLog.Data.Page
open MyWebLog.Entities
open Nancy
open Nancy.ModelBinding
open Nancy.Security
@ -22,9 +22,9 @@ type PageModule(conn : IConnection, clock : IClock) as this =
/// List all pages
member this.PageList () =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let model = PagesModel(this.Context, this.WebLog, (findAllPages conn this.WebLog.id
let model = PagesModel(this.Context, this.WebLog, (findAllPages conn this.WebLog.Id
|> List.map (fun p -> PageForDisplay(this.WebLog, p))))
model.pageTitle <- Resources.Pages
model.PageTitle <- Resources.Pages
upcast this.View.["admin/page/list", model]
/// Edit a page
@ -32,17 +32,15 @@ type PageModule(conn : IConnection, clock : IClock) as this =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let pageId = parameters.["id"].ToString ()
match (match pageId with
| "new" -> Some Page.empty
| _ -> tryFindPage conn this.WebLog.id pageId) with
| Some page -> let rev = match page.revisions
|> List.sortByDescending (fun r -> r.asOf)
| "new" -> Some Page.Empty
| _ -> tryFindPage conn this.WebLog.Id pageId) with
| Some page -> let rev = match page.Revisions
|> List.sortByDescending (fun r -> r.AsOf)
|> List.tryHead with
| Some r -> r
| None -> Revision.empty
| None -> Revision.Empty
let model = EditPageModel(this.Context, this.WebLog, page, rev)
model.pageTitle <- match pageId with
| "new" -> Resources.AddNewPage
| _ -> Resources.EditPage
model.PageTitle <- match pageId with "new" -> Resources.AddNewPage | _ -> Resources.EditPage
upcast this.View.["admin/page/edit", model]
| None -> this.NotFound ()
@ -54,30 +52,28 @@ type PageModule(conn : IConnection, clock : IClock) as this =
let form = this.Bind<EditPageForm> ()
let now = clock.Now.Ticks
match (match pageId with
| "new" -> Some Page.empty
| _ -> tryFindPage conn this.WebLog.id pageId) with
| Some p -> let page = match pageId with
| "new" -> { p with webLogId = this.WebLog.id }
| _ -> p
| "new" -> Some Page.Empty
| _ -> tryFindPage conn this.WebLog.Id pageId) with
| Some p -> let page = match pageId with "new" -> { p with WebLogId = this.WebLog.Id } | _ -> p
let pId = { p with
title = form.title
permalink = form.permalink
publishedOn = match pageId with | "new" -> now | _ -> page.publishedOn
updatedOn = now
text = match form.source with
| RevisionSource.Markdown -> Markdown.TransformHtml form.text
| _ -> form.text
revisions = { asOf = now
sourceType = form.source
text = form.text } :: page.revisions }
Title = form.Title
Permalink = form.Permalink
PublishedOn = match pageId with "new" -> now | _ -> page.PublishedOn
UpdatedOn = now
Text = match form.Source with
| RevisionSource.Markdown -> Markdown.TransformHtml form.Text
| _ -> form.Text
Revisions = { AsOf = now
SourceType = form.Source
Text = form.Text } :: page.Revisions }
|> savePage conn
let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info
message = System.String.Format
(Resources.MsgPageEditSuccess,
(match pageId with | "new" -> Resources.Added | _ -> Resources.Updated))
details = None }
|> model.addMessage
{ UserMessage.Empty with
Level = Level.Info
Message = System.String.Format
(Resources.MsgPageEditSuccess,
(match pageId with | "new" -> Resources.Added | _ -> Resources.Updated)) }
|> model.AddMessage
this.Redirect (sprintf "/page/%s/edit" pId) model
| None -> this.NotFound ()
@ -86,12 +82,11 @@ type PageModule(conn : IConnection, clock : IClock) as this =
this.ValidateCsrfToken ()
this.RequiresAccessLevel AuthorizationLevel.Administrator
let pageId = parameters.["id"].ToString ()
match tryFindPageWithoutRevisions conn this.WebLog.id pageId with
| Some page -> deletePage conn page.webLogId page.id
match tryFindPageWithoutRevisions conn this.WebLog.Id pageId with
| Some page -> deletePage conn page.WebLogId page.Id
let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info
message = Resources.MsgPageDeleted
details = None }
|> model.addMessage
{ UserMessage.Empty with Level = Level.Info
Message = Resources.MsgPageDeleted }
|> model.AddMessage
this.Redirect "/pages" model
| None -> this.NotFound ()

View File

@ -1,10 +1,10 @@
namespace myWebLog
namespace MyWebLog
open FSharp.Markdown
open myWebLog.Data.Category
open myWebLog.Data.Page
open myWebLog.Data.Post
open myWebLog.Entities
open MyWebLog.Data.Category
open MyWebLog.Data.Page
open MyWebLog.Data.Post
open MyWebLog.Entities
open Nancy
open Nancy.ModelBinding
open Nancy.Security
@ -20,39 +20,39 @@ type PostModule(conn : IConnection, clock : IClock) as this =
/// Get the page number from the dictionary
let getPage (parameters : DynamicDictionary) =
match parameters.ContainsKey "page" with | true -> System.Int32.Parse (parameters.["page"].ToString ()) | _ -> 1
match parameters.ContainsKey "page" with true -> System.Int32.Parse (parameters.["page"].ToString ()) | _ -> 1
/// Convert a list of posts to a list of posts for display
let forDisplay posts = posts |> List.map (fun post -> PostForDisplay(this.WebLog, post))
/// Generate an RSS/Atom feed of the latest posts
let generateFeed format : obj =
let posts = findFeedPosts conn this.WebLog.id 10
let posts = findFeedPosts conn this.WebLog.Id 10
let feed =
SyndicationFeed(
this.WebLog.name, defaultArg this.WebLog.subtitle null,
Uri(sprintf "%s://%s" this.Request.Url.Scheme this.WebLog.urlBase), null,
this.WebLog.Name, defaultArg this.WebLog.Subtitle null,
Uri(sprintf "%s://%s" this.Request.Url.Scheme this.WebLog.UrlBase), null,
(match posts |> List.tryHead with
| Some (post, _) -> Instant(post.updatedOn).ToDateTimeOffset ()
| Some (post, _) -> Instant(post.UpdatedOn).ToDateTimeOffset ()
| _ -> System.DateTimeOffset(System.DateTime.MinValue)),
posts
|> List.map (fun (post, user) ->
let item =
SyndicationItem(
BaseUri = Uri(sprintf "%s://%s/%s" this.Request.Url.Scheme this.WebLog.urlBase post.permalink),
PublishDate = Instant(post.publishedOn).ToDateTimeOffset (),
LastUpdatedTime = Instant(post.updatedOn).ToDateTimeOffset (),
Title = TextSyndicationContent(post.title),
Content = TextSyndicationContent(post.text, TextSyndicationContentKind.Html))
BaseUri = Uri(sprintf "%s://%s/%s" this.Request.Url.Scheme this.WebLog.UrlBase post.Permalink),
PublishDate = Instant(post.PublishedOn).ToDateTimeOffset (),
LastUpdatedTime = Instant(post.UpdatedOn).ToDateTimeOffset (),
Title = TextSyndicationContent(post.Title),
Content = TextSyndicationContent(post.Text, TextSyndicationContentKind.Html))
user
|> Option.iter (fun u -> item.Authors.Add
(SyndicationPerson(u.userName, u.preferredName, defaultArg u.url null)))
post.categories
|> List.iter (fun c -> item.Categories.Add(SyndicationCategory(c.name)))
(SyndicationPerson(u.UserName, u.PreferredName, defaultArg u.Url null)))
post.Categories
|> List.iter (fun c -> item.Categories.Add(SyndicationCategory(c.Name)))
item))
let stream = new IO.MemoryStream()
Xml.XmlWriter.Create(stream)
|> match format with | "atom" -> feed.SaveAsAtom10 | _ -> feed.SaveAsRss20
|> match format with "atom" -> feed.SaveAsAtom10 | _ -> feed.SaveAsRss20
stream.Position <- int64 0
upcast this.Response.FromStream(stream, sprintf "application/%s+xml" format)
@ -75,77 +75,77 @@ type PostModule(conn : IConnection, clock : IClock) as this =
/// Display a page of published posts
member this.PublishedPostsPage pageNbr =
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10 |> forDisplay
model.hasNewer <- match pageNbr with
model.PageNbr <- pageNbr
model.Posts <- findPageOfPublishedPosts conn this.WebLog.Id pageNbr 10 |> forDisplay
model.HasNewer <- match pageNbr with
| 1 -> false
| _ -> match List.isEmpty model.posts with
| _ -> match List.isEmpty model.Posts with
| true -> false
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.posts).post
model.hasOlder <- match List.isEmpty model.posts with
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.Posts).Post
model.HasOlder <- match List.isEmpty model.Posts with
| true -> false
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.posts).post
model.urlPrefix <- "/posts"
model.pageTitle <- match pageNbr with
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.Posts).Post
model.UrlPrefix <- "/posts"
model.PageTitle <- match pageNbr with
| 1 -> ""
| _ -> sprintf "%s%i" Resources.PageHash pageNbr
this.ThemedView "index" model
/// Display either the newest posts or the configured home page
member this.HomePage () =
match this.WebLog.defaultPage with
match this.WebLog.DefaultPage with
| "posts" -> this.PublishedPostsPage 1
| pageId -> match tryFindPageWithoutRevisions conn this.WebLog.id pageId with
| pageId -> match tryFindPageWithoutRevisions conn this.WebLog.Id pageId with
| Some page -> let model = PageModel(this.Context, this.WebLog, page)
model.pageTitle <- page.title
model.PageTitle <- page.Title
this.ThemedView "page" model
| None -> this.NotFound ()
/// Derive a post or page from the URL, or redirect from a prior URL to the current one
member this.CatchAll (parameters : DynamicDictionary) =
let url = parameters.["permalink"].ToString ()
match tryFindPostByPermalink conn this.WebLog.id url with
match tryFindPostByPermalink conn this.WebLog.Id url with
| Some post -> // Hopefully the most common result; the permalink is a permalink!
let model = PostModel(this.Context, this.WebLog, post)
model.newerPost <- tryFindNewerPost conn post
model.olderPost <- tryFindOlderPost conn post
model.pageTitle <- post.title
model.NewerPost <- tryFindNewerPost conn post
model.OlderPost <- tryFindOlderPost conn post
model.PageTitle <- post.Title
this.ThemedView "single" model
| None -> // Maybe it's a page permalink instead...
match tryFindPageByPermalink conn this.WebLog.id url with
match tryFindPageByPermalink conn this.WebLog.Id url with
| Some page -> // ...and it is!
let model = PageModel(this.Context, this.WebLog, page)
model.pageTitle <- page.title
model.PageTitle <- page.Title
this.ThemedView "page" model
| None -> // Maybe it's an old permalink for a post
match tryFindPostByPriorPermalink conn this.WebLog.id url with
match tryFindPostByPriorPermalink conn this.WebLog.Id url with
| Some post -> // Redirect them to the proper permalink
upcast this.Response.AsRedirect(sprintf "/%s" post.permalink)
upcast this.Response.AsRedirect(sprintf "/%s" post.Permalink)
.WithStatusCode HttpStatusCode.MovedPermanently
| None -> this.NotFound ()
/// Display categorized posts
member this.CategorizedPosts (parameters : DynamicDictionary) =
let slug = parameters.["slug"].ToString ()
match tryFindCategoryBySlug conn this.WebLog.id slug with
match tryFindCategoryBySlug conn this.WebLog.Id slug with
| Some cat -> let pageNbr = getPage parameters
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfCategorizedPosts conn this.WebLog.id cat.id pageNbr 10 |> forDisplay
model.hasNewer <- match List.isEmpty model.posts with
model.PageNbr <- pageNbr
model.Posts <- findPageOfCategorizedPosts conn this.WebLog.Id cat.Id pageNbr 10 |> forDisplay
model.HasNewer <- match List.isEmpty model.Posts with
| true -> false
| _ -> Option.isSome <| tryFindNewerCategorizedPost conn cat.id
(List.head model.posts).post
model.hasOlder <- match List.isEmpty model.posts with
| _ -> Option.isSome <| tryFindNewerCategorizedPost conn cat.Id
(List.head model.Posts).Post
model.HasOlder <- match List.isEmpty model.Posts with
| true -> false
| _ -> Option.isSome <| tryFindOlderCategorizedPost conn cat.id
(List.last model.posts).post
model.urlPrefix <- sprintf "/category/%s" slug
model.pageTitle <- sprintf "\"%s\" Category%s" cat.name
| _ -> Option.isSome <| tryFindOlderCategorizedPost conn cat.Id
(List.last model.Posts).Post
model.UrlPrefix <- sprintf "/category/%s" slug
model.PageTitle <- sprintf "\"%s\" Category%s" cat.Name
(match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
model.subtitle <- Some <| match cat.description with
model.Subtitle <- Some <| match cat.Description with
| Some desc -> desc
| None -> sprintf "Posts in the \"%s\" category" cat.name
| None -> sprintf "Posts in the \"%s\" category" cat.Name
this.ThemedView "index" model
| None -> this.NotFound ()
@ -154,17 +154,17 @@ type PostModule(conn : IConnection, clock : IClock) as this =
let tag = parameters.["tag"].ToString ()
let pageNbr = getPage parameters
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfTaggedPosts conn this.WebLog.id tag pageNbr 10 |> forDisplay
model.hasNewer <- match List.isEmpty model.posts with
model.PageNbr <- pageNbr
model.Posts <- findPageOfTaggedPosts conn this.WebLog.Id tag pageNbr 10 |> forDisplay
model.HasNewer <- match List.isEmpty model.Posts with
| true -> false
| _ -> Option.isSome <| tryFindNewerTaggedPost conn tag (List.head model.posts).post
model.hasOlder <- match List.isEmpty model.posts with
| _ -> Option.isSome <| tryFindNewerTaggedPost conn tag (List.head model.Posts).Post
model.HasOlder <- match List.isEmpty model.Posts with
| true -> false
| _ -> Option.isSome <| tryFindOlderTaggedPost conn tag (List.last model.posts).post
model.urlPrefix <- sprintf "/tag/%s" tag
model.pageTitle <- sprintf "\"%s\" Tag%s" tag (match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
model.subtitle <- Some <| sprintf "Posts tagged \"%s\"" tag
| _ -> Option.isSome <| tryFindOlderTaggedPost conn tag (List.last model.Posts).Post
model.UrlPrefix <- sprintf "/tag/%s" tag
model.PageTitle <- sprintf "\"%s\" Tag%s" tag (match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
model.Subtitle <- Some <| sprintf "Posts tagged \"%s\"" tag
this.ThemedView "index" model
/// Generate an RSS feed
@ -183,35 +183,33 @@ type PostModule(conn : IConnection, clock : IClock) as this =
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 |> forDisplay
model.hasNewer <- pageNbr > 1
model.hasOlder <- List.length model.posts > 24
model.urlPrefix <- "/posts/list"
model.pageTitle <- Resources.Posts
model.PageNbr <- pageNbr
model.Posts <- findPageOfAllPosts conn this.WebLog.Id pageNbr 25 |> forDisplay
model.HasNewer <- pageNbr > 1
model.HasOlder <- List.length model.Posts > 24
model.UrlPrefix <- "/posts/list"
model.PageTitle <- Resources.Posts
upcast this.View.["admin/post/list", model]
/// Edit a post
member this.EditPost (parameters : DynamicDictionary) =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let postId : string = downcast parameters.["postId"]
let postId = parameters.["postId"].ToString ()
match (match postId with
| "new" -> Some Post.empty
| _ -> tryFindPost conn this.WebLog.id postId) with
| Some post -> let rev = match post.revisions
|> List.sortByDescending (fun r -> r.asOf)
| "new" -> Some Post.Empty
| _ -> tryFindPost conn this.WebLog.Id postId) with
| Some post -> let rev = match post.Revisions
|> List.sortByDescending (fun r -> r.AsOf)
|> List.tryHead with
| Some r -> r
| None -> Revision.empty
| None -> Revision.Empty
let model = EditPostModel(this.Context, this.WebLog, post, rev)
model.categories <- getAllCategories conn this.WebLog.id
|> List.map (fun cat -> string (fst cat).id,
model.Categories <- getAllCategories conn this.WebLog.Id
|> List.map (fun cat -> string (fst cat).Id,
sprintf "%s%s"
(String.replicate (snd cat) " &nbsp; &nbsp; ")
(fst cat).name)
model.pageTitle <- match post.id with
| "new" -> Resources.AddNewPost
| _ -> Resources.EditPost
(fst cat).Name)
model.PageTitle <- match post.Id with "new" -> Resources.AddNewPost | _ -> Resources.EditPost
upcast this.View.["admin/post/edit"]
| None -> this.NotFound ()
@ -219,45 +217,45 @@ type PostModule(conn : IConnection, clock : IClock) as this =
member this.SavePost (parameters : DynamicDictionary) =
this.RequiresAccessLevel AuthorizationLevel.Administrator
this.ValidateCsrfToken ()
let postId : string = downcast parameters.["postId"]
let form = this.Bind<EditPostForm>()
let now = clock.Now.Ticks
let postId = parameters.["postId"].ToString ()
let form = this.Bind<EditPostForm>()
let now = clock.Now.Ticks
match (match postId with
| "new" -> Some Post.empty
| _ -> tryFindPost conn this.WebLog.id postId) with
| Some p -> let justPublished = p.publishedOn = int64 0 && form.publishNow
| "new" -> Some Post.Empty
| _ -> tryFindPost conn this.WebLog.Id postId) with
| Some p -> let justPublished = p.PublishedOn = int64 0 && form.PublishNow
let post = match postId with
| "new" -> { p with
webLogId = this.WebLog.id
authorId = (this.Request.PersistableSession.GetOrDefault<User>
(Keys.User, User.empty)).id }
| _ -> p
WebLogId = this.WebLog.Id
AuthorId = (this.Request.PersistableSession.GetOrDefault<User>
(Keys.User, User.Empty)).Id }
| _ -> p
let pId = { post with
status = match form.publishNow with
Status = match form.PublishNow with
| true -> PostStatus.Published
| _ -> PostStatus.Draft
title = form.title
permalink = form.permalink
publishedOn = match justPublished with | true -> now | _ -> int64 0
updatedOn = now
text = match form.source with
| RevisionSource.Markdown -> Markdown.TransformHtml form.text
| _ -> form.text
categoryIds = Array.toList form.categories
tags = form.tags.Split ','
| _ -> PostStatus.Draft
Title = form.Title
Permalink = form.Permalink
PublishedOn = match justPublished with true -> now | _ -> int64 0
UpdatedOn = now
Text = match form.Source with
| RevisionSource.Markdown -> Markdown.TransformHtml form.Text
| _ -> form.Text
CategoryIds = Array.toList form.Categories
Tags = form.Tags.Split ','
|> Seq.map (fun t -> t.Trim().ToLowerInvariant())
|> Seq.toList
revisions = { asOf = now
sourceType = form.source
text = form.text } :: post.revisions }
Revisions = { AsOf = now
SourceType = form.Source
Text = form.Text } :: post.Revisions }
|> savePost conn
let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info
message = System.String.Format
(Resources.MsgPostEditSuccess,
(match postId with | "new" -> Resources.Added | _ -> Resources.Updated),
(match justPublished with | true -> Resources.AndPublished | _ -> ""))
details = None }
|> model.addMessage
{ UserMessage.Empty with
Level = Level.Info
Message = System.String.Format
(Resources.MsgPostEditSuccess,
(match postId with | "new" -> Resources.Added | _ -> Resources.Updated),
(match justPublished with | true -> Resources.AndPublished | _ -> "")) }
|> model.AddMessage
this.Redirect (sprintf "/post/%s/edit" pId) model
| None -> this.NotFound ()

View File

@ -1,7 +1,7 @@
namespace myWebLog
namespace MyWebLog
open myWebLog.Data.User
open myWebLog.Entities
open MyWebLog.Data.User
open MyWebLog.Entities
open Nancy
open Nancy.Authentication.Forms
open Nancy.Cryptography
@ -21,15 +21,16 @@ type UserModule(conn : IConnection) as this =
|> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) ""
do
this.Get .["/logon" ] <- fun parms -> this.ShowLogOn (downcast parms)
this.Get .["/logon" ] <- fun _ -> this.ShowLogOn ()
this.Post.["/logon" ] <- fun parms -> this.DoLogOn (downcast parms)
this.Get .["/logoff"] <- fun parms -> this.LogOff ()
this.Get .["/logoff"] <- fun _ -> this.LogOff ()
/// Show the log on page
member this.ShowLogOn (parameters : DynamicDictionary) =
member this.ShowLogOn () =
let model = LogOnModel(this.Context, this.WebLog)
model.form.returnUrl <- match parameters.ContainsKey "returnUrl" with
| true -> parameters.["returnUrl"].ToString ()
let query = this.Request.Query :?> DynamicDictionary
model.Form.ReturnUrl <- match query.ContainsKey "returnUrl" with
| true -> query.["returnUrl"].ToString ()
| _ -> ""
upcast this.View.["admin/user/logon", model]
@ -38,30 +39,28 @@ type UserModule(conn : IConnection) as this =
this.ValidateCsrfToken ()
let form = this.Bind<LogOnForm> ()
let model = MyWebLogModel(this.Context, this.WebLog)
match tryUserLogOn conn form.email (pbkdf2 form.password) with
match tryUserLogOn conn form.Email (pbkdf2 form.Password) with
| Some user -> this.Session.[Keys.User] <- user
{ level = Level.Info
message = Resources.MsgLogOnSuccess
details = None }
|> model.addMessage
{ UserMessage.Empty with Level = Level.Info
Message = Resources.MsgLogOnSuccess }
|> model.AddMessage
this.Redirect "" model |> ignore // Save the messages in the session before the Nancy redirect
// TODO: investigate if addMessage should update the session when it's called
upcast this.LoginAndRedirect (System.Guid.Parse user.id,
fallbackRedirectUrl = defaultArg (Option.ofObj(form.returnUrl)) "/")
| None -> { level = Level.Error
message = Resources.ErrBadLogOnAttempt
details = None }
|> model.addMessage
this.Redirect (sprintf "/user/logon?returnUrl=%s" form.returnUrl) model
upcast this.LoginAndRedirect (System.Guid.Parse user.Id,
fallbackRedirectUrl = defaultArg (Option.ofObj form.ReturnUrl) "/")
| None -> { UserMessage.Empty with Level = Level.Error
Message = Resources.ErrBadLogOnAttempt }
|> model.AddMessage
this.Redirect (sprintf "/user/logon?returnUrl=%s" form.ReturnUrl) model
/// Log a user off
member this.LogOff () =
let user = this.Request.PersistableSession.GetOrDefault<User> (Keys.User, User.empty)
// FIXME: why are we getting the user here if we don't do anything with it?
let user = this.Request.PersistableSession.GetOrDefault<User> (Keys.User, User.Empty)
this.Session.DeleteAll ()
let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info
message = Resources.MsgLogOffSuccess
details = None }
|> model.addMessage
{ UserMessage.Empty with Level = Level.Info
Message = Resources.MsgLogOffSuccess }
|> model.AddMessage
this.Redirect "" model |> ignore
upcast this.LogoutAndRedirect "/"

View File

@ -1,7 +1,7 @@
namespace myWebLog
namespace MyWebLog
open myWebLog.Data.WebLog
open myWebLog.Entities
open MyWebLog.Data.WebLog
open MyWebLog.Entities
open Nancy
open Nancy.Session.Persistable
open Newtonsoft.Json
@ -21,25 +21,23 @@ module Level =
/// 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
}
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
}
static member Empty =
{ Level = Level.Info
Message = ""
Details = None }
/// Display version
[<JsonIgnore>]
member this.toDisplay =
member this.ToDisplay =
let classAndLabel =
dict [
Level.Error, ("danger", Resources.Error)
@ -48,23 +46,23 @@ with
]
seq {
yield "<div class=\"alert alert-dismissable alert-"
yield fst classAndLabel.[this.level]
yield fst classAndLabel.[this.Level]
yield "\" role=\"alert\"><button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\""
yield Resources.Close
yield "\">&times;</button><strong>"
match snd classAndLabel.[this.level] with
match snd classAndLabel.[this.Level] with
| "" -> ()
| lbl -> yield lbl.ToUpper ()
yield " &#xbb; "
yield this.message
yield this.Message
yield "</strong>"
match this.details with
match this.Details with
| Some d -> yield "<br />"
yield d
| None -> ()
yield "</div>"
}
|> Seq.reduce (fun acc x -> acc + x)
|> Seq.reduce (+)
/// Helpers to format local date/time using NodaTime
@ -94,58 +92,58 @@ 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)
let msg = ctx.Request.PersistableSession.GetOrDefault<UserMessage list>(Keys.Messages, [])
match List.length msg with
| 0 -> ()
| _ -> ctx.Request.Session.Delete Keys.Messages
msg
/// The web log for this request
member this.webLog = webLog
member this.WebLog = webLog
/// The subtitle for the webLog (SSVE can't do IsSome that deep)
member this.webLogSubtitle = defaultArg this.webLog.subtitle ""
member this.WebLogSubtitle = defaultArg this.WebLog.Subtitle ""
/// User messages
member val messages = getMessages () with get, set
member val Messages = getMessages () with get, set
/// The currently logged in user
member this.user = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.empty)
member this.User = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.Empty)
/// The title of the page
member val pageTitle = "" with get, set
member val PageTitle = "" with get, set
/// The name and version of the application
member this.generator = sprintf "myWebLog %s" (ctx.Items.[Keys.Version].ToString ())
member this.Generator = sprintf "myWebLog %s" (ctx.Items.[Keys.Version].ToString ())
/// The request start time
member this.requestStart = ctx.Items.[Keys.RequestStart] :?> int64
member this.RequestStart = ctx.Items.[Keys.RequestStart] :?> int64
/// Is a user authenticated for this request?
member this.isAuthenticated = "" <> this.user.id
member this.IsAuthenticated = "" <> this.User.Id
/// Add a message to the output
member this.addMessage message = this.messages <- message :: this.messages
member this.AddMessage message = this.Messages <- message :: this.Messages
/// Display a long date
member this.displayLongDate ticks = FormatDateTime.longDate this.webLog.timeZone ticks
member this.DisplayLongDate ticks = FormatDateTime.longDate this.WebLog.TimeZone ticks
/// Display a short date
member this.displayShortDate ticks = FormatDateTime.shortDate this.webLog.timeZone ticks
member this.DisplayShortDate ticks = FormatDateTime.shortDate this.WebLog.TimeZone ticks
/// Display the time
member this.displayTime ticks = FormatDateTime.time this.webLog.timeZone ticks
member this.DisplayTime ticks = FormatDateTime.time this.WebLog.TimeZone ticks
/// The page title with the web log name appended
member this.displayPageTitle =
match this.pageTitle with
| "" -> match this.webLog.subtitle with
| Some st -> sprintf "%s | %s" this.webLog.name st
| None -> this.webLog.name
| pt -> sprintf "%s | %s" pt this.webLog.name
member this.DisplayPageTitle =
match this.PageTitle with
| "" -> match this.WebLog.Subtitle with
| Some st -> sprintf "%s | %s" this.WebLog.Name st
| None -> this.WebLog.Name
| pt -> sprintf "%s | %s" pt this.WebLog.Name
/// An image with the version and load time in the tool tip
member this.footerLogo =
member this.FooterLogo =
seq {
yield "<img src=\"/default/footer-logo.png\" alt=\"myWebLog\" title=\""
yield sprintf "%s %s &bull; " Resources.PoweredBy this.generator
yield sprintf "%s %s &bull; " Resources.PoweredBy this.Generator
yield Resources.LoadedIn
yield " "
yield TimeSpan(System.DateTime.Now.Ticks - this.requestStart).TotalSeconds.ToString "f3"
yield TimeSpan(System.DateTime.Now.Ticks - this.RequestStart).TotalSeconds.ToString "f3"
yield " "
yield Resources.Seconds.ToLower ()
yield "\" />"
}
|> Seq.reduce (fun acc x -> acc + x)
|> Seq.reduce (+)
// ---- Admin models ----
@ -154,68 +152,67 @@ type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
type DashboardModel(ctx, webLog, counts : DashboardCounts) =
inherit MyWebLogModel(ctx, webLog)
/// The number of posts for the current web log
member val posts = counts.posts with get, set
member val Posts = counts.Posts with get, set
/// The number of pages for the current web log
member val pages = counts.pages with get, set
member val Pages = counts.Pages with get, set
/// The number of categories for the current web log
member val categories = counts.categories with get, set
member val Categories = counts.Categories with get, set
// ---- Category models ----
type IndentedCategory = {
category : Category
indent : int
selected : bool
}
type IndentedCategory =
{ Category : Category
Indent : int
Selected : bool }
with
/// Create an indented category
static member create (cat : Category * int) (isSelected : string -> bool) =
{ category = fst cat
indent = snd cat
selected = isSelected (fst cat).id }
static member Create (cat : Category * int) (isSelected : string -> bool) =
{ Category = fst cat
Indent = snd cat
Selected = isSelected (fst cat).Id }
/// Display name for a category on the list page, complete with indents
member this.listName = sprintf "%s%s" (String.replicate this.indent " &#xbb; &nbsp; ") this.category.name
member this.ListName = sprintf "%s%s" (String.replicate this.Indent " &#xbb; &nbsp; ") this.Category.Name
/// Display for this category as an option within a select box
member this.option =
member this.Option =
seq {
yield sprintf "<option value=\"%s\"" this.category.id
yield (match this.selected with | true -> """ selected="selected">""" | _ -> ">")
yield String.replicate this.indent " &nbsp; &nbsp; "
yield this.category.name
yield sprintf "<option value=\"%s\"" this.Category.Id
yield (match this.Selected with | true -> """ selected="selected">""" | _ -> ">")
yield String.replicate this.Indent " &nbsp; &nbsp; "
yield this.Category.Name
yield "</option>"
}
|> String.concat ""
/// Does the category have a description?
member this.hasDescription = this.category.description.IsSome
member this.HasDescription = this.Category.Description.IsSome
/// Model for the list of categories
type CategoryListModel(ctx, webLog, categories) =
inherit MyWebLogModel(ctx, webLog)
/// The categories
member this.categories : IndentedCategory list = categories
member this.Categories : IndentedCategory list = categories
/// Form for editing a category
type CategoryForm(category : Category) =
new() = CategoryForm(Category.empty)
/// The name of the category
member val name = category.name with get, set
member val Name = category.Name with get, set
/// The slug of the category (used in category URLs)
member val slug = category.slug with get, set
member val Slug = category.Slug with get, set
/// The description of the category
member val description = defaultArg category.description "" with get, set
member val Description = defaultArg category.Description "" with get, set
/// The parent category for this one
member val parentId = defaultArg category.parentId "" with get, set
member val ParentId = defaultArg category.ParentId "" with get, set
/// Model for editing a category
type CategoryEditModel(ctx, webLog, category) =
inherit MyWebLogModel(ctx, webLog)
/// The form with the category information
member val form = CategoryForm(category) with get, set
member val Form = CategoryForm(category) with get, set
/// The categories
member val categories : IndentedCategory list = List.empty with get, set
member val Categories : IndentedCategory list = [] with get, set
// ---- Page models ----
@ -223,54 +220,53 @@ type CategoryEditModel(ctx, webLog, category) =
/// Model for page display
type PageModel(ctx, webLog, page) =
inherit MyWebLogModel(ctx, webLog)
/// The page to be displayed
member this.page : Page = page
member this.Page : Page = page
/// Wrapper for a page with additional properties
type PageForDisplay(webLog, page) =
/// The page
member this.page : Page = page
member this.Page : Page = page
/// The time zone of the web log
member this.timeZone = webLog.timeZone
member this.TimeZone = webLog.TimeZone
/// The date the page was last updated
member this.updatedDate = FormatDateTime.longDate this.timeZone page.updatedOn
member this.UpdatedDate = FormatDateTime.longDate this.TimeZone page.UpdatedOn
/// The time the page was last updated
member this.updatedTime = FormatDateTime.time this.timeZone page.updatedOn
member this.UpdatedTime = FormatDateTime.time this.TimeZone page.UpdatedOn
/// Model for page list display
type PagesModel(ctx, webLog, pages) =
inherit MyWebLogModel(ctx, webLog)
/// The pages
member this.pages : PageForDisplay list = pages
member this.Pages : PageForDisplay list = pages
/// Form used to edit a page
type EditPageForm() =
/// The title of the page
member val title = "" with get, set
member val Title = "" with get, set
/// The link for the page
member val permalink = "" with get, set
member val Permalink = "" with get, set
/// The source type of the revision
member val source = "" with get, set
member val Source = "" with get, set
/// The text of the revision
member val text = "" with get, set
member val Text = "" with get, set
/// Whether to show the page in the web log's page list
member val showInPageList = false with get, set
member val ShowInPageList = false with get, set
/// Fill the form with applicable values from a page
member this.forPage (page : Page) =
this.title <- page.title
this.permalink <- page.permalink
this.showInPageList <- page.showInPageList
member this.ForPage (page : Page) =
this.Title <- page.Title
this.Permalink <- page.Permalink
this.ShowInPageList <- page.ShowInPageList
this
/// Fill the form with applicable values from a revision
member this.forRevision rev =
this.source <- rev.sourceType
this.text <- rev.text
member this.ForRevision rev =
this.Source <- rev.SourceType
this.Text <- rev.Text
this
@ -278,21 +274,21 @@ type EditPageForm() =
type EditPageModel(ctx, webLog, page, revision) =
inherit MyWebLogModel(ctx, webLog)
/// The page edit form
member val form = EditPageForm().forPage(page).forRevision(revision)
member val Form = EditPageForm().ForPage(page).ForRevision(revision)
/// The page itself
member this.page = page
member this.Page = page
/// The page's published date
member this.publishedDate = this.displayLongDate page.publishedOn
member this.PublishedDate = this.DisplayLongDate page.PublishedOn
/// The page's published time
member this.publishedTime = this.displayTime page.publishedOn
member this.PublishedTime = this.DisplayTime page.PublishedOn
/// The page's last updated date
member this.lastUpdatedDate = this.displayLongDate page.updatedOn
member this.LastUpdatedDate = this.DisplayLongDate page.UpdatedOn
/// The page's last updated time
member this.lastUpdatedTime = this.displayTime page.updatedOn
member this.LastUpdatedTime = this.DisplayTime page.UpdatedOn
/// Is this a new page?
member this.isNew = "new" = page.id
member this.IsNew = "new" = page.Id
/// Generate a checked attribute if this page shows in the page list
member this.pageListChecked = match page.showInPageList with | true -> "checked=\"checked\"" | _ -> ""
member this.PageListChecked = match page.ShowInPageList with true -> "checked=\"checked\"" | _ -> ""
// ---- Post models ----
@ -301,102 +297,103 @@ type EditPageModel(ctx, webLog, page, revision) =
type PostModel(ctx, webLog, post) =
inherit MyWebLogModel(ctx, webLog)
/// The post being displayed
member this.post : Post = post
member this.Post : Post = post
/// The next newer post
member val newerPost = Option<Post>.None with get, set
member val NewerPost = Option<Post>.None with get, set
/// The next older post
member val olderPost = Option<Post>.None with get, set
member val OlderPost = Option<Post>.None with get, set
/// The date the post was published
member this.publishedDate = this.displayLongDate this.post.publishedOn
member this.PublishedDate = this.DisplayLongDate this.Post.PublishedOn
/// The time the post was published
member this.publishedTime = this.displayTime this.post.publishedOn
member this.PublishedTime = this.DisplayTime this.Post.PublishedOn
/// Does the post have tags?
member this.hasTags = List.length post.tags > 0
member this.HasTags = not (List.isEmpty post.Tags)
/// Get the tags sorted
member this.tags = post.tags
member this.Tags = post.Tags
|> List.sort
|> List.map (fun tag -> tag, tag.Replace(' ', '+'))
/// Does this post have a newer post?
member this.hasNewer = this.newerPost.IsSome
member this.HasNewer = this.NewerPost.IsSome
/// Does this post have an older post?
member this.hasOlder = this.olderPost.IsSome
member this.HasOlder = this.OlderPost.IsSome
/// Wrapper for a post with additional properties
type PostForDisplay(webLog : WebLog, post : Post) =
/// Turn tags into a pipe-delimited string of tags
let pipedTags tags = tags |> List.reduce (fun acc x -> sprintf "%s | %s" acc x)
/// The actual post
member this.post = post
member this.Post = post
/// The time zone for the web log to which this post belongs
member this.timeZone = webLog.timeZone
member this.TimeZone = webLog.TimeZone
/// The date the post was published
member this.publishedDate = FormatDateTime.longDate this.timeZone this.post.publishedOn
member this.PublishedDate = FormatDateTime.longDate this.TimeZone this.Post.PublishedOn
/// The time the post was published
member this.publishedTime = FormatDateTime.time this.timeZone this.post.publishedOn
member this.PublishedTime = FormatDateTime.time this.TimeZone this.Post.PublishedOn
/// Tags
member this.tags =
match List.length this.post.tags with
| 0 -> ""
| 1 | 2 | 3 | 4 | 5 -> this.post.tags |> pipedTags
| count -> sprintf "%s %s" (this.post.tags |> List.take 3 |> pipedTags)
(System.String.Format(Resources.andXMore, count - 3))
member this.Tags =
match List.length this.Post.Tags with
| 0 -> ""
| 1 | 2 | 3 | 4 | 5 -> this.Post.Tags |> pipedTags
| count -> sprintf "%s %s" (this.Post.Tags |> List.take 3 |> pipedTags)
(System.String.Format(Resources.andXMore, count - 3))
/// Model for all page-of-posts pages
type PostsModel(ctx, webLog) =
inherit MyWebLogModel(ctx, webLog)
/// The subtitle for the page
member val subtitle = Option<string>.None with get, set
member val Subtitle = Option<string>.None with get, set
/// The posts to display
member val posts = List.empty<PostForDisplay> with get, set
member val Posts : PostForDisplay list = [] with get, set
/// The page number of the post list
member val pageNbr = 0 with get, set
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
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
member val HasOlder = true with get, set
/// The prefix for the next/prior links
member val urlPrefix = "" with get, set
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
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)
| _ -> 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)
member this.OlderLink = sprintf "%s/page/%i" this.UrlPrefix (this.PageNbr + 1)
/// Form for editing a post
type EditPostForm() =
/// The title of the post
member val title = "" with get, set
member val Title = "" with get, set
/// The permalink for the post
member val permalink = "" with get, set
member val Permalink = "" with get, set
/// The source type for this revision
member val source = "" with get, set
member val Source = "" with get, set
/// The text
member val text = "" with get, set
member val Text = "" with get, set
/// Tags for the post
member val tags = "" with get, set
member val Tags = "" with get, set
/// The selected category Ids for the post
member val categories = Array.empty<string> with get, set
member val Categories : string[] = [||] with get, set
/// Whether the post should be published
member val publishNow = true with get, set
member val PublishNow = true with get, set
/// Fill the form with applicable values from a post
member this.forPost post =
this.title <- post.title
this.permalink <- post.permalink
this.tags <- List.reduce (fun acc x -> sprintf "%s, %s" acc x) post.tags
this.categories <- List.toArray post.categoryIds
member this.ForPost post =
this.Title <- post.Title
this.Permalink <- post.Permalink
this.Tags <- List.reduce (fun acc x -> sprintf "%s, %s" acc x) post.Tags
this.Categories <- List.toArray post.CategoryIds
this
/// Fill the form with applicable values from a revision
member this.forRevision rev =
this.source <- rev.sourceType
this.text <- rev.text
member this.ForRevision rev =
this.Source <- rev.SourceType
this.Text <- rev.Text
this
/// View model for the edit post page
@ -404,17 +401,17 @@ type EditPostModel(ctx, webLog, post, revision) =
inherit MyWebLogModel(ctx, webLog)
/// The form
member val form = EditPostForm().forPost(post).forRevision(revision) with get, set
member val Form = EditPostForm().ForPost(post).ForRevision(revision) with get, set
/// The post being edited
member val post = post with get, set
member val Post = post with get, set
/// The categories to which the post may be assigned
member val categories = List.empty<string * string> with get, set
member val Categories : (string * string) list = [] with get, set
/// Whether the post is currently published
member this.isPublished = PostStatus.Published = this.post.status
member this.IsPublished = PostStatus.Published = this.Post.Status
/// The published date
member this.publishedDate = this.displayLongDate this.post.publishedOn
member this.PublishedDate = this.DisplayLongDate this.Post.PublishedOn
/// The published time
member this.publishedTime = this.displayTime this.post.publishedOn
member this.PublishedTime = this.DisplayTime this.Post.PublishedOn
// ---- User models ----
@ -422,15 +419,15 @@ type EditPostModel(ctx, webLog, post, revision) =
/// Form for the log on page
type LogOnForm() =
/// The URL to which the user will be directed upon successful log on
member val returnUrl = "" with get, set
member val ReturnUrl = "" with get, set
/// The e-mail address
member val email = "" with get, set
member val Email = "" with get, set
/// The user's passwor
member val password = "" with get, set
member val Password = "" with get, set
/// Model to support the user log on page
type LogOnModel(ctx, webLog) =
inherit MyWebLogModel(ctx, webLog)
/// The log on form
member val form = LogOnForm() with get, set
member val Form = LogOnForm() with get, set

View File

@ -8,7 +8,7 @@
<ProjectGuid>e6ee110a-27a6-4a19-b0cb-d24f48f71b53</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>myWebLog.Web</RootNamespace>
<AssemblyName>myWebLog.Web</AssemblyName>
<AssemblyName>MyWebLog.Web</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>

View File

@ -1,10 +1,10 @@
namespace myWebLog
namespace MyWebLog
{
class Program
{
static void Main(string[] args)
{
App.run();
App.Run();
}
}
}

View File

@ -1,11 +1,11 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("myWebLog")]
[assembly: AssemblyTitle("MyWebLog")]
[assembly: AssemblyDescription("A lightweight blogging platform built on Suave, Nancy, and RethinkDB")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("myWebLog")]
[assembly: AssemblyProduct("MyWebLog")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

View File

@ -7,8 +7,8 @@
<ProjectGuid>{B9F6DB52-65A1-4C2A-8C97-739E08A1D4FB}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>myWebLog</RootNamespace>
<AssemblyName>myWebLog</AssemblyName>
<RootNamespace>MyWebLog</RootNamespace>
<AssemblyName>MyWebLog</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
@ -51,7 +51,6 @@
<Content Include="data-config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="views\admin\message.html" />
<Content Include="views\themes\default\content\bootstrap-theme.css.map">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@Model.pageTitle | @Translate.Admin | @Model.webLog.name</title>
<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" />
@ -14,17 +14,17 @@
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">@Model.webLog.name</a>
<a class="navbar-brand" href="/">@Model.WebLog.Name</a>
</div>
<div class="navbar-left">
<p class="navbar-text">@Model.pageTitle</p>
<p class="navbar-text">@Model.PageTitle</p>
</div>
<ul class="nav navbar-nav navbar-right">
@If.isAuthenticated
@If.IsAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li>
@EndIf
@IfNot.isAuthenticated
@IfNot.IsAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li>
@EndIf
</ul>
@ -32,15 +32,15 @@
</nav>
</header>
<div class="container">
@Each.messages
@Current.toDisplay
@Each.Messages
@Current.ToDisplay
@EndEach
@Section['Content'];
</div>
<footer>
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 text-right">@Model.generator</div>
<div class="col-xs-12 text-right">@Model.Generator</div>
</div>
</div>
</footer>

View File

@ -1,34 +1,34 @@
@Master['admin/admin-layout']
@Section['Content']
<form action="/category/@Model.category.id/edit" method="post">
<form action="/category/@Model.Category.Id/edit" method="post">
@AntiForgeryToken
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<label class="control-label" for="name">@Translate.Name</label>
<input type="text" class="form-control" id="name" name="name" value="@Model.form.name" />
<label class="control-label" for="Name">@Translate.Name</label>
<input type="text" class="form-control" id="Name" name="Name" value="@Model.Form.Name" />
</div>
</div>
</div>
<div class="row">
<div class="col-xs-8">
<div class="form-group">
<label class="control-label" for="slug">@Translate.Slug</label>
<input type="text" class="form-control" id="slug" name="slug" value="@Model.form.slug}" />
<label class="control-label" for="Slug">@Translate.Slug</label>
<input type="text" class="form-control" id="Slug" name="Slug" value="@Model.Form.Slug" />
</div>
<div class="form-group">
<label class="control-label" for="description">@Translate.Description</label>
<textarea class="form-control" rows="4" id="description" name="description">@Model.form.description</textarea>
<label class="control-label" for="Description">@Translate.Description</label>
<textarea class="form-control" rows="4" id="Description" name="Description">@Model.Form.Description</textarea>
</div>
</div>
<div class="col-xs-4">
<div class="form-group">
<label class="control-label" for="parentId">@Translate.ParentCategory</label>
<select class="form-control" id="parentId" name="parentId">
<label class="control-label" for="ParentId">@Translate.ParentCategory</label>
<select class="form-control" id="ParentId" name="ParentId">
<option value="">&mdash; @Translate.NoParent &mdash;</option>
@Each.categories
@Current.option
@Each.Categories
@Current.Option
@EndEach
</select>
</div>
@ -46,7 +46,7 @@
@Section['Scripts']
<script type="text/javascript">
/* <![CDATA[ */
$(document).ready(function () { $("#name").focus() })
$(document).ready(function () { $("#Name").focus() })
/* ]] */
</script>
@EndSection

View File

@ -11,20 +11,20 @@
<th>@Translate.Category</th>
<th>@Translate.Description</th>
</tr>
@Each.categories
@Each.Categories
<tr>
<td>
<a href="/category/@Current.category.id/edit">@Translate.Edit</a> &nbsp;
<a href="javascript:void(0)" onclick="deleteCategory('@Current.category.id', '@Current.category.name')">
<a href="/category/@Current.Category.Id/edit">@Translate.Edit</a> &nbsp;
<a href="javascript:void(0)" onclick="deleteCategory('@Current.Category.Id', '@Current.Category.Name')">
@Translate.Delete
</a>
</td>
<td>@Current.listName</td>
<td>@Current.ListName</td>
<td>
@If.hasDescription
@Current.category.description.Value
@If.HasDescription
@Current.Category.Description.Value
@EndIf
@IfNot.hasDescription
@IfNot.HasDescription
&nbsp;
@EndIf
</td>

View File

@ -3,7 +3,7 @@
@Section['Content']
<div class="row">
<div class="col-xs-6">
<h3>@Translate.Posts &nbsp;<span class="badge">@Model.posts</span></h3>
<h3>@Translate.Posts &nbsp;<span class="badge">@Model.Posts</span></h3>
<p>
<a href="/posts/list"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
&nbsp; &nbsp;
@ -11,7 +11,7 @@
</p>
</div>
<div class="col-xs-6">
<h3>@Translate.Pages &nbsp;<span class="badge">@Model.pages</span></h3>
<h3>@Translate.Pages &nbsp;<span class="badge">@Model.Pages</span></h3>
<p>
<a href="/pages"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
&nbsp; &nbsp;
@ -21,7 +21,7 @@
</div>
<div class="row">
<div class="col-xs-6">
<h3>@Translate.Categories &nbsp;<span class="badge">@Model.categories</span></h3>
<h3>@Translate.Categories &nbsp;<span class="badge">@Model.Categories</span></h3>
<p>
<a href="/categories"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
&nbsp; &nbsp;

View File

@ -1,18 +0,0 @@
if session && 0 < (session.messages || []).length
while 0 < session.messages.length
- var message = session.messages.shift()
<div class="alert alert-dismissable alert-@Model.level" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="@Translate.Close">&times;</button>
<strong>
if 'danger' == message.type
=__("Error").toUpperCase()
| &nbsp;&#xbb;
else if 'warning' == message.type
=__("Warning").toUpperCase()
| &nbsp;&#xbb;
!= message.text
</strong>
if message.detail
br
!= message.detail
</div>

View File

@ -1,22 +1,22 @@
@Master['admin/admin-layout']
@Section['Content']
<form action="/page/@Model.page.id/edit" method="post">
<form action="/page/@Model.Page.Id/edit" method="post">
@AntiForgeryToken
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<label class="control-label" for="title">@Translate.Title</label>
<input type="text" name="title" id="title" class="form-control" value="@Model.form.title" />
<label class="control-label" for="Title">@Translate.Title</label>
<input type="text" name="Title" id="Title" class="form-control" value="@Model.Form.Title" />
</div>
<div class="form-group">
<label class="control-label" for="permalink">@Translate.Permalink</label>
<input type="text" name="permalink" id="permalink" class="form-control" value="@Model.form.permalink" />
<label class="control-label" for="Permalink">@Translate.Permalink</label>
<input type="text" name="Permalink" id="Permalink" class="form-control" value="@Model.Form.Permalink" />
<p class="form-hint"><em>@Translate.startingWith</em> http://@Model.webLog.urlBase/ </p>
</div>
<!-- // TODO: Markdown / HTML choice -->
<div class="form-group">
<textarea name="text" id="text" rows="15" class="form-control">@Model.form.text</textarea>
<textarea name="Text" id="Text" rows="15" class="form-control">@Model.Form.Text</textarea>
</div>
</div>
<div class="col-sm-3">
@ -26,16 +26,16 @@
@IfNot.isNew
<div class="form-group">
<label class="control-label">@Translate.PublishedDate</label>
<p class="static-control">@Model.publishedDate<br />@Model.publishedTime</p>
<p class="static-control">@Model.PublishedDate<br />@Model.PublishedTime</p>
</div>
<div class="form-group">
<label class="control-label">@Translate.LastUpdatedDate</label>
<p class="static-control">@Model.lastUpdatedDate<br />@Model.lastUpdatedTime</p>
<p class="static-control">@Model.LastUpdatedDate<br />@Model.LastUpdatedTime</p>
</div>
@EndIf
<div class="form-group">
<input type="checkbox" name="showInPageList" id="showInPageList" @Model.pageListChecked />
&nbsp; <label for="showInPageList">@Translate.ShowInPageList</label>
<input type="checkbox" name="ShowInPageList" id="ShowInPageList" @Model.PageListChecked />
&nbsp; <label for="ShowInPageList">@Translate.ShowInPageList</label>
</div>
</div>
</div>
@ -51,7 +51,7 @@
<script type="text/javascript" src="/content/scripts/tinymce-init.js"></script>
<script type="text/javascript">
/* <![CDATA[ */
$(document).ready(function () { $("#title").focus() })
$(document).ready(function () { $("#Title").focus() })
/* ]]> */
</script>
@EndSection

View File

@ -10,15 +10,15 @@
<th>@Translate.Title</th>
<th>@Translate.LastUpdated</th>
</tr>
@Each.pages
@Each.Pages
<tr>
<td>
@Current.page.title<br />
<a href="/@Current.page.permalink">@Translate.View</a> &nbsp;
<a href="/page/@Current.page.id/edit">@Translate.Edit</a> &nbsp;
<a href="javascript:void(0)" onclick="deletePage('@Current.page.id', '@Current.title')">@Translate.Delete</a>
@Current.Page.Title<br />
<a href="/@Current.Page.Permalink">@Translate.View</a> &nbsp;
<a href="/page/@Current.Page.Id/edit">@Translate.Edit</a> &nbsp;
<a href="javascript:void(0)" onclick="deletePage('@Current.Page.Id', '@Current.Page.Title')">@Translate.Delete</a>
</td>
<td>@Current.updatedDate<br />@Translate.at @Current.updatedTime</td>
<td>@Current.UpdatedDate<br />@Translate.at @Current.UpdatedTime</td>
</tr>
@EndEach
</table>

View File

@ -1,27 +1,27 @@
@Master['admin/admin-layout']
@Section['Content']
<form action='/post/@Model.post.id/edit' method="post">
<form action='/post/@Model.Post.Id/edit' method="post">
@AntiForgeryToken
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<label class="control-label" for="title">@Translate.Title</label>
<input type="text" name="title" id="title" class="form-control" value="@Model.form.title" />
<label class="control-label" for="Title">@Translate.Title</label>
<input type="text" name="Title" id="Title" class="form-control" value="@Model.Form.Title" />
</div>
<div class="form-group">
<label class="control-label" for="permalink">@Translate.Permalink</label>
<input type="text" name="permalink" id="permalink" class="form-control" value="@Model.form.permalink" />
<label class="control-label" for="Permalink">@Translate.Permalink</label>
<input type="text" name="Permalink" id="Permalink" class="form-control" value="@Model.Form.Permalink" />
<!-- // FIXME: hard-coded "http" -->
<p class="form-hint"><em>@Translate.startingWith</em> http://@Model.webLog.urlBase/ </p>
</div>
<!-- // TODO: Markdown / HTML choice -->
<div class="form-group">
<textarea name="text" id="text" rows="15">@Model.form.text</textarea>
<textarea name="Text" id="Text" rows="15">@Model.Form.Text</textarea>
</div>
<div class="form-group">
<label class="control-label" for="tags">@Translate.Tags</label>
<input type="text" name="tags" id="tags" class="form-control" value="@Model.form.tags" />
<label class="control-label" for="Tags">@Translate.Tags</label>
<input type="text" name="Tags" id="Tags" class="form-control" value="@Model.Form.Tags" />
</div>
</div>
<div class="col-sm-3">
@ -32,12 +32,12 @@
<div class="panel-body">
<div class="form-group">
<label class="control-label">@Translate.PostStatus</label>
<p class="static-control">@Model.post.status</p>
<p class="static-control">@Model.Post.Status</p>
</div>
@If.isPublished
@If.IsPublished
<div class="form-group">
<label class="control-label">@Translate.PublishedDate</label>
<p class="static-control">@Model.publishedDate<br />@Model.publishedTime</p>
<p class="static-control">@Model.PublishedDate<br />@Model.PublishedTime</p>
</div>
@EndIf
</div>
@ -48,7 +48,7 @@
</div>
<div class="panel-body" style="max-height:350px;overflow:scroll;">
<!-- // TODO: how to check the ones that are already selected? -->
@Each.categories
@Each.Categories
<!-- - var tab = 0
while tab < item.indent
| &nbsp; &nbsp;
@ -56,22 +56,22 @@
- var attributes = {}
if -1 < currentCategories.indexOf(item.category.id)
- attributes.checked = 'checked' -->
<input type="checkbox" id="category-@Current.Item1" name="category", value="@Current.Item1" />
<input type="checkbox" id="Category-@Current.Item1" name="Category", value="@Current.Item1" />
&nbsp;
<!-- // FIXME: the title should be the category description -->
<label for="category-@Current.Item1" title="@Current.Item2">@Current.Item2</label>
<label for="Category-@Current.Item1" title="@Current.Item2">@Current.Item2</label>
<br/>
@EndEach
</div>
</div>
<div class="text-center">
@If.isPublished
<input type="hidden" name="publishNow" value="true" />
@If.IsPublished
<input type="hidden" name="PublishNow" value="true" />
@EndIf
@IfNot.isPublished
@IfNot.IsPublished
<div>
<input type="checkbox" name="publishNow" id="publishNow" value="true" checked="checked" />
&nbsp; <label for="publishNow">@Translate.PublishThisPost</label>
<input type="checkbox" name="PublishNow" id="PublishNow" value="true" checked="checked" />
&nbsp; <label for="PublishNow">@Translate.PublishThisPost</label>
</div>
@EndIf
<p>
@ -89,7 +89,7 @@
<script type="text/javascript" src="/content/scripts/tinymce-init.js"></script>
<script type="text/javascript">
/** <![CDATA[ */
$(document).ready(function () { $("#title").focus() })
$(document).ready(function () { $("#Title").focus() })
/** ]]> */
</script>
@EndSection

View File

@ -16,34 +16,34 @@
<th>@Translate.Status</th>
<th>@Translate.Tags</th>
</tr>
@Each.posts
@Each.Posts
<tr>
<td style="white-space:nowrap;">
@Current.publishedDate<br />
@Translate.at @Current.publishedTime
@Current.PublishedDate<br />
@Translate.at @Current.PublishedTime
</td>
<td>
@Current.post.title<br />
<a href="/@Current.post.permalink">@Translate.View</a> &nbsp;|&nbsp;
<a href="/post/@Current.post.id/edit">@Translate.Edit</a> &nbsp;|&nbsp;
<a href="/post/@Current.post.id/delete">@Translate.Delete</a>
@Current.Post.Title<br />
<a href="/@Current.Post.Permalink">@Translate.View</a> &nbsp;|&nbsp;
<a href="/post/@Current.Post.Id/edit">@Translate.Edit</a> &nbsp;|&nbsp;
<a href="/post/@Current.Post.Id/delete">@Translate.Delete</a>
</td>
<td>@Current.post.status</td>
<td>@Current.tags</td>
<td>@Current.Post.Status</td>
<td>@Current.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">&#xab; &nbsp;@Translate.NewerPosts</a></p>
@If.HasNewer
<p><a class="btn btn-default" href="@Model.NewerLink">&#xab; &nbsp;@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&nbsp; &#xbb;</a></p>
@If.HasOlder
<p><a class="btn btn-default" href="@Model.OlderLink">@Translate.OlderPosts&nbsp; &#xbb;</a></p>
@EndIf
</div>
</div>
@EndSection
@EndSection

View File

@ -3,12 +3,12 @@
@Section['Content']
<form action="/user/logon" method="post">
@AntiForgeryToken
<input type="hidden" name="returnUrl" value="@Model.form.returnUrl" />
<input type="hidden" name="ReturnUrl" value="@Model.Form.ReturnUrl" />
<div class="row">
<div class="col-sm-offset-1 col-sm-8 col-md-offset-3 col-md-6">
<div class="input-group">
<span class="input-group-addon" title="@Translate.EmailAddress"><i class="fa fa-envelope"></i></span>
<input type="text" name="email" id="email" class="form-control" placeholder="@Translate.EmailAddress" />
<input type="text" name="Email" id="Email" class="form-control" placeholder="@Translate.EmailAddress" />
</div>
</div>
</div>
@ -17,7 +17,7 @@
<br />
<div class="input-group">
<span class="input-group-addon" title="@Translate.Password"><i class="fa fa-key"></i></span>
<input type="password" name="password" class="form-control" placeholder="@Translate.Password" />
<input type="password" name="Password" class="form-control" placeholder="@Translate.Password" />
</div>
</div>
</div>
@ -35,7 +35,7 @@
@Section['Scripts']
<script type="text/javascript">
/* <![CDATA[ */
$(document).ready(function () { $("#email").focus() })
$(document).ready(function () { $("#Email").focus() })
/* ]]> */
</script>
@EndSection

View File

@ -3,7 +3,7 @@
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 text-right">
@Model.footerLogo
@Model.FooterLogo
</div>
</div>
</div>

View File

@ -1,24 +1,24 @@
@Each.messages
@Current.toDisplay
@Each.Messages
@Current.ToDisplay
@EndEach
@If.subTitle.IsSome
@If.SubTitle.IsSome
<h2>
<span class="label label-info">@Model.subTitle</span>
<span class="label label-info">@Model.SubTitle</span>
</h2>
@EndIf
@Each.posts
@Each.Posts
<div class="row">
<div class="col-xs-12">
<article>
<h1>
<a href="/@Current.post.permalink"
title="@Translate.PermanentLinkTo &quot;@Current.post.title@quot;">@Current.post.title</a>
<a href="/@Current.Post.Permalink"
title="@Translate.PermanentLinkTo &quot;@Current.Post.Title@quot;">@Current.Post.Title</a>
</h1>
<p>
<i class="fa fa-calendar" title="@Translate.Date"></i> @Current.publishedDate &nbsp;
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.publishedTime
<i class="fa fa-calendar" title="@Translate.Date"></i> @Current.PublishedDate &nbsp;
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.PublishedTime
</p>
@Current.post.text
@Current.Post.Text
</article>
<hr />
</div>
@ -26,16 +26,16 @@
@EndEach
<div class="row">
<div class="col-xs-3 col-xs-offset-3">
@If.hasNewer
@If.HasNewer
<p>
<a class="btn btn-primary" href="@Model.newerLink">@Translate.NewerPosts</a>
<a class="btn btn-primary" href="@Model.NewerLink">@Translate.NewerPosts</a>
</p>
@EndIf
</div>
<div class="col-xs-3 text-right">
@If.hasOlder
@If.HasOlder
<p>
<a class="btn btn-primary" href="@Model.olderLink">@Translate.OlderPosts</a>
<a class="btn btn-primary" href="@Model.OlderLink">@Translate.OlderPosts</a>
</p>
@EndIf
</div>

View File

@ -3,13 +3,13 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width" />
<meta name="generator" content="@Model.generator" />
<title>@Model.displayPageTitle</title>
<meta name="generator" content="@Model.Generator" />
<title>@Model.DisplayPageTitle</title>
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="/default/bootstrap-theme.min.css" />
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" />
<link rel="alternate" type="application/atom+xml" href="//@Model.webLog.urlBase/feed?format=atom" />
<link rel="alternate" type="application/rss+xml" href="//@Model.webLog.urlBase/feed" />
<link rel="alternate" type="application/atom+xml" href="//@Model.WebLog.UrlBase/feed?format=atom" />
<link rel="alternate" type="application/rss+xml" href="//@Model.WebLog.UrlBase/feed" />
@Section['Head'];
</head>
<body>
@ -17,20 +17,20 @@
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">@Model.webLog.name</a>
<a class="navbar-brand" href="/">@Model.WebLog.Name</a>
</div>
<p class="navbar-text">@Model.webLogSubtitle</p>
<p class="navbar-text">@Model.WebLogSubtitle</p>
<ul class="nav navbar-nav navbar-left">
@Each.webLog.pageList
<li><a href="/@Current.permalink">@Current.title</a></li>
@Each.WebLog.PageList
<li><a href="/@Current.Permalink">@Current.Title</a></li>
@EndEach
</ul>
<ul class="nav navbar-nav navbar-right">
@If.isAuthenticated
@If.IsAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li>
@EndIf
@IfNot.isAuthenticated
@IfNot.IsAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li>
@EndIf
</ul>

View File

@ -1,4 +1,4 @@
<article>
<h1>@Model.page.title</h1>
@Model.page.text
<h1>@Model.Page.Title</h1>
@Model.Page.Text
</article>

View File

@ -1,16 +1,16 @@
<article>
<div class="row">
<div class="col-xs-12"><h1>@Model.post.title</h1></div>
<div class="col-xs-12"><h1>@Model.Post.Title</h1></div>
</div>
<div class="row">
<div class="col-xs-12">
<h4>
<i class="fa fa-calendar" title="@Translate.Date"></i> @Model.publishedDate &nbsp;
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.publishedTime &nbsp; &nbsp; &nbsp;
@Each.post.categories
<i class="fa fa-calendar" title="@Translate.Date"></i> @Model.PublishedDate &nbsp;
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.PublishedTime &nbsp; &nbsp; &nbsp;
@Each.Post.Categories
<span style="white-space:nowrap;">
<i class="fa fa-folder-open-o" title="@Translate.Category"></i>
<a href="/category/@Current.slug" title="@Translate.CategorizedUnder @Current.name">@Current.name</a>
<a href="/category/@Current.Slug" title="@Translate.CategorizedUnder @Current.Name">@Current.Name</a>
&nbsp; &nbsp;
</span>
@EndEach
@ -18,12 +18,12 @@
</div>
</div>
<div class="row">
<div class="col-xs-12">@Model.post.text</div>
<div class="col-xs-12">@Model.Post.Text</div>
</div>
@If.hasTags
@If.HasTags
<div class="row">
<div class="col-xs-12">
@Each.tags
@Each.Tags
<span style="white-space:nowrap;">
<a href="/tag/@Current.Item2" title="@Translate.PostsTagged &quot;@Current.Item1&quot;">
<i class="fa fa-tag"></i> @Current.Item1
@ -56,17 +56,17 @@
</div>
<div class="row">
<div class="col-xs-6">
@If.hasNewer
<a href="/@Model.newerPost.Value.permalink" title="@Translate.NextPost - &quot;@Model.newerPost.Value.title&quot;">
&#xab;&nbsp; @Model.newerPost.Value.title
@If.HasNewer
<a href="/@Model.NewerPost.Value.Permalink" title="@Translate.NextPost - &quot;@Model.NewerPost.Value.Title&quot;">
&#xab;&nbsp; @Model.NewerPost.Value.Title
</a>
@EndIf
</div>
<div class="col-xs-6 text-right">
@If.hasOlder
<a href="/@Model.olderPost.Value.permalink"
title="@Translate.PreviousPost - &quot;@Model.olderPost.Value.title&quot;">
@Model.olderPost.Value.title &nbsp;&#xbb;
@If.HasOlder
<a href="/@Model.OlderPost.Value.Permalink"
title="@Translate.PreviousPost - &quot;@Model.OlderPost.Value.Title&quot;">
@Model.OlderPost.Value.Title &nbsp;&#xbb;
</a>
@EndIf
</div>