post edit page

The majority of the changes in this commit have to do with the post edit
page; also caught up resource strings that hadn't been actually put in
Resources.resx yet
This commit is contained in:
Daniel J. Summers 2016-07-09 21:11:50 -05:00
parent 3656bb384c
commit 6b24d416fc
18 changed files with 818 additions and 34 deletions

View File

@ -0,0 +1,28 @@
module myWebLog.Data.Category
open myWebLog.Entities
open Rethink
/// 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
yield! getChildren child (indent + 1)
}
categories
|> List.filter (fun c -> c.parentId.IsNone)
|> List.map (fun c -> getChildren c 0)
|> Seq.collect id
|> Seq.toList
/// Get all categories for a web log
let getAllCategories conn webLogId =
table Table.Category
|> getAll [| webLogId |]
|> optArg "index" "webLogId"
|> orderBy (fun c -> upcast c.["name"])
|> runCursorAsync<Category> conn
|> Seq.toList
|> sortCategories

View File

@ -6,17 +6,23 @@ open Newtonsoft.Json
/// Constants to use for revision source language
module RevisionSource =
[<Literal>]
let Markdown = "markdown"
[<Literal>]
let HTML = "html"
/// Constants to use for authorization levels
module AuthorizationLevel =
[<Literal>]
let Administrator = "Administrator"
[<Literal>]
let User = "User"
/// Constants to use for post statuses
module PostStatus =
[<Literal>]
let Draft = "Draft"
[<Literal>]
let Published = "Published"
// ---- Entities ----
@ -30,7 +36,12 @@ type Revision = {
/// The text
text : string
}
with
/// An empty revision
static member empty =
{ asOf = int64 0
sourceType = RevisionSource.HTML
text = "" }
/// A page with static content
type Page = {
@ -247,7 +258,7 @@ type Post = {
}
with
static member empty =
{ id = ""
{ id = "new"
webLogId = ""
authorId = ""
status = PostStatus.Draft

View File

@ -11,4 +11,15 @@ let tryFindPage conn webLogId pageId : Page option =
|> runAtomAsync<Page> conn
|> box with
| null -> None
| page -> Some <| unbox page
/// Get a page by its Id (excluding revisions)
let tryFindPageWithoutRevisions conn webLogId pageId : Page option =
match table Table.Page
|> get pageId
|> filter (fun p -> upcast p.["webLogId"].Eq(webLogId))
|> without [| "revisions" |]
|> runAtomAsync<Page> conn
|> box with
| null -> None
| page -> Some <| unbox page

View File

@ -47,3 +47,29 @@ let findPageOfAllPosts conn webLogId pageNbr nbrPerPage =
|> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage)
|> runCursorAsync<Post> conn
|> Seq.toList
/// Try to find a post by its Id and web log Id
let tryFindPost conn webLogId postId : Post option =
match table Table.Post
|> get postId
|> filter (fun p -> upcast p.["webLogId"].Eq(webLogId))
|> runAtomAsync<Post> conn
|> box with
| null -> None
| post -> Some <| unbox post
/// Save a post
let savePost conn post =
match post.id with
| "new" -> let newPost = { post with id = string <| System.Guid.NewGuid() }
table Table.Post
|> insert newPost
|> runResultAsync conn
|> ignore
newPost.id
| _ -> table Table.Post
|> get post.id
|> replace post
|> runResultAsync conn
|> ignore
post.id

View File

@ -14,7 +14,7 @@ let insert (expr : obj) (table : Table) = table.Insert expr
let limit (expr : obj) (table : ReqlExpr) = table.Limit expr
let optArg key (value : obj) (expr : GetAll) = expr.OptArg (key, value)
let orderBy (exprA : ReqlExpr -> ReqlExpr) (expr : ReqlExpr) = expr.OrderBy exprA
let replace (exprA : obj) (expr : ReqlExpr) = expr.Replace exprA
let replace (exprA : obj) (expr : Get) = expr.Replace exprA
let runAtomAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<'T> conn |> await
let runCursorAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunCursorAsync<'T> conn |> await
let runListAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<System.Collections.Generic.List<'T>> conn

View File

@ -56,6 +56,7 @@
<Compile Include="DataConfig.fs" />
<Compile Include="Rethink.fs" />
<Compile Include="SetUp.fs" />
<Compile Include="Category.fs" />
<Compile Include="Page.fs" />
<Compile Include="Post.fs" />
<Compile Include="WebLog.fs" />

View File

@ -60,6 +60,132 @@ namespace myWebLog {
}
}
/// <summary>
/// Looks up a localized string similar to Added.
/// </summary>
public static string Added {
get {
return ResourceManager.GetString("Added", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add New.
/// </summary>
public static string AddNew {
get {
return ResourceManager.GetString("AddNew", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add New Post.
/// </summary>
public static string AddNewPost {
get {
return ResourceManager.GetString("AddNewPost", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Admin.
/// </summary>
public static string Admin {
get {
return ResourceManager.GetString("Admin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to and Published.
/// </summary>
public static string AndPublished {
get {
return ResourceManager.GetString("AndPublished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Categories.
/// </summary>
public static string Categories {
get {
return ResourceManager.GetString("Categories", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dashboard.
/// </summary>
public static string Dashboard {
get {
return ResourceManager.GetString("Dashboard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Date.
/// </summary>
public static string Date {
get {
return ResourceManager.GetString("Date", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
public static string Delete {
get {
return ResourceManager.GetString("Delete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
public static string Edit {
get {
return ResourceManager.GetString("Edit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit Post.
/// </summary>
public static string EditPost {
get {
return ResourceManager.GetString("EditPost", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Could not convert data-config.json to RethinkDB connection.
/// </summary>
public static string ErrDataConfig {
get {
return ResourceManager.GetString("ErrDataConfig", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to is not properly configured for myWebLog.
/// </summary>
public static string ErrNotConfigured {
get {
return ResourceManager.GetString("ErrNotConfigured", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Log Off.
/// </summary>
public static string LogOff {
get {
return ResourceManager.GetString("LogOff", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Log On.
/// </summary>
@ -68,5 +194,176 @@ namespace myWebLog {
return ResourceManager.GetString("LogOn", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}{1} post successfully.
/// </summary>
public static string MsgPostEditSuccess {
get {
return ResourceManager.GetString("MsgPostEditSuccess", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Newer Posts.
/// </summary>
public static string NewerPosts {
get {
return ResourceManager.GetString("NewerPosts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Older Posts.
/// </summary>
public static string OlderPosts {
get {
return ResourceManager.GetString("OlderPosts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Page #.
/// </summary>
public static string PageHash {
get {
return ResourceManager.GetString("PageHash", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Permalink.
/// </summary>
public static string Permalink {
get {
return ResourceManager.GetString("Permalink", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Permanent link to.
/// </summary>
public static string PermanentLinkTo {
get {
return ResourceManager.GetString("PermanentLinkTo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Post Details.
/// </summary>
public static string PostDetails {
get {
return ResourceManager.GetString("PostDetails", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Posts.
/// </summary>
public static string Posts {
get {
return ResourceManager.GetString("Posts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Post Status.
/// </summary>
public static string PostStatus {
get {
return ResourceManager.GetString("PostStatus", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PublishedDate.
/// </summary>
public static string PublishedDate {
get {
return ResourceManager.GetString("PublishedDate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Publish This Post.
/// </summary>
public static string PublishThisPost {
get {
return ResourceManager.GetString("PublishThisPost", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save.
/// </summary>
public static string Save {
get {
return ResourceManager.GetString("Save", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to starting with.
/// </summary>
public static string startingWith {
get {
return ResourceManager.GetString("startingWith", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Status.
/// </summary>
public static string Status {
get {
return ResourceManager.GetString("Status", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tags.
/// </summary>
public static string Tags {
get {
return ResourceManager.GetString("Tags", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Time.
/// </summary>
public static string Time {
get {
return ResourceManager.GetString("Time", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Title.
/// </summary>
public static string Title {
get {
return ResourceManager.GetString("Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Updated.
/// </summary>
public static string Updated {
get {
return ResourceManager.GetString("Updated", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to View.
/// </summary>
public static string View {
get {
return ResourceManager.GetString("View", resourceCulture);
}
}
}
}

View File

@ -117,7 +117,106 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Added" xml:space="preserve">
<value>Added</value>
</data>
<data name="AddNew" xml:space="preserve">
<value>Add New</value>
</data>
<data name="AddNewPost" xml:space="preserve">
<value>Add New Post</value>
</data>
<data name="Admin" xml:space="preserve">
<value>Admin</value>
</data>
<data name="AndPublished" xml:space="preserve">
<value> and Published</value>
</data>
<data name="Categories" xml:space="preserve">
<value>Categories</value>
</data>
<data name="Dashboard" xml:space="preserve">
<value>Dashboard</value>
</data>
<data name="Date" xml:space="preserve">
<value>Date</value>
</data>
<data name="Delete" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Edit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="EditPost" xml:space="preserve">
<value>Edit Post</value>
</data>
<data name="ErrDataConfig" xml:space="preserve">
<value>Could not convert data-config.json to RethinkDB connection</value>
</data>
<data name="ErrNotConfigured" xml:space="preserve">
<value>is not properly configured for myWebLog</value>
</data>
<data name="LogOff" xml:space="preserve">
<value>Log Off</value>
</data>
<data name="LogOn" xml:space="preserve">
<value>Log On</value>
</data>
<data name="MsgPostEditSuccess" xml:space="preserve">
<value>{0}{1} post successfully</value>
</data>
<data name="NewerPosts" xml:space="preserve">
<value>Newer Posts</value>
</data>
<data name="OlderPosts" xml:space="preserve">
<value>Older Posts</value>
</data>
<data name="PageHash" xml:space="preserve">
<value>Page #</value>
</data>
<data name="Permalink" xml:space="preserve">
<value>Permalink</value>
</data>
<data name="PermanentLinkTo" xml:space="preserve">
<value>Permanent link to</value>
</data>
<data name="PostDetails" xml:space="preserve">
<value>Post Details</value>
</data>
<data name="Posts" xml:space="preserve">
<value>Posts</value>
</data>
<data name="PostStatus" xml:space="preserve">
<value>Post Status</value>
</data>
<data name="PublishedDate" xml:space="preserve">
<value>PublishedDate</value>
</data>
<data name="PublishThisPost" xml:space="preserve">
<value>Publish This Post</value>
</data>
<data name="Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="startingWith" xml:space="preserve">
<value>starting with</value>
</data>
<data name="Status" xml:space="preserve">
<value>Status</value>
</data>
<data name="Tags" xml:space="preserve">
<value>Tags</value>
</data>
<data name="Time" xml:space="preserve">
<value>Time</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
<data name="Updated" xml:space="preserve">
<value>Updated</value>
</data>
<data name="View" xml:space="preserve">
<value>View</value>
</data>
</root>

View File

@ -15,6 +15,7 @@ open Nancy.Session.Persistable
open Nancy.Session.RethinkDb
open Nancy.TinyIoc
open Nancy.ViewEngines.SuperSimpleViewEngine
open NodaTime
open RethinkDb.Driver.Net
open Suave
open Suave.Owin
@ -23,8 +24,7 @@ open System.Text.RegularExpressions
/// Set up a database connection
let cfg = try DataConfig.fromJson (System.IO.File.ReadAllText "data-config.json")
with ex -> ApplicationException("Could not convert data-config.json to RethinkDB connection", ex)
|> raise
with ex -> raise <| ApplicationException(Resources.ErrDataConfig, ex)
do
startUpCheck cfg
@ -75,6 +75,9 @@ type MyWebLogBootstrapper() =
|> ignore
container.Register<IConnection>(cfg.conn)
|> ignore
// NodaTime
container.Register<IClock>(SystemClock.Instance)
|> ignore
// I18N in SSVE
container.Register<seq<ISuperSimpleViewEngineMatcher>>(fun _ _ ->
Seq.singleton (TranslateTokenViewEngineMatcher() :> ISuperSimpleViewEngineMatcher))
@ -116,7 +119,7 @@ type RequestEnvironment() =
match tryFindWebLogByUrlBase cfg.conn ctx.Request.Url.HostName with
| Some webLog -> ctx.Items.[Keys.WebLog] <- webLog
| None -> ApplicationException
(sprintf "%s is not properly configured for myWebLog" ctx.Request.Url.HostName)
(sprintf "%s %s" ctx.Request.Url.HostName Resources.ErrNotConfigured)
|> raise
ctx.Items.[Keys.Version] <- version
null)

View File

@ -1,23 +1,30 @@
namespace myWebLog
[<AutoOpen>]
module myWebLog.ModuleExtensions
open myWebLog
open myWebLog.Entities
open Nancy
open Nancy.Security
/// Parent class for all myWebLog Nancy modules
[<AbstractClass>]
type MyWebLogModule() =
inherit NancyModule()
type NancyModule with
/// Strongly-typed access to the web log for the current request
member this.WebLog = this.Context.Items.[Keys.WebLog] :?> WebLog
/// Display a view using the theme specified for the web log
member this.ThemedRender view model = this.View.[(sprintf "%s/%s" this.WebLog.themePath view), model]
member this.ThemedView view model = this.View.[(sprintf "%s/%s" this.WebLog.themePath view), model]
/// Return a 404
member this.NotFound () = this.Negotiate.WithStatusCode 404
/// Redirect a request, storing messages in the session if they exist
member this.Redirect url (model : MyWebLogModel) =
match List.length model.messages with
| 0 -> ()
| _ -> this.Session.[Keys.Messages] <- model.messages
this.Negotiate.WithHeader("Location", url).WithStatusCode(HttpStatusCode.TemporaryRedirect)
/// Require a specific level of access for the current web log
member this.RequiresAccessLevel level =
this.RequiresAuthentication()

View File

@ -1,28 +1,36 @@
namespace myWebLog
open FSharp.Markdown
open myWebLog.Data.Category
open myWebLog.Data.Page
open myWebLog.Data.Post
open myWebLog.Entities
open Nancy
open Nancy.Authentication.Forms
open Nancy.ModelBinding
open Nancy.Security
open Nancy.Session.Persistable
open NodaTime
open RethinkDb.Driver.Net
open myWebLog.Entities
type PostModule(conn : IConnection) as this =
inherit MyWebLogModule()
/// Routes dealing with posts (including the home page)
type PostModule(conn : IConnection, clock : IClock) as this =
inherit NancyModule()
let getPage (parms : obj) = ((parms :?> DynamicDictionary).["page"] :?> int)
do
this.Get.["/"] <- fun _ -> upcast this.HomePage ()
this.Get.["/posts/page/{page:int}"] <- fun parms -> upcast this.GetPageOfPublishedPosts (downcast parms)
this.Get.["/posts/list"] <- fun _ -> upcast this.PostList 1
this.Get.["/posts/list/page/{page:int"] <- fun parms -> upcast this.PostList
((parms :?> DynamicDictionary).["page"] :?> int)
this.Get .["/" ] <- fun _ -> upcast this.HomePage ()
this.Get .["/posts/page/{page:int}" ] <- fun parms -> upcast this.DisplayPageOfPublishedPosts (getPage parms)
this.Get .["/posts/list" ] <- fun _ -> upcast this.PostList 1
this.Get .["/posts/list/page/{page:int}"] <- fun parms -> upcast this.PostList (getPage parms)
this.Get .["/post/{postId}/edit" ] <- fun parms -> upcast this.EditPost (downcast parms)
this.Post.["/post/{postId}/edit" ] <- fun parms -> upcast this.SavePost (downcast parms)
// ---- Display posts to users ----
/// Display a page of published posts
member private this.DisplayPageOfPublishedPosts pageNbr =
member this.DisplayPageOfPublishedPosts pageNbr =
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10
@ -35,22 +43,18 @@ type PostModule(conn : IConnection) as this =
model.urlPrefix <- "/posts"
model.pageTitle <- match pageNbr with
| 1 -> ""
| _ -> sprintf "Page #%i" pageNbr
this.ThemedRender "posts" model
| _ -> sprintf "%s%i" Resources.PageHash pageNbr
this.ThemedView "posts" model
/// Display either the newest posts or the configured home page
member this.HomePage () =
match this.WebLog.defaultPage with
| "posts" -> this.DisplayPageOfPublishedPosts 1
| page -> match tryFindPage conn this.WebLog.id page with
| page -> match tryFindPageWithoutRevisions conn this.WebLog.id page with
| Some page -> let model = PageModel(this.Context, this.WebLog, page)
model.pageTitle <- page.title
this.ThemedRender "page" model
| None -> this.Negotiate.WithStatusCode 404
/// Get a page of public posts (other than the first one if the home page is a page of posts)
member this.GetPageOfPublishedPosts (parameters : DynamicDictionary) =
this.DisplayPageOfPublishedPosts (parameters.["page"] :?> int)
this.ThemedView "page" model
| None -> this.NotFound ()
// ---- Administer posts ----
@ -61,7 +65,78 @@ type PostModule(conn : IConnection) as this =
model.pageNbr <- pageNbr
model.posts <- findPageOfAllPosts conn this.WebLog.id pageNbr 25
model.hasNewer <- pageNbr > 1
model.hasOlder <- 25 > List.length model.posts
model.hasOlder <- List.length model.posts < 25
model.urlPrefix <- "/post/list"
model.pageTitle <- "Posts"
model.pageTitle <- Resources.Posts
this.View.["admin/post/list", model]
/// Edit a post
member this.EditPost (parameters : DynamicDictionary) =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let postId : string = downcast parameters.["postId"]
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)
|> List.tryHead with
| Some r -> r
| 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,
sprintf "%s%s"
(String.replicate (snd cat) " &nbsp; &nbsp; ")
(fst cat).name)
model.pageTitle <- match post.id with
| "new" -> Resources.AddNewPost
| _ -> Resources.EditPost
this.View.["admin/post/edit"]
| None -> this.NotFound ()
/// Save a post
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
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
let post = match postId with
| "new" -> { p with
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
| 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 ','
|> Seq.map (fun t -> t.Trim().ToLowerInvariant())
|> Seq.toList
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
this.Redirect (sprintf "/post/%s/edit" pId) model
| None -> this.NotFound ()

View File

@ -3,6 +3,9 @@
open myWebLog.Entities
open Nancy
open Nancy.Session.Persistable
open NodaTime
open NodaTime.Text
/// Levels for a user message
module Level =
@ -57,6 +60,23 @@ type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
/// Add a message to the output
member this.addMessage message = this.messages <- message :: this.messages
/// Convert ticks to a zoned date/time for the current web log
member this.zonedTime ticks = Instant(ticks).InZone(DateTimeZoneProviders.Tzdb.[this.webLog.timeZone])
/// Display a long date
member this.displayLongDate ticks =
this.zonedTime ticks
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
/// Display a short date
member this.displayShortDate ticks =
this.zonedTime ticks
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
/// Display the time
member this.displayTime ticks =
(this.zonedTime ticks
|> ZonedDateTimePattern.CreateWithCurrentCulture("h':'mmtt", DateTimeZoneProviders.Tzdb).Format).ToLower()
/// Model for all page-of-posts pages
type PostsModel(ctx, webLog) =
@ -93,3 +113,52 @@ type PageModel(ctx, webLog, page) =
/// The page to be displayed
member this.page : Page = page
/// Form for editing a post
type EditPostForm() =
/// The title of the post
member val title = "" with get, set
/// The permalink for the post
member val permalink = "" with get, set
/// The source type for this revision
member val source = "" with get, set
/// The text
member val text = "" with get, set
/// Tags for the post
member val tags = "" with get, set
/// The selected category Ids for the post
member val categories = Array.empty<string> with get, set
/// Whether the post should be published
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
this
/// Fill the form with applicable values from a revision
member this.forRevision rev =
this.source <- rev.sourceType
this.text <- rev.text
this
/// View model for the edit post page
type EditPostModel(ctx, webLog, post, revision) =
inherit MyWebLogModel(ctx, webLog)
/// The form
member val form = EditPostForm().forPost(post).forRevision(revision) with get, set
/// The post being edited
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
/// Whether the post is currently published
member this.isPublished = PostStatus.Published = this.post.status
/// The published date
member this.publishedDate = this.displayLongDate this.post.publishedOn
/// The published time
member this.publishedTime = this.displayTime this.post.publishedOn

View File

@ -53,7 +53,7 @@
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Keys.fs" />
<Compile Include="ViewModels.fs" />
<Compile Include="MyWebLogModule.fs" />
<Compile Include="ModuleExtensions.fs" />
<Compile Include="PostModule.fs" />
<Compile Include="App.fs" />
<Content Include="packages.config" />
@ -67,10 +67,42 @@
<HintPath>..\packages\Common.Logging.Core.3.3.1\lib\net40\Common.Logging.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="CSharpFormat">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\CSharpFormat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharp.CodeFormat">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.CodeFormat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharp.Compiler.Service">
<HintPath>..\packages\FSharp.Compiler.Service.2.0.0.6\lib\net45\FSharp.Compiler.Service.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharp.Core">
<HintPath>..\packages\FSharp.Core.4.0.0.1\lib\net40\FSharp.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharp.Formatting.Common">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.Formatting.Common.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharp.Literate">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.Literate.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharp.Markdown">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.Markdown.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharp.MetadataFormat">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.MetadataFormat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FSharpVSPowerTools.Core">
<HintPath>..\packages\FSharpVSPowerTools.Core.2.3.0\lib\net45\FSharpVSPowerTools.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="mscorlib" />
<Reference Include="Nancy">
<HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath>
@ -92,6 +124,14 @@
<HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NodaTime">
<HintPath>..\packages\NodaTime.1.3.2\lib\net35-Client\NodaTime.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="RazorEngine">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\RazorEngine.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="RethinkDb.Driver">
<HintPath>..\packages\RethinkDb.Driver.2.3.8\lib\net45\RethinkDb.Driver.dll</HintPath>
<Private>True</Private>
@ -101,8 +141,14 @@
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Web.Razor">
<HintPath>..\packages\FSharp.Formatting.2.14.4\lib\net40\System.Web.Razor.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\myWebLog.Data\myWebLog.Data.fsproj">

View File

@ -2,12 +2,16 @@
<packages>
<package id="Common.Logging" version="3.3.1" targetFramework="net452" />
<package id="Common.Logging.Core" version="3.3.1" targetFramework="net452" />
<package id="FSharp.Compiler.Service" version="2.0.0.6" targetFramework="net452" />
<package id="FSharp.Core" version="4.0.0.1" targetFramework="net452" />
<package id="FSharp.Formatting" version="2.14.4" targetFramework="net452" />
<package id="FSharpVSPowerTools.Core" version="2.3.0" targetFramework="net452" />
<package id="Nancy" version="1.4.3" targetFramework="net452" />
<package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net452" />
<package id="Nancy.Session.Persistable" version="0.8.6" targetFramework="net452" />
<package id="Nancy.Session.RethinkDB" version="0.8.6" targetFramework="net452" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net452" />
<package id="NodaTime" version="1.3.2" targetFramework="net452" />
<package id="RethinkDb.Driver" version="2.3.8" targetFramework="net452" />
<package id="Suave" version="1.1.3" targetFramework="net452" />
</packages>

View File

@ -0,0 +1,10 @@
tinymce.init({
menubar: false,
plugins: [
"advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker",
"searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking",
"save table contextmenu directionality emoticons template paste textcolor"
],
selector: "textarea",
toolbar: "styleselect | forecolor backcolor | bullist numlist | link unlink anchor | paste pastetext | spellchecker | visualblocks visualchars | code fullscreen"
})

View File

@ -66,8 +66,10 @@
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Content Include="content\scripts\tinymce-init.js" />
<Content Include="content\styles\admin.css" />
<Content Include="views\admin\admin-layout.html" />
<Content Include="views\admin\post\edit.html" />
<Content Include="views\admin\post\list.html" />
<Content Include="views\default\index-content.html" />
<Content Include="views\default\index.html" />

View File

@ -0,0 +1,95 @@
@Master['admin/admin-layout']
@Section['Content']
<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" />
</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" />
<!-- // 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>
</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" />
</div>
</div>
<div class="col-sm-3">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">@Translate.PostDetails</h4>
</div>
<div class="panel-body">
<div class="form-group">
<label class="control-label">@Translate.PostStatus</label>
<p class="static-control">@Model.post.status</p>
</div>
@If.isPublished
<div class="form-group">
<label class="control-label">@Translate.PublishedDate</label>
<p class="static-control">@Model.publishedDate<br />@Model.publishedTime</p>
</div>
@EndIf
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">@Translate.Categories</h4>
</div>
<div class="panel-body" style="max-height:350px;overflow:scroll;">
<!-- // TODO: how to check the ones that are already selected? -->
@Each.categories
<!-- - var tab = 0
while tab < item.indent
| &nbsp; &nbsp;
- tab++
- var attributes = {}
if -1 < currentCategories.indexOf(item.category.id)
- attributes.checked = 'checked' -->
<input type="checkbox" id="category-@Current.Item1" name="category", value="@Current.Item1" />
&nbsp;
<!-- // FIXME: the title should be the category description -->
<label for="category-@Current.Item1" title="@Current.Item2">@Current.Item2</label>
<br/>
@EndEach
</div>
</div>
<div class="text-center">
@If.isPublished
<input type="hidden" name="publishNow" value="true" />
@EndIf
@IfNot.isPublished
<div>
<input type="checkbox" name="publishNow" id="publishNow" value="true" checked="checked" />
&nbsp; <label for="publishNow">@Translate.PublishThisPost</label>
</div>
@EndIf
<p>
<button type="submit" class="btn btn-primary">
<i class="fa fa-floppy-o"></i> &nbsp; @Translate.Save
</button>
</p>
</div>
</div>
</div>
</form>
@EndSection
@Section['Scripts']
<script type="text/javascript" src="/content/scripts/tinymce-init.js"></script>
<script type="text/javascript">
/** <![CDATA[ */
$(document).ready(function () { $("#title").focus() })
/** ]]> */
</script>
@EndSection

View File

@ -8,7 +8,7 @@
<div class="col-xs-12">
<article>
<h1>
<a href="/@Current.permalink" title="Permanent Link to &quot;@Current.title@quot;">@Current.title</a>
<a href="/@Current.permalink" title="@Translate.PermanentLinkTo &quot;@Current.title@quot;">@Current.title</a>
</h1>
<!-- var pubDate = moment(post.publishedDate) -->
<p>