diff --git a/src/myWebLog.Data/Category.fs b/src/myWebLog.Data/Category.fs new file mode 100644 index 0000000..567ca2f --- /dev/null +++ b/src/myWebLog.Data/Category.fs @@ -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 conn + |> Seq.toList + |> sortCategories diff --git a/src/myWebLog.Data/Entities.fs b/src/myWebLog.Data/Entities.fs index 990007f..7a45a61 100644 --- a/src/myWebLog.Data/Entities.fs +++ b/src/myWebLog.Data/Entities.fs @@ -6,17 +6,23 @@ open Newtonsoft.Json /// Constants to use for revision source language module RevisionSource = + [] let Markdown = "markdown" + [] let HTML = "html" /// Constants to use for authorization levels module AuthorizationLevel = + [] let Administrator = "Administrator" + [] let User = "User" /// Constants to use for post statuses module PostStatus = + [] let Draft = "Draft" + [] 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 diff --git a/src/myWebLog.Data/Page.fs b/src/myWebLog.Data/Page.fs index d31bf8b..db585ec 100644 --- a/src/myWebLog.Data/Page.fs +++ b/src/myWebLog.Data/Page.fs @@ -11,4 +11,15 @@ let tryFindPage conn webLogId pageId : Page option = |> runAtomAsync 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 conn + |> box with + | null -> None | page -> Some <| unbox page \ No newline at end of file diff --git a/src/myWebLog.Data/Post.fs b/src/myWebLog.Data/Post.fs index c728d59..8231f15 100644 --- a/src/myWebLog.Data/Post.fs +++ b/src/myWebLog.Data/Post.fs @@ -47,3 +47,29 @@ let findPageOfAllPosts conn webLogId pageNbr nbrPerPage = |> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage) |> runCursorAsync 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 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 diff --git a/src/myWebLog.Data/Rethink.fs b/src/myWebLog.Data/Rethink.fs index cdbc414..c4a4486 100644 --- a/src/myWebLog.Data/Rethink.fs +++ b/src/myWebLog.Data/Rethink.fs @@ -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> conn diff --git a/src/myWebLog.Data/myWebLog.Data.fsproj b/src/myWebLog.Data/myWebLog.Data.fsproj index c82224d..5aade4d 100644 --- a/src/myWebLog.Data/myWebLog.Data.fsproj +++ b/src/myWebLog.Data/myWebLog.Data.fsproj @@ -56,6 +56,7 @@ + diff --git a/src/myWebLog.Resources/Resources.Designer.cs b/src/myWebLog.Resources/Resources.Designer.cs index de30025..e22aa74 100644 --- a/src/myWebLog.Resources/Resources.Designer.cs +++ b/src/myWebLog.Resources/Resources.Designer.cs @@ -60,6 +60,132 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Added. + /// + public static string Added { + get { + return ResourceManager.GetString("Added", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add New. + /// + public static string AddNew { + get { + return ResourceManager.GetString("AddNew", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add New Post. + /// + public static string AddNewPost { + get { + return ResourceManager.GetString("AddNewPost", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Admin. + /// + public static string Admin { + get { + return ResourceManager.GetString("Admin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to and Published. + /// + public static string AndPublished { + get { + return ResourceManager.GetString("AndPublished", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Categories. + /// + public static string Categories { + get { + return ResourceManager.GetString("Categories", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dashboard. + /// + public static string Dashboard { + get { + return ResourceManager.GetString("Dashboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date. + /// + public static string Date { + get { + return ResourceManager.GetString("Date", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete. + /// + public static string Delete { + get { + return ResourceManager.GetString("Delete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit. + /// + public static string Edit { + get { + return ResourceManager.GetString("Edit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Edit Post. + /// + public static string EditPost { + get { + return ResourceManager.GetString("EditPost", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not convert data-config.json to RethinkDB connection. + /// + public static string ErrDataConfig { + get { + return ResourceManager.GetString("ErrDataConfig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to is not properly configured for myWebLog. + /// + public static string ErrNotConfigured { + get { + return ResourceManager.GetString("ErrNotConfigured", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log Off. + /// + public static string LogOff { + get { + return ResourceManager.GetString("LogOff", resourceCulture); + } + } + /// /// Looks up a localized string similar to Log On. /// @@ -68,5 +194,176 @@ namespace myWebLog { return ResourceManager.GetString("LogOn", resourceCulture); } } + + /// + /// Looks up a localized string similar to {0}{1} post successfully. + /// + public static string MsgPostEditSuccess { + get { + return ResourceManager.GetString("MsgPostEditSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Newer Posts. + /// + public static string NewerPosts { + get { + return ResourceManager.GetString("NewerPosts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Older Posts. + /// + public static string OlderPosts { + get { + return ResourceManager.GetString("OlderPosts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Page #. + /// + public static string PageHash { + get { + return ResourceManager.GetString("PageHash", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Permalink. + /// + public static string Permalink { + get { + return ResourceManager.GetString("Permalink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Permanent link to. + /// + public static string PermanentLinkTo { + get { + return ResourceManager.GetString("PermanentLinkTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Post Details. + /// + public static string PostDetails { + get { + return ResourceManager.GetString("PostDetails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Posts. + /// + public static string Posts { + get { + return ResourceManager.GetString("Posts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Post Status. + /// + public static string PostStatus { + get { + return ResourceManager.GetString("PostStatus", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PublishedDate. + /// + public static string PublishedDate { + get { + return ResourceManager.GetString("PublishedDate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publish This Post. + /// + public static string PublishThisPost { + get { + return ResourceManager.GetString("PublishThisPost", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save. + /// + public static string Save { + get { + return ResourceManager.GetString("Save", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to starting with. + /// + public static string startingWith { + get { + return ResourceManager.GetString("startingWith", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Status. + /// + public static string Status { + get { + return ResourceManager.GetString("Status", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tags. + /// + public static string Tags { + get { + return ResourceManager.GetString("Tags", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Time. + /// + public static string Time { + get { + return ResourceManager.GetString("Time", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Title. + /// + public static string Title { + get { + return ResourceManager.GetString("Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated. + /// + public static string Updated { + get { + return ResourceManager.GetString("Updated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View. + /// + public static string View { + get { + return ResourceManager.GetString("View", resourceCulture); + } + } } } diff --git a/src/myWebLog.Resources/Resources.resx b/src/myWebLog.Resources/Resources.resx index cf2393c..fd9397a 100644 --- a/src/myWebLog.Resources/Resources.resx +++ b/src/myWebLog.Resources/Resources.resx @@ -117,7 +117,106 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Added + + + Add New + + + Add New Post + + + Admin + + + and Published + + + Categories + + + Dashboard + + + Date + + + Delete + + + Edit + + + Edit Post + + + Could not convert data-config.json to RethinkDB connection + + + is not properly configured for myWebLog + + + Log Off + Log On + + {0}{1} post successfully + + + Newer Posts + + + Older Posts + + + Page # + + + Permalink + + + Permanent link to + + + Post Details + + + Posts + + + Post Status + + + PublishedDate + + + Publish This Post + + + Save + + + starting with + + + Status + + + Tags + + + Time + + + Title + + + Updated + + + View + \ No newline at end of file diff --git a/src/myWebLog.Web/App.fs b/src/myWebLog.Web/App.fs index 3737906..ba2c47c 100644 --- a/src/myWebLog.Web/App.fs +++ b/src/myWebLog.Web/App.fs @@ -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(cfg.conn) |> ignore + // NodaTime + container.Register(SystemClock.Instance) + |> ignore // I18N in SSVE container.Register>(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) diff --git a/src/myWebLog.Web/MyWebLogModule.fs b/src/myWebLog.Web/ModuleExtensions.fs similarity index 53% rename from src/myWebLog.Web/MyWebLogModule.fs rename to src/myWebLog.Web/ModuleExtensions.fs index e91da1b..c5be2ba 100644 --- a/src/myWebLog.Web/MyWebLogModule.fs +++ b/src/myWebLog.Web/ModuleExtensions.fs @@ -1,23 +1,30 @@ -namespace myWebLog +[] +module myWebLog.ModuleExtensions +open myWebLog open myWebLog.Entities open Nancy open Nancy.Security /// Parent class for all myWebLog Nancy modules -[] -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() diff --git a/src/myWebLog.Web/PostModule.fs b/src/myWebLog.Web/PostModule.fs index 4e498ed..803fab0 100644 --- a/src/myWebLog.Web/PostModule.fs +++ b/src/myWebLog.Web/PostModule.fs @@ -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) "     ") + (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() + 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 + (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 () diff --git a/src/myWebLog.Web/ViewModels.fs b/src/myWebLog.Web/ViewModels.fs index 808c874..e83278f 100644 --- a/src/myWebLog.Web/ViewModels.fs +++ b/src/myWebLog.Web/ViewModels.fs @@ -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 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 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 diff --git a/src/myWebLog.Web/myWebLog.Web.fsproj b/src/myWebLog.Web/myWebLog.Web.fsproj index ac279d5..845781a 100644 --- a/src/myWebLog.Web/myWebLog.Web.fsproj +++ b/src/myWebLog.Web/myWebLog.Web.fsproj @@ -53,7 +53,7 @@ - + @@ -67,10 +67,42 @@ ..\packages\Common.Logging.Core.3.3.1\lib\net40\Common.Logging.Core.dll True + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\CSharpFormat.dll + True + + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.CodeFormat.dll + True + + + ..\packages\FSharp.Compiler.Service.2.0.0.6\lib\net45\FSharp.Compiler.Service.dll + True + ..\packages\FSharp.Core.4.0.0.1\lib\net40\FSharp.Core.dll True + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.Formatting.Common.dll + True + + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.Literate.dll + True + + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.Markdown.dll + True + + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\FSharp.MetadataFormat.dll + True + + + ..\packages\FSharpVSPowerTools.Core.2.3.0\lib\net45\FSharpVSPowerTools.Core.dll + True + ..\packages\Nancy.1.4.3\lib\net40\Nancy.dll @@ -92,6 +124,14 @@ ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll True + + ..\packages\NodaTime.1.3.2\lib\net35-Client\NodaTime.dll + True + + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\RazorEngine.dll + True + ..\packages\RethinkDb.Driver.2.3.8\lib\net45\RethinkDb.Driver.dll True @@ -101,8 +141,14 @@ True + + + ..\packages\FSharp.Formatting.2.14.4\lib\net40\System.Web.Razor.dll + True + + diff --git a/src/myWebLog.Web/packages.config b/src/myWebLog.Web/packages.config index 4dce297..5bcf0fd 100644 --- a/src/myWebLog.Web/packages.config +++ b/src/myWebLog.Web/packages.config @@ -2,12 +2,16 @@ + + + + \ No newline at end of file diff --git a/src/myWebLog/content/scripts/tinymce-init.js b/src/myWebLog/content/scripts/tinymce-init.js new file mode 100644 index 0000000..48c9339 --- /dev/null +++ b/src/myWebLog/content/scripts/tinymce-init.js @@ -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" +}) \ No newline at end of file diff --git a/src/myWebLog/myWebLog.csproj b/src/myWebLog/myWebLog.csproj index f68710a..f87e3a1 100644 --- a/src/myWebLog/myWebLog.csproj +++ b/src/myWebLog/myWebLog.csproj @@ -66,8 +66,10 @@ + + diff --git a/src/myWebLog/views/admin/post/edit.html b/src/myWebLog/views/admin/post/edit.html new file mode 100644 index 0000000..fbba390 --- /dev/null +++ b/src/myWebLog/views/admin/post/edit.html @@ -0,0 +1,95 @@ +@Master['admin/admin-layout'] + +@Section['Content'] +
+ @AntiForgeryToken +
+
+
+ + +
+
+ + + +

@Translate.startingWith http://@Model.webLog.urlBase/

+
+ +
+ +
+
+ + +
+
+
+
+
+

@Translate.PostDetails

+
+
+
+ +

@Model.post.status

+
+ @If.isPublished +
+ +

@Model.publishedDate
@Model.publishedTime

+
+ @EndIf +
+
+
+
+

@Translate.Categories

+
+
+ + @Each.categories + + +   + + +
+ @EndEach +
+
+
+ @If.isPublished + + @EndIf + @IfNot.isPublished +
+ +   +
+ @EndIf +

+ +

+
+
+
+
+@EndSection + +@Section['Scripts'] + + +@EndSection diff --git a/src/myWebLog/views/default/index-content.html b/src/myWebLog/views/default/index-content.html index 10735ae..0706aed 100644 --- a/src/myWebLog/views/default/index-content.html +++ b/src/myWebLog/views/default/index-content.html @@ -8,7 +8,7 @@