From 08ee8990d3ade799481682e8d3d5c00c01855bcc Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 11 Jul 2016 22:04:18 -0500 Subject: [PATCH] category / part of page Category editing pages done; page list and delete are done --- src/myWebLog.Data/Category.fs | 96 +++++++++++++++ src/myWebLog.Data/Entities.fs | 5 +- src/myWebLog.Data/Page.fs | 30 ++++- src/myWebLog.Resources/Resources.Designer.cs | 117 +++++++++++++++++++ src/myWebLog.Resources/Resources.resx | 39 +++++++ src/myWebLog.Web/CategoryModule.fs | 91 +++++++++++++++ src/myWebLog.Web/PageModule.fs | 39 +++++++ src/myWebLog.Web/ViewModels.fs | 60 ++++++++++ src/myWebLog.Web/myWebLog.Web.fsproj | 2 + src/myWebLog/myWebLog.csproj | 3 + src/myWebLog/views/admin/category/edit.html | 52 +++++++++ src/myWebLog/views/admin/category/list.html | 44 +++++++ src/myWebLog/views/admin/dashboard.html | 4 +- src/myWebLog/views/admin/page/list.html | 47 ++++++++ 14 files changed, 620 insertions(+), 9 deletions(-) create mode 100644 src/myWebLog.Web/CategoryModule.fs create mode 100644 src/myWebLog.Web/PageModule.fs create mode 100644 src/myWebLog/views/admin/category/edit.html create mode 100644 src/myWebLog/views/admin/category/list.html create mode 100644 src/myWebLog/views/admin/page/list.html diff --git a/src/myWebLog.Data/Category.fs b/src/myWebLog.Data/Category.fs index 3f7bdcf..7beea8f 100644 --- a/src/myWebLog.Data/Category.fs +++ b/src/myWebLog.Data/Category.fs @@ -1,7 +1,15 @@ module myWebLog.Data.Category +open FSharp.Interop.Dynamic open myWebLog.Entities open Rethink +open System.Dynamic + +/// Shorthand to get a category by Id and filter by web log Id +let private category webLogId catId = + table Table.Category + |> get catId + |> filter (fun c -> upcast c.["webLogId"].Eq(webLogId)) /// Sort categories by their name, with their children sorted below them, including an indent level let sortCategories categories = @@ -34,3 +42,91 @@ let countCategories conn webLogId = |> optArg "index" "webLogId" |> count |> runAtomAsync conn + +/// Get a specific category by its Id +let tryFindCategory conn webLogId catId : Category option = + match category webLogId catId + |> runAtomAsync conn + |> box with + | null -> None + | cat -> Some <| unbox cat + +/// 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 } + table Table.Category + |> insert newCat + |> runResultAsync conn + |> ignore + 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 + |> update upd8 + |> runResultAsync conn + |> ignore + 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 + |> List.filter (fun ch -> ch <> catId) + category webLogId parentId + |> update upd8 + |> runResultAsync conn + |> ignore + | None -> () + +/// Add a category to a given parent +let addCategoryToParent conn webLogId parentId catId = + match tryFindCategory conn webLogId parentId with + | Some parent -> let upd8 = ExpandoObject() + upd8?children <- catId :: parent.children + category webLogId parentId + |> update upd8 + |> runResultAsync conn + |> ignore + | None -> () + +/// 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 + | 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 + |> update newParent + |> runResultAsync conn + |> ignore) + // Remove the category from posts where it is assigned + table Table.Post + |> getAll [| cat.webLogId |] + |> optArg "index" "webLogId" + |> filter (fun p -> upcast p.["categoryIds"].Contains(cat.id)) + |> runCursorAsync conn + |> Seq.toList + |> List.iter (fun post -> let newCats = ExpandoObject() + newCats?categoryIds <- post.categoryIds + |> List.filter (fun c -> c <> cat.id) + table Table.Post + |> get post.id + |> update newCats + |> runResultAsync conn + |> ignore) + // Now, delete the category + table Table.Category + |> get cat.id + |> delete + |> runResultAsync conn + |> ignore diff --git a/src/myWebLog.Data/Entities.fs b/src/myWebLog.Data/Entities.fs index 7a45a61..fe3cb3b 100644 --- a/src/myWebLog.Data/Entities.fs +++ b/src/myWebLog.Data/Entities.fs @@ -166,6 +166,8 @@ with 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) @@ -180,7 +182,8 @@ type Category = { with /// An empty category static member empty = - { id = "" + { id = "new" + webLogId = "" name = "" slug = "" description = None diff --git a/src/myWebLog.Data/Page.fs b/src/myWebLog.Data/Page.fs index f28b048..ccc3f1e 100644 --- a/src/myWebLog.Data/Page.fs +++ b/src/myWebLog.Data/Page.fs @@ -3,11 +3,15 @@ open myWebLog.Entities open Rethink +/// Shorthand to get the page by its Id, filtering on web log Id +let private page webLogId pageId = + table Table.Page + |> get pageId + |> filter (fun p -> upcast p.["webLogId"].Eq(webLogId)) + /// Get a page by its Id let tryFindPage conn webLogId pageId : Page option = - match table Table.Page - |> get pageId - |> filter (fun p -> upcast p.["webLogId"].Eq(webLogId)) + match page webLogId pageId |> runAtomAsync conn |> box with | null -> None @@ -15,9 +19,7 @@ let tryFindPage conn webLogId pageId : Page option = /// 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)) + match page webLogId pageId |> without [| "revisions" |] |> runAtomAsync conn |> box with @@ -40,3 +42,19 @@ let countPages conn webLogId = |> optArg "index" "webLogId" |> count |> runAtomAsync conn + +/// Get a list of all pages (excludes page text and revisions) +let findAllPages conn webLogId = + table Table.Page + |> getAll [| webLogId |] + |> orderBy (fun p -> upcast p.["title"]) + |> without [| "text"; "revisions" |] + |> runCursorAsync conn + |> Seq.toList + +/// Delete a page +let deletePage conn webLogId pageId = + page webLogId pageId + |> delete + |> runResultAsync conn + |> ignore diff --git a/src/myWebLog.Resources/Resources.Designer.cs b/src/myWebLog.Resources/Resources.Designer.cs index 33654fa..edd0e09 100644 --- a/src/myWebLog.Resources/Resources.Designer.cs +++ b/src/myWebLog.Resources/Resources.Designer.cs @@ -60,6 +60,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Action. + /// + public static string Action { + get { + return ResourceManager.GetString("Action", resourceCulture); + } + } + /// /// Looks up a localized string similar to Added. /// @@ -114,6 +123,24 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Category. + /// + public static string Category { + get { + return ResourceManager.GetString("Category", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Are you sure you wish to delete the category. + /// + public static string CategoryDeleteWarning { + get { + return ResourceManager.GetString("CategoryDeleteWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Dashboard. /// @@ -141,6 +168,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Description. + /// + public static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Edit. /// @@ -177,6 +213,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Last Updated. + /// + public static string LastUpdated { + get { + return ResourceManager.GetString("LastUpdated", resourceCulture); + } + } + /// /// Looks up a localized string similar to List All. /// @@ -204,6 +249,33 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Deleted category {0} successfully. + /// + public static string MsgCategoryDeleted { + get { + return ResourceManager.GetString("MsgCategoryDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} category successfully. + /// + public static string MsgCategoryEditSuccess { + get { + return ResourceManager.GetString("MsgCategoryEditSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted page successfully. + /// + public static string MsgPageDeleted { + get { + return ResourceManager.GetString("MsgPageDeleted", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0}{1} post successfully. /// @@ -213,6 +285,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Name. + /// + public static string Name { + get { + return ResourceManager.GetString("Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Newer Posts. /// @@ -231,6 +312,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to No Parent. + /// + public static string NoParent { + get { + return ResourceManager.GetString("NoParent", resourceCulture); + } + } + /// /// Looks up a localized string similar to Older Posts. /// @@ -240,6 +330,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Are you sure you wish to delete the page. + /// + public static string PageDeleteWarning { + get { + return ResourceManager.GetString("PageDeleteWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Page #. /// @@ -258,6 +357,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Parent Category. + /// + public static string ParentCategory { + get { + return ResourceManager.GetString("ParentCategory", resourceCulture); + } + } + /// /// Looks up a localized string similar to Permalink. /// @@ -348,6 +456,15 @@ namespace myWebLog { } } + /// + /// Looks up a localized string similar to Slug. + /// + public static string Slug { + get { + return ResourceManager.GetString("Slug", resourceCulture); + } + } + /// /// Looks up a localized string similar to starting with. /// diff --git a/src/myWebLog.Resources/Resources.resx b/src/myWebLog.Resources/Resources.resx index 075e497..c31b4cf 100644 --- a/src/myWebLog.Resources/Resources.resx +++ b/src/myWebLog.Resources/Resources.resx @@ -234,4 +234,43 @@ View + + Action + + + Category + + + Are you sure you wish to delete the category + + + Description + + + Last Updated + + + Deleted category {0} successfully + + + {0} category successfully + + + Deleted page successfully + + + Name + + + No Parent + + + Are you sure you wish to delete the page + + + Parent Category + + + Slug + \ No newline at end of file diff --git a/src/myWebLog.Web/CategoryModule.fs b/src/myWebLog.Web/CategoryModule.fs new file mode 100644 index 0000000..4b77c88 --- /dev/null +++ b/src/myWebLog.Web/CategoryModule.fs @@ -0,0 +1,91 @@ +namespace myWebLog + +open myWebLog.Data.Category +open myWebLog.Entities +open Nancy +open Nancy.ModelBinding +open Nancy.Security +open RethinkDb.Driver.Net + +/// Handle /category and /categories URLs +type CategoryModule(conn : IConnection) as this = + inherit NancyModule() + + do + this.Get .["/categories" ] <- fun _ -> upcast this.CategoryList () + this.Get .["/category/{id}/edit" ] <- fun parms -> upcast this.EditCategory (downcast parms) + this.Post .["/category/{id}/edit" ] <- fun parms -> upcast this.SaveCategory (downcast parms) + this.Delete.["/category/{id}/delete"] <- fun parms -> upcast this.DeleteCategory (downcast parms) + + /// Display a list of categories + 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)))) + this.View.["/admin/category/list", model] + + /// Edit a category + member this.EditCategory (parameters : DynamicDictionary) = + this.RequiresAccessLevel AuthorizationLevel.Administrator + let catId : string = downcast parameters.["id"] + match (match catId with + | "new" -> Some Category.empty + | _ -> 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 "")) + this.View.["admin/category/edit", model] + | None -> this.NotFound () + + /// Save a category + member this.SaveCategory (parameters : DynamicDictionary) = + this.ValidateCsrfToken () + this.RequiresAccessLevel AuthorizationLevel.Administrator + let catId : string = downcast parameters.["id"] + let form = this.Bind () + let oldCat = match catId with + | "new" -> Some Category.empty + | _ -> 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 + | true -> () + | _ -> 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 + | 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 + this.Redirect (sprintf "/category/%s/edit" newCatId) model + | None -> this.NotFound () + + /// Delete a category + 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 + | 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 + this.Redirect "/categories" model + | None -> this.NotFound () diff --git a/src/myWebLog.Web/PageModule.fs b/src/myWebLog.Web/PageModule.fs new file mode 100644 index 0000000..fc0b2bc --- /dev/null +++ b/src/myWebLog.Web/PageModule.fs @@ -0,0 +1,39 @@ +namespace myWebLog + +open myWebLog.Data.Page +open myWebLog.Entities +open Nancy +open Nancy.Security +open RethinkDb.Driver.Net + +/// Handle /pages and /page URLs +type PageModule(conn : IConnection) as this = + inherit NancyModule() + + do + this.Get .["/pages" ] <- fun _ -> upcast this.PageList () + this.Delete.["/page/{id}/delete"] <- fun parms -> upcast this.DeletePage (downcast parms) + + /// List all pages + member this.PageList () = + this.RequiresAccessLevel AuthorizationLevel.Administrator + let model = PagesModel(this.Context, this.WebLog, findAllPages conn this.WebLog.id) + model.pageTitle <- Resources.Pages + this.View.["admin/page/list", model] + + // TODO: edit goes here! + + /// Delete a page + member this.DeletePage (parameters : DynamicDictionary) = + this.ValidateCsrfToken () + this.RequiresAccessLevel AuthorizationLevel.Administrator + let pageId : string = downcast parameters.["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 + this.Redirect "/pages" model + | None -> this.NotFound () diff --git a/src/myWebLog.Web/ViewModels.fs b/src/myWebLog.Web/ViewModels.fs index 520c166..eac66e3 100644 --- a/src/myWebLog.Web/ViewModels.fs +++ b/src/myWebLog.Web/ViewModels.fs @@ -92,6 +92,60 @@ type DashboardModel(ctx, webLog) = member val categories = 0 with get, set +// ---- Category models ---- + +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 } + /// 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 + /// Display for this category as an option within a select box + member this.option = + seq { + yield sprintf "" + } + |> String.concat "" + +/// Model for the list of categories +type CategoryListModel(ctx, webLog, categories) = + inherit MyWebLogModel(ctx, webLog) + /// The 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 + /// The slug of the category (used in category URLs) + member val slug = category.slug with get, set + /// The description of the category + member val description = defaultArg category.description "" with get, set + /// The parent category for this one + 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 + /// The categories + member val categories : IndentedCategory list = List.empty with get, set + + // ---- Page models ---- /// Model for page display @@ -102,6 +156,12 @@ type PageModel(ctx, webLog, page) = member this.page : Page = page +/// Model for page list display +type PagesModel(ctx, webLog, pages) = + inherit MyWebLogModel(ctx, webLog) + /// The pages + member this.pages : Page list = pages + // ---- Post models ---- /// Model for post display diff --git a/src/myWebLog.Web/myWebLog.Web.fsproj b/src/myWebLog.Web/myWebLog.Web.fsproj index 8146ae8..a5e5e43 100644 --- a/src/myWebLog.Web/myWebLog.Web.fsproj +++ b/src/myWebLog.Web/myWebLog.Web.fsproj @@ -55,6 +55,8 @@ + + diff --git a/src/myWebLog/myWebLog.csproj b/src/myWebLog/myWebLog.csproj index 6569d51..c86e362 100644 --- a/src/myWebLog/myWebLog.csproj +++ b/src/myWebLog/myWebLog.csproj @@ -69,7 +69,10 @@ + + + diff --git a/src/myWebLog/views/admin/category/edit.html b/src/myWebLog/views/admin/category/edit.html new file mode 100644 index 0000000..dd74069 --- /dev/null +++ b/src/myWebLog/views/admin/category/edit.html @@ -0,0 +1,52 @@ +@Master['admin/admin-layout'] + +@Section['Content'] +
+ @AntiForgeryToken +
+
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

+ +

+
+
+
+@EndSection + +@Section['Scripts'] + +@EndSection diff --git a/src/myWebLog/views/admin/category/list.html b/src/myWebLog/views/admin/category/list.html new file mode 100644 index 0000000..8d98e8a --- /dev/null +++ b/src/myWebLog/views/admin/category/list.html @@ -0,0 +1,44 @@ +@Master['admin/admin-layout'] + +@Section['Content'] + +
+ + + + + + + @Each.categories + + + + + + @EndEach +
@Translate.Action@Translate.Category@Translate.Description
+ @Translate.Edit   + + @Translate.Delete + + @Current.listName@Current.category.description
+
+
+ @AntiForgeryToken +
+@EndSection + +@Section['Scripts'] + +@EndSection \ No newline at end of file diff --git a/src/myWebLog/views/admin/dashboard.html b/src/myWebLog/views/admin/dashboard.html index b079df5..99628fb 100644 --- a/src/myWebLog/views/admin/dashboard.html +++ b/src/myWebLog/views/admin/dashboard.html @@ -13,7 +13,7 @@

@Translate.Pages  @Model.pages

- @Translate.ListAll + @Translate.ListAll     @Translate.AddNew

@@ -23,7 +23,7 @@

@Translate.Categories  @Model.categories

- @Translate.ListAll + @Translate.ListAll     @Translate.AddNew

diff --git a/src/myWebLog/views/admin/page/list.html b/src/myWebLog/views/admin/page/list.html new file mode 100644 index 0000000..06bff88 --- /dev/null +++ b/src/myWebLog/views/admin/page/list.html @@ -0,0 +1,47 @@ +@Master['admin/admin-layout'] + +@Section['Content'] + +
+ + + + + + @Each.pages + + + + + @EndEach +
@Translate.Title@Translate.LastUpdated
+ @Current.title
+ @Translate.View   + @Translate.Edit   + @Translate.Delete +
+ +
+
+
+ @AntiForgeryToken +
+@EndSection + +@Section['Scripts'] + +@EndSection