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:
parent
2574501ccd
commit
ac8fa084d1
5
src/Settings.FSharpLint
Normal file
5
src/Settings.FSharpLint
Normal 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>
|
@ -4,11 +4,11 @@ open System.Reflection
|
||||
open System.Runtime.CompilerServices
|
||||
open System.Runtime.InteropServices
|
||||
|
||||
[<assembly: AssemblyTitle("myWebLog.Data")>]
|
||||
[<assembly: AssemblyTitle("MyWebLog.Data")>]
|
||||
[<assembly: AssemblyDescription("Data access for myWebLog")>]
|
||||
[<assembly: AssemblyConfiguration("")>]
|
||||
[<assembly: AssemblyCompany("DJS Consulting")>]
|
||||
[<assembly: AssemblyProduct("myWebLog.Data")>]
|
||||
[<assembly: AssemblyProduct("MyWebLog.Data")>]
|
||||
[<assembly: AssemblyCopyright("Copyright © 2016")>]
|
||||
[<assembly: AssemblyTrademark("")>]
|
||||
[<assembly: AssemblyCulture("")>]
|
||||
|
@ -1,8 +1,9 @@
|
||||
module myWebLog.Data.Category
|
||||
module MyWebLog.Data.Category
|
||||
|
||||
open FSharp.Interop.Dynamic
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Entities
|
||||
open Rethink
|
||||
open RethinkDb.Driver.Ast
|
||||
open System.Dynamic
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
@ -11,18 +12,18 @@ let private r = RethinkDb.Driver.RethinkDB.R
|
||||
let private category (webLogId : string) (catId : string) =
|
||||
r.Table(Table.Category)
|
||||
.Get(catId)
|
||||
.Filter(fun c -> c.["webLogId"].Eq(webLogId))
|
||||
.Filter(fun c -> c.["WebLogId"].Eq(webLogId))
|
||||
|
||||
/// Sort categories by their name, with their children sorted below them, including an indent level
|
||||
let sortCategories categories =
|
||||
let rec getChildren (cat : Category) indent =
|
||||
seq {
|
||||
yield cat, indent
|
||||
for child in categories |> List.filter (fun c -> c.parentId = Some cat.id) do
|
||||
for child in categories |> List.filter (fun c -> c.ParentId = Some cat.Id) do
|
||||
yield! getChildren child (indent + 1)
|
||||
}
|
||||
categories
|
||||
|> List.filter (fun c -> c.parentId.IsNone)
|
||||
|> List.filter (fun c -> c.ParentId.IsNone)
|
||||
|> List.map (fun c -> getChildren c 0)
|
||||
|> Seq.collect id
|
||||
|> Seq.toList
|
||||
@ -30,8 +31,8 @@ let sortCategories categories =
|
||||
/// Get all categories for a web log
|
||||
let getAllCategories conn (webLogId : string) =
|
||||
r.Table(Table.Category)
|
||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
||||
.OrderBy("name")
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy("Name")
|
||||
.RunListAsync<Category>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
@ -46,28 +47,28 @@ let tryFindCategory conn webLogId catId : Category option =
|
||||
|
||||
/// Save a category
|
||||
let saveCategory conn webLogId (cat : Category) =
|
||||
match cat.id with
|
||||
| "new" -> let newCat = { cat with id = string <| System.Guid.NewGuid()
|
||||
webLogId = webLogId }
|
||||
match cat.Id with
|
||||
| "new" -> let newCat = { cat with Id = string <| System.Guid.NewGuid()
|
||||
WebLogId = webLogId }
|
||||
r.Table(Table.Category)
|
||||
.Insert(newCat)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
newCat.id
|
||||
newCat.Id
|
||||
| _ -> let upd8 = ExpandoObject()
|
||||
upd8?name <- cat.name
|
||||
upd8?slug <- cat.slug
|
||||
upd8?description <- cat.description
|
||||
upd8?parentId <- cat.parentId
|
||||
(category webLogId cat.id)
|
||||
upd8?Name <- cat.Name
|
||||
upd8?Slug <- cat.Slug
|
||||
upd8?Description <- cat.Description
|
||||
upd8?ParentId <- cat.ParentId
|
||||
(category webLogId cat.Id)
|
||||
.Update(upd8)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
cat.id
|
||||
cat.Id
|
||||
|
||||
/// Remove a category from a given parent
|
||||
let removeCategoryFromParent conn webLogId parentId catId =
|
||||
match tryFindCategory conn webLogId parentId with
|
||||
| Some parent -> let upd8 = ExpandoObject()
|
||||
upd8?children <- parent.children
|
||||
upd8?Children <- parent.Children
|
||||
|> List.filter (fun childId -> childId <> catId)
|
||||
(category webLogId parentId)
|
||||
.Update(upd8)
|
||||
@ -78,7 +79,7 @@ let removeCategoryFromParent conn webLogId parentId catId =
|
||||
let addCategoryToParent conn webLogId parentId catId =
|
||||
match tryFindCategory conn webLogId parentId with
|
||||
| Some parent -> let upd8 = ExpandoObject()
|
||||
upd8?children <- catId :: parent.children
|
||||
upd8?Children <- catId :: parent.Children
|
||||
(category webLogId parentId)
|
||||
.Update(upd8)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
@ -87,40 +88,40 @@ let addCategoryToParent conn webLogId parentId catId =
|
||||
/// Delete a category
|
||||
let deleteCategory conn cat =
|
||||
// Remove the category from its parent
|
||||
match cat.parentId with
|
||||
| Some parentId -> removeCategoryFromParent conn cat.webLogId parentId cat.id
|
||||
match cat.ParentId with
|
||||
| Some parentId -> removeCategoryFromParent conn cat.WebLogId parentId cat.Id
|
||||
| None -> ()
|
||||
// Move this category's children to its parent
|
||||
let newParent = ExpandoObject()
|
||||
newParent?parentId <- cat.parentId
|
||||
cat.children
|
||||
|> List.iter (fun childId -> (category cat.webLogId childId)
|
||||
newParent?ParentId <- cat.ParentId
|
||||
cat.Children
|
||||
|> List.iter (fun childId -> (category cat.WebLogId childId)
|
||||
.Update(newParent)
|
||||
.RunResultAsync(conn) |> await |> ignore)
|
||||
// Remove the category from posts where it is assigned
|
||||
r.Table(Table.Post)
|
||||
.GetAll(cat.webLogId).OptArg("index", "webLogId")
|
||||
.Filter(fun p -> p.["categoryIds"].Contains(cat.id))
|
||||
.GetAll(cat.WebLogId).OptArg("index", "WebLogId")
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["CategoryIds"].Contains(cat.Id)))
|
||||
.RunCursorAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|> List.iter (fun post -> let newCats = ExpandoObject()
|
||||
newCats?categoryIds <- post.categoryIds
|
||||
|> List.filter (fun c -> c <> cat.id)
|
||||
newCats?CategoryIds <- post.CategoryIds
|
||||
|> List.filter (fun c -> c <> cat.Id)
|
||||
r.Table(Table.Post)
|
||||
.Get(post.id)
|
||||
.Get(post.Id)
|
||||
.Update(newCats)
|
||||
.RunResultAsync(conn) |> await |> ignore)
|
||||
// Now, delete the category
|
||||
r.Table(Table.Category)
|
||||
.Get(cat.id)
|
||||
.Get(cat.Id)
|
||||
.Delete()
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
|
||||
/// Get a category by its slug
|
||||
let tryFindCategoryBySlug conn (webLogId : string) (slug : string) =
|
||||
r.Table(Table.Category)
|
||||
.GetAll(r.Array(webLogId, slug)).OptArg("index", "slug")
|
||||
.GetAll(r.Array(webLogId, slug)).OptArg("index", "Slug")
|
||||
.RunCursorAsync<Category>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
@ -1,47 +1,59 @@
|
||||
namespace myWebLog.Data
|
||||
namespace MyWebLog.Data
|
||||
|
||||
open RethinkDb.Driver
|
||||
open RethinkDb.Driver.Net
|
||||
open Newtonsoft.Json
|
||||
|
||||
/// Data configuration
|
||||
type DataConfig = {
|
||||
/// The hostname for the RethinkDB server
|
||||
hostname : string
|
||||
/// The port for the RethinkDB server
|
||||
port : int
|
||||
/// The authorization key to use when connecting to the server
|
||||
authKey : string
|
||||
/// How long an attempt to connect to the server should wait before giving up
|
||||
timeout : int
|
||||
/// The name of the default database to use on the connection
|
||||
database : string
|
||||
/// A connection to the RethinkDB server using the configuration in this object
|
||||
conn : IConnection
|
||||
}
|
||||
type DataConfig =
|
||||
{ /// The hostname for the RethinkDB server
|
||||
[<JsonProperty("hostname")>]
|
||||
Hostname : string
|
||||
/// The port for the RethinkDB server
|
||||
[<JsonProperty("port")>]
|
||||
Port : int
|
||||
/// The authorization key to use when connecting to the server
|
||||
[<JsonProperty("authKey")>]
|
||||
AuthKey : string
|
||||
/// How long an attempt to connect to the server should wait before giving up
|
||||
[<JsonProperty("timeout")>]
|
||||
Timeout : int
|
||||
/// The name of the default database to use on the connection
|
||||
[<JsonProperty("database")>]
|
||||
Database : string
|
||||
/// A connection to the RethinkDB server using the configuration in this object
|
||||
[<JsonIgnore>]
|
||||
Conn : IConnection }
|
||||
with
|
||||
/// Create a data configuration from JSON
|
||||
static member fromJson json =
|
||||
let mutable cfg = JsonConvert.DeserializeObject<DataConfig> json
|
||||
cfg <- match cfg.hostname with
|
||||
| null -> { cfg with hostname = RethinkDBConstants.DefaultHostname }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.port with
|
||||
| 0 -> { cfg with port = RethinkDBConstants.DefaultPort }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.authKey with
|
||||
| null -> { cfg with authKey = RethinkDBConstants.DefaultAuthkey }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.timeout with
|
||||
| 0 -> { cfg with timeout = RethinkDBConstants.DefaultTimeout }
|
||||
| _ -> cfg
|
||||
cfg <- match cfg.database with
|
||||
| null -> { cfg with database = RethinkDBConstants.DefaultDbName }
|
||||
| _ -> cfg
|
||||
{ cfg with conn = RethinkDB.R.Connection()
|
||||
.Hostname(cfg.hostname)
|
||||
.Port(cfg.port)
|
||||
.AuthKey(cfg.authKey)
|
||||
.Db(cfg.database)
|
||||
.Timeout(cfg.timeout)
|
||||
.Connect() }
|
||||
static member FromJson json =
|
||||
let ensureHostname cfg = match cfg.Hostname with
|
||||
| null -> { cfg with Hostname = RethinkDBConstants.DefaultHostname }
|
||||
| _ -> cfg
|
||||
let ensurePort cfg = match cfg.Port with
|
||||
| 0 -> { cfg with Port = RethinkDBConstants.DefaultPort }
|
||||
| _ -> cfg
|
||||
let ensureAuthKey cfg = match cfg.AuthKey with
|
||||
| null -> { cfg with AuthKey = RethinkDBConstants.DefaultAuthkey }
|
||||
| _ -> cfg
|
||||
let ensureTimeout cfg = match cfg.Timeout with
|
||||
| 0 -> { cfg with Timeout = RethinkDBConstants.DefaultTimeout }
|
||||
| _ -> cfg
|
||||
let ensureDatabase cfg = match cfg.Database with
|
||||
| null -> { cfg with Database = RethinkDBConstants.DefaultDbName }
|
||||
| _ -> cfg
|
||||
let connect cfg = { cfg with Conn = RethinkDB.R.Connection()
|
||||
.Hostname(cfg.Hostname)
|
||||
.Port(cfg.Port)
|
||||
.AuthKey(cfg.AuthKey)
|
||||
.Db(cfg.Database)
|
||||
.Timeout(cfg.Timeout)
|
||||
.Connect() }
|
||||
JsonConvert.DeserializeObject<DataConfig> json
|
||||
|> ensureHostname
|
||||
|> ensurePort
|
||||
|> ensureAuthKey
|
||||
|> ensureTimeout
|
||||
|> ensureDatabase
|
||||
|> connect
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace myWebLog.Entities
|
||||
namespace MyWebLog.Entities
|
||||
|
||||
open Newtonsoft.Json
|
||||
|
||||
@ -37,261 +37,244 @@ module CommentStatus =
|
||||
// ---- Entities ----
|
||||
|
||||
/// A revision of a post or page
|
||||
type Revision = {
|
||||
/// The instant which this revision was saved
|
||||
asOf : int64
|
||||
/// The source language
|
||||
sourceType : string
|
||||
/// The text
|
||||
text : string
|
||||
}
|
||||
type Revision =
|
||||
{ /// The instant which this revision was saved
|
||||
AsOf : int64
|
||||
/// The source language
|
||||
SourceType : string
|
||||
/// The text
|
||||
Text : string }
|
||||
with
|
||||
/// An empty revision
|
||||
static member empty =
|
||||
{ asOf = int64 0
|
||||
sourceType = RevisionSource.HTML
|
||||
text = "" }
|
||||
static member Empty =
|
||||
{ AsOf = int64 0
|
||||
SourceType = RevisionSource.HTML
|
||||
Text = "" }
|
||||
|
||||
/// A page with static content
|
||||
type Page = {
|
||||
/// The Id
|
||||
id : string
|
||||
/// The Id of the web log to which this page belongs
|
||||
webLogId : string
|
||||
/// The Id of the author of this page
|
||||
authorId : string
|
||||
/// The title of the page
|
||||
title : string
|
||||
/// The link at which this page is displayed
|
||||
permalink : string
|
||||
/// The instant this page was published
|
||||
publishedOn : int64
|
||||
/// The instant this page was last updated
|
||||
updatedOn : int64
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
showInPageList : bool
|
||||
/// The current text of the page
|
||||
text : string
|
||||
/// Revisions of this page
|
||||
revisions : Revision list
|
||||
}
|
||||
type Page =
|
||||
{ /// The Id
|
||||
Id : string
|
||||
/// The Id of the web log to which this page belongs
|
||||
WebLogId : string
|
||||
/// The Id of the author of this page
|
||||
AuthorId : string
|
||||
/// The title of the page
|
||||
Title : string
|
||||
/// The link at which this page is displayed
|
||||
Permalink : string
|
||||
/// The instant this page was published
|
||||
PublishedOn : int64
|
||||
/// The instant this page was last updated
|
||||
UpdatedOn : int64
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
ShowInPageList : bool
|
||||
/// The current text of the page
|
||||
Text : string
|
||||
/// Revisions of this page
|
||||
Revisions : Revision list }
|
||||
with
|
||||
static member empty =
|
||||
{ id = ""
|
||||
webLogId = ""
|
||||
authorId = ""
|
||||
title = ""
|
||||
permalink = ""
|
||||
publishedOn = int64 0
|
||||
updatedOn = int64 0
|
||||
showInPageList = false
|
||||
text = ""
|
||||
revisions = List.empty
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
WebLogId = ""
|
||||
AuthorId = ""
|
||||
Title = ""
|
||||
Permalink = ""
|
||||
PublishedOn = int64 0
|
||||
UpdatedOn = int64 0
|
||||
ShowInPageList = false
|
||||
Text = ""
|
||||
Revisions = List.empty
|
||||
}
|
||||
|
||||
|
||||
/// An entry in the list of pages displayed as part of the web log (derived via query)
|
||||
type PageListEntry = {
|
||||
permalink : string
|
||||
title : string
|
||||
}
|
||||
type PageListEntry =
|
||||
{ Permalink : string
|
||||
Title : string }
|
||||
|
||||
/// A web log
|
||||
type WebLog = {
|
||||
/// The Id
|
||||
id : string
|
||||
/// The name
|
||||
name : string
|
||||
/// The subtitle
|
||||
subtitle : string option
|
||||
/// The default page ("posts" or a page Id)
|
||||
defaultPage : string
|
||||
/// The path of the theme (within /views/themes)
|
||||
themePath : string
|
||||
/// The URL base
|
||||
urlBase : string
|
||||
/// The time zone in which dates/times should be displayed
|
||||
timeZone : string
|
||||
/// A list of pages to be rendered as part of the site navigation
|
||||
[<JsonIgnore>]
|
||||
pageList : PageListEntry list
|
||||
}
|
||||
type WebLog =
|
||||
{ /// The Id
|
||||
Id : string
|
||||
/// The name
|
||||
Name : string
|
||||
/// The subtitle
|
||||
Subtitle : string option
|
||||
/// The default page ("posts" or a page Id)
|
||||
DefaultPage : string
|
||||
/// The path of the theme (within /views/themes)
|
||||
ThemePath : string
|
||||
/// The URL base
|
||||
UrlBase : string
|
||||
/// The time zone in which dates/times should be displayed
|
||||
TimeZone : string
|
||||
/// A list of pages to be rendered as part of the site navigation (not stored)
|
||||
PageList : PageListEntry list }
|
||||
with
|
||||
/// An empty web log
|
||||
static member empty =
|
||||
{ id = ""
|
||||
name = ""
|
||||
subtitle = None
|
||||
defaultPage = ""
|
||||
themePath = "default"
|
||||
urlBase = ""
|
||||
timeZone = "America/New_York"
|
||||
pageList = List.empty
|
||||
}
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
Name = ""
|
||||
Subtitle = None
|
||||
DefaultPage = ""
|
||||
ThemePath = "default"
|
||||
UrlBase = ""
|
||||
TimeZone = "America/New_York"
|
||||
PageList = List.empty }
|
||||
|
||||
|
||||
/// An authorization between a user and a web log
|
||||
type Authorization = {
|
||||
/// The Id of the web log to which this authorization grants access
|
||||
webLogId : string
|
||||
/// The level of access granted by this authorization
|
||||
level : string
|
||||
}
|
||||
type Authorization =
|
||||
{ /// The Id of the web log to which this authorization grants access
|
||||
WebLogId : string
|
||||
/// The level of access granted by this authorization
|
||||
Level : string }
|
||||
|
||||
|
||||
/// A user of myWebLog
|
||||
type User = {
|
||||
/// The Id
|
||||
id : string
|
||||
/// The user name (e-mail address)
|
||||
userName : string
|
||||
/// The first name
|
||||
firstName : string
|
||||
/// The last name
|
||||
lastName : string
|
||||
/// The user's preferred name
|
||||
preferredName : string
|
||||
/// The hash of the user's password
|
||||
passwordHash : string
|
||||
/// The URL of the user's personal site
|
||||
url : string option
|
||||
/// The user's authorizations
|
||||
authorizations : Authorization list
|
||||
}
|
||||
type User =
|
||||
{ /// The Id
|
||||
Id : string
|
||||
/// The user name (e-mail address)
|
||||
UserName : string
|
||||
/// The first name
|
||||
FirstName : string
|
||||
/// The last name
|
||||
LastName : string
|
||||
/// The user's preferred name
|
||||
PreferredName : string
|
||||
/// The hash of the user's password
|
||||
PasswordHash : string
|
||||
/// The URL of the user's personal site
|
||||
Url : string option
|
||||
/// The user's authorizations
|
||||
Authorizations : Authorization list }
|
||||
with
|
||||
/// An empty user
|
||||
static member empty =
|
||||
{ id = ""
|
||||
userName = ""
|
||||
firstName = ""
|
||||
lastName = ""
|
||||
preferredName = ""
|
||||
passwordHash = ""
|
||||
url = None
|
||||
authorizations = List.empty
|
||||
}
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
UserName = ""
|
||||
FirstName = ""
|
||||
LastName = ""
|
||||
PreferredName = ""
|
||||
PasswordHash = ""
|
||||
Url = None
|
||||
Authorizations = List.empty }
|
||||
|
||||
/// Claims for this user
|
||||
[<JsonIgnore>]
|
||||
member this.claims = this.authorizations
|
||||
|> List.map (fun auth -> sprintf "%s|%s" auth.webLogId auth.level)
|
||||
member this.Claims = this.Authorizations
|
||||
|> List.map (fun auth -> sprintf "%s|%s" auth.WebLogId auth.Level)
|
||||
|
||||
|
||||
/// A category to which posts may be assigned
|
||||
type Category = {
|
||||
/// The Id
|
||||
id : string
|
||||
/// The Id of the web log to which this category belongs
|
||||
webLogId : string
|
||||
/// The displayed name
|
||||
name : string
|
||||
/// The slug (used in category URLs)
|
||||
slug : string
|
||||
/// A longer description of the category
|
||||
description : string option
|
||||
/// The parent Id of this category (if a subcategory)
|
||||
parentId : string option
|
||||
/// The categories for which this category is the parent
|
||||
children : string list
|
||||
}
|
||||
type Category =
|
||||
{ /// The Id
|
||||
Id : string
|
||||
/// The Id of the web log to which this category belongs
|
||||
WebLogId : string
|
||||
/// The displayed name
|
||||
Name : string
|
||||
/// The slug (used in category URLs)
|
||||
Slug : string
|
||||
/// A longer description of the category
|
||||
Description : string option
|
||||
/// The parent Id of this category (if a subcategory)
|
||||
ParentId : string option
|
||||
/// The categories for which this category is the parent
|
||||
Children : string list }
|
||||
with
|
||||
/// An empty category
|
||||
static member empty =
|
||||
{ id = "new"
|
||||
webLogId = ""
|
||||
name = ""
|
||||
slug = ""
|
||||
description = None
|
||||
parentId = None
|
||||
children = List.empty
|
||||
}
|
||||
{ Id = "new"
|
||||
WebLogId = ""
|
||||
Name = ""
|
||||
Slug = ""
|
||||
Description = None
|
||||
ParentId = None
|
||||
Children = List.empty }
|
||||
|
||||
|
||||
/// A comment (applies to a post)
|
||||
type Comment = {
|
||||
/// The Id
|
||||
id : string
|
||||
/// The Id of the post to which this comment applies
|
||||
postId : string
|
||||
/// The Id of the comment to which this comment is a reply
|
||||
inReplyToId : string option
|
||||
/// The name of the commentor
|
||||
name : string
|
||||
/// The e-mail address of the commentor
|
||||
email : string
|
||||
/// The URL of the commentor's personal website
|
||||
url : string option
|
||||
/// The status of the comment
|
||||
status : string
|
||||
/// The instant the comment was posted
|
||||
postedOn : int64
|
||||
/// The text of the comment
|
||||
text : string
|
||||
}
|
||||
type Comment =
|
||||
{ /// The Id
|
||||
Id : string
|
||||
/// The Id of the post to which this comment applies
|
||||
PostId : string
|
||||
/// The Id of the comment to which this comment is a reply
|
||||
InReplyToId : string option
|
||||
/// The name of the commentor
|
||||
Name : string
|
||||
/// The e-mail address of the commentor
|
||||
Email : string
|
||||
/// The URL of the commentor's personal website
|
||||
Url : string option
|
||||
/// The status of the comment
|
||||
Status : string
|
||||
/// The instant the comment was posted
|
||||
PostedOn : int64
|
||||
/// The text of the comment
|
||||
Text : string }
|
||||
with
|
||||
static member empty =
|
||||
{ id = ""
|
||||
postId = ""
|
||||
inReplyToId = None
|
||||
name = ""
|
||||
email = ""
|
||||
url = None
|
||||
status = CommentStatus.Pending
|
||||
postedOn = int64 0
|
||||
text = ""
|
||||
}
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
PostId = ""
|
||||
InReplyToId = None
|
||||
Name = ""
|
||||
Email = ""
|
||||
Url = None
|
||||
Status = CommentStatus.Pending
|
||||
PostedOn = int64 0
|
||||
Text = "" }
|
||||
|
||||
|
||||
/// A post
|
||||
type Post = {
|
||||
/// The Id
|
||||
id : string
|
||||
/// The Id of the web log to which this post belongs
|
||||
webLogId : string
|
||||
/// The Id of the author of this post
|
||||
authorId : string
|
||||
/// The status
|
||||
status : string
|
||||
/// The title
|
||||
title : string
|
||||
/// The link at which the post resides
|
||||
permalink : string
|
||||
/// The instant on which the post was originally published
|
||||
publishedOn : int64
|
||||
/// The instant on which the post was last updated
|
||||
updatedOn : int64
|
||||
/// The text of the post
|
||||
text : string
|
||||
/// The Ids of the categories to which this is assigned
|
||||
categoryIds : string list
|
||||
/// The tags for the post
|
||||
tags : string list
|
||||
/// The permalinks at which this post may have once resided
|
||||
priorPermalinks : string list
|
||||
/// Revisions of this post
|
||||
revisions : Revision list
|
||||
/// The categories to which this is assigned
|
||||
[<JsonIgnore>]
|
||||
categories : Category list
|
||||
/// The comments
|
||||
[<JsonIgnore>]
|
||||
comments : Comment list
|
||||
}
|
||||
type Post =
|
||||
{ /// The Id
|
||||
Id : string
|
||||
/// The Id of the web log to which this post belongs
|
||||
WebLogId : string
|
||||
/// The Id of the author of this post
|
||||
AuthorId : string
|
||||
/// The status
|
||||
Status : string
|
||||
/// The title
|
||||
Title : string
|
||||
/// The link at which the post resides
|
||||
Permalink : string
|
||||
/// The instant on which the post was originally published
|
||||
PublishedOn : int64
|
||||
/// The instant on which the post was last updated
|
||||
UpdatedOn : int64
|
||||
/// The text of the post
|
||||
Text : string
|
||||
/// The Ids of the categories to which this is assigned
|
||||
CategoryIds : string list
|
||||
/// The tags for the post
|
||||
Tags : string list
|
||||
/// The permalinks at which this post may have once resided
|
||||
PriorPermalinks : string list
|
||||
/// Revisions of this post
|
||||
Revisions : Revision list
|
||||
/// The categories to which this is assigned (not stored in database)
|
||||
Categories : Category list
|
||||
/// The comments (not stored in database)
|
||||
Comments : Comment list }
|
||||
with
|
||||
static member empty =
|
||||
{ id = "new"
|
||||
webLogId = ""
|
||||
authorId = ""
|
||||
status = PostStatus.Draft
|
||||
title = ""
|
||||
permalink = ""
|
||||
publishedOn = int64 0
|
||||
updatedOn = int64 0
|
||||
text = ""
|
||||
categoryIds = List.empty
|
||||
tags = List.empty
|
||||
priorPermalinks = List.empty
|
||||
revisions = List.empty
|
||||
categories = List.empty
|
||||
comments = List.empty
|
||||
}
|
||||
static member Empty =
|
||||
{ Id = "new"
|
||||
WebLogId = ""
|
||||
AuthorId = ""
|
||||
Status = PostStatus.Draft
|
||||
Title = ""
|
||||
Permalink = ""
|
||||
PublishedOn = int64 0
|
||||
UpdatedOn = int64 0
|
||||
Text = ""
|
||||
CategoryIds = List.empty
|
||||
Tags = List.empty
|
||||
PriorPermalinks = List.empty
|
||||
Revisions = List.empty
|
||||
Categories = List.empty
|
||||
Comments = List.empty }
|
||||
|
@ -1,7 +1,7 @@
|
||||
module myWebLog.Data.Page
|
||||
module MyWebLog.Data.Page
|
||||
|
||||
open FSharp.Interop.Dynamic
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Entities
|
||||
open Rethink
|
||||
open RethinkDb.Driver.Ast
|
||||
open System.Dynamic
|
||||
@ -12,7 +12,7 @@ let private r = RethinkDb.Driver.RethinkDB.R
|
||||
let private page (webLogId : string) (pageId : string) =
|
||||
r.Table(Table.Page)
|
||||
.Get(pageId)
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["webLogId"].Eq(webLogId)))
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["WebLogId"].Eq(webLogId)))
|
||||
|
||||
/// Get a page by its Id
|
||||
let tryFindPage conn webLogId pageId =
|
||||
@ -21,14 +21,14 @@ let tryFindPage conn webLogId pageId =
|
||||
.RunAtomAsync<Page>(conn) |> await |> box with
|
||||
| null -> None
|
||||
| page -> let pg : Page = unbox page
|
||||
match pg.webLogId = webLogId with
|
||||
match pg.WebLogId = webLogId with
|
||||
| true -> Some pg
|
||||
| _ -> None
|
||||
|
||||
/// Get a page by its Id (excluding revisions)
|
||||
let tryFindPageWithoutRevisions conn webLogId pageId : Page option =
|
||||
match (page webLogId pageId)
|
||||
.Without("revisions")
|
||||
.Without("Revisions")
|
||||
.RunAtomAsync<Page>(conn) |> await |> box with
|
||||
| null -> None
|
||||
| page -> Some <| unbox page
|
||||
@ -36,8 +36,8 @@ let tryFindPageWithoutRevisions conn webLogId pageId : Page option =
|
||||
/// Find a page by its permalink
|
||||
let tryFindPageByPermalink conn (webLogId : string) (permalink : string) =
|
||||
r.Table(Table.Page)
|
||||
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "permalink")
|
||||
.Without("revisions")
|
||||
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "Permalink")
|
||||
.Without("Revisions")
|
||||
.RunCursorAsync<Page>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
@ -45,32 +45,32 @@ let tryFindPageByPermalink conn (webLogId : string) (permalink : string) =
|
||||
/// Get a list of all pages (excludes page text and revisions)
|
||||
let findAllPages conn (webLogId : string) =
|
||||
r.Table(Table.Page)
|
||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
||||
.OrderBy("title")
|
||||
.Without("text", "revisions")
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy("Title")
|
||||
.Without("Text", "Revisions")
|
||||
.RunListAsync<Page>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|
||||
/// Save a page
|
||||
let savePage conn (pg : Page) =
|
||||
match pg.id with
|
||||
| "new" -> let newPage = { pg with id = string <| System.Guid.NewGuid() }
|
||||
match pg.Id with
|
||||
| "new" -> let newPage = { pg with Id = string <| System.Guid.NewGuid() }
|
||||
r.Table(Table.Page)
|
||||
.Insert(page)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
newPage.id
|
||||
newPage.Id
|
||||
| _ -> let upd8 = ExpandoObject()
|
||||
upd8?title <- pg.title
|
||||
upd8?permalink <- pg.permalink
|
||||
upd8?publishedOn <- pg.publishedOn
|
||||
upd8?updatedOn <- pg.updatedOn
|
||||
upd8?text <- pg.text
|
||||
upd8?revisions <- pg.revisions
|
||||
(page pg.webLogId pg.id)
|
||||
upd8?Title <- pg.Title
|
||||
upd8?Permalink <- pg.Permalink
|
||||
upd8?PublishedOn <- pg.PublishedOn
|
||||
upd8?UpdatedOn <- pg.UpdatedOn
|
||||
upd8?Text <- pg.Text
|
||||
upd8?Revisions <- pg.Revisions
|
||||
(page pg.WebLogId pg.Id)
|
||||
.Update(upd8)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
pg.id
|
||||
pg.Id
|
||||
|
||||
/// Delete a page
|
||||
let deletePage conn webLogId pageId =
|
||||
|
@ -1,7 +1,7 @@
|
||||
module myWebLog.Data.Post
|
||||
module MyWebLog.Data.Post
|
||||
|
||||
open FSharp.Interop.Dynamic
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Entities
|
||||
open Rethink
|
||||
open RethinkDb.Driver.Ast
|
||||
open System.Dynamic
|
||||
@ -11,22 +11,20 @@ let private r = RethinkDb.Driver.RethinkDB.R
|
||||
/// Shorthand to select all published posts for a web log
|
||||
let private publishedPosts (webLogId : string)=
|
||||
r.Table(Table.Post)
|
||||
.GetAll(r.Array(webLogId, PostStatus.Published)).OptArg("index", "webLogAndStatus")
|
||||
.GetAll(r.Array(webLogId, PostStatus.Published)).OptArg("index", "WebLogAndStatus")
|
||||
|
||||
/// Shorthand to sort posts by published date, slice for the given page, and return a list
|
||||
let private toPostList conn pageNbr nbrPerPage (filter : ReqlExpr) =
|
||||
filter
|
||||
.OrderBy(r.Desc("publishedOn"))
|
||||
.OrderBy(r.Desc("PublishedOn"))
|
||||
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
||||
.RunListAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|
||||
/// Shorthand to get a newer or older post
|
||||
// TODO: older posts need to sort by published on DESC
|
||||
//let private adjacentPost conn post (theFilter : ReqlExpr -> ReqlExpr) (sort :ReqlExpr) : Post option =
|
||||
let private adjacentPost conn post (theFilter : ReqlExpr -> obj) (sort : obj) : Post option =
|
||||
(publishedPosts post.webLogId)
|
||||
let private adjacentPost conn post (theFilter : ReqlExpr -> obj) (sort : obj) =
|
||||
(publishedPosts post.WebLogId)
|
||||
.Filter(theFilter)
|
||||
.OrderBy(sort)
|
||||
.Limit(1)
|
||||
@ -48,45 +46,45 @@ let findPageOfPublishedPosts conn webLogId pageNbr nbrPerPage =
|
||||
/// Get a page of published posts assigned to a given category
|
||||
let findPageOfCategorizedPosts conn webLogId (categoryId : string) pageNbr nbrPerPage =
|
||||
(publishedPosts webLogId)
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["categoryIds"].Contains(categoryId)))
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["CategoryIds"].Contains(categoryId)))
|
||||
|> toPostList conn pageNbr nbrPerPage
|
||||
|
||||
/// Get a page of published posts tagged with a given tag
|
||||
let findPageOfTaggedPosts conn webLogId (tag : string) pageNbr nbrPerPage =
|
||||
(publishedPosts webLogId)
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["tags"].Contains(tag)))
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["Tags"].Contains(tag)))
|
||||
|> toPostList conn pageNbr nbrPerPage
|
||||
|
||||
/// Try to get the next newest post from the given post
|
||||
let tryFindNewerPost conn post = newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn))
|
||||
let tryFindNewerPost conn post = newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn))
|
||||
|
||||
/// Try to get the next newest post assigned to the given category
|
||||
let tryFindNewerCategorizedPost conn (categoryId : string) post =
|
||||
newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn)
|
||||
.And(p.["categoryIds"].Contains(categoryId)))
|
||||
newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn)
|
||||
.And(p.["CategoryIds"].Contains(categoryId)))
|
||||
|
||||
/// Try to get the next newest tagged post from the given tagged post
|
||||
let tryFindNewerTaggedPost conn (tag : string) post =
|
||||
newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn).And(p.["tags"].Contains(tag)))
|
||||
newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn).And(p.["Tags"].Contains(tag)))
|
||||
|
||||
/// Try to get the next oldest post from the given post
|
||||
let tryFindOlderPost conn post = olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn))
|
||||
let tryFindOlderPost conn post = olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn))
|
||||
|
||||
/// Try to get the next oldest post assigned to the given category
|
||||
let tryFindOlderCategorizedPost conn (categoryId : string) post =
|
||||
olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn)
|
||||
.And(p.["categoryIds"].Contains(categoryId)))
|
||||
olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn)
|
||||
.And(p.["CategoryIds"].Contains(categoryId)))
|
||||
|
||||
/// Try to get the next oldest tagged post from the given tagged post
|
||||
let tryFindOlderTaggedPost conn (tag : string) post =
|
||||
olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn).And(p.["tags"].Contains(tag)))
|
||||
olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn).And(p.["Tags"].Contains(tag)))
|
||||
|
||||
/// Get a page of all posts in all statuses
|
||||
let findPageOfAllPosts conn (webLogId : string) pageNbr nbrPerPage =
|
||||
// FIXME: sort unpublished posts by their last updated date
|
||||
r.Table(Table.Post)
|
||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
||||
.OrderBy(r.Desc("publishedOn"))
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy(r.Desc("PublishedOn"))
|
||||
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
||||
.RunListAsync<Post>(conn)
|
||||
|> await
|
||||
@ -96,7 +94,7 @@ let findPageOfAllPosts conn (webLogId : string) pageNbr nbrPerPage =
|
||||
let tryFindPost conn webLogId postId : Post option =
|
||||
match r.Table(Table.Post)
|
||||
.Get(postId)
|
||||
.Filter(fun p -> p.["webLogId"].Eq(webLogId))
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["WebLogId"].Eq(webLogId)))
|
||||
.RunAtomAsync<Post>(conn)
|
||||
|> box with
|
||||
| null -> None
|
||||
@ -106,27 +104,22 @@ let tryFindPost conn webLogId postId : Post option =
|
||||
// TODO: see if we can make .Merge work for page list even though the attribute is ignored
|
||||
// (needs to be ignored for serialization, but included for deserialization)
|
||||
let tryFindPostByPermalink conn webLogId permalink =
|
||||
match r.Table(Table.Post)
|
||||
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "permalink")
|
||||
.Filter(fun p -> p.["status"].Eq(PostStatus.Published))
|
||||
.Without("revisions")
|
||||
.RunCursorAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead with
|
||||
| Some p -> Some { p with categories = r.Table(Table.Category)
|
||||
.GetAll(p.categoryIds |> List.toArray)
|
||||
.Without("children")
|
||||
.OrderBy("name")
|
||||
.RunListAsync<Category>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
comments = r.Table(Table.Comment)
|
||||
.GetAll(p.id).OptArg("index", "postId")
|
||||
.OrderBy("postedOn")
|
||||
.RunListAsync<Comment>(conn)
|
||||
|> await
|
||||
|> Seq.toList }
|
||||
| None -> None
|
||||
r.Table(Table.Post)
|
||||
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "Permalink")
|
||||
.Filter(fun p -> p.["Status"].Eq(PostStatus.Published))
|
||||
.Without("Revisions")
|
||||
.Merge(fun p -> r.HashMap("Categories", r.Table(Table.Category)
|
||||
.GetAll(p.["CategoryIds"])
|
||||
.Without("Children")
|
||||
.OrderBy("Name")
|
||||
.CoerceTo("array")))
|
||||
.Merge(fun p -> r.HashMap("Comments", r.Table(Table.Comment)
|
||||
.GetAll(p.["Id"]).OptArg("index", "PostId")
|
||||
.OrderBy("PostedOn")
|
||||
.CoerceTo("array")))
|
||||
.RunCursorAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Try to find a post by its prior permalink
|
||||
let tryFindPostByPriorPermalink conn (webLogId : string) (permalink : string) =
|
||||
@ -140,34 +133,34 @@ let tryFindPostByPriorPermalink conn (webLogId : string) (permalink : string) =
|
||||
|
||||
/// Get a set of posts for RSS
|
||||
let findFeedPosts conn webLogId nbr : (Post * User option) list =
|
||||
findPageOfPublishedPosts conn webLogId 1 nbr
|
||||
|> List.map (fun post -> { post with categories = r.Table(Table.Category)
|
||||
.GetAll(post.categoryIds |> List.toArray)
|
||||
.OrderBy("name")
|
||||
.Pluck("id", "name")
|
||||
.RunListAsync<Category>(conn)
|
||||
|> await
|
||||
|> Seq.toList },
|
||||
(match r.Table(Table.User)
|
||||
.Get(post.authorId)
|
||||
.RunAtomAsync<User>(conn)
|
||||
|> await
|
||||
|> box with
|
||||
| null -> None
|
||||
| user -> Some <| unbox user))
|
||||
(publishedPosts webLogId)
|
||||
.Merge(fun post -> r.HashMap("Categories", r.Table(Table.Category)
|
||||
.GetAll(post.["CategoryIds"])
|
||||
.OrderBy("Name")
|
||||
.Pluck("Id", "Name")
|
||||
.CoerceTo("array")))
|
||||
|> toPostList conn 1 nbr
|
||||
|> List.map (fun post -> post, match r.Table(Table.User)
|
||||
.Get(post.AuthorId)
|
||||
.RunAtomAsync<User>(conn)
|
||||
|> await
|
||||
|> box with
|
||||
| null -> None
|
||||
| user -> Some <| unbox user)
|
||||
|
||||
/// Save a post
|
||||
let savePost conn post =
|
||||
match post.id with
|
||||
| "new" -> let newPost = { post with id = string <| System.Guid.NewGuid() }
|
||||
match post.Id with
|
||||
| "new" -> let newPost = { post with Id = string <| System.Guid.NewGuid() }
|
||||
r.Table(Table.Post)
|
||||
.Insert(newPost)
|
||||
.RunResultAsync(conn)
|
||||
|> ignore
|
||||
newPost.id
|
||||
newPost.Id
|
||||
| _ -> r.Table(Table.Post)
|
||||
.Get(post.id)
|
||||
.Replace(post)
|
||||
.Get(post.Id)
|
||||
.Replace( { post with Categories = List.empty
|
||||
Comments = List.empty } )
|
||||
.RunResultAsync(conn)
|
||||
|> ignore
|
||||
post.id
|
||||
post.Id
|
||||
|
@ -1,4 +1,4 @@
|
||||
module myWebLog.Data.Rethink
|
||||
module MyWebLog.Data.Rethink
|
||||
|
||||
open RethinkDb.Driver.Ast
|
||||
open RethinkDb.Driver.Net
|
||||
|
@ -1,4 +1,4 @@
|
||||
module myWebLog.Data.SetUp
|
||||
module MyWebLog.Data.SetUp
|
||||
|
||||
open Rethink
|
||||
open RethinkDb.Driver.Ast
|
||||
@ -12,17 +12,17 @@ let private logStepDone () = Console.Out.WriteLine (" done.")
|
||||
/// Ensure the myWebLog database exists
|
||||
let checkDatabase (cfg : DataConfig) =
|
||||
logStep "|> Checking database"
|
||||
let dbs = r.DbList().RunListAsync<string>(cfg.conn) |> await
|
||||
match dbs.Contains cfg.database with
|
||||
let dbs = r.DbList().RunListAsync<string>(cfg.Conn) |> await
|
||||
match dbs.Contains cfg.Database with
|
||||
| true -> ()
|
||||
| _ -> logStepStart (sprintf " %s database not found - creating" cfg.database)
|
||||
r.DbCreate(cfg.database).RunResultAsync(cfg.conn) |> await |> ignore
|
||||
| _ -> logStepStart (sprintf " %s database not found - creating" cfg.Database)
|
||||
r.DbCreate(cfg.Database).RunResultAsync(cfg.Conn) |> await |> ignore
|
||||
logStepDone ()
|
||||
|
||||
/// Ensure all required tables exist
|
||||
let checkTables cfg =
|
||||
logStep "|> Checking tables"
|
||||
let tables = r.Db(cfg.database).TableList().RunListAsync<string>(cfg.conn) |> await
|
||||
let tables = r.Db(cfg.Database).TableList().RunListAsync<string>(cfg.Conn) |> await
|
||||
[ Table.Category; Table.Comment; Table.Page; Table.Post; Table.User; Table.WebLog ]
|
||||
|> List.map (fun tbl -> match tables.Contains tbl with
|
||||
| true -> None
|
||||
@ -30,27 +30,27 @@ let checkTables cfg =
|
||||
|> List.filter (fun create -> create.IsSome)
|
||||
|> List.map (fun create -> create.Value)
|
||||
|> List.iter (fun (tbl, create) -> logStepStart (sprintf " Creating table %s" tbl)
|
||||
create.RunResultAsync(cfg.conn) |> await |> ignore
|
||||
create.RunResultAsync(cfg.Conn) |> await |> ignore
|
||||
logStepDone ())
|
||||
|
||||
/// Shorthand to get the table
|
||||
let tbl cfg table = r.Db(cfg.database).Table(table)
|
||||
let tbl cfg table = r.Db(cfg.Database).Table(table)
|
||||
|
||||
/// Create the given index
|
||||
let createIndex cfg table (index : string * (ReqlExpr -> obj) option) =
|
||||
let idxName, idxFunc = index
|
||||
logStepStart (sprintf """ Creating index "%s" on table %s""" idxName table)
|
||||
match idxFunc with
|
||||
| Some f -> (tbl cfg table).IndexCreate(idxName, f).RunResultAsync(cfg.conn)
|
||||
| None -> (tbl cfg table).IndexCreate(idxName ).RunResultAsync(cfg.conn)
|
||||
| Some f -> (tbl cfg table).IndexCreate(idxName, f).RunResultAsync(cfg.Conn)
|
||||
| None -> (tbl cfg table).IndexCreate(idxName ).RunResultAsync(cfg.Conn)
|
||||
|> await |> ignore
|
||||
(tbl cfg table).IndexWait(idxName).RunAtomAsync(cfg.conn) |> await |> ignore
|
||||
(tbl cfg table).IndexWait(idxName).RunAtomAsync(cfg.Conn) |> await |> ignore
|
||||
logStepDone ()
|
||||
|
||||
/// Ensure that the given indexes exist, and create them if required
|
||||
let ensureIndexes cfg (indexes : (string * (string * (ReqlExpr -> obj) option) list) list) =
|
||||
let ensureForTable tabl =
|
||||
let idx = (tbl cfg (fst tabl)).IndexList().RunListAsync<string>(cfg.conn) |> await
|
||||
let idx = (tbl cfg (fst tabl)).IndexList().RunListAsync<string>(cfg.Conn) |> await
|
||||
snd tabl
|
||||
|> List.iter (fun index -> match idx.Contains (fst index) with
|
||||
| true -> ()
|
||||
@ -68,21 +68,21 @@ let webLogField (name : string) : (ReqlExpr -> obj) option =
|
||||
/// Ensure all the required indexes exist
|
||||
let checkIndexes cfg =
|
||||
logStep "|> Checking indexes"
|
||||
[ Table.Category, [ "webLogId", None
|
||||
"slug", webLogField "slug"
|
||||
[ Table.Category, [ "WebLogId", None
|
||||
"Slug", webLogField "Slug"
|
||||
]
|
||||
Table.Comment, [ "postId", None
|
||||
Table.Comment, [ "PostId", None
|
||||
]
|
||||
Table.Page, [ "webLogId", None
|
||||
"permalink", webLogField "permalink"
|
||||
Table.Page, [ "WebLogId", None
|
||||
"Permalink", webLogField "Permalink"
|
||||
]
|
||||
Table.Post, [ "webLogId", None
|
||||
"webLogAndStatus", webLogField "status"
|
||||
"permalink", webLogField "permalink"
|
||||
Table.Post, [ "WebLogId", None
|
||||
"WebLogAndStatus", webLogField "Status"
|
||||
"Permalink", webLogField "Permalink"
|
||||
]
|
||||
Table.User, [ "userName", None
|
||||
Table.User, [ "UserName", None
|
||||
]
|
||||
Table.WebLog, [ "urlBase", None
|
||||
Table.WebLog, [ "UrlBase", None
|
||||
]
|
||||
]
|
||||
|> ensureIndexes cfg
|
||||
|
@ -1,4 +1,4 @@
|
||||
module myWebLog.Data.Table
|
||||
module MyWebLog.Data.Table
|
||||
|
||||
/// The Category table
|
||||
let Category = "Category"
|
||||
|
@ -1,6 +1,6 @@
|
||||
module myWebLog.Data.User
|
||||
module MyWebLog.Data.User
|
||||
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Entities
|
||||
open Rethink
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
@ -11,8 +11,8 @@ let private r = RethinkDb.Driver.RethinkDB.R
|
||||
// http://rethinkdb.com/docs/secondary-indexes/java/ for more information.
|
||||
let tryUserLogOn conn (email : string) (passwordHash : string) =
|
||||
r.Table(Table.User)
|
||||
.GetAll(email).OptArg("index", "userName")
|
||||
.Filter(fun u -> u.["passwordHash"].Eq(passwordHash))
|
||||
.GetAll(email).OptArg("index", "UserName")
|
||||
.Filter(fun u -> u.["PasswordHash"].Eq(passwordHash))
|
||||
.RunCursorAsync<User>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
@ -1,43 +1,39 @@
|
||||
module myWebLog.Data.WebLog
|
||||
module MyWebLog.Data.WebLog
|
||||
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Entities
|
||||
open Rethink
|
||||
open RethinkDb.Driver.Ast
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
|
||||
/// Counts of items displayed on the admin dashboard
|
||||
type DashboardCounts = {
|
||||
/// The number of pages for the web log
|
||||
pages : int
|
||||
/// The number of pages for the web log
|
||||
posts : int
|
||||
/// The number of categories for the web log
|
||||
categories : int
|
||||
}
|
||||
type DashboardCounts =
|
||||
{ /// The number of pages for the web log
|
||||
Pages : int
|
||||
/// The number of pages for the web log
|
||||
Posts : int
|
||||
/// The number of categories for the web log
|
||||
Categories : int }
|
||||
|
||||
/// Detemine the web log by the URL base
|
||||
// TODO: see if we can make .Merge work for page list even though the attribute is ignored
|
||||
// (needs to be ignored for serialization, but included for deserialization)
|
||||
let tryFindWebLogByUrlBase conn (urlBase : string) =
|
||||
let webLog = r.Table(Table.WebLog)
|
||||
.GetAll(urlBase).OptArg("index", "urlBase")
|
||||
.RunCursorAsync<WebLog>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
match webLog with
|
||||
| Some w -> Some { w with pageList = r.Table(Table.Page)
|
||||
.GetAll(w.id).OptArg("index", "webLogId")
|
||||
.Filter(fun pg -> pg.["showInPageList"].Eq(true))
|
||||
.OrderBy("title")
|
||||
.Pluck("title", "permalink")
|
||||
.RunListAsync<PageListEntry>(conn) |> await |> Seq.toList }
|
||||
| None -> None
|
||||
r.Table(Table.WebLog)
|
||||
.GetAll(urlBase).OptArg("index", "urlBase")
|
||||
.Merge(fun w -> r.HashMap("PageList", r.Table(Table.Page)
|
||||
.GetAll(w.["Id"]).OptArg("index", "WebLogId")
|
||||
.Filter(ReqlFunction1(fun pg -> upcast pg.["ShowInPageList"].Eq(true)))
|
||||
.OrderBy("Title")
|
||||
.Pluck("Title", "Permalink")
|
||||
.CoerceTo("array")))
|
||||
.RunCursorAsync<WebLog>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Get counts for the admin dashboard
|
||||
let findDashboardCounts conn (webLogId : string) =
|
||||
r.Expr( r.HashMap("pages", r.Table(Table.Page ).GetAll(webLogId).OptArg("index", "webLogId").Count()))
|
||||
.Merge(r.HashMap("posts", r.Table(Table.Post ).GetAll(webLogId).OptArg("index", "webLogId").Count()))
|
||||
.Merge(r.HashMap("categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "webLogId").Count()))
|
||||
r.Expr( r.HashMap("Pages", r.Table(Table.Page ).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
|
||||
.Merge(r.HashMap("Posts", r.Table(Table.Post ).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
|
||||
.Merge(r.HashMap("Categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
|
||||
.RunAtomAsync<DashboardCounts>(conn)
|
||||
|> await
|
||||
|
@ -8,7 +8,7 @@
|
||||
<ProjectGuid>1fba0b84-b09e-4b16-b9b6-5730dea27192</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>myWebLog.Data</RootNamespace>
|
||||
<AssemblyName>myWebLog.Data</AssemblyName>
|
||||
<AssemblyName>MyWebLog.Data</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
@ -22,7 +22,7 @@
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<WarningLevel>3</WarningLevel>
|
||||
<DocumentationFile>bin\Debug\myWebLog.Data.xml</DocumentationFile>
|
||||
<DocumentationFile>bin\Debug\MyWebLog.Data.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
|
4
src/myWebLog.Resources/Resources.Designer.cs
generated
4
src/myWebLog.Resources/Resources.Designer.cs
generated
@ -8,7 +8,7 @@
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace myWebLog {
|
||||
namespace MyWebLog {
|
||||
using System;
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ namespace myWebLog {
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("myWebLog.Resources", typeof(Resources).Assembly);
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MyWebLog.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
|
@ -7,8 +7,8 @@
|
||||
<ProjectGuid>{A12EA8DA-88BC-4447-90CB-A0E2DCC37523}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>myWebLog</RootNamespace>
|
||||
<AssemblyName>myWebLog.Resources</AssemblyName>
|
||||
<RootNamespace>MyWebLog</RootNamespace>
|
||||
<AssemblyName>MyWebLog.Resources</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace myWebLog
|
||||
namespace MyWebLog
|
||||
|
||||
open myWebLog.Data.WebLog
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Data.WebLog
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
@ -15,6 +15,6 @@ type AdminModule(conn : IConnection) as this =
|
||||
/// Admin dashboard
|
||||
member this.Dashboard () =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let model = DashboardModel(this.Context, this.WebLog, findDashboardCounts conn this.WebLog.id)
|
||||
model.pageTitle <- Resources.Dashboard
|
||||
let model = DashboardModel(this.Context, this.WebLog, findDashboardCounts conn this.WebLog.Id)
|
||||
model.PageTitle <- Resources.Dashboard
|
||||
upcast this.View.["admin/dashboard", model]
|
||||
|
@ -1,10 +1,10 @@
|
||||
module myWebLog.App
|
||||
module MyWebLog.App
|
||||
|
||||
open myWebLog
|
||||
open myWebLog.Data
|
||||
open myWebLog.Data.SetUp
|
||||
open myWebLog.Data.WebLog
|
||||
open myWebLog.Entities
|
||||
open MyWebLog
|
||||
open MyWebLog.Data
|
||||
open MyWebLog.Data.SetUp
|
||||
open MyWebLog.Data.WebLog
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.Authentication.Forms
|
||||
open Nancy.Bootstrapper
|
||||
@ -25,7 +25,7 @@ open System.IO
|
||||
open System.Text.RegularExpressions
|
||||
|
||||
/// Set up a database connection
|
||||
let cfg = try DataConfig.fromJson (System.IO.File.ReadAllText "data-config.json")
|
||||
let cfg = try DataConfig.FromJson (System.IO.File.ReadAllText "data-config.json")
|
||||
with ex -> raise <| ApplicationException(Resources.ErrDataConfig, ex)
|
||||
|
||||
do
|
||||
@ -37,7 +37,7 @@ type TranslateTokenViewEngineMatcher() =
|
||||
interface ISuperSimpleViewEngineMatcher with
|
||||
member this.Invoke (content, model, host) =
|
||||
regex.Replace(content, fun m -> let key = m.Groups.["TranslationKey"].Value
|
||||
match myWebLog.Resources.ResourceManager.GetString key with
|
||||
match MyWebLog.Resources.ResourceManager.GetString key with
|
||||
| null -> key
|
||||
| xlat -> xlat)
|
||||
|
||||
@ -54,8 +54,8 @@ type MyWebLogUserMapper(container : TinyIoCContainer) =
|
||||
|
||||
interface IUserMapper with
|
||||
member this.GetUserFromIdentifier (identifier, context) =
|
||||
match context.Request.PersistableSession.GetOrDefault(Keys.User, User.empty) with
|
||||
| user when user.id = string identifier -> upcast MyWebLogUser(user.preferredName, user.claims)
|
||||
match context.Request.PersistableSession.GetOrDefault(Keys.User, User.Empty) with
|
||||
| user when user.Id = string identifier -> upcast MyWebLogUser(user.PreferredName, user.Claims)
|
||||
| _ -> null
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@ type MyWebLogBootstrapper() =
|
||||
// Data configuration (both config and the connection; Nancy modules just need the connection)
|
||||
container.Register<DataConfig>(cfg)
|
||||
|> ignore
|
||||
container.Register<IConnection>(cfg.conn)
|
||||
container.Register<IConnection>(cfg.Conn)
|
||||
|> ignore
|
||||
// NodaTime
|
||||
container.Register<IClock>(SystemClock.Instance)
|
||||
@ -109,8 +109,8 @@ type MyWebLogBootstrapper() =
|
||||
// CSRF
|
||||
Csrf.Enable pipelines
|
||||
// Sessions
|
||||
let sessions = RethinkDbSessionConfiguration(cfg.conn)
|
||||
sessions.Database <- cfg.database
|
||||
let sessions = RethinkDbSessionConfiguration(cfg.Conn)
|
||||
sessions.Database <- cfg.Database
|
||||
PersistableSessions.Enable (pipelines, sessions)
|
||||
()
|
||||
|
||||
@ -128,17 +128,18 @@ let version =
|
||||
type RequestEnvironment() =
|
||||
interface IRequestStartup with
|
||||
member this.Initialize (pipelines, context) =
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline
|
||||
(fun ctx -> ctx.Items.[Keys.RequestStart] <- DateTime.Now.Ticks
|
||||
match tryFindWebLogByUrlBase cfg.conn ctx.Request.Url.HostName with
|
||||
| Some webLog -> ctx.Items.[Keys.WebLog] <- webLog
|
||||
| None -> ApplicationException
|
||||
(sprintf "%s %s" ctx.Request.Url.HostName Resources.ErrNotConfigured)
|
||||
|> raise
|
||||
ctx.Items.[Keys.Version] <- version
|
||||
null)
|
||||
let establishEnv (ctx : NancyContext) =
|
||||
ctx.Items.[Keys.RequestStart] <- DateTime.Now.Ticks
|
||||
match tryFindWebLogByUrlBase cfg.Conn ctx.Request.Url.HostName with
|
||||
| Some webLog -> ctx.Items.[Keys.WebLog] <- webLog
|
||||
| None -> // TODO: redirect to domain set up page
|
||||
ApplicationException (sprintf "%s %s" ctx.Request.Url.HostName Resources.ErrNotConfigured)
|
||||
|> raise
|
||||
ctx.Items.[Keys.Version] <- version
|
||||
null
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline establishEnv
|
||||
|
||||
|
||||
let app = OwinApp.ofMidFunc "/" (NancyMiddleware.UseNancy (NancyOptions()))
|
||||
|
||||
let run () = startWebServer defaultConfig app // webPart
|
||||
let Run () = startWebServer defaultConfig app // webPart
|
||||
|
@ -4,11 +4,11 @@ open System.Reflection
|
||||
open System.Runtime.CompilerServices
|
||||
open System.Runtime.InteropServices
|
||||
|
||||
[<assembly: AssemblyTitle("myWebLog.Web")>]
|
||||
[<assembly: AssemblyTitle("MyWebLog.Web")>]
|
||||
[<assembly: AssemblyDescription("Main Nancy assembly for myWebLog")>]
|
||||
[<assembly: AssemblyConfiguration("")>]
|
||||
[<assembly: AssemblyCompany("DJS Consulting")>]
|
||||
[<assembly: AssemblyProduct("myWebLog.Web")>]
|
||||
[<assembly: AssemblyProduct("MyWebLog.Web")>]
|
||||
[<assembly: AssemblyCopyright("Copyright © 2016")>]
|
||||
[<assembly: AssemblyTrademark("")>]
|
||||
[<assembly: AssemblyCulture("")>]
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace myWebLog
|
||||
namespace MyWebLog
|
||||
|
||||
open myWebLog.Data.Category
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Data.Category
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.ModelBinding
|
||||
open Nancy.Security
|
||||
@ -21,24 +21,21 @@ type CategoryModule(conn : IConnection) as this =
|
||||
member this.CategoryList () =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let model = CategoryListModel(this.Context, this.WebLog,
|
||||
(getAllCategories conn this.WebLog.id
|
||||
|> List.map (fun cat -> IndentedCategory.create cat (fun _ -> false))))
|
||||
(getAllCategories conn this.WebLog.Id
|
||||
|> List.map (fun cat -> IndentedCategory.Create cat (fun _ -> false))))
|
||||
upcast this.View.["/admin/category/list", model]
|
||||
|
||||
/// Edit a category
|
||||
member this.EditCategory (parameters : DynamicDictionary) =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let catId : string = downcast parameters.["id"]
|
||||
let catId = parameters.["id"].ToString ()
|
||||
match (match catId with
|
||||
| "new" -> Some Category.empty
|
||||
| _ -> tryFindCategory conn this.WebLog.id catId) with
|
||||
| _ -> tryFindCategory conn this.WebLog.Id catId) with
|
||||
| Some cat -> let model = CategoryEditModel(this.Context, this.WebLog, cat)
|
||||
let cats = getAllCategories conn this.WebLog.id
|
||||
|> List.map (fun cat -> IndentedCategory.create cat
|
||||
(fun c -> c = defaultArg (fst cat).parentId ""))
|
||||
model.categories <- getAllCategories conn this.WebLog.id
|
||||
|> List.map (fun cat -> IndentedCategory.create cat
|
||||
(fun c -> c = defaultArg (fst cat).parentId ""))
|
||||
model.Categories <- getAllCategories conn this.WebLog.Id
|
||||
|> List.map (fun cat -> IndentedCategory.Create cat
|
||||
(fun c -> c = defaultArg (fst cat).ParentId ""))
|
||||
upcast this.View.["admin/category/edit", model]
|
||||
| None -> this.NotFound ()
|
||||
|
||||
@ -46,32 +43,32 @@ type CategoryModule(conn : IConnection) as this =
|
||||
member this.SaveCategory (parameters : DynamicDictionary) =
|
||||
this.ValidateCsrfToken ()
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let catId : string = downcast parameters.["id"]
|
||||
let catId = parameters.["id"].ToString ()
|
||||
let form = this.Bind<CategoryForm> ()
|
||||
let oldCat = match catId with
|
||||
| "new" -> Some Category.empty
|
||||
| _ -> tryFindCategory conn this.WebLog.id catId
|
||||
| _ -> tryFindCategory conn this.WebLog.Id catId
|
||||
match oldCat with
|
||||
| Some old -> let cat = { old with name = form.name
|
||||
slug = form.slug
|
||||
description = match form.description with | "" -> None | d -> Some d
|
||||
parentId = match form.parentId with | "" -> None | p -> Some p }
|
||||
let newCatId = saveCategory conn this.WebLog.id cat
|
||||
match old.parentId = cat.parentId with
|
||||
| Some old -> let cat = { old with Name = form.Name
|
||||
Slug = form.Slug
|
||||
Description = match form.Description with "" -> None | d -> Some d
|
||||
ParentId = match form.ParentId with "" -> None | p -> Some p }
|
||||
let newCatId = saveCategory conn this.WebLog.Id cat
|
||||
match old.ParentId = cat.ParentId with
|
||||
| true -> ()
|
||||
| _ -> match old.parentId with
|
||||
| Some parentId -> removeCategoryFromParent conn this.WebLog.id parentId newCatId
|
||||
| _ -> match old.ParentId with
|
||||
| Some parentId -> removeCategoryFromParent conn this.WebLog.Id parentId newCatId
|
||||
| None -> ()
|
||||
match cat.parentId with
|
||||
| Some parentId -> addCategoryToParent conn this.WebLog.id parentId newCatId
|
||||
match cat.ParentId with
|
||||
| Some parentId -> addCategoryToParent conn this.WebLog.Id parentId newCatId
|
||||
| None -> ()
|
||||
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||
{ level = Level.Info
|
||||
message = System.String.Format
|
||||
(Resources.MsgCategoryEditSuccess,
|
||||
(match catId with | "new" -> Resources.Added | _ -> Resources.Updated))
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
{ UserMessage.Empty with
|
||||
Level = Level.Info
|
||||
Message = System.String.Format
|
||||
(Resources.MsgCategoryEditSuccess,
|
||||
(match catId with | "new" -> Resources.Added | _ -> Resources.Updated)) }
|
||||
|> model.AddMessage
|
||||
this.Redirect (sprintf "/category/%s/edit" newCatId) model
|
||||
| None -> this.NotFound ()
|
||||
|
||||
@ -79,13 +76,12 @@ type CategoryModule(conn : IConnection) as this =
|
||||
member this.DeleteCategory (parameters : DynamicDictionary) =
|
||||
this.ValidateCsrfToken ()
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let catId : string = downcast parameters.["id"]
|
||||
match tryFindCategory conn this.WebLog.id catId with
|
||||
let catId = parameters.["id"].ToString ()
|
||||
match tryFindCategory conn this.WebLog.Id catId with
|
||||
| Some cat -> deleteCategory conn cat
|
||||
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||
{ level = Level.Info
|
||||
message = System.String.Format(Resources.MsgCategoryDeleted, cat.name)
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
{ UserMessage.Empty with Level = Level.Info
|
||||
Message = System.String.Format(Resources.MsgCategoryDeleted, cat.Name) }
|
||||
|> model.AddMessage
|
||||
this.Redirect "/categories" model
|
||||
| None -> this.NotFound ()
|
||||
|
@ -1,4 +1,4 @@
|
||||
module myWebLog.Keys
|
||||
module MyWebLog.Keys
|
||||
|
||||
let Messages = "messages"
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
[<AutoOpen>]
|
||||
module myWebLog.ModuleExtensions
|
||||
module MyWebLog.ModuleExtensions
|
||||
|
||||
open myWebLog
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.Security
|
||||
|
||||
@ -14,19 +13,19 @@ type NancyModule with
|
||||
|
||||
/// Display a view using the theme specified for the web log
|
||||
member this.ThemedView view (model : MyWebLogModel) : obj =
|
||||
upcast this.View.[(sprintf "themes/%s/%s" this.WebLog.themePath view), model]
|
||||
upcast this.View.[(sprintf "themes/%s/%s" this.WebLog.ThemePath view), model]
|
||||
|
||||
/// Return a 404
|
||||
member this.NotFound () : obj = upcast HttpStatusCode.NotFound
|
||||
|
||||
/// Redirect a request, storing messages in the session if they exist
|
||||
member this.Redirect url (model : MyWebLogModel) : obj =
|
||||
match List.length model.messages with
|
||||
match List.length model.Messages with
|
||||
| 0 -> ()
|
||||
| _ -> this.Session.[Keys.Messages] <- model.messages
|
||||
| _ -> this.Session.[Keys.Messages] <- model.Messages
|
||||
upcast this.Response.AsRedirect(url).WithStatusCode HttpStatusCode.TemporaryRedirect
|
||||
|
||||
/// Require a specific level of access for the current web log
|
||||
member this.RequiresAccessLevel level =
|
||||
this.RequiresAuthentication()
|
||||
this.RequiresClaims [| sprintf "%s|%s" this.WebLog.id level |]
|
||||
this.RequiresClaims [| sprintf "%s|%s" this.WebLog.Id level |]
|
||||
|
@ -1,8 +1,8 @@
|
||||
namespace myWebLog
|
||||
namespace MyWebLog
|
||||
|
||||
open FSharp.Markdown
|
||||
open myWebLog.Data.Page
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Data.Page
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.ModelBinding
|
||||
open Nancy.Security
|
||||
@ -22,9 +22,9 @@ type PageModule(conn : IConnection, clock : IClock) as this =
|
||||
/// List all pages
|
||||
member this.PageList () =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let model = PagesModel(this.Context, this.WebLog, (findAllPages conn this.WebLog.id
|
||||
let model = PagesModel(this.Context, this.WebLog, (findAllPages conn this.WebLog.Id
|
||||
|> List.map (fun p -> PageForDisplay(this.WebLog, p))))
|
||||
model.pageTitle <- Resources.Pages
|
||||
model.PageTitle <- Resources.Pages
|
||||
upcast this.View.["admin/page/list", model]
|
||||
|
||||
/// Edit a page
|
||||
@ -32,17 +32,15 @@ type PageModule(conn : IConnection, clock : IClock) as this =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let pageId = parameters.["id"].ToString ()
|
||||
match (match pageId with
|
||||
| "new" -> Some Page.empty
|
||||
| _ -> tryFindPage conn this.WebLog.id pageId) with
|
||||
| Some page -> let rev = match page.revisions
|
||||
|> List.sortByDescending (fun r -> r.asOf)
|
||||
| "new" -> Some Page.Empty
|
||||
| _ -> tryFindPage conn this.WebLog.Id pageId) with
|
||||
| Some page -> let rev = match page.Revisions
|
||||
|> List.sortByDescending (fun r -> r.AsOf)
|
||||
|> List.tryHead with
|
||||
| Some r -> r
|
||||
| None -> Revision.empty
|
||||
| None -> Revision.Empty
|
||||
let model = EditPageModel(this.Context, this.WebLog, page, rev)
|
||||
model.pageTitle <- match pageId with
|
||||
| "new" -> Resources.AddNewPage
|
||||
| _ -> Resources.EditPage
|
||||
model.PageTitle <- match pageId with "new" -> Resources.AddNewPage | _ -> Resources.EditPage
|
||||
upcast this.View.["admin/page/edit", model]
|
||||
| None -> this.NotFound ()
|
||||
|
||||
@ -54,30 +52,28 @@ type PageModule(conn : IConnection, clock : IClock) as this =
|
||||
let form = this.Bind<EditPageForm> ()
|
||||
let now = clock.Now.Ticks
|
||||
match (match pageId with
|
||||
| "new" -> Some Page.empty
|
||||
| _ -> tryFindPage conn this.WebLog.id pageId) with
|
||||
| Some p -> let page = match pageId with
|
||||
| "new" -> { p with webLogId = this.WebLog.id }
|
||||
| _ -> p
|
||||
| "new" -> Some Page.Empty
|
||||
| _ -> tryFindPage conn this.WebLog.Id pageId) with
|
||||
| Some p -> let page = match pageId with "new" -> { p with WebLogId = this.WebLog.Id } | _ -> p
|
||||
let pId = { p with
|
||||
title = form.title
|
||||
permalink = form.permalink
|
||||
publishedOn = match pageId with | "new" -> now | _ -> page.publishedOn
|
||||
updatedOn = now
|
||||
text = match form.source with
|
||||
| RevisionSource.Markdown -> Markdown.TransformHtml form.text
|
||||
| _ -> form.text
|
||||
revisions = { asOf = now
|
||||
sourceType = form.source
|
||||
text = form.text } :: page.revisions }
|
||||
Title = form.Title
|
||||
Permalink = form.Permalink
|
||||
PublishedOn = match pageId with "new" -> now | _ -> page.PublishedOn
|
||||
UpdatedOn = now
|
||||
Text = match form.Source with
|
||||
| RevisionSource.Markdown -> Markdown.TransformHtml form.Text
|
||||
| _ -> form.Text
|
||||
Revisions = { AsOf = now
|
||||
SourceType = form.Source
|
||||
Text = form.Text } :: page.Revisions }
|
||||
|> savePage conn
|
||||
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||
{ level = Level.Info
|
||||
message = System.String.Format
|
||||
(Resources.MsgPageEditSuccess,
|
||||
(match pageId with | "new" -> Resources.Added | _ -> Resources.Updated))
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
{ UserMessage.Empty with
|
||||
Level = Level.Info
|
||||
Message = System.String.Format
|
||||
(Resources.MsgPageEditSuccess,
|
||||
(match pageId with | "new" -> Resources.Added | _ -> Resources.Updated)) }
|
||||
|> model.AddMessage
|
||||
this.Redirect (sprintf "/page/%s/edit" pId) model
|
||||
| None -> this.NotFound ()
|
||||
|
||||
@ -86,12 +82,11 @@ type PageModule(conn : IConnection, clock : IClock) as this =
|
||||
this.ValidateCsrfToken ()
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let pageId = parameters.["id"].ToString ()
|
||||
match tryFindPageWithoutRevisions conn this.WebLog.id pageId with
|
||||
| Some page -> deletePage conn page.webLogId page.id
|
||||
match tryFindPageWithoutRevisions conn this.WebLog.Id pageId with
|
||||
| Some page -> deletePage conn page.WebLogId page.Id
|
||||
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||
{ level = Level.Info
|
||||
message = Resources.MsgPageDeleted
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
{ UserMessage.Empty with Level = Level.Info
|
||||
Message = Resources.MsgPageDeleted }
|
||||
|> model.AddMessage
|
||||
this.Redirect "/pages" model
|
||||
| None -> this.NotFound ()
|
||||
|
@ -1,10 +1,10 @@
|
||||
namespace myWebLog
|
||||
namespace MyWebLog
|
||||
|
||||
open FSharp.Markdown
|
||||
open myWebLog.Data.Category
|
||||
open myWebLog.Data.Page
|
||||
open myWebLog.Data.Post
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Data.Category
|
||||
open MyWebLog.Data.Page
|
||||
open MyWebLog.Data.Post
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.ModelBinding
|
||||
open Nancy.Security
|
||||
@ -20,39 +20,39 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
||||
|
||||
/// Get the page number from the dictionary
|
||||
let getPage (parameters : DynamicDictionary) =
|
||||
match parameters.ContainsKey "page" with | true -> System.Int32.Parse (parameters.["page"].ToString ()) | _ -> 1
|
||||
match parameters.ContainsKey "page" with true -> System.Int32.Parse (parameters.["page"].ToString ()) | _ -> 1
|
||||
|
||||
/// Convert a list of posts to a list of posts for display
|
||||
let forDisplay posts = posts |> List.map (fun post -> PostForDisplay(this.WebLog, post))
|
||||
|
||||
/// Generate an RSS/Atom feed of the latest posts
|
||||
let generateFeed format : obj =
|
||||
let posts = findFeedPosts conn this.WebLog.id 10
|
||||
let posts = findFeedPosts conn this.WebLog.Id 10
|
||||
let feed =
|
||||
SyndicationFeed(
|
||||
this.WebLog.name, defaultArg this.WebLog.subtitle null,
|
||||
Uri(sprintf "%s://%s" this.Request.Url.Scheme this.WebLog.urlBase), null,
|
||||
this.WebLog.Name, defaultArg this.WebLog.Subtitle null,
|
||||
Uri(sprintf "%s://%s" this.Request.Url.Scheme this.WebLog.UrlBase), null,
|
||||
(match posts |> List.tryHead with
|
||||
| Some (post, _) -> Instant(post.updatedOn).ToDateTimeOffset ()
|
||||
| Some (post, _) -> Instant(post.UpdatedOn).ToDateTimeOffset ()
|
||||
| _ -> System.DateTimeOffset(System.DateTime.MinValue)),
|
||||
posts
|
||||
|> List.map (fun (post, user) ->
|
||||
let item =
|
||||
SyndicationItem(
|
||||
BaseUri = Uri(sprintf "%s://%s/%s" this.Request.Url.Scheme this.WebLog.urlBase post.permalink),
|
||||
PublishDate = Instant(post.publishedOn).ToDateTimeOffset (),
|
||||
LastUpdatedTime = Instant(post.updatedOn).ToDateTimeOffset (),
|
||||
Title = TextSyndicationContent(post.title),
|
||||
Content = TextSyndicationContent(post.text, TextSyndicationContentKind.Html))
|
||||
BaseUri = Uri(sprintf "%s://%s/%s" this.Request.Url.Scheme this.WebLog.UrlBase post.Permalink),
|
||||
PublishDate = Instant(post.PublishedOn).ToDateTimeOffset (),
|
||||
LastUpdatedTime = Instant(post.UpdatedOn).ToDateTimeOffset (),
|
||||
Title = TextSyndicationContent(post.Title),
|
||||
Content = TextSyndicationContent(post.Text, TextSyndicationContentKind.Html))
|
||||
user
|
||||
|> Option.iter (fun u -> item.Authors.Add
|
||||
(SyndicationPerson(u.userName, u.preferredName, defaultArg u.url null)))
|
||||
post.categories
|
||||
|> List.iter (fun c -> item.Categories.Add(SyndicationCategory(c.name)))
|
||||
(SyndicationPerson(u.UserName, u.PreferredName, defaultArg u.Url null)))
|
||||
post.Categories
|
||||
|> List.iter (fun c -> item.Categories.Add(SyndicationCategory(c.Name)))
|
||||
item))
|
||||
let stream = new IO.MemoryStream()
|
||||
Xml.XmlWriter.Create(stream)
|
||||
|> match format with | "atom" -> feed.SaveAsAtom10 | _ -> feed.SaveAsRss20
|
||||
|> match format with "atom" -> feed.SaveAsAtom10 | _ -> feed.SaveAsRss20
|
||||
stream.Position <- int64 0
|
||||
upcast this.Response.FromStream(stream, sprintf "application/%s+xml" format)
|
||||
|
||||
@ -75,77 +75,77 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
||||
/// Display a page of published posts
|
||||
member this.PublishedPostsPage pageNbr =
|
||||
let model = PostsModel(this.Context, this.WebLog)
|
||||
model.pageNbr <- pageNbr
|
||||
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10 |> forDisplay
|
||||
model.hasNewer <- match pageNbr with
|
||||
model.PageNbr <- pageNbr
|
||||
model.Posts <- findPageOfPublishedPosts conn this.WebLog.Id pageNbr 10 |> forDisplay
|
||||
model.HasNewer <- match pageNbr with
|
||||
| 1 -> false
|
||||
| _ -> match List.isEmpty model.posts with
|
||||
| _ -> match List.isEmpty model.Posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.posts).post
|
||||
model.hasOlder <- match List.isEmpty model.posts with
|
||||
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.Posts).Post
|
||||
model.HasOlder <- match List.isEmpty model.Posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.posts).post
|
||||
model.urlPrefix <- "/posts"
|
||||
model.pageTitle <- match pageNbr with
|
||||
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.Posts).Post
|
||||
model.UrlPrefix <- "/posts"
|
||||
model.PageTitle <- match pageNbr with
|
||||
| 1 -> ""
|
||||
| _ -> sprintf "%s%i" Resources.PageHash pageNbr
|
||||
this.ThemedView "index" model
|
||||
|
||||
/// Display either the newest posts or the configured home page
|
||||
member this.HomePage () =
|
||||
match this.WebLog.defaultPage with
|
||||
match this.WebLog.DefaultPage with
|
||||
| "posts" -> this.PublishedPostsPage 1
|
||||
| pageId -> match tryFindPageWithoutRevisions conn this.WebLog.id pageId with
|
||||
| pageId -> match tryFindPageWithoutRevisions conn this.WebLog.Id pageId with
|
||||
| Some page -> let model = PageModel(this.Context, this.WebLog, page)
|
||||
model.pageTitle <- page.title
|
||||
model.PageTitle <- page.Title
|
||||
this.ThemedView "page" model
|
||||
| None -> this.NotFound ()
|
||||
|
||||
/// Derive a post or page from the URL, or redirect from a prior URL to the current one
|
||||
member this.CatchAll (parameters : DynamicDictionary) =
|
||||
let url = parameters.["permalink"].ToString ()
|
||||
match tryFindPostByPermalink conn this.WebLog.id url with
|
||||
match tryFindPostByPermalink conn this.WebLog.Id url with
|
||||
| Some post -> // Hopefully the most common result; the permalink is a permalink!
|
||||
let model = PostModel(this.Context, this.WebLog, post)
|
||||
model.newerPost <- tryFindNewerPost conn post
|
||||
model.olderPost <- tryFindOlderPost conn post
|
||||
model.pageTitle <- post.title
|
||||
model.NewerPost <- tryFindNewerPost conn post
|
||||
model.OlderPost <- tryFindOlderPost conn post
|
||||
model.PageTitle <- post.Title
|
||||
this.ThemedView "single" model
|
||||
| None -> // Maybe it's a page permalink instead...
|
||||
match tryFindPageByPermalink conn this.WebLog.id url with
|
||||
match tryFindPageByPermalink conn this.WebLog.Id url with
|
||||
| Some page -> // ...and it is!
|
||||
let model = PageModel(this.Context, this.WebLog, page)
|
||||
model.pageTitle <- page.title
|
||||
model.PageTitle <- page.Title
|
||||
this.ThemedView "page" model
|
||||
| None -> // Maybe it's an old permalink for a post
|
||||
match tryFindPostByPriorPermalink conn this.WebLog.id url with
|
||||
match tryFindPostByPriorPermalink conn this.WebLog.Id url with
|
||||
| Some post -> // Redirect them to the proper permalink
|
||||
upcast this.Response.AsRedirect(sprintf "/%s" post.permalink)
|
||||
upcast this.Response.AsRedirect(sprintf "/%s" post.Permalink)
|
||||
.WithStatusCode HttpStatusCode.MovedPermanently
|
||||
| None -> this.NotFound ()
|
||||
|
||||
/// Display categorized posts
|
||||
member this.CategorizedPosts (parameters : DynamicDictionary) =
|
||||
let slug = parameters.["slug"].ToString ()
|
||||
match tryFindCategoryBySlug conn this.WebLog.id slug with
|
||||
match tryFindCategoryBySlug conn this.WebLog.Id slug with
|
||||
| Some cat -> let pageNbr = getPage parameters
|
||||
let model = PostsModel(this.Context, this.WebLog)
|
||||
model.pageNbr <- pageNbr
|
||||
model.posts <- findPageOfCategorizedPosts conn this.WebLog.id cat.id pageNbr 10 |> forDisplay
|
||||
model.hasNewer <- match List.isEmpty model.posts with
|
||||
model.PageNbr <- pageNbr
|
||||
model.Posts <- findPageOfCategorizedPosts conn this.WebLog.Id cat.Id pageNbr 10 |> forDisplay
|
||||
model.HasNewer <- match List.isEmpty model.Posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindNewerCategorizedPost conn cat.id
|
||||
(List.head model.posts).post
|
||||
model.hasOlder <- match List.isEmpty model.posts with
|
||||
| _ -> Option.isSome <| tryFindNewerCategorizedPost conn cat.Id
|
||||
(List.head model.Posts).Post
|
||||
model.HasOlder <- match List.isEmpty model.Posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindOlderCategorizedPost conn cat.id
|
||||
(List.last model.posts).post
|
||||
model.urlPrefix <- sprintf "/category/%s" slug
|
||||
model.pageTitle <- sprintf "\"%s\" Category%s" cat.name
|
||||
| _ -> Option.isSome <| tryFindOlderCategorizedPost conn cat.Id
|
||||
(List.last model.Posts).Post
|
||||
model.UrlPrefix <- sprintf "/category/%s" slug
|
||||
model.PageTitle <- sprintf "\"%s\" Category%s" cat.Name
|
||||
(match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
|
||||
model.subtitle <- Some <| match cat.description with
|
||||
model.Subtitle <- Some <| match cat.Description with
|
||||
| Some desc -> desc
|
||||
| None -> sprintf "Posts in the \"%s\" category" cat.name
|
||||
| None -> sprintf "Posts in the \"%s\" category" cat.Name
|
||||
this.ThemedView "index" model
|
||||
| None -> this.NotFound ()
|
||||
|
||||
@ -154,17 +154,17 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
||||
let tag = parameters.["tag"].ToString ()
|
||||
let pageNbr = getPage parameters
|
||||
let model = PostsModel(this.Context, this.WebLog)
|
||||
model.pageNbr <- pageNbr
|
||||
model.posts <- findPageOfTaggedPosts conn this.WebLog.id tag pageNbr 10 |> forDisplay
|
||||
model.hasNewer <- match List.isEmpty model.posts with
|
||||
model.PageNbr <- pageNbr
|
||||
model.Posts <- findPageOfTaggedPosts conn this.WebLog.Id tag pageNbr 10 |> forDisplay
|
||||
model.HasNewer <- match List.isEmpty model.Posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindNewerTaggedPost conn tag (List.head model.posts).post
|
||||
model.hasOlder <- match List.isEmpty model.posts with
|
||||
| _ -> Option.isSome <| tryFindNewerTaggedPost conn tag (List.head model.Posts).Post
|
||||
model.HasOlder <- match List.isEmpty model.Posts with
|
||||
| true -> false
|
||||
| _ -> Option.isSome <| tryFindOlderTaggedPost conn tag (List.last model.posts).post
|
||||
model.urlPrefix <- sprintf "/tag/%s" tag
|
||||
model.pageTitle <- sprintf "\"%s\" Tag%s" tag (match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
|
||||
model.subtitle <- Some <| sprintf "Posts tagged \"%s\"" tag
|
||||
| _ -> Option.isSome <| tryFindOlderTaggedPost conn tag (List.last model.Posts).Post
|
||||
model.UrlPrefix <- sprintf "/tag/%s" tag
|
||||
model.PageTitle <- sprintf "\"%s\" Tag%s" tag (match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
|
||||
model.Subtitle <- Some <| sprintf "Posts tagged \"%s\"" tag
|
||||
this.ThemedView "index" model
|
||||
|
||||
/// Generate an RSS feed
|
||||
@ -183,35 +183,33 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
||||
member this.PostList pageNbr =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let model = PostsModel(this.Context, this.WebLog)
|
||||
model.pageNbr <- pageNbr
|
||||
model.posts <- findPageOfAllPosts conn this.WebLog.id pageNbr 25 |> forDisplay
|
||||
model.hasNewer <- pageNbr > 1
|
||||
model.hasOlder <- List.length model.posts > 24
|
||||
model.urlPrefix <- "/posts/list"
|
||||
model.pageTitle <- Resources.Posts
|
||||
model.PageNbr <- pageNbr
|
||||
model.Posts <- findPageOfAllPosts conn this.WebLog.Id pageNbr 25 |> forDisplay
|
||||
model.HasNewer <- pageNbr > 1
|
||||
model.HasOlder <- List.length model.Posts > 24
|
||||
model.UrlPrefix <- "/posts/list"
|
||||
model.PageTitle <- Resources.Posts
|
||||
upcast this.View.["admin/post/list", model]
|
||||
|
||||
/// Edit a post
|
||||
member this.EditPost (parameters : DynamicDictionary) =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
let postId : string = downcast parameters.["postId"]
|
||||
let postId = parameters.["postId"].ToString ()
|
||||
match (match postId with
|
||||
| "new" -> Some Post.empty
|
||||
| _ -> tryFindPost conn this.WebLog.id postId) with
|
||||
| Some post -> let rev = match post.revisions
|
||||
|> List.sortByDescending (fun r -> r.asOf)
|
||||
| "new" -> Some Post.Empty
|
||||
| _ -> tryFindPost conn this.WebLog.Id postId) with
|
||||
| Some post -> let rev = match post.Revisions
|
||||
|> List.sortByDescending (fun r -> r.AsOf)
|
||||
|> List.tryHead with
|
||||
| Some r -> r
|
||||
| None -> Revision.empty
|
||||
| None -> Revision.Empty
|
||||
let model = EditPostModel(this.Context, this.WebLog, post, rev)
|
||||
model.categories <- getAllCategories conn this.WebLog.id
|
||||
|> List.map (fun cat -> string (fst cat).id,
|
||||
model.Categories <- getAllCategories conn this.WebLog.Id
|
||||
|> List.map (fun cat -> string (fst cat).Id,
|
||||
sprintf "%s%s"
|
||||
(String.replicate (snd cat) " ")
|
||||
(fst cat).name)
|
||||
model.pageTitle <- match post.id with
|
||||
| "new" -> Resources.AddNewPost
|
||||
| _ -> Resources.EditPost
|
||||
(fst cat).Name)
|
||||
model.PageTitle <- match post.Id with "new" -> Resources.AddNewPost | _ -> Resources.EditPost
|
||||
upcast this.View.["admin/post/edit"]
|
||||
| None -> this.NotFound ()
|
||||
|
||||
@ -219,45 +217,45 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
||||
member this.SavePost (parameters : DynamicDictionary) =
|
||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||
this.ValidateCsrfToken ()
|
||||
let postId : string = downcast parameters.["postId"]
|
||||
let form = this.Bind<EditPostForm>()
|
||||
let now = clock.Now.Ticks
|
||||
let postId = parameters.["postId"].ToString ()
|
||||
let form = this.Bind<EditPostForm>()
|
||||
let now = clock.Now.Ticks
|
||||
match (match postId with
|
||||
| "new" -> Some Post.empty
|
||||
| _ -> tryFindPost conn this.WebLog.id postId) with
|
||||
| Some p -> let justPublished = p.publishedOn = int64 0 && form.publishNow
|
||||
| "new" -> Some Post.Empty
|
||||
| _ -> tryFindPost conn this.WebLog.Id postId) with
|
||||
| Some p -> let justPublished = p.PublishedOn = int64 0 && form.PublishNow
|
||||
let post = match postId with
|
||||
| "new" -> { p with
|
||||
webLogId = this.WebLog.id
|
||||
authorId = (this.Request.PersistableSession.GetOrDefault<User>
|
||||
(Keys.User, User.empty)).id }
|
||||
| _ -> p
|
||||
WebLogId = this.WebLog.Id
|
||||
AuthorId = (this.Request.PersistableSession.GetOrDefault<User>
|
||||
(Keys.User, User.Empty)).Id }
|
||||
| _ -> p
|
||||
let pId = { post with
|
||||
status = match form.publishNow with
|
||||
Status = match form.PublishNow with
|
||||
| true -> PostStatus.Published
|
||||
| _ -> PostStatus.Draft
|
||||
title = form.title
|
||||
permalink = form.permalink
|
||||
publishedOn = match justPublished with | true -> now | _ -> int64 0
|
||||
updatedOn = now
|
||||
text = match form.source with
|
||||
| RevisionSource.Markdown -> Markdown.TransformHtml form.text
|
||||
| _ -> form.text
|
||||
categoryIds = Array.toList form.categories
|
||||
tags = form.tags.Split ','
|
||||
| _ -> PostStatus.Draft
|
||||
Title = form.Title
|
||||
Permalink = form.Permalink
|
||||
PublishedOn = match justPublished with true -> now | _ -> int64 0
|
||||
UpdatedOn = now
|
||||
Text = match form.Source with
|
||||
| RevisionSource.Markdown -> Markdown.TransformHtml form.Text
|
||||
| _ -> form.Text
|
||||
CategoryIds = Array.toList form.Categories
|
||||
Tags = form.Tags.Split ','
|
||||
|> Seq.map (fun t -> t.Trim().ToLowerInvariant())
|
||||
|> Seq.toList
|
||||
revisions = { asOf = now
|
||||
sourceType = form.source
|
||||
text = form.text } :: post.revisions }
|
||||
Revisions = { AsOf = now
|
||||
SourceType = form.Source
|
||||
Text = form.Text } :: post.Revisions }
|
||||
|> savePost conn
|
||||
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||
{ level = Level.Info
|
||||
message = System.String.Format
|
||||
(Resources.MsgPostEditSuccess,
|
||||
(match postId with | "new" -> Resources.Added | _ -> Resources.Updated),
|
||||
(match justPublished with | true -> Resources.AndPublished | _ -> ""))
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
{ UserMessage.Empty with
|
||||
Level = Level.Info
|
||||
Message = System.String.Format
|
||||
(Resources.MsgPostEditSuccess,
|
||||
(match postId with | "new" -> Resources.Added | _ -> Resources.Updated),
|
||||
(match justPublished with | true -> Resources.AndPublished | _ -> "")) }
|
||||
|> model.AddMessage
|
||||
this.Redirect (sprintf "/post/%s/edit" pId) model
|
||||
| None -> this.NotFound ()
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace myWebLog
|
||||
namespace MyWebLog
|
||||
|
||||
open myWebLog.Data.User
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Data.User
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.Authentication.Forms
|
||||
open Nancy.Cryptography
|
||||
@ -21,15 +21,16 @@ type UserModule(conn : IConnection) as this =
|
||||
|> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) ""
|
||||
|
||||
do
|
||||
this.Get .["/logon" ] <- fun parms -> this.ShowLogOn (downcast parms)
|
||||
this.Get .["/logon" ] <- fun _ -> this.ShowLogOn ()
|
||||
this.Post.["/logon" ] <- fun parms -> this.DoLogOn (downcast parms)
|
||||
this.Get .["/logoff"] <- fun parms -> this.LogOff ()
|
||||
this.Get .["/logoff"] <- fun _ -> this.LogOff ()
|
||||
|
||||
/// Show the log on page
|
||||
member this.ShowLogOn (parameters : DynamicDictionary) =
|
||||
member this.ShowLogOn () =
|
||||
let model = LogOnModel(this.Context, this.WebLog)
|
||||
model.form.returnUrl <- match parameters.ContainsKey "returnUrl" with
|
||||
| true -> parameters.["returnUrl"].ToString ()
|
||||
let query = this.Request.Query :?> DynamicDictionary
|
||||
model.Form.ReturnUrl <- match query.ContainsKey "returnUrl" with
|
||||
| true -> query.["returnUrl"].ToString ()
|
||||
| _ -> ""
|
||||
upcast this.View.["admin/user/logon", model]
|
||||
|
||||
@ -38,30 +39,28 @@ type UserModule(conn : IConnection) as this =
|
||||
this.ValidateCsrfToken ()
|
||||
let form = this.Bind<LogOnForm> ()
|
||||
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||
match tryUserLogOn conn form.email (pbkdf2 form.password) with
|
||||
match tryUserLogOn conn form.Email (pbkdf2 form.Password) with
|
||||
| Some user -> this.Session.[Keys.User] <- user
|
||||
{ level = Level.Info
|
||||
message = Resources.MsgLogOnSuccess
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
{ UserMessage.Empty with Level = Level.Info
|
||||
Message = Resources.MsgLogOnSuccess }
|
||||
|> model.AddMessage
|
||||
this.Redirect "" model |> ignore // Save the messages in the session before the Nancy redirect
|
||||
// TODO: investigate if addMessage should update the session when it's called
|
||||
upcast this.LoginAndRedirect (System.Guid.Parse user.id,
|
||||
fallbackRedirectUrl = defaultArg (Option.ofObj(form.returnUrl)) "/")
|
||||
| None -> { level = Level.Error
|
||||
message = Resources.ErrBadLogOnAttempt
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
this.Redirect (sprintf "/user/logon?returnUrl=%s" form.returnUrl) model
|
||||
upcast this.LoginAndRedirect (System.Guid.Parse user.Id,
|
||||
fallbackRedirectUrl = defaultArg (Option.ofObj form.ReturnUrl) "/")
|
||||
| None -> { UserMessage.Empty with Level = Level.Error
|
||||
Message = Resources.ErrBadLogOnAttempt }
|
||||
|> model.AddMessage
|
||||
this.Redirect (sprintf "/user/logon?returnUrl=%s" form.ReturnUrl) model
|
||||
|
||||
/// Log a user off
|
||||
member this.LogOff () =
|
||||
let user = this.Request.PersistableSession.GetOrDefault<User> (Keys.User, User.empty)
|
||||
// FIXME: why are we getting the user here if we don't do anything with it?
|
||||
let user = this.Request.PersistableSession.GetOrDefault<User> (Keys.User, User.Empty)
|
||||
this.Session.DeleteAll ()
|
||||
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||
{ level = Level.Info
|
||||
message = Resources.MsgLogOffSuccess
|
||||
details = None }
|
||||
|> model.addMessage
|
||||
{ UserMessage.Empty with Level = Level.Info
|
||||
Message = Resources.MsgLogOffSuccess }
|
||||
|> model.AddMessage
|
||||
this.Redirect "" model |> ignore
|
||||
upcast this.LogoutAndRedirect "/"
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace myWebLog
|
||||
namespace MyWebLog
|
||||
|
||||
open myWebLog.Data.WebLog
|
||||
open myWebLog.Entities
|
||||
open MyWebLog.Data.WebLog
|
||||
open MyWebLog.Entities
|
||||
open Nancy
|
||||
open Nancy.Session.Persistable
|
||||
open Newtonsoft.Json
|
||||
@ -21,25 +21,23 @@ module Level =
|
||||
|
||||
|
||||
/// A message for the user
|
||||
type UserMessage = {
|
||||
/// The level of the message (use Level module constants)
|
||||
level : string
|
||||
/// The text of the message
|
||||
message : string
|
||||
/// Further details regarding the message
|
||||
details : string option
|
||||
}
|
||||
type UserMessage =
|
||||
{ /// The level of the message (use Level module constants)
|
||||
Level : string
|
||||
/// The text of the message
|
||||
Message : string
|
||||
/// Further details regarding the message
|
||||
Details : string option }
|
||||
with
|
||||
/// An empty message
|
||||
static member empty = {
|
||||
level = Level.Info
|
||||
message = ""
|
||||
details = None
|
||||
}
|
||||
static member Empty =
|
||||
{ Level = Level.Info
|
||||
Message = ""
|
||||
Details = None }
|
||||
|
||||
/// Display version
|
||||
[<JsonIgnore>]
|
||||
member this.toDisplay =
|
||||
member this.ToDisplay =
|
||||
let classAndLabel =
|
||||
dict [
|
||||
Level.Error, ("danger", Resources.Error)
|
||||
@ -48,23 +46,23 @@ with
|
||||
]
|
||||
seq {
|
||||
yield "<div class=\"alert alert-dismissable alert-"
|
||||
yield fst classAndLabel.[this.level]
|
||||
yield fst classAndLabel.[this.Level]
|
||||
yield "\" role=\"alert\"><button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\""
|
||||
yield Resources.Close
|
||||
yield "\">×</button><strong>"
|
||||
match snd classAndLabel.[this.level] with
|
||||
match snd classAndLabel.[this.Level] with
|
||||
| "" -> ()
|
||||
| lbl -> yield lbl.ToUpper ()
|
||||
yield " » "
|
||||
yield this.message
|
||||
yield this.Message
|
||||
yield "</strong>"
|
||||
match this.details with
|
||||
match this.Details with
|
||||
| Some d -> yield "<br />"
|
||||
yield d
|
||||
| None -> ()
|
||||
yield "</div>"
|
||||
}
|
||||
|> Seq.reduce (fun acc x -> acc + x)
|
||||
|> Seq.reduce (+)
|
||||
|
||||
|
||||
/// Helpers to format local date/time using NodaTime
|
||||
@ -94,58 +92,58 @@ type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
|
||||
|
||||
/// Get the messages from the session
|
||||
let getMessages () =
|
||||
let msg = ctx.Request.PersistableSession.GetOrDefault<UserMessage list>(Keys.Messages, List.empty)
|
||||
let msg = ctx.Request.PersistableSession.GetOrDefault<UserMessage list>(Keys.Messages, [])
|
||||
match List.length msg with
|
||||
| 0 -> ()
|
||||
| _ -> ctx.Request.Session.Delete Keys.Messages
|
||||
msg
|
||||
|
||||
/// The web log for this request
|
||||
member this.webLog = webLog
|
||||
member this.WebLog = webLog
|
||||
/// The subtitle for the webLog (SSVE can't do IsSome that deep)
|
||||
member this.webLogSubtitle = defaultArg this.webLog.subtitle ""
|
||||
member this.WebLogSubtitle = defaultArg this.WebLog.Subtitle ""
|
||||
/// User messages
|
||||
member val messages = getMessages () with get, set
|
||||
member val Messages = getMessages () with get, set
|
||||
/// The currently logged in user
|
||||
member this.user = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.empty)
|
||||
member this.User = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.Empty)
|
||||
/// The title of the page
|
||||
member val pageTitle = "" with get, set
|
||||
member val PageTitle = "" with get, set
|
||||
/// The name and version of the application
|
||||
member this.generator = sprintf "myWebLog %s" (ctx.Items.[Keys.Version].ToString ())
|
||||
member this.Generator = sprintf "myWebLog %s" (ctx.Items.[Keys.Version].ToString ())
|
||||
/// The request start time
|
||||
member this.requestStart = ctx.Items.[Keys.RequestStart] :?> int64
|
||||
member this.RequestStart = ctx.Items.[Keys.RequestStart] :?> int64
|
||||
/// Is a user authenticated for this request?
|
||||
member this.isAuthenticated = "" <> this.user.id
|
||||
member this.IsAuthenticated = "" <> this.User.Id
|
||||
/// Add a message to the output
|
||||
member this.addMessage message = this.messages <- message :: this.messages
|
||||
member this.AddMessage message = this.Messages <- message :: this.Messages
|
||||
|
||||
/// Display a long date
|
||||
member this.displayLongDate ticks = FormatDateTime.longDate this.webLog.timeZone ticks
|
||||
member this.DisplayLongDate ticks = FormatDateTime.longDate this.WebLog.TimeZone ticks
|
||||
/// Display a short date
|
||||
member this.displayShortDate ticks = FormatDateTime.shortDate this.webLog.timeZone ticks
|
||||
member this.DisplayShortDate ticks = FormatDateTime.shortDate this.WebLog.TimeZone ticks
|
||||
/// Display the time
|
||||
member this.displayTime ticks = FormatDateTime.time this.webLog.timeZone ticks
|
||||
member this.DisplayTime ticks = FormatDateTime.time this.WebLog.TimeZone ticks
|
||||
/// The page title with the web log name appended
|
||||
member this.displayPageTitle =
|
||||
match this.pageTitle with
|
||||
| "" -> match this.webLog.subtitle with
|
||||
| Some st -> sprintf "%s | %s" this.webLog.name st
|
||||
| None -> this.webLog.name
|
||||
| pt -> sprintf "%s | %s" pt this.webLog.name
|
||||
member this.DisplayPageTitle =
|
||||
match this.PageTitle with
|
||||
| "" -> match this.WebLog.Subtitle with
|
||||
| Some st -> sprintf "%s | %s" this.WebLog.Name st
|
||||
| None -> this.WebLog.Name
|
||||
| pt -> sprintf "%s | %s" pt this.WebLog.Name
|
||||
|
||||
/// An image with the version and load time in the tool tip
|
||||
member this.footerLogo =
|
||||
member this.FooterLogo =
|
||||
seq {
|
||||
yield "<img src=\"/default/footer-logo.png\" alt=\"myWebLog\" title=\""
|
||||
yield sprintf "%s %s • " Resources.PoweredBy this.generator
|
||||
yield sprintf "%s %s • " Resources.PoweredBy this.Generator
|
||||
yield Resources.LoadedIn
|
||||
yield " "
|
||||
yield TimeSpan(System.DateTime.Now.Ticks - this.requestStart).TotalSeconds.ToString "f3"
|
||||
yield TimeSpan(System.DateTime.Now.Ticks - this.RequestStart).TotalSeconds.ToString "f3"
|
||||
yield " "
|
||||
yield Resources.Seconds.ToLower ()
|
||||
yield "\" />"
|
||||
}
|
||||
|> Seq.reduce (fun acc x -> acc + x)
|
||||
|> Seq.reduce (+)
|
||||
|
||||
|
||||
// ---- Admin models ----
|
||||
@ -154,68 +152,67 @@ type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
|
||||
type DashboardModel(ctx, webLog, counts : DashboardCounts) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The number of posts for the current web log
|
||||
member val posts = counts.posts with get, set
|
||||
member val Posts = counts.Posts with get, set
|
||||
/// The number of pages for the current web log
|
||||
member val pages = counts.pages with get, set
|
||||
member val Pages = counts.Pages with get, set
|
||||
/// The number of categories for the current web log
|
||||
member val categories = counts.categories with get, set
|
||||
member val Categories = counts.Categories with get, set
|
||||
|
||||
|
||||
// ---- Category models ----
|
||||
|
||||
type IndentedCategory = {
|
||||
category : Category
|
||||
indent : int
|
||||
selected : bool
|
||||
}
|
||||
type IndentedCategory =
|
||||
{ Category : Category
|
||||
Indent : int
|
||||
Selected : bool }
|
||||
with
|
||||
/// Create an indented category
|
||||
static member create (cat : Category * int) (isSelected : string -> bool) =
|
||||
{ category = fst cat
|
||||
indent = snd cat
|
||||
selected = isSelected (fst cat).id }
|
||||
static member Create (cat : Category * int) (isSelected : string -> bool) =
|
||||
{ Category = fst cat
|
||||
Indent = snd cat
|
||||
Selected = isSelected (fst cat).Id }
|
||||
/// Display name for a category on the list page, complete with indents
|
||||
member this.listName = sprintf "%s%s" (String.replicate this.indent " » ") this.category.name
|
||||
member this.ListName = sprintf "%s%s" (String.replicate this.Indent " » ") this.Category.Name
|
||||
/// Display for this category as an option within a select box
|
||||
member this.option =
|
||||
member this.Option =
|
||||
seq {
|
||||
yield sprintf "<option value=\"%s\"" this.category.id
|
||||
yield (match this.selected with | true -> """ selected="selected">""" | _ -> ">")
|
||||
yield String.replicate this.indent " "
|
||||
yield this.category.name
|
||||
yield sprintf "<option value=\"%s\"" this.Category.Id
|
||||
yield (match this.Selected with | true -> """ selected="selected">""" | _ -> ">")
|
||||
yield String.replicate this.Indent " "
|
||||
yield this.Category.Name
|
||||
yield "</option>"
|
||||
}
|
||||
|> String.concat ""
|
||||
/// Does the category have a description?
|
||||
member this.hasDescription = this.category.description.IsSome
|
||||
member this.HasDescription = this.Category.Description.IsSome
|
||||
|
||||
|
||||
/// Model for the list of categories
|
||||
type CategoryListModel(ctx, webLog, categories) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The categories
|
||||
member this.categories : IndentedCategory list = categories
|
||||
member this.Categories : IndentedCategory list = categories
|
||||
|
||||
|
||||
/// Form for editing a category
|
||||
type CategoryForm(category : Category) =
|
||||
new() = CategoryForm(Category.empty)
|
||||
/// The name of the category
|
||||
member val name = category.name with get, set
|
||||
member val Name = category.Name with get, set
|
||||
/// The slug of the category (used in category URLs)
|
||||
member val slug = category.slug with get, set
|
||||
member val Slug = category.Slug with get, set
|
||||
/// The description of the category
|
||||
member val description = defaultArg category.description "" with get, set
|
||||
member val Description = defaultArg category.Description "" with get, set
|
||||
/// The parent category for this one
|
||||
member val parentId = defaultArg category.parentId "" with get, set
|
||||
member val ParentId = defaultArg category.ParentId "" with get, set
|
||||
|
||||
/// Model for editing a category
|
||||
type CategoryEditModel(ctx, webLog, category) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The form with the category information
|
||||
member val form = CategoryForm(category) with get, set
|
||||
member val Form = CategoryForm(category) with get, set
|
||||
/// The categories
|
||||
member val categories : IndentedCategory list = List.empty with get, set
|
||||
member val Categories : IndentedCategory list = [] with get, set
|
||||
|
||||
|
||||
// ---- Page models ----
|
||||
@ -223,54 +220,53 @@ type CategoryEditModel(ctx, webLog, category) =
|
||||
/// Model for page display
|
||||
type PageModel(ctx, webLog, page) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
|
||||
/// The page to be displayed
|
||||
member this.page : Page = page
|
||||
member this.Page : Page = page
|
||||
|
||||
|
||||
/// Wrapper for a page with additional properties
|
||||
type PageForDisplay(webLog, page) =
|
||||
/// The page
|
||||
member this.page : Page = page
|
||||
member this.Page : Page = page
|
||||
/// The time zone of the web log
|
||||
member this.timeZone = webLog.timeZone
|
||||
member this.TimeZone = webLog.TimeZone
|
||||
/// The date the page was last updated
|
||||
member this.updatedDate = FormatDateTime.longDate this.timeZone page.updatedOn
|
||||
member this.UpdatedDate = FormatDateTime.longDate this.TimeZone page.UpdatedOn
|
||||
/// The time the page was last updated
|
||||
member this.updatedTime = FormatDateTime.time this.timeZone page.updatedOn
|
||||
member this.UpdatedTime = FormatDateTime.time this.TimeZone page.UpdatedOn
|
||||
|
||||
|
||||
/// Model for page list display
|
||||
type PagesModel(ctx, webLog, pages) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The pages
|
||||
member this.pages : PageForDisplay list = pages
|
||||
member this.Pages : PageForDisplay list = pages
|
||||
|
||||
|
||||
/// Form used to edit a page
|
||||
type EditPageForm() =
|
||||
/// The title of the page
|
||||
member val title = "" with get, set
|
||||
member val Title = "" with get, set
|
||||
/// The link for the page
|
||||
member val permalink = "" with get, set
|
||||
member val Permalink = "" with get, set
|
||||
/// The source type of the revision
|
||||
member val source = "" with get, set
|
||||
member val Source = "" with get, set
|
||||
/// The text of the revision
|
||||
member val text = "" with get, set
|
||||
member val Text = "" with get, set
|
||||
/// Whether to show the page in the web log's page list
|
||||
member val showInPageList = false with get, set
|
||||
member val ShowInPageList = false with get, set
|
||||
|
||||
/// Fill the form with applicable values from a page
|
||||
member this.forPage (page : Page) =
|
||||
this.title <- page.title
|
||||
this.permalink <- page.permalink
|
||||
this.showInPageList <- page.showInPageList
|
||||
member this.ForPage (page : Page) =
|
||||
this.Title <- page.Title
|
||||
this.Permalink <- page.Permalink
|
||||
this.ShowInPageList <- page.ShowInPageList
|
||||
this
|
||||
|
||||
/// Fill the form with applicable values from a revision
|
||||
member this.forRevision rev =
|
||||
this.source <- rev.sourceType
|
||||
this.text <- rev.text
|
||||
member this.ForRevision rev =
|
||||
this.Source <- rev.SourceType
|
||||
this.Text <- rev.Text
|
||||
this
|
||||
|
||||
|
||||
@ -278,21 +274,21 @@ type EditPageForm() =
|
||||
type EditPageModel(ctx, webLog, page, revision) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The page edit form
|
||||
member val form = EditPageForm().forPage(page).forRevision(revision)
|
||||
member val Form = EditPageForm().ForPage(page).ForRevision(revision)
|
||||
/// The page itself
|
||||
member this.page = page
|
||||
member this.Page = page
|
||||
/// The page's published date
|
||||
member this.publishedDate = this.displayLongDate page.publishedOn
|
||||
member this.PublishedDate = this.DisplayLongDate page.PublishedOn
|
||||
/// The page's published time
|
||||
member this.publishedTime = this.displayTime page.publishedOn
|
||||
member this.PublishedTime = this.DisplayTime page.PublishedOn
|
||||
/// The page's last updated date
|
||||
member this.lastUpdatedDate = this.displayLongDate page.updatedOn
|
||||
member this.LastUpdatedDate = this.DisplayLongDate page.UpdatedOn
|
||||
/// The page's last updated time
|
||||
member this.lastUpdatedTime = this.displayTime page.updatedOn
|
||||
member this.LastUpdatedTime = this.DisplayTime page.UpdatedOn
|
||||
/// Is this a new page?
|
||||
member this.isNew = "new" = page.id
|
||||
member this.IsNew = "new" = page.Id
|
||||
/// Generate a checked attribute if this page shows in the page list
|
||||
member this.pageListChecked = match page.showInPageList with | true -> "checked=\"checked\"" | _ -> ""
|
||||
member this.PageListChecked = match page.ShowInPageList with true -> "checked=\"checked\"" | _ -> ""
|
||||
|
||||
|
||||
// ---- Post models ----
|
||||
@ -301,102 +297,103 @@ type EditPageModel(ctx, webLog, page, revision) =
|
||||
type PostModel(ctx, webLog, post) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The post being displayed
|
||||
member this.post : Post = post
|
||||
member this.Post : Post = post
|
||||
/// The next newer post
|
||||
member val newerPost = Option<Post>.None with get, set
|
||||
member val NewerPost = Option<Post>.None with get, set
|
||||
/// The next older post
|
||||
member val olderPost = Option<Post>.None with get, set
|
||||
member val OlderPost = Option<Post>.None with get, set
|
||||
/// The date the post was published
|
||||
member this.publishedDate = this.displayLongDate this.post.publishedOn
|
||||
member this.PublishedDate = this.DisplayLongDate this.Post.PublishedOn
|
||||
/// The time the post was published
|
||||
member this.publishedTime = this.displayTime this.post.publishedOn
|
||||
member this.PublishedTime = this.DisplayTime this.Post.PublishedOn
|
||||
/// Does the post have tags?
|
||||
member this.hasTags = List.length post.tags > 0
|
||||
member this.HasTags = not (List.isEmpty post.Tags)
|
||||
/// Get the tags sorted
|
||||
member this.tags = post.tags
|
||||
member this.Tags = post.Tags
|
||||
|> List.sort
|
||||
|> List.map (fun tag -> tag, tag.Replace(' ', '+'))
|
||||
/// Does this post have a newer post?
|
||||
member this.hasNewer = this.newerPost.IsSome
|
||||
member this.HasNewer = this.NewerPost.IsSome
|
||||
/// Does this post have an older post?
|
||||
member this.hasOlder = this.olderPost.IsSome
|
||||
member this.HasOlder = this.OlderPost.IsSome
|
||||
|
||||
|
||||
/// Wrapper for a post with additional properties
|
||||
type PostForDisplay(webLog : WebLog, post : Post) =
|
||||
/// Turn tags into a pipe-delimited string of tags
|
||||
let pipedTags tags = tags |> List.reduce (fun acc x -> sprintf "%s | %s" acc x)
|
||||
/// The actual post
|
||||
member this.post = post
|
||||
member this.Post = post
|
||||
/// The time zone for the web log to which this post belongs
|
||||
member this.timeZone = webLog.timeZone
|
||||
member this.TimeZone = webLog.TimeZone
|
||||
/// The date the post was published
|
||||
member this.publishedDate = FormatDateTime.longDate this.timeZone this.post.publishedOn
|
||||
member this.PublishedDate = FormatDateTime.longDate this.TimeZone this.Post.PublishedOn
|
||||
/// The time the post was published
|
||||
member this.publishedTime = FormatDateTime.time this.timeZone this.post.publishedOn
|
||||
member this.PublishedTime = FormatDateTime.time this.TimeZone this.Post.PublishedOn
|
||||
/// Tags
|
||||
member this.tags =
|
||||
match List.length this.post.tags with
|
||||
| 0 -> ""
|
||||
| 1 | 2 | 3 | 4 | 5 -> this.post.tags |> pipedTags
|
||||
| count -> sprintf "%s %s" (this.post.tags |> List.take 3 |> pipedTags)
|
||||
(System.String.Format(Resources.andXMore, count - 3))
|
||||
member this.Tags =
|
||||
match List.length this.Post.Tags with
|
||||
| 0 -> ""
|
||||
| 1 | 2 | 3 | 4 | 5 -> this.Post.Tags |> pipedTags
|
||||
| count -> sprintf "%s %s" (this.Post.Tags |> List.take 3 |> pipedTags)
|
||||
(System.String.Format(Resources.andXMore, count - 3))
|
||||
|
||||
|
||||
/// Model for all page-of-posts pages
|
||||
type PostsModel(ctx, webLog) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The subtitle for the page
|
||||
member val subtitle = Option<string>.None with get, set
|
||||
member val Subtitle = Option<string>.None with get, set
|
||||
/// The posts to display
|
||||
member val posts = List.empty<PostForDisplay> with get, set
|
||||
member val Posts : PostForDisplay list = [] with get, set
|
||||
/// The page number of the post list
|
||||
member val pageNbr = 0 with get, set
|
||||
member val PageNbr = 0 with get, set
|
||||
/// Whether there is a newer page of posts for the list
|
||||
member val hasNewer = false with get, set
|
||||
member val HasNewer = false with get, set
|
||||
/// Whether there is an older page of posts for the list
|
||||
member val hasOlder = true with get, set
|
||||
member val HasOlder = true with get, set
|
||||
/// The prefix for the next/prior links
|
||||
member val urlPrefix = "" with get, set
|
||||
member val UrlPrefix = "" with get, set
|
||||
|
||||
/// The link for the next newer page of posts
|
||||
member this.newerLink =
|
||||
match this.urlPrefix = "/posts" && this.pageNbr = 2 && this.webLog.defaultPage = "posts" with
|
||||
member this.NewerLink =
|
||||
match this.UrlPrefix = "/posts" && this.PageNbr = 2 && this.WebLog.DefaultPage = "posts" with
|
||||
| true -> "/"
|
||||
| _ -> sprintf "%s/page/%i" this.urlPrefix (this.pageNbr - 1)
|
||||
| _ -> sprintf "%s/page/%i" this.UrlPrefix (this.PageNbr - 1)
|
||||
|
||||
/// The link for the prior (older) page of posts
|
||||
member this.olderLink = sprintf "%s/page/%i" this.urlPrefix (this.pageNbr + 1)
|
||||
member this.OlderLink = sprintf "%s/page/%i" this.UrlPrefix (this.PageNbr + 1)
|
||||
|
||||
|
||||
/// Form for editing a post
|
||||
type EditPostForm() =
|
||||
/// The title of the post
|
||||
member val title = "" with get, set
|
||||
member val Title = "" with get, set
|
||||
/// The permalink for the post
|
||||
member val permalink = "" with get, set
|
||||
member val Permalink = "" with get, set
|
||||
/// The source type for this revision
|
||||
member val source = "" with get, set
|
||||
member val Source = "" with get, set
|
||||
/// The text
|
||||
member val text = "" with get, set
|
||||
member val Text = "" with get, set
|
||||
/// Tags for the post
|
||||
member val tags = "" with get, set
|
||||
member val Tags = "" with get, set
|
||||
/// The selected category Ids for the post
|
||||
member val categories = Array.empty<string> with get, set
|
||||
member val Categories : string[] = [||] with get, set
|
||||
/// Whether the post should be published
|
||||
member val publishNow = true with get, set
|
||||
member val PublishNow = true with get, set
|
||||
|
||||
/// Fill the form with applicable values from a post
|
||||
member this.forPost post =
|
||||
this.title <- post.title
|
||||
this.permalink <- post.permalink
|
||||
this.tags <- List.reduce (fun acc x -> sprintf "%s, %s" acc x) post.tags
|
||||
this.categories <- List.toArray post.categoryIds
|
||||
member this.ForPost post =
|
||||
this.Title <- post.Title
|
||||
this.Permalink <- post.Permalink
|
||||
this.Tags <- List.reduce (fun acc x -> sprintf "%s, %s" acc x) post.Tags
|
||||
this.Categories <- List.toArray post.CategoryIds
|
||||
this
|
||||
|
||||
/// Fill the form with applicable values from a revision
|
||||
member this.forRevision rev =
|
||||
this.source <- rev.sourceType
|
||||
this.text <- rev.text
|
||||
member this.ForRevision rev =
|
||||
this.Source <- rev.SourceType
|
||||
this.Text <- rev.Text
|
||||
this
|
||||
|
||||
/// View model for the edit post page
|
||||
@ -404,17 +401,17 @@ type EditPostModel(ctx, webLog, post, revision) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
|
||||
/// The form
|
||||
member val form = EditPostForm().forPost(post).forRevision(revision) with get, set
|
||||
member val Form = EditPostForm().ForPost(post).ForRevision(revision) with get, set
|
||||
/// The post being edited
|
||||
member val post = post with get, set
|
||||
member val Post = post with get, set
|
||||
/// The categories to which the post may be assigned
|
||||
member val categories = List.empty<string * string> with get, set
|
||||
member val Categories : (string * string) list = [] with get, set
|
||||
/// Whether the post is currently published
|
||||
member this.isPublished = PostStatus.Published = this.post.status
|
||||
member this.IsPublished = PostStatus.Published = this.Post.Status
|
||||
/// The published date
|
||||
member this.publishedDate = this.displayLongDate this.post.publishedOn
|
||||
member this.PublishedDate = this.DisplayLongDate this.Post.PublishedOn
|
||||
/// The published time
|
||||
member this.publishedTime = this.displayTime this.post.publishedOn
|
||||
member this.PublishedTime = this.DisplayTime this.Post.PublishedOn
|
||||
|
||||
|
||||
// ---- User models ----
|
||||
@ -422,15 +419,15 @@ type EditPostModel(ctx, webLog, post, revision) =
|
||||
/// Form for the log on page
|
||||
type LogOnForm() =
|
||||
/// The URL to which the user will be directed upon successful log on
|
||||
member val returnUrl = "" with get, set
|
||||
member val ReturnUrl = "" with get, set
|
||||
/// The e-mail address
|
||||
member val email = "" with get, set
|
||||
member val Email = "" with get, set
|
||||
/// The user's passwor
|
||||
member val password = "" with get, set
|
||||
member val Password = "" with get, set
|
||||
|
||||
|
||||
/// Model to support the user log on page
|
||||
type LogOnModel(ctx, webLog) =
|
||||
inherit MyWebLogModel(ctx, webLog)
|
||||
/// The log on form
|
||||
member val form = LogOnForm() with get, set
|
||||
member val Form = LogOnForm() with get, set
|
||||
|
@ -8,7 +8,7 @@
|
||||
<ProjectGuid>e6ee110a-27a6-4a19-b0cb-d24f48f71b53</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>myWebLog.Web</RootNamespace>
|
||||
<AssemblyName>myWebLog.Web</AssemblyName>
|
||||
<AssemblyName>MyWebLog.Web</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
|
@ -1,10 +1,10 @@
|
||||
namespace myWebLog
|
||||
namespace MyWebLog
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
App.run();
|
||||
App.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("myWebLog")]
|
||||
[assembly: AssemblyTitle("MyWebLog")]
|
||||
[assembly: AssemblyDescription("A lightweight blogging platform built on Suave, Nancy, and RethinkDB")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("myWebLog")]
|
||||
[assembly: AssemblyProduct("MyWebLog")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2016")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
@ -7,8 +7,8 @@
|
||||
<ProjectGuid>{B9F6DB52-65A1-4C2A-8C97-739E08A1D4FB}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>myWebLog</RootNamespace>
|
||||
<AssemblyName>myWebLog</AssemblyName>
|
||||
<RootNamespace>MyWebLog</RootNamespace>
|
||||
<AssemblyName>MyWebLog</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
@ -51,7 +51,6 @@
|
||||
<Content Include="data-config.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="views\admin\message.html" />
|
||||
<Content Include="views\themes\default\content\bootstrap-theme.css.map">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>@Model.pageTitle | @Translate.Admin | @Model.webLog.name</title>
|
||||
<title>@Model.PageTitle | @Translate.Admin | @Model.WebLog.Name</title>
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.css" />
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.4/cosmo/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" />
|
||||
@ -14,17 +14,17 @@
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">@Model.webLog.name</a>
|
||||
<a class="navbar-brand" href="/">@Model.WebLog.Name</a>
|
||||
</div>
|
||||
<div class="navbar-left">
|
||||
<p class="navbar-text">@Model.pageTitle</p>
|
||||
<p class="navbar-text">@Model.PageTitle</p>
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
@If.isAuthenticated
|
||||
@If.IsAuthenticated
|
||||
<li><a href="/admin">@Translate.Dashboard</a></li>
|
||||
<li><a href="/user/logoff">@Translate.LogOff</a></li>
|
||||
@EndIf
|
||||
@IfNot.isAuthenticated
|
||||
@IfNot.IsAuthenticated
|
||||
<li><a href="/user/logon">@Translate.LogOn</a></li>
|
||||
@EndIf
|
||||
</ul>
|
||||
@ -32,15 +32,15 @@
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container">
|
||||
@Each.messages
|
||||
@Current.toDisplay
|
||||
@Each.Messages
|
||||
@Current.ToDisplay
|
||||
@EndEach
|
||||
@Section['Content'];
|
||||
</div>
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-right">@Model.generator</div>
|
||||
<div class="col-xs-12 text-right">@Model.Generator</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -1,34 +1,34 @@
|
||||
@Master['admin/admin-layout']
|
||||
|
||||
@Section['Content']
|
||||
<form action="/category/@Model.category.id/edit" method="post">
|
||||
<form action="/category/@Model.Category.Id/edit" method="post">
|
||||
@AntiForgeryToken
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="name">@Translate.Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="@Model.form.name" />
|
||||
<label class="control-label" for="Name">@Translate.Name</label>
|
||||
<input type="text" class="form-control" id="Name" name="Name" value="@Model.Form.Name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="slug">@Translate.Slug</label>
|
||||
<input type="text" class="form-control" id="slug" name="slug" value="@Model.form.slug}" />
|
||||
<label class="control-label" for="Slug">@Translate.Slug</label>
|
||||
<input type="text" class="form-control" id="Slug" name="Slug" value="@Model.Form.Slug" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="description">@Translate.Description</label>
|
||||
<textarea class="form-control" rows="4" id="description" name="description">@Model.form.description</textarea>
|
||||
<label class="control-label" for="Description">@Translate.Description</label>
|
||||
<textarea class="form-control" rows="4" id="Description" name="Description">@Model.Form.Description</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="parentId">@Translate.ParentCategory</label>
|
||||
<select class="form-control" id="parentId" name="parentId">
|
||||
<label class="control-label" for="ParentId">@Translate.ParentCategory</label>
|
||||
<select class="form-control" id="ParentId" name="ParentId">
|
||||
<option value="">— @Translate.NoParent —</option>
|
||||
@Each.categories
|
||||
@Current.option
|
||||
@Each.Categories
|
||||
@Current.Option
|
||||
@EndEach
|
||||
</select>
|
||||
</div>
|
||||
@ -46,7 +46,7 @@
|
||||
@Section['Scripts']
|
||||
<script type="text/javascript">
|
||||
/* <![CDATA[ */
|
||||
$(document).ready(function () { $("#name").focus() })
|
||||
$(document).ready(function () { $("#Name").focus() })
|
||||
/* ]] */
|
||||
</script>
|
||||
@EndSection
|
||||
|
@ -11,20 +11,20 @@
|
||||
<th>@Translate.Category</th>
|
||||
<th>@Translate.Description</th>
|
||||
</tr>
|
||||
@Each.categories
|
||||
@Each.Categories
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/category/@Current.category.id/edit">@Translate.Edit</a>
|
||||
<a href="javascript:void(0)" onclick="deleteCategory('@Current.category.id', '@Current.category.name')">
|
||||
<a href="/category/@Current.Category.Id/edit">@Translate.Edit</a>
|
||||
<a href="javascript:void(0)" onclick="deleteCategory('@Current.Category.Id', '@Current.Category.Name')">
|
||||
@Translate.Delete
|
||||
</a>
|
||||
</td>
|
||||
<td>@Current.listName</td>
|
||||
<td>@Current.ListName</td>
|
||||
<td>
|
||||
@If.hasDescription
|
||||
@Current.category.description.Value
|
||||
@If.HasDescription
|
||||
@Current.Category.Description.Value
|
||||
@EndIf
|
||||
@IfNot.hasDescription
|
||||
@IfNot.HasDescription
|
||||
|
||||
@EndIf
|
||||
</td>
|
||||
|
@ -3,7 +3,7 @@
|
||||
@Section['Content']
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h3>@Translate.Posts <span class="badge">@Model.posts</span></h3>
|
||||
<h3>@Translate.Posts <span class="badge">@Model.Posts</span></h3>
|
||||
<p>
|
||||
<a href="/posts/list"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<h3>@Translate.Pages <span class="badge">@Model.pages</span></h3>
|
||||
<h3>@Translate.Pages <span class="badge">@Model.Pages</span></h3>
|
||||
<p>
|
||||
<a href="/pages"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h3>@Translate.Categories <span class="badge">@Model.categories</span></h3>
|
||||
<h3>@Translate.Categories <span class="badge">@Model.Categories</span></h3>
|
||||
<p>
|
||||
<a href="/categories"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
|
||||
|
||||
|
@ -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">×</button>
|
||||
<strong>
|
||||
if 'danger' == message.type
|
||||
=__("Error").toUpperCase()
|
||||
| »
|
||||
else if 'warning' == message.type
|
||||
=__("Warning").toUpperCase()
|
||||
| »
|
||||
!= message.text
|
||||
</strong>
|
||||
if message.detail
|
||||
br
|
||||
!= message.detail
|
||||
</div>
|
@ -1,22 +1,22 @@
|
||||
@Master['admin/admin-layout']
|
||||
|
||||
@Section['Content']
|
||||
<form action="/page/@Model.page.id/edit" method="post">
|
||||
<form action="/page/@Model.Page.Id/edit" method="post">
|
||||
@AntiForgeryToken
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="title">@Translate.Title</label>
|
||||
<input type="text" name="title" id="title" class="form-control" value="@Model.form.title" />
|
||||
<label class="control-label" for="Title">@Translate.Title</label>
|
||||
<input type="text" name="Title" id="Title" class="form-control" value="@Model.Form.Title" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="permalink">@Translate.Permalink</label>
|
||||
<input type="text" name="permalink" id="permalink" class="form-control" value="@Model.form.permalink" />
|
||||
<label class="control-label" for="Permalink">@Translate.Permalink</label>
|
||||
<input type="text" name="Permalink" id="Permalink" class="form-control" value="@Model.Form.Permalink" />
|
||||
<p class="form-hint"><em>@Translate.startingWith</em> http://@Model.webLog.urlBase/ </p>
|
||||
</div>
|
||||
<!-- // TODO: Markdown / HTML choice -->
|
||||
<div class="form-group">
|
||||
<textarea name="text" id="text" rows="15" class="form-control">@Model.form.text</textarea>
|
||||
<textarea name="Text" id="Text" rows="15" class="form-control">@Model.Form.Text</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
@ -26,16 +26,16 @@
|
||||
@IfNot.isNew
|
||||
<div class="form-group">
|
||||
<label class="control-label">@Translate.PublishedDate</label>
|
||||
<p class="static-control">@Model.publishedDate<br />@Model.publishedTime</p>
|
||||
<p class="static-control">@Model.PublishedDate<br />@Model.PublishedTime</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">@Translate.LastUpdatedDate</label>
|
||||
<p class="static-control">@Model.lastUpdatedDate<br />@Model.lastUpdatedTime</p>
|
||||
<p class="static-control">@Model.LastUpdatedDate<br />@Model.LastUpdatedTime</p>
|
||||
</div>
|
||||
@EndIf
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="showInPageList" id="showInPageList" @Model.pageListChecked />
|
||||
<label for="showInPageList">@Translate.ShowInPageList</label>
|
||||
<input type="checkbox" name="ShowInPageList" id="ShowInPageList" @Model.PageListChecked />
|
||||
<label for="ShowInPageList">@Translate.ShowInPageList</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -51,7 +51,7 @@
|
||||
<script type="text/javascript" src="/content/scripts/tinymce-init.js"></script>
|
||||
<script type="text/javascript">
|
||||
/* <![CDATA[ */
|
||||
$(document).ready(function () { $("#title").focus() })
|
||||
$(document).ready(function () { $("#Title").focus() })
|
||||
/* ]]> */
|
||||
</script>
|
||||
@EndSection
|
||||
|
@ -10,15 +10,15 @@
|
||||
<th>@Translate.Title</th>
|
||||
<th>@Translate.LastUpdated</th>
|
||||
</tr>
|
||||
@Each.pages
|
||||
@Each.Pages
|
||||
<tr>
|
||||
<td>
|
||||
@Current.page.title<br />
|
||||
<a href="/@Current.page.permalink">@Translate.View</a>
|
||||
<a href="/page/@Current.page.id/edit">@Translate.Edit</a>
|
||||
<a href="javascript:void(0)" onclick="deletePage('@Current.page.id', '@Current.title')">@Translate.Delete</a>
|
||||
@Current.Page.Title<br />
|
||||
<a href="/@Current.Page.Permalink">@Translate.View</a>
|
||||
<a href="/page/@Current.Page.Id/edit">@Translate.Edit</a>
|
||||
<a href="javascript:void(0)" onclick="deletePage('@Current.Page.Id', '@Current.Page.Title')">@Translate.Delete</a>
|
||||
</td>
|
||||
<td>@Current.updatedDate<br />@Translate.at @Current.updatedTime</td>
|
||||
<td>@Current.UpdatedDate<br />@Translate.at @Current.UpdatedTime</td>
|
||||
</tr>
|
||||
@EndEach
|
||||
</table>
|
||||
|
@ -1,27 +1,27 @@
|
||||
@Master['admin/admin-layout']
|
||||
|
||||
@Section['Content']
|
||||
<form action='/post/@Model.post.id/edit' method="post">
|
||||
<form action='/post/@Model.Post.Id/edit' method="post">
|
||||
@AntiForgeryToken
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="title">@Translate.Title</label>
|
||||
<input type="text" name="title" id="title" class="form-control" value="@Model.form.title" />
|
||||
<label class="control-label" for="Title">@Translate.Title</label>
|
||||
<input type="text" name="Title" id="Title" class="form-control" value="@Model.Form.Title" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="permalink">@Translate.Permalink</label>
|
||||
<input type="text" name="permalink" id="permalink" class="form-control" value="@Model.form.permalink" />
|
||||
<label class="control-label" for="Permalink">@Translate.Permalink</label>
|
||||
<input type="text" name="Permalink" id="Permalink" class="form-control" value="@Model.Form.Permalink" />
|
||||
<!-- // FIXME: hard-coded "http" -->
|
||||
<p class="form-hint"><em>@Translate.startingWith</em> http://@Model.webLog.urlBase/ </p>
|
||||
</div>
|
||||
<!-- // TODO: Markdown / HTML choice -->
|
||||
<div class="form-group">
|
||||
<textarea name="text" id="text" rows="15">@Model.form.text</textarea>
|
||||
<textarea name="Text" id="Text" rows="15">@Model.Form.Text</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="tags">@Translate.Tags</label>
|
||||
<input type="text" name="tags" id="tags" class="form-control" value="@Model.form.tags" />
|
||||
<label class="control-label" for="Tags">@Translate.Tags</label>
|
||||
<input type="text" name="Tags" id="Tags" class="form-control" value="@Model.Form.Tags" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
@ -32,12 +32,12 @@
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">@Translate.PostStatus</label>
|
||||
<p class="static-control">@Model.post.status</p>
|
||||
<p class="static-control">@Model.Post.Status</p>
|
||||
</div>
|
||||
@If.isPublished
|
||||
@If.IsPublished
|
||||
<div class="form-group">
|
||||
<label class="control-label">@Translate.PublishedDate</label>
|
||||
<p class="static-control">@Model.publishedDate<br />@Model.publishedTime</p>
|
||||
<p class="static-control">@Model.PublishedDate<br />@Model.PublishedTime</p>
|
||||
</div>
|
||||
@EndIf
|
||||
</div>
|
||||
@ -48,7 +48,7 @@
|
||||
</div>
|
||||
<div class="panel-body" style="max-height:350px;overflow:scroll;">
|
||||
<!-- // TODO: how to check the ones that are already selected? -->
|
||||
@Each.categories
|
||||
@Each.Categories
|
||||
<!-- - var tab = 0
|
||||
while tab < item.indent
|
||||
|
|
||||
@ -56,22 +56,22 @@
|
||||
- var attributes = {}
|
||||
if -1 < currentCategories.indexOf(item.category.id)
|
||||
- attributes.checked = 'checked' -->
|
||||
<input type="checkbox" id="category-@Current.Item1" name="category", value="@Current.Item1" />
|
||||
<input type="checkbox" id="Category-@Current.Item1" name="Category", value="@Current.Item1" />
|
||||
|
||||
<!-- // FIXME: the title should be the category description -->
|
||||
<label for="category-@Current.Item1" title="@Current.Item2">@Current.Item2</label>
|
||||
<label for="Category-@Current.Item1" title="@Current.Item2">@Current.Item2</label>
|
||||
<br/>
|
||||
@EndEach
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
@If.isPublished
|
||||
<input type="hidden" name="publishNow" value="true" />
|
||||
@If.IsPublished
|
||||
<input type="hidden" name="PublishNow" value="true" />
|
||||
@EndIf
|
||||
@IfNot.isPublished
|
||||
@IfNot.IsPublished
|
||||
<div>
|
||||
<input type="checkbox" name="publishNow" id="publishNow" value="true" checked="checked" />
|
||||
<label for="publishNow">@Translate.PublishThisPost</label>
|
||||
<input type="checkbox" name="PublishNow" id="PublishNow" value="true" checked="checked" />
|
||||
<label for="PublishNow">@Translate.PublishThisPost</label>
|
||||
</div>
|
||||
@EndIf
|
||||
<p>
|
||||
@ -89,7 +89,7 @@
|
||||
<script type="text/javascript" src="/content/scripts/tinymce-init.js"></script>
|
||||
<script type="text/javascript">
|
||||
/** <![CDATA[ */
|
||||
$(document).ready(function () { $("#title").focus() })
|
||||
$(document).ready(function () { $("#Title").focus() })
|
||||
/** ]]> */
|
||||
</script>
|
||||
@EndSection
|
||||
|
@ -16,34 +16,34 @@
|
||||
<th>@Translate.Status</th>
|
||||
<th>@Translate.Tags</th>
|
||||
</tr>
|
||||
@Each.posts
|
||||
@Each.Posts
|
||||
<tr>
|
||||
<td style="white-space:nowrap;">
|
||||
@Current.publishedDate<br />
|
||||
@Translate.at @Current.publishedTime
|
||||
@Current.PublishedDate<br />
|
||||
@Translate.at @Current.PublishedTime
|
||||
</td>
|
||||
<td>
|
||||
@Current.post.title<br />
|
||||
<a href="/@Current.post.permalink">@Translate.View</a> |
|
||||
<a href="/post/@Current.post.id/edit">@Translate.Edit</a> |
|
||||
<a href="/post/@Current.post.id/delete">@Translate.Delete</a>
|
||||
@Current.Post.Title<br />
|
||||
<a href="/@Current.Post.Permalink">@Translate.View</a> |
|
||||
<a href="/post/@Current.Post.Id/edit">@Translate.Edit</a> |
|
||||
<a href="/post/@Current.Post.Id/delete">@Translate.Delete</a>
|
||||
</td>
|
||||
<td>@Current.post.status</td>
|
||||
<td>@Current.tags</td>
|
||||
<td>@Current.Post.Status</td>
|
||||
<td>@Current.Tags</td>
|
||||
</tr>
|
||||
@EndEach
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3 col-xs-offset-2">
|
||||
@If.hasNewer
|
||||
<p><a class="btn btn-default" href="@Model.newerLink">« @Translate.NewerPosts</a></p>
|
||||
@If.HasNewer
|
||||
<p><a class="btn btn-default" href="@Model.NewerLink">« @Translate.NewerPosts</a></p>
|
||||
@EndIf
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-1 text-right">
|
||||
@If.hasOlder
|
||||
<p><a class="btn btn-default" href="@Model.olderLink">@Translate.OlderPosts »</a></p>
|
||||
@If.HasOlder
|
||||
<p><a class="btn btn-default" href="@Model.OlderLink">@Translate.OlderPosts »</a></p>
|
||||
@EndIf
|
||||
</div>
|
||||
</div>
|
||||
@EndSection
|
||||
@EndSection
|
||||
|
@ -3,12 +3,12 @@
|
||||
@Section['Content']
|
||||
<form action="/user/logon" method="post">
|
||||
@AntiForgeryToken
|
||||
<input type="hidden" name="returnUrl" value="@Model.form.returnUrl" />
|
||||
<input type="hidden" name="ReturnUrl" value="@Model.Form.ReturnUrl" />
|
||||
<div class="row">
|
||||
<div class="col-sm-offset-1 col-sm-8 col-md-offset-3 col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" title="@Translate.EmailAddress"><i class="fa fa-envelope"></i></span>
|
||||
<input type="text" name="email" id="email" class="form-control" placeholder="@Translate.EmailAddress" />
|
||||
<input type="text" name="Email" id="Email" class="form-control" placeholder="@Translate.EmailAddress" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -17,7 +17,7 @@
|
||||
<br />
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon" title="@Translate.Password"><i class="fa fa-key"></i></span>
|
||||
<input type="password" name="password" class="form-control" placeholder="@Translate.Password" />
|
||||
<input type="password" name="Password" class="form-control" placeholder="@Translate.Password" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -35,7 +35,7 @@
|
||||
@Section['Scripts']
|
||||
<script type="text/javascript">
|
||||
/* <![CDATA[ */
|
||||
$(document).ready(function () { $("#email").focus() })
|
||||
$(document).ready(function () { $("#Email").focus() })
|
||||
/* ]]> */
|
||||
</script>
|
||||
@EndSection
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-right">
|
||||
@Model.footerLogo
|
||||
@Model.FooterLogo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,24 +1,24 @@
|
||||
@Each.messages
|
||||
@Current.toDisplay
|
||||
@Each.Messages
|
||||
@Current.ToDisplay
|
||||
@EndEach
|
||||
@If.subTitle.IsSome
|
||||
@If.SubTitle.IsSome
|
||||
<h2>
|
||||
<span class="label label-info">@Model.subTitle</span>
|
||||
<span class="label label-info">@Model.SubTitle</span>
|
||||
</h2>
|
||||
@EndIf
|
||||
@Each.posts
|
||||
@Each.Posts
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<article>
|
||||
<h1>
|
||||
<a href="/@Current.post.permalink"
|
||||
title="@Translate.PermanentLinkTo "@Current.post.title@quot;">@Current.post.title</a>
|
||||
<a href="/@Current.Post.Permalink"
|
||||
title="@Translate.PermanentLinkTo "@Current.Post.Title@quot;">@Current.Post.Title</a>
|
||||
</h1>
|
||||
<p>
|
||||
<i class="fa fa-calendar" title="@Translate.Date"></i> @Current.publishedDate
|
||||
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.publishedTime
|
||||
<i class="fa fa-calendar" title="@Translate.Date"></i> @Current.PublishedDate
|
||||
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.PublishedTime
|
||||
</p>
|
||||
@Current.post.text
|
||||
@Current.Post.Text
|
||||
</article>
|
||||
<hr />
|
||||
</div>
|
||||
@ -26,16 +26,16 @@
|
||||
@EndEach
|
||||
<div class="row">
|
||||
<div class="col-xs-3 col-xs-offset-3">
|
||||
@If.hasNewer
|
||||
@If.HasNewer
|
||||
<p>
|
||||
<a class="btn btn-primary" href="@Model.newerLink">@Translate.NewerPosts</a>
|
||||
<a class="btn btn-primary" href="@Model.NewerLink">@Translate.NewerPosts</a>
|
||||
</p>
|
||||
@EndIf
|
||||
</div>
|
||||
<div class="col-xs-3 text-right">
|
||||
@If.hasOlder
|
||||
@If.HasOlder
|
||||
<p>
|
||||
<a class="btn btn-primary" href="@Model.olderLink">@Translate.OlderPosts</a>
|
||||
<a class="btn btn-primary" href="@Model.OlderLink">@Translate.OlderPosts</a>
|
||||
</p>
|
||||
@EndIf
|
||||
</div>
|
||||
|
@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content="@Model.generator" />
|
||||
<title>@Model.displayPageTitle</title>
|
||||
<meta name="generator" content="@Model.Generator" />
|
||||
<title>@Model.DisplayPageTitle</title>
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/default/bootstrap-theme.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" />
|
||||
<link rel="alternate" type="application/atom+xml" href="//@Model.webLog.urlBase/feed?format=atom" />
|
||||
<link rel="alternate" type="application/rss+xml" href="//@Model.webLog.urlBase/feed" />
|
||||
<link rel="alternate" type="application/atom+xml" href="//@Model.WebLog.UrlBase/feed?format=atom" />
|
||||
<link rel="alternate" type="application/rss+xml" href="//@Model.WebLog.UrlBase/feed" />
|
||||
@Section['Head'];
|
||||
</head>
|
||||
<body>
|
||||
@ -17,20 +17,20 @@
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">@Model.webLog.name</a>
|
||||
<a class="navbar-brand" href="/">@Model.WebLog.Name</a>
|
||||
</div>
|
||||
<p class="navbar-text">@Model.webLogSubtitle</p>
|
||||
<p class="navbar-text">@Model.WebLogSubtitle</p>
|
||||
<ul class="nav navbar-nav navbar-left">
|
||||
@Each.webLog.pageList
|
||||
<li><a href="/@Current.permalink">@Current.title</a></li>
|
||||
@Each.WebLog.PageList
|
||||
<li><a href="/@Current.Permalink">@Current.Title</a></li>
|
||||
@EndEach
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
@If.isAuthenticated
|
||||
@If.IsAuthenticated
|
||||
<li><a href="/admin">@Translate.Dashboard</a></li>
|
||||
<li><a href="/user/logoff">@Translate.LogOff</a></li>
|
||||
@EndIf
|
||||
@IfNot.isAuthenticated
|
||||
@IfNot.IsAuthenticated
|
||||
<li><a href="/user/logon">@Translate.LogOn</a></li>
|
||||
@EndIf
|
||||
</ul>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<article>
|
||||
<h1>@Model.page.title</h1>
|
||||
@Model.page.text
|
||||
<h1>@Model.Page.Title</h1>
|
||||
@Model.Page.Text
|
||||
</article>
|
@ -1,16 +1,16 @@
|
||||
<article>
|
||||
<div class="row">
|
||||
<div class="col-xs-12"><h1>@Model.post.title</h1></div>
|
||||
<div class="col-xs-12"><h1>@Model.Post.Title</h1></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h4>
|
||||
<i class="fa fa-calendar" title="@Translate.Date"></i> @Model.publishedDate
|
||||
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.publishedTime
|
||||
@Each.post.categories
|
||||
<i class="fa fa-calendar" title="@Translate.Date"></i> @Model.PublishedDate
|
||||
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.PublishedTime
|
||||
@Each.Post.Categories
|
||||
<span style="white-space:nowrap;">
|
||||
<i class="fa fa-folder-open-o" title="@Translate.Category"></i>
|
||||
<a href="/category/@Current.slug" title="@Translate.CategorizedUnder @Current.name">@Current.name</a>
|
||||
<a href="/category/@Current.Slug" title="@Translate.CategorizedUnder @Current.Name">@Current.Name</a>
|
||||
|
||||
</span>
|
||||
@EndEach
|
||||
@ -18,12 +18,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">@Model.post.text</div>
|
||||
<div class="col-xs-12">@Model.Post.Text</div>
|
||||
</div>
|
||||
@If.hasTags
|
||||
@If.HasTags
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
@Each.tags
|
||||
@Each.Tags
|
||||
<span style="white-space:nowrap;">
|
||||
<a href="/tag/@Current.Item2" title="@Translate.PostsTagged "@Current.Item1"">
|
||||
<i class="fa fa-tag"></i> @Current.Item1
|
||||
@ -56,17 +56,17 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
@If.hasNewer
|
||||
<a href="/@Model.newerPost.Value.permalink" title="@Translate.NextPost - "@Model.newerPost.Value.title"">
|
||||
« @Model.newerPost.Value.title
|
||||
@If.HasNewer
|
||||
<a href="/@Model.NewerPost.Value.Permalink" title="@Translate.NextPost - "@Model.NewerPost.Value.Title"">
|
||||
« @Model.NewerPost.Value.Title
|
||||
</a>
|
||||
@EndIf
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
@If.hasOlder
|
||||
<a href="/@Model.olderPost.Value.permalink"
|
||||
title="@Translate.PreviousPost - "@Model.olderPost.Value.title"">
|
||||
@Model.olderPost.Value.title »
|
||||
@If.HasOlder
|
||||
<a href="/@Model.OlderPost.Value.Permalink"
|
||||
title="@Translate.PreviousPost - "@Model.OlderPost.Value.Title"">
|
||||
@Model.OlderPost.Value.Title »
|
||||
</a>
|
||||
@EndIf
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user