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.CompilerServices
open System.Runtime.InteropServices open System.Runtime.InteropServices
[<assembly: AssemblyTitle("myWebLog.Data")>] [<assembly: AssemblyTitle("MyWebLog.Data")>]
[<assembly: AssemblyDescription("Data access for myWebLog")>] [<assembly: AssemblyDescription("Data access for myWebLog")>]
[<assembly: AssemblyConfiguration("")>] [<assembly: AssemblyConfiguration("")>]
[<assembly: AssemblyCompany("DJS Consulting")>] [<assembly: AssemblyCompany("DJS Consulting")>]
[<assembly: AssemblyProduct("myWebLog.Data")>] [<assembly: AssemblyProduct("MyWebLog.Data")>]
[<assembly: AssemblyCopyright("Copyright © 2016")>] [<assembly: AssemblyCopyright("Copyright © 2016")>]
[<assembly: AssemblyTrademark("")>] [<assembly: AssemblyTrademark("")>]
[<assembly: AssemblyCulture("")>] [<assembly: AssemblyCulture("")>]

View File

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

View File

@ -1,47 +1,59 @@
namespace myWebLog.Data namespace MyWebLog.Data
open RethinkDb.Driver open RethinkDb.Driver
open RethinkDb.Driver.Net open RethinkDb.Driver.Net
open Newtonsoft.Json open Newtonsoft.Json
/// Data configuration /// Data configuration
type DataConfig = { type DataConfig =
/// The hostname for the RethinkDB server { /// The hostname for the RethinkDB server
hostname : string [<JsonProperty("hostname")>]
/// The port for the RethinkDB server Hostname : string
port : int /// The port for the RethinkDB server
/// The authorization key to use when connecting to the server [<JsonProperty("port")>]
authKey : string Port : int
/// How long an attempt to connect to the server should wait before giving up /// The authorization key to use when connecting to the server
timeout : int [<JsonProperty("authKey")>]
/// The name of the default database to use on the connection AuthKey : string
database : string /// How long an attempt to connect to the server should wait before giving up
/// A connection to the RethinkDB server using the configuration in this object [<JsonProperty("timeout")>]
conn : IConnection 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 with
/// Create a data configuration from JSON /// Create a data configuration from JSON
static member fromJson json = static member FromJson json =
let mutable cfg = JsonConvert.DeserializeObject<DataConfig> json let ensureHostname cfg = match cfg.Hostname with
cfg <- match cfg.hostname with | null -> { cfg with Hostname = RethinkDBConstants.DefaultHostname }
| null -> { cfg with hostname = RethinkDBConstants.DefaultHostname } | _ -> cfg
| _ -> cfg let ensurePort cfg = match cfg.Port with
cfg <- match cfg.port with | 0 -> { cfg with Port = RethinkDBConstants.DefaultPort }
| 0 -> { cfg with port = RethinkDBConstants.DefaultPort } | _ -> cfg
| _ -> cfg let ensureAuthKey cfg = match cfg.AuthKey with
cfg <- match cfg.authKey with | null -> { cfg with AuthKey = RethinkDBConstants.DefaultAuthkey }
| null -> { cfg with authKey = RethinkDBConstants.DefaultAuthkey } | _ -> cfg
| _ -> cfg let ensureTimeout cfg = match cfg.Timeout with
cfg <- match cfg.timeout with | 0 -> { cfg with Timeout = RethinkDBConstants.DefaultTimeout }
| 0 -> { cfg with timeout = RethinkDBConstants.DefaultTimeout } | _ -> cfg
| _ -> cfg let ensureDatabase cfg = match cfg.Database with
cfg <- match cfg.database with | null -> { cfg with Database = RethinkDBConstants.DefaultDbName }
| null -> { cfg with database = RethinkDBConstants.DefaultDbName } | _ -> cfg
| _ -> cfg let connect cfg = { cfg with Conn = RethinkDB.R.Connection()
{ cfg with conn = RethinkDB.R.Connection() .Hostname(cfg.Hostname)
.Hostname(cfg.hostname) .Port(cfg.Port)
.Port(cfg.port) .AuthKey(cfg.AuthKey)
.AuthKey(cfg.authKey) .Db(cfg.Database)
.Db(cfg.database) .Timeout(cfg.Timeout)
.Timeout(cfg.timeout) .Connect() }
.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 open Newtonsoft.Json
@ -37,261 +37,244 @@ module CommentStatus =
// ---- Entities ---- // ---- Entities ----
/// A revision of a post or page /// A revision of a post or page
type Revision = { type Revision =
/// The instant which this revision was saved { /// The instant which this revision was saved
asOf : int64 AsOf : int64
/// The source language /// The source language
sourceType : string SourceType : string
/// The text /// The text
text : string Text : string }
}
with with
/// An empty revision /// An empty revision
static member empty = static member Empty =
{ asOf = int64 0 { AsOf = int64 0
sourceType = RevisionSource.HTML SourceType = RevisionSource.HTML
text = "" } Text = "" }
/// A page with static content /// A page with static content
type Page = { type Page =
/// The Id { /// The Id
id : string Id : string
/// The Id of the web log to which this page belongs /// The Id of the web log to which this page belongs
webLogId : string WebLogId : string
/// The Id of the author of this page /// The Id of the author of this page
authorId : string AuthorId : string
/// The title of the page /// The title of the page
title : string Title : string
/// The link at which this page is displayed /// The link at which this page is displayed
permalink : string Permalink : string
/// The instant this page was published /// The instant this page was published
publishedOn : int64 PublishedOn : int64
/// The instant this page was last updated /// The instant this page was last updated
updatedOn : int64 UpdatedOn : int64
/// Whether this page shows as part of the web log's navigation /// Whether this page shows as part of the web log's navigation
showInPageList : bool ShowInPageList : bool
/// The current text of the page /// The current text of the page
text : string Text : string
/// Revisions of this page /// Revisions of this page
revisions : Revision list Revisions : Revision list }
}
with with
static member empty = static member Empty =
{ id = "" { Id = ""
webLogId = "" WebLogId = ""
authorId = "" AuthorId = ""
title = "" Title = ""
permalink = "" Permalink = ""
publishedOn = int64 0 PublishedOn = int64 0
updatedOn = int64 0 UpdatedOn = int64 0
showInPageList = false ShowInPageList = false
text = "" Text = ""
revisions = List.empty Revisions = List.empty
} }
/// An entry in the list of pages displayed as part of the web log (derived via query) /// An entry in the list of pages displayed as part of the web log (derived via query)
type PageListEntry = { type PageListEntry =
permalink : string { Permalink : string
title : string Title : string }
}
/// A web log /// A web log
type WebLog = { type WebLog =
/// The Id { /// The Id
id : string Id : string
/// The name /// The name
name : string Name : string
/// The subtitle /// The subtitle
subtitle : string option Subtitle : string option
/// The default page ("posts" or a page Id) /// The default page ("posts" or a page Id)
defaultPage : string DefaultPage : string
/// The path of the theme (within /views/themes) /// The path of the theme (within /views/themes)
themePath : string ThemePath : string
/// The URL base /// The URL base
urlBase : string UrlBase : string
/// The time zone in which dates/times should be displayed /// The time zone in which dates/times should be displayed
timeZone : string TimeZone : string
/// A list of pages to be rendered as part of the site navigation /// A list of pages to be rendered as part of the site navigation (not stored)
[<JsonIgnore>] PageList : PageListEntry list }
pageList : PageListEntry list
}
with with
/// An empty web log /// An empty web log
static member empty = static member Empty =
{ id = "" { Id = ""
name = "" Name = ""
subtitle = None Subtitle = None
defaultPage = "" DefaultPage = ""
themePath = "default" ThemePath = "default"
urlBase = "" UrlBase = ""
timeZone = "America/New_York" TimeZone = "America/New_York"
pageList = List.empty PageList = List.empty }
}
/// An authorization between a user and a web log /// An authorization between a user and a web log
type Authorization = { type Authorization =
/// The Id of the web log to which this authorization grants access { /// The Id of the web log to which this authorization grants access
webLogId : string WebLogId : string
/// The level of access granted by this authorization /// The level of access granted by this authorization
level : string Level : string }
}
/// A user of myWebLog /// A user of myWebLog
type User = { type User =
/// The Id { /// The Id
id : string Id : string
/// The user name (e-mail address) /// The user name (e-mail address)
userName : string UserName : string
/// The first name /// The first name
firstName : string FirstName : string
/// The last name /// The last name
lastName : string LastName : string
/// The user's preferred name /// The user's preferred name
preferredName : string PreferredName : string
/// The hash of the user's password /// The hash of the user's password
passwordHash : string PasswordHash : string
/// The URL of the user's personal site /// The URL of the user's personal site
url : string option Url : string option
/// The user's authorizations /// The user's authorizations
authorizations : Authorization list Authorizations : Authorization list }
}
with with
/// An empty user /// An empty user
static member empty = static member Empty =
{ id = "" { Id = ""
userName = "" UserName = ""
firstName = "" FirstName = ""
lastName = "" LastName = ""
preferredName = "" PreferredName = ""
passwordHash = "" PasswordHash = ""
url = None Url = None
authorizations = List.empty Authorizations = List.empty }
}
/// Claims for this user /// Claims for this user
[<JsonIgnore>] [<JsonIgnore>]
member this.claims = this.authorizations member this.Claims = this.Authorizations
|> List.map (fun auth -> sprintf "%s|%s" auth.webLogId auth.level) |> List.map (fun auth -> sprintf "%s|%s" auth.WebLogId auth.Level)
/// A category to which posts may be assigned /// A category to which posts may be assigned
type Category = { type Category =
/// The Id { /// The Id
id : string Id : string
/// The Id of the web log to which this category belongs /// The Id of the web log to which this category belongs
webLogId : string WebLogId : string
/// The displayed name /// The displayed name
name : string Name : string
/// The slug (used in category URLs) /// The slug (used in category URLs)
slug : string Slug : string
/// A longer description of the category /// A longer description of the category
description : string option Description : string option
/// The parent Id of this category (if a subcategory) /// The parent Id of this category (if a subcategory)
parentId : string option ParentId : string option
/// The categories for which this category is the parent /// The categories for which this category is the parent
children : string list Children : string list }
}
with with
/// An empty category /// An empty category
static member empty = static member empty =
{ id = "new" { Id = "new"
webLogId = "" WebLogId = ""
name = "" Name = ""
slug = "" Slug = ""
description = None Description = None
parentId = None ParentId = None
children = List.empty Children = List.empty }
}
/// A comment (applies to a post) /// A comment (applies to a post)
type Comment = { type Comment =
/// The Id { /// The Id
id : string Id : string
/// The Id of the post to which this comment applies /// The Id of the post to which this comment applies
postId : string PostId : string
/// The Id of the comment to which this comment is a reply /// The Id of the comment to which this comment is a reply
inReplyToId : string option InReplyToId : string option
/// The name of the commentor /// The name of the commentor
name : string Name : string
/// The e-mail address of the commentor /// The e-mail address of the commentor
email : string Email : string
/// The URL of the commentor's personal website /// The URL of the commentor's personal website
url : string option Url : string option
/// The status of the comment /// The status of the comment
status : string Status : string
/// The instant the comment was posted /// The instant the comment was posted
postedOn : int64 PostedOn : int64
/// The text of the comment /// The text of the comment
text : string Text : string }
}
with with
static member empty = static member Empty =
{ id = "" { Id = ""
postId = "" PostId = ""
inReplyToId = None InReplyToId = None
name = "" Name = ""
email = "" Email = ""
url = None Url = None
status = CommentStatus.Pending Status = CommentStatus.Pending
postedOn = int64 0 PostedOn = int64 0
text = "" Text = "" }
}
/// A post /// A post
type Post = { type Post =
/// The Id { /// The Id
id : string Id : string
/// The Id of the web log to which this post belongs /// The Id of the web log to which this post belongs
webLogId : string WebLogId : string
/// The Id of the author of this post /// The Id of the author of this post
authorId : string AuthorId : string
/// The status /// The status
status : string Status : string
/// The title /// The title
title : string Title : string
/// The link at which the post resides /// The link at which the post resides
permalink : string Permalink : string
/// The instant on which the post was originally published /// The instant on which the post was originally published
publishedOn : int64 PublishedOn : int64
/// The instant on which the post was last updated /// The instant on which the post was last updated
updatedOn : int64 UpdatedOn : int64
/// The text of the post /// The text of the post
text : string Text : string
/// The Ids of the categories to which this is assigned /// The Ids of the categories to which this is assigned
categoryIds : string list CategoryIds : string list
/// The tags for the post /// The tags for the post
tags : string list Tags : string list
/// The permalinks at which this post may have once resided /// The permalinks at which this post may have once resided
priorPermalinks : string list PriorPermalinks : string list
/// Revisions of this post /// Revisions of this post
revisions : Revision list Revisions : Revision list
/// The categories to which this is assigned /// The categories to which this is assigned (not stored in database)
[<JsonIgnore>] Categories : Category list
categories : Category list /// The comments (not stored in database)
/// The comments Comments : Comment list }
[<JsonIgnore>]
comments : Comment list
}
with with
static member empty = static member Empty =
{ id = "new" { Id = "new"
webLogId = "" WebLogId = ""
authorId = "" AuthorId = ""
status = PostStatus.Draft Status = PostStatus.Draft
title = "" Title = ""
permalink = "" Permalink = ""
publishedOn = int64 0 PublishedOn = int64 0
updatedOn = int64 0 UpdatedOn = int64 0
text = "" Text = ""
categoryIds = List.empty CategoryIds = List.empty
tags = List.empty Tags = List.empty
priorPermalinks = List.empty PriorPermalinks = List.empty
revisions = List.empty Revisions = List.empty
categories = List.empty Categories = List.empty
comments = List.empty Comments = List.empty }
}

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
module myWebLog.Data.Table module MyWebLog.Data.Table
/// The Category table /// The Category table
let Category = "Category" 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 open Rethink
let private r = RethinkDb.Driver.RethinkDB.R 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. // http://rethinkdb.com/docs/secondary-indexes/java/ for more information.
let tryUserLogOn conn (email : string) (passwordHash : string) = let tryUserLogOn conn (email : string) (passwordHash : string) =
r.Table(Table.User) r.Table(Table.User)
.GetAll(email).OptArg("index", "userName") .GetAll(email).OptArg("index", "UserName")
.Filter(fun u -> u.["passwordHash"].Eq(passwordHash)) .Filter(fun u -> u.["PasswordHash"].Eq(passwordHash))
.RunCursorAsync<User>(conn) .RunCursorAsync<User>(conn)
|> await |> await
|> Seq.tryHead |> 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 Rethink
open RethinkDb.Driver.Ast
let private r = RethinkDb.Driver.RethinkDB.R let private r = RethinkDb.Driver.RethinkDB.R
/// Counts of items displayed on the admin dashboard /// Counts of items displayed on the admin dashboard
type DashboardCounts = { type DashboardCounts =
/// The number of pages for the web log { /// The number of pages for the web log
pages : int Pages : int
/// The number of pages for the web log /// The number of pages for the web log
posts : int Posts : int
/// The number of categories for the web log /// The number of categories for the web log
categories : int Categories : int }
}
/// Detemine the web log by the URL base /// 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 tryFindWebLogByUrlBase conn (urlBase : string) =
let webLog = r.Table(Table.WebLog) r.Table(Table.WebLog)
.GetAll(urlBase).OptArg("index", "urlBase") .GetAll(urlBase).OptArg("index", "urlBase")
.RunCursorAsync<WebLog>(conn) .Merge(fun w -> r.HashMap("PageList", r.Table(Table.Page)
|> await .GetAll(w.["Id"]).OptArg("index", "WebLogId")
|> Seq.tryHead .Filter(ReqlFunction1(fun pg -> upcast pg.["ShowInPageList"].Eq(true)))
match webLog with .OrderBy("Title")
| Some w -> Some { w with pageList = r.Table(Table.Page) .Pluck("Title", "Permalink")
.GetAll(w.id).OptArg("index", "webLogId") .CoerceTo("array")))
.Filter(fun pg -> pg.["showInPageList"].Eq(true)) .RunCursorAsync<WebLog>(conn)
.OrderBy("title") |> await
.Pluck("title", "permalink") |> Seq.tryHead
.RunListAsync<PageListEntry>(conn) |> await |> Seq.toList }
| None -> None
/// Get counts for the admin dashboard /// Get counts for the admin dashboard
let findDashboardCounts conn (webLogId : string) = let findDashboardCounts conn (webLogId : string) =
r.Expr( r.HashMap("pages", r.Table(Table.Page ).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("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())) .Merge(r.HashMap("Categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
.RunAtomAsync<DashboardCounts>(conn) .RunAtomAsync<DashboardCounts>(conn)
|> await |> await

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
[<AutoOpen>] [<AutoOpen>]
module myWebLog.ModuleExtensions module MyWebLog.ModuleExtensions
open myWebLog open MyWebLog.Entities
open myWebLog.Entities
open Nancy open Nancy
open Nancy.Security open Nancy.Security
@ -14,19 +13,19 @@ type NancyModule with
/// Display a view using the theme specified for the web log /// Display a view using the theme specified for the web log
member this.ThemedView view (model : MyWebLogModel) : obj = 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 /// Return a 404
member this.NotFound () : obj = upcast HttpStatusCode.NotFound member this.NotFound () : obj = upcast HttpStatusCode.NotFound
/// Redirect a request, storing messages in the session if they exist /// Redirect a request, storing messages in the session if they exist
member this.Redirect url (model : MyWebLogModel) : obj = member this.Redirect url (model : MyWebLogModel) : obj =
match List.length model.messages with match List.length model.Messages with
| 0 -> () | 0 -> ()
| _ -> this.Session.[Keys.Messages] <- model.messages | _ -> this.Session.[Keys.Messages] <- model.Messages
upcast this.Response.AsRedirect(url).WithStatusCode HttpStatusCode.TemporaryRedirect upcast this.Response.AsRedirect(url).WithStatusCode HttpStatusCode.TemporaryRedirect
/// Require a specific level of access for the current web log /// Require a specific level of access for the current web log
member this.RequiresAccessLevel level = member this.RequiresAccessLevel level =
this.RequiresAuthentication() 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 FSharp.Markdown
open myWebLog.Data.Page open MyWebLog.Data.Page
open myWebLog.Entities open MyWebLog.Entities
open Nancy open Nancy
open Nancy.ModelBinding open Nancy.ModelBinding
open Nancy.Security open Nancy.Security
@ -22,9 +22,9 @@ type PageModule(conn : IConnection, clock : IClock) as this =
/// List all pages /// List all pages
member this.PageList () = member this.PageList () =
this.RequiresAccessLevel AuthorizationLevel.Administrator 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)))) |> List.map (fun p -> PageForDisplay(this.WebLog, p))))
model.pageTitle <- Resources.Pages model.PageTitle <- Resources.Pages
upcast this.View.["admin/page/list", model] upcast this.View.["admin/page/list", model]
/// Edit a page /// Edit a page
@ -32,17 +32,15 @@ type PageModule(conn : IConnection, clock : IClock) as this =
this.RequiresAccessLevel AuthorizationLevel.Administrator this.RequiresAccessLevel AuthorizationLevel.Administrator
let pageId = parameters.["id"].ToString () let pageId = parameters.["id"].ToString ()
match (match pageId with match (match pageId with
| "new" -> Some Page.empty | "new" -> Some Page.Empty
| _ -> tryFindPage conn this.WebLog.id pageId) with | _ -> tryFindPage conn this.WebLog.Id pageId) with
| Some page -> let rev = match page.revisions | Some page -> let rev = match page.Revisions
|> List.sortByDescending (fun r -> r.asOf) |> List.sortByDescending (fun r -> r.AsOf)
|> List.tryHead with |> List.tryHead with
| Some r -> r | Some r -> r
| None -> Revision.empty | None -> Revision.Empty
let model = EditPageModel(this.Context, this.WebLog, page, rev) let model = EditPageModel(this.Context, this.WebLog, page, rev)
model.pageTitle <- match pageId with model.PageTitle <- match pageId with "new" -> Resources.AddNewPage | _ -> Resources.EditPage
| "new" -> Resources.AddNewPage
| _ -> Resources.EditPage
upcast this.View.["admin/page/edit", model] upcast this.View.["admin/page/edit", model]
| None -> this.NotFound () | None -> this.NotFound ()
@ -54,30 +52,28 @@ type PageModule(conn : IConnection, clock : IClock) as this =
let form = this.Bind<EditPageForm> () let form = this.Bind<EditPageForm> ()
let now = clock.Now.Ticks let now = clock.Now.Ticks
match (match pageId with match (match pageId with
| "new" -> Some Page.empty | "new" -> Some Page.Empty
| _ -> tryFindPage conn this.WebLog.id pageId) with | _ -> tryFindPage conn this.WebLog.Id pageId) with
| Some p -> let page = match pageId with | Some p -> let page = match pageId with "new" -> { p with WebLogId = this.WebLog.Id } | _ -> p
| "new" -> { p with webLogId = this.WebLog.id }
| _ -> p
let pId = { p with let pId = { p with
title = form.title Title = form.Title
permalink = form.permalink Permalink = form.Permalink
publishedOn = match pageId with | "new" -> now | _ -> page.publishedOn PublishedOn = match pageId with "new" -> now | _ -> page.PublishedOn
updatedOn = now UpdatedOn = now
text = match form.source with Text = match form.Source with
| RevisionSource.Markdown -> Markdown.TransformHtml form.text | RevisionSource.Markdown -> Markdown.TransformHtml form.Text
| _ -> form.text | _ -> form.Text
revisions = { asOf = now Revisions = { AsOf = now
sourceType = form.source SourceType = form.Source
text = form.text } :: page.revisions } Text = form.Text } :: page.Revisions }
|> savePage conn |> savePage conn
let model = MyWebLogModel(this.Context, this.WebLog) let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info { UserMessage.Empty with
message = System.String.Format Level = Level.Info
(Resources.MsgPageEditSuccess, Message = System.String.Format
(match pageId with | "new" -> Resources.Added | _ -> Resources.Updated)) (Resources.MsgPageEditSuccess,
details = None } (match pageId with | "new" -> Resources.Added | _ -> Resources.Updated)) }
|> model.addMessage |> model.AddMessage
this.Redirect (sprintf "/page/%s/edit" pId) model this.Redirect (sprintf "/page/%s/edit" pId) model
| None -> this.NotFound () | None -> this.NotFound ()
@ -86,12 +82,11 @@ type PageModule(conn : IConnection, clock : IClock) as this =
this.ValidateCsrfToken () this.ValidateCsrfToken ()
this.RequiresAccessLevel AuthorizationLevel.Administrator this.RequiresAccessLevel AuthorizationLevel.Administrator
let pageId = parameters.["id"].ToString () let pageId = parameters.["id"].ToString ()
match tryFindPageWithoutRevisions conn this.WebLog.id pageId with match tryFindPageWithoutRevisions conn this.WebLog.Id pageId with
| Some page -> deletePage conn page.webLogId page.id | Some page -> deletePage conn page.WebLogId page.Id
let model = MyWebLogModel(this.Context, this.WebLog) let model = MyWebLogModel(this.Context, this.WebLog)
{ level = Level.Info { UserMessage.Empty with Level = Level.Info
message = Resources.MsgPageDeleted Message = Resources.MsgPageDeleted }
details = None } |> model.AddMessage
|> model.addMessage
this.Redirect "/pages" model this.Redirect "/pages" model
| None -> this.NotFound () | None -> this.NotFound ()

View File

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

View File

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

View File

@ -1,7 +1,7 @@
namespace myWebLog namespace MyWebLog
open myWebLog.Data.WebLog open MyWebLog.Data.WebLog
open myWebLog.Entities open MyWebLog.Entities
open Nancy open Nancy
open Nancy.Session.Persistable open Nancy.Session.Persistable
open Newtonsoft.Json open Newtonsoft.Json
@ -21,25 +21,23 @@ module Level =
/// A message for the user /// A message for the user
type UserMessage = { type UserMessage =
/// The level of the message (use Level module constants) { /// The level of the message (use Level module constants)
level : string Level : string
/// The text of the message /// The text of the message
message : string Message : string
/// Further details regarding the message /// Further details regarding the message
details : string option Details : string option }
}
with with
/// An empty message /// An empty message
static member empty = { static member Empty =
level = Level.Info { Level = Level.Info
message = "" Message = ""
details = None Details = None }
}
/// Display version /// Display version
[<JsonIgnore>] [<JsonIgnore>]
member this.toDisplay = member this.ToDisplay =
let classAndLabel = let classAndLabel =
dict [ dict [
Level.Error, ("danger", Resources.Error) Level.Error, ("danger", Resources.Error)
@ -48,23 +46,23 @@ with
] ]
seq { seq {
yield "<div class=\"alert alert-dismissable alert-" 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 "\" role=\"alert\"><button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\""
yield Resources.Close yield Resources.Close
yield "\">&times;</button><strong>" yield "\">&times;</button><strong>"
match snd classAndLabel.[this.level] with match snd classAndLabel.[this.Level] with
| "" -> () | "" -> ()
| lbl -> yield lbl.ToUpper () | lbl -> yield lbl.ToUpper ()
yield " &#xbb; " yield " &#xbb; "
yield this.message yield this.Message
yield "</strong>" yield "</strong>"
match this.details with match this.Details with
| Some d -> yield "<br />" | Some d -> yield "<br />"
yield d yield d
| None -> () | None -> ()
yield "</div>" yield "</div>"
} }
|> Seq.reduce (fun acc x -> acc + x) |> Seq.reduce (+)
/// Helpers to format local date/time using NodaTime /// Helpers to format local date/time using NodaTime
@ -94,58 +92,58 @@ type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
/// Get the messages from the session /// Get the messages from the session
let getMessages () = 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 match List.length msg with
| 0 -> () | 0 -> ()
| _ -> ctx.Request.Session.Delete Keys.Messages | _ -> ctx.Request.Session.Delete Keys.Messages
msg msg
/// The web log for this request /// 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) /// 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 /// User messages
member val messages = getMessages () with get, set member val Messages = getMessages () with get, set
/// The currently logged in user /// 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 /// The title of the page
member val pageTitle = "" with get, set member val PageTitle = "" with get, set
/// The name and version of the application /// 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 /// 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? /// 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 /// 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 /// 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 /// 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 /// 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 /// The page title with the web log name appended
member this.displayPageTitle = member this.DisplayPageTitle =
match this.pageTitle with match this.PageTitle with
| "" -> match this.webLog.subtitle with | "" -> match this.WebLog.Subtitle with
| Some st -> sprintf "%s | %s" this.webLog.name st | Some st -> sprintf "%s | %s" this.WebLog.Name st
| None -> this.webLog.name | None -> this.WebLog.Name
| pt -> sprintf "%s | %s" pt this.webLog.name | pt -> sprintf "%s | %s" pt this.WebLog.Name
/// An image with the version and load time in the tool tip /// An image with the version and load time in the tool tip
member this.footerLogo = member this.FooterLogo =
seq { seq {
yield "<img src=\"/default/footer-logo.png\" alt=\"myWebLog\" title=\"" 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 Resources.LoadedIn
yield " " 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 " "
yield Resources.Seconds.ToLower () yield Resources.Seconds.ToLower ()
yield "\" />" yield "\" />"
} }
|> Seq.reduce (fun acc x -> acc + x) |> Seq.reduce (+)
// ---- Admin models ---- // ---- Admin models ----
@ -154,68 +152,67 @@ type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
type DashboardModel(ctx, webLog, counts : DashboardCounts) = type DashboardModel(ctx, webLog, counts : DashboardCounts) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The number of posts for the current web log /// 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 /// 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 /// 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 ---- // ---- Category models ----
type IndentedCategory = { type IndentedCategory =
category : Category { Category : Category
indent : int Indent : int
selected : bool Selected : bool }
}
with with
/// Create an indented category /// Create an indented category
static member create (cat : Category * int) (isSelected : string -> bool) = static member Create (cat : Category * int) (isSelected : string -> bool) =
{ category = fst cat { Category = fst cat
indent = snd cat Indent = snd cat
selected = isSelected (fst cat).id } Selected = isSelected (fst cat).Id }
/// Display name for a category on the list page, complete with indents /// 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 /// Display for this category as an option within a select box
member this.option = member this.Option =
seq { seq {
yield sprintf "<option value=\"%s\"" this.category.id yield sprintf "<option value=\"%s\"" this.Category.Id
yield (match this.selected with | true -> """ selected="selected">""" | _ -> ">") yield (match this.Selected with | true -> """ selected="selected">""" | _ -> ">")
yield String.replicate this.indent " &nbsp; &nbsp; " yield String.replicate this.Indent " &nbsp; &nbsp; "
yield this.category.name yield this.Category.Name
yield "</option>" yield "</option>"
} }
|> String.concat "" |> String.concat ""
/// Does the category have a description? /// 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 /// Model for the list of categories
type CategoryListModel(ctx, webLog, categories) = type CategoryListModel(ctx, webLog, categories) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The categories /// The categories
member this.categories : IndentedCategory list = categories member this.Categories : IndentedCategory list = categories
/// Form for editing a category /// Form for editing a category
type CategoryForm(category : Category) = type CategoryForm(category : Category) =
new() = CategoryForm(Category.empty) new() = CategoryForm(Category.empty)
/// The name of the category /// 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) /// 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 /// 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 /// 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 /// Model for editing a category
type CategoryEditModel(ctx, webLog, category) = type CategoryEditModel(ctx, webLog, category) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The form with the category information /// The form with the category information
member val form = CategoryForm(category) with get, set member val Form = CategoryForm(category) with get, set
/// The categories /// The categories
member val categories : IndentedCategory list = List.empty with get, set member val Categories : IndentedCategory list = [] with get, set
// ---- Page models ---- // ---- Page models ----
@ -223,54 +220,53 @@ type CategoryEditModel(ctx, webLog, category) =
/// Model for page display /// Model for page display
type PageModel(ctx, webLog, page) = type PageModel(ctx, webLog, page) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The page to be displayed /// The page to be displayed
member this.page : Page = page member this.Page : Page = page
/// Wrapper for a page with additional properties /// Wrapper for a page with additional properties
type PageForDisplay(webLog, page) = type PageForDisplay(webLog, page) =
/// The page /// The page
member this.page : Page = page member this.Page : Page = page
/// The time zone of the web log /// The time zone of the web log
member this.timeZone = webLog.timeZone member this.TimeZone = webLog.TimeZone
/// The date the page was last updated /// 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 /// 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 /// Model for page list display
type PagesModel(ctx, webLog, pages) = type PagesModel(ctx, webLog, pages) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The pages /// The pages
member this.pages : PageForDisplay list = pages member this.Pages : PageForDisplay list = pages
/// Form used to edit a page /// Form used to edit a page
type EditPageForm() = type EditPageForm() =
/// The title of the page /// The title of the page
member val title = "" with get, set member val Title = "" with get, set
/// The link for the page /// The link for the page
member val permalink = "" with get, set member val Permalink = "" with get, set
/// The source type of the revision /// The source type of the revision
member val source = "" with get, set member val Source = "" with get, set
/// The text of the revision /// 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 /// 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 /// Fill the form with applicable values from a page
member this.forPage (page : Page) = member this.ForPage (page : Page) =
this.title <- page.title this.Title <- page.Title
this.permalink <- page.permalink this.Permalink <- page.Permalink
this.showInPageList <- page.showInPageList this.ShowInPageList <- page.ShowInPageList
this this
/// Fill the form with applicable values from a revision /// Fill the form with applicable values from a revision
member this.forRevision rev = member this.ForRevision rev =
this.source <- rev.sourceType this.Source <- rev.SourceType
this.text <- rev.text this.Text <- rev.Text
this this
@ -278,21 +274,21 @@ type EditPageForm() =
type EditPageModel(ctx, webLog, page, revision) = type EditPageModel(ctx, webLog, page, revision) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The page edit form /// The page edit form
member val form = EditPageForm().forPage(page).forRevision(revision) member val Form = EditPageForm().ForPage(page).ForRevision(revision)
/// The page itself /// The page itself
member this.page = page member this.Page = page
/// The page's published date /// The page's published date
member this.publishedDate = this.displayLongDate page.publishedOn member this.PublishedDate = this.DisplayLongDate page.PublishedOn
/// The page's published time /// 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 /// 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 /// 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? /// 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 /// 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 ---- // ---- Post models ----
@ -301,102 +297,103 @@ type EditPageModel(ctx, webLog, page, revision) =
type PostModel(ctx, webLog, post) = type PostModel(ctx, webLog, post) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The post being displayed /// The post being displayed
member this.post : Post = post member this.Post : Post = post
/// The next newer 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 /// 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 /// 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 /// 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? /// 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 /// Get the tags sorted
member this.tags = post.tags member this.Tags = post.Tags
|> List.sort |> List.sort
|> List.map (fun tag -> tag, tag.Replace(' ', '+')) |> List.map (fun tag -> tag, tag.Replace(' ', '+'))
/// Does this post have a newer post? /// 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? /// 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 /// Wrapper for a post with additional properties
type PostForDisplay(webLog : WebLog, post : Post) = type PostForDisplay(webLog : WebLog, post : Post) =
/// Turn tags into a pipe-delimited string of tags /// Turn tags into a pipe-delimited string of tags
let pipedTags tags = tags |> List.reduce (fun acc x -> sprintf "%s | %s" acc x) let pipedTags tags = tags |> List.reduce (fun acc x -> sprintf "%s | %s" acc x)
/// The actual post /// The actual post
member this.post = post member this.Post = post
/// The time zone for the web log to which this post belongs /// 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 /// 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 /// 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 /// Tags
member this.tags = member this.Tags =
match List.length this.post.tags with match List.length this.Post.Tags with
| 0 -> "" | 0 -> ""
| 1 | 2 | 3 | 4 | 5 -> this.post.tags |> pipedTags | 1 | 2 | 3 | 4 | 5 -> this.Post.Tags |> pipedTags
| count -> sprintf "%s %s" (this.post.tags |> List.take 3 |> pipedTags) | count -> sprintf "%s %s" (this.Post.Tags |> List.take 3 |> pipedTags)
(System.String.Format(Resources.andXMore, count - 3)) (System.String.Format(Resources.andXMore, count - 3))
/// Model for all page-of-posts pages /// Model for all page-of-posts pages
type PostsModel(ctx, webLog) = type PostsModel(ctx, webLog) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The subtitle for the page /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// The link for the next newer page of posts
member this.newerLink = member this.NewerLink =
match this.urlPrefix = "/posts" && this.pageNbr = 2 && this.webLog.defaultPage = "posts" with match this.UrlPrefix = "/posts" && this.PageNbr = 2 && this.WebLog.DefaultPage = "posts" with
| true -> "/" | 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 /// 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 /// Form for editing a post
type EditPostForm() = type EditPostForm() =
/// The title of the post /// The title of the post
member val title = "" with get, set member val Title = "" with get, set
/// The permalink for the post /// The permalink for the post
member val permalink = "" with get, set member val Permalink = "" with get, set
/// The source type for this revision /// The source type for this revision
member val source = "" with get, set member val Source = "" with get, set
/// The text /// The text
member val text = "" with get, set member val Text = "" with get, set
/// Tags for the post /// Tags for the post
member val tags = "" with get, set member val Tags = "" with get, set
/// The selected category Ids for the post /// 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 /// 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 /// Fill the form with applicable values from a post
member this.forPost post = member this.ForPost post =
this.title <- post.title this.Title <- post.Title
this.permalink <- post.permalink this.Permalink <- post.Permalink
this.tags <- List.reduce (fun acc x -> sprintf "%s, %s" acc x) post.tags this.Tags <- List.reduce (fun acc x -> sprintf "%s, %s" acc x) post.Tags
this.categories <- List.toArray post.categoryIds this.Categories <- List.toArray post.CategoryIds
this this
/// Fill the form with applicable values from a revision /// Fill the form with applicable values from a revision
member this.forRevision rev = member this.ForRevision rev =
this.source <- rev.sourceType this.Source <- rev.SourceType
this.text <- rev.text this.Text <- rev.Text
this this
/// View model for the edit post page /// View model for the edit post page
@ -404,17 +401,17 @@ type EditPostModel(ctx, webLog, post, revision) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The form /// 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 /// 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 /// 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 /// 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 /// The published date
member this.publishedDate = this.displayLongDate this.post.publishedOn member this.PublishedDate = this.DisplayLongDate this.Post.PublishedOn
/// The published time /// The published time
member this.publishedTime = this.displayTime this.post.publishedOn member this.PublishedTime = this.DisplayTime this.Post.PublishedOn
// ---- User models ---- // ---- User models ----
@ -422,15 +419,15 @@ type EditPostModel(ctx, webLog, post, revision) =
/// Form for the log on page /// Form for the log on page
type LogOnForm() = type LogOnForm() =
/// The URL to which the user will be directed upon successful log on /// 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 /// The e-mail address
member val email = "" with get, set member val Email = "" with get, set
/// The user's passwor /// The user's passwor
member val password = "" with get, set member val Password = "" with get, set
/// Model to support the user log on page /// Model to support the user log on page
type LogOnModel(ctx, webLog) = type LogOnModel(ctx, webLog) =
inherit MyWebLogModel(ctx, webLog) inherit MyWebLogModel(ctx, webLog)
/// The log on form /// 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> <ProjectGuid>e6ee110a-27a6-4a19-b0cb-d24f48f71b53</ProjectGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<RootNamespace>myWebLog.Web</RootNamespace> <RootNamespace>myWebLog.Web</RootNamespace>
<AssemblyName>myWebLog.Web</AssemblyName> <AssemblyName>MyWebLog.Web</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion> <TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <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/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/bootswatch/3.3.4/cosmo/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" /> <link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" />
@ -14,17 +14,17 @@
<nav class="navbar navbar-inverse"> <nav class="navbar navbar-inverse">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="/">@Model.webLog.name</a> <a class="navbar-brand" href="/">@Model.WebLog.Name</a>
</div> </div>
<div class="navbar-left"> <div class="navbar-left">
<p class="navbar-text">@Model.pageTitle</p> <p class="navbar-text">@Model.PageTitle</p>
</div> </div>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
@If.isAuthenticated @If.IsAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li> <li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li> <li><a href="/user/logoff">@Translate.LogOff</a></li>
@EndIf @EndIf
@IfNot.isAuthenticated @IfNot.IsAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li> <li><a href="/user/logon">@Translate.LogOn</a></li>
@EndIf @EndIf
</ul> </ul>
@ -32,15 +32,15 @@
</nav> </nav>
</header> </header>
<div class="container"> <div class="container">
@Each.messages @Each.Messages
@Current.toDisplay @Current.ToDisplay
@EndEach @EndEach
@Section['Content']; @Section['Content'];
</div> </div>
<footer> <footer>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <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>
</div> </div>
</footer> </footer>

View File

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

View File

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

View File

@ -3,7 +3,7 @@
@Section['Content'] @Section['Content']
<div class="row"> <div class="row">
<div class="col-xs-6"> <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> <p>
<a href="/posts/list"><i class="fa fa-list-ul"></i> @Translate.ListAll</a> <a href="/posts/list"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
&nbsp; &nbsp; &nbsp; &nbsp;
@ -11,7 +11,7 @@
</p> </p>
</div> </div>
<div class="col-xs-6"> <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> <p>
<a href="/pages"><i class="fa fa-list-ul"></i> @Translate.ListAll</a> <a href="/pages"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
&nbsp; &nbsp; &nbsp; &nbsp;
@ -21,7 +21,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6"> <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> <p>
<a href="/categories"><i class="fa fa-list-ul"></i> @Translate.ListAll</a> <a href="/categories"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
&nbsp; &nbsp; &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'] @Master['admin/admin-layout']
@Section['Content'] @Section['Content']
<form action="/page/@Model.page.id/edit" method="post"> <form action="/page/@Model.Page.Id/edit" method="post">
@AntiForgeryToken @AntiForgeryToken
<div class="row"> <div class="row">
<div class="col-sm-9"> <div class="col-sm-9">
<div class="form-group"> <div class="form-group">
<label class="control-label" for="title">@Translate.Title</label> <label class="control-label" for="Title">@Translate.Title</label>
<input type="text" name="title" id="title" class="form-control" value="@Model.form.title" /> <input type="text" name="Title" id="Title" class="form-control" value="@Model.Form.Title" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label" for="permalink">@Translate.Permalink</label> <label class="control-label" for="Permalink">@Translate.Permalink</label>
<input type="text" name="permalink" id="permalink" class="form-control" value="@Model.form.permalink" /> <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> <p class="form-hint"><em>@Translate.startingWith</em> http://@Model.webLog.urlBase/ </p>
</div> </div>
<!-- // TODO: Markdown / HTML choice --> <!-- // TODO: Markdown / HTML choice -->
<div class="form-group"> <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> </div>
<div class="col-sm-3"> <div class="col-sm-3">
@ -26,16 +26,16 @@
@IfNot.isNew @IfNot.isNew
<div class="form-group"> <div class="form-group">
<label class="control-label">@Translate.PublishedDate</label> <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>
<div class="form-group"> <div class="form-group">
<label class="control-label">@Translate.LastUpdatedDate</label> <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> </div>
@EndIf @EndIf
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="showInPageList" id="showInPageList" @Model.pageListChecked /> <input type="checkbox" name="ShowInPageList" id="ShowInPageList" @Model.PageListChecked />
&nbsp; <label for="showInPageList">@Translate.ShowInPageList</label> &nbsp; <label for="ShowInPageList">@Translate.ShowInPageList</label>
</div> </div>
</div> </div>
</div> </div>
@ -51,7 +51,7 @@
<script type="text/javascript" src="/content/scripts/tinymce-init.js"></script> <script type="text/javascript" src="/content/scripts/tinymce-init.js"></script>
<script type="text/javascript"> <script type="text/javascript">
/* <![CDATA[ */ /* <![CDATA[ */
$(document).ready(function () { $("#title").focus() }) $(document).ready(function () { $("#Title").focus() })
/* ]]> */ /* ]]> */
</script> </script>
@EndSection @EndSection

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,13 @@
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta name="generator" content="@Model.generator" /> <meta name="generator" content="@Model.Generator" />
<title>@Model.displayPageTitle</title> <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="//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="/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="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/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/rss+xml" href="//@Model.WebLog.UrlBase/feed" />
@Section['Head']; @Section['Head'];
</head> </head>
<body> <body>
@ -17,20 +17,20 @@
<nav class="navbar navbar-default"> <nav class="navbar navbar-default">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="/">@Model.webLog.name</a> <a class="navbar-brand" href="/">@Model.WebLog.Name</a>
</div> </div>
<p class="navbar-text">@Model.webLogSubtitle</p> <p class="navbar-text">@Model.WebLogSubtitle</p>
<ul class="nav navbar-nav navbar-left"> <ul class="nav navbar-nav navbar-left">
@Each.webLog.pageList @Each.WebLog.PageList
<li><a href="/@Current.permalink">@Current.title</a></li> <li><a href="/@Current.Permalink">@Current.Title</a></li>
@EndEach @EndEach
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
@If.isAuthenticated @If.IsAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li> <li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li> <li><a href="/user/logoff">@Translate.LogOff</a></li>
@EndIf @EndIf
@IfNot.isAuthenticated @IfNot.IsAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li> <li><a href="/user/logon">@Translate.LogOn</a></li>
@EndIf @EndIf
</ul> </ul>

View File

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

View File

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