So close...

- Added comment display and counts
- Fixed problem with set-up-from-scratch bombing on index creation
- RSS feed is returning errors; need to determine why Nginx is
intercepting theme content requests
This commit is contained in:
Daniel J. Summers 2016-11-11 22:44:23 -06:00
parent 458b8308ae
commit 739fe3ff9c
14 changed files with 102 additions and 58 deletions
src
MyWebLog.App
MyWebLog.Data.RethinkDB
MyWebLog.Resources
MyWebLog/views
admin/user
themes/default
myWebLog/views

View File

@ -112,7 +112,7 @@ type MyWebLogBootstrapper() =
CryptographyConfiguration ( CryptographyConfiguration (
AesEncryptionProvider (PassphraseKeyGenerator (cfg.AuthEncryptionPassphrase, cfg.AuthSalt)), AesEncryptionProvider (PassphraseKeyGenerator (cfg.AuthEncryptionPassphrase, cfg.AuthSalt)),
DefaultHmacProvider (PassphraseKeyGenerator (cfg.AuthHmacPassphrase, cfg.AuthSalt))), DefaultHmacProvider (PassphraseKeyGenerator (cfg.AuthHmacPassphrase, cfg.AuthSalt))),
RedirectUrl = "~/user/logon", RedirectUrl = "~/user/log-on",
UserMapper = container.Resolve<IUserMapper> ()) UserMapper = container.Resolve<IUserMapper> ())
FormsAuthentication.Enable (pipelines, auth) FormsAuthentication.Enable (pipelines, auth)
// CSRF // CSRF
@ -163,6 +163,7 @@ let Run () =
use host = use host =
WebHostBuilder() WebHostBuilder()
.UseContentRoot(System.IO.Directory.GetCurrentDirectory ()) .UseContentRoot(System.IO.Directory.GetCurrentDirectory ())
.UseUrls("http://localhost:5001")
.UseKestrel() .UseKestrel()
.UseStartup<Startup>() .UseStartup<Startup>()
.Build () .Build ()

View File

@ -14,10 +14,10 @@ type CategoryModule (data : IMyWebLogData) as this =
inherit NancyModule () inherit NancyModule ()
do do
this.Get ("/categories", fun _ -> this.CategoryList ()) this.Get ("/categories", fun _ -> this.CategoryList ())
this.Get ("/category/{id}/edit", fun parms -> this.EditCategory (downcast parms)) this.Get ("/category/{id}/edit", fun p -> this.EditCategory (downcast p))
this.Post ("/category/{id}/edit", fun parms -> this.SaveCategory (downcast parms)) this.Post ("/category/{id}/edit", fun p -> this.SaveCategory (downcast p))
this.Delete ("/category/{id}/delete", fun parms -> this.DeleteCategory (downcast parms)) this.Post ("/category/{id}/delete", fun p -> this.DeleteCategory (downcast p))
/// Display a list of categories /// Display a list of categories
member this.CategoryList () : obj = member this.CategoryList () : obj =
@ -26,6 +26,7 @@ type CategoryModule (data : IMyWebLogData) as this =
CategoryListModel ( CategoryListModel (
this.Context, this.WebLog, findAllCategories data this.WebLog.Id this.Context, this.WebLog, findAllCategories data this.WebLog.Id
|> List.map (fun cat -> IndentedCategory.Create cat (fun _ -> false))) |> List.map (fun cat -> IndentedCategory.Create cat (fun _ -> false)))
model.PageTitle <- Strings.get "Categories"
upcast this.View.["admin/category/list", model] upcast this.View.["admin/category/list", model]
/// Edit a category /// Edit a category
@ -35,10 +36,11 @@ type CategoryModule (data : IMyWebLogData) as this =
match catId with "new" -> Some Category.Empty | _ -> tryFindCategory data this.WebLog.Id catId match catId with "new" -> Some Category.Empty | _ -> tryFindCategory data this.WebLog.Id catId
|> function |> function
| Some cat -> | Some cat ->
let model = CategoryEditModel(this.Context, this.WebLog, cat) let model = CategoryEditModel (this.Context, this.WebLog, cat)
model.Categories <- findAllCategories data this.WebLog.Id model.Categories <- findAllCategories data this.WebLog.Id
|> List.map (fun cat -> IndentedCategory.Create cat |> List.map (fun c ->
(fun c -> c = defaultArg (fst cat).ParentId "")) IndentedCategory.Create c (fun catId -> catId = defaultArg cat.ParentId ""))
model.PageTitle <- Strings.get <| match catId with "new" -> "AddNewCategory" | _ -> "EditCategory"
upcast this.View.["admin/category/edit", model] upcast this.View.["admin/category/edit", model]
| _ -> this.NotFound () | _ -> this.NotFound ()
@ -53,10 +55,13 @@ type CategoryModule (data : IMyWebLogData) as this =
| _ -> tryFindCategory data this.WebLog.Id catId | _ -> tryFindCategory data this.WebLog.Id catId
|> function |> function
| Some old -> | Some old ->
let cat = { old with Name = form.Name let cat =
Slug = form.Slug { old with
Description = match form.Description with "" -> None | d -> Some d Name = form.Name
ParentId = match form.ParentId with "" -> None | p -> Some p } Slug = form.Slug
Description = match form.Description with "" -> None | d -> Some d
ParentId = match form.ParentId with "" -> None | p -> Some p
}
let newCatId = saveCategory data cat let newCatId = saveCategory data cat
match old.ParentId = cat.ParentId with match old.ParentId = cat.ParentId with
| true -> () | true -> ()
@ -68,12 +73,12 @@ type CategoryModule (data : IMyWebLogData) as this =
| Some parentId -> addCategoryToParent data this.WebLog.Id parentId newCatId | Some parentId -> addCategoryToParent data this.WebLog.Id parentId newCatId
| _ -> () | _ -> ()
let model = MyWebLogModel (this.Context, this.WebLog) let model = MyWebLogModel (this.Context, this.WebLog)
{ UserMessage.Empty with model.AddMessage
Level = Level.Info { UserMessage.Empty with
Message = System.String.Format Message = System.String.Format
(Strings.get "MsgCategoryEditSuccess", (Strings.get "MsgCategoryEditSuccess",
Strings.get (match catId with "new" -> "Added" | _ -> "Updated")) } Strings.get (match catId with "new" -> "Added" | _ -> "Updated"))
|> model.AddMessage }
this.Redirect (sprintf "/category/%s/edit" newCatId) model this.Redirect (sprintf "/category/%s/edit" newCatId) model
| _ -> this.NotFound () | _ -> this.NotFound ()
@ -85,10 +90,8 @@ type CategoryModule (data : IMyWebLogData) as this =
match tryFindCategory data this.WebLog.Id catId with match tryFindCategory data this.WebLog.Id catId with
| Some cat -> | Some cat ->
deleteCategory data cat deleteCategory data cat
let model = MyWebLogModel(this.Context, this.WebLog) let model = MyWebLogModel (this.Context, this.WebLog)
{ UserMessage.Empty with model.AddMessage
Level = Level.Info { UserMessage.Empty with Message = System.String.Format(Strings.get "MsgCategoryDeleted", cat.Name) }
Message = System.String.Format(Strings.get "MsgCategoryDeleted", cat.Name) }
|> model.AddMessage
this.Redirect "/categories" model this.Redirect "/categories" model
| _ -> this.NotFound () | _ -> this.NotFound ()

View File

@ -23,16 +23,17 @@ type UserModule (data : IMyWebLogData, cfg : AppConfig) as this =
|> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) "" |> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) ""
do do
this.Get ("/logon", fun _ -> this.ShowLogOn ()) this.Get ("/log-on", fun _ -> this.ShowLogOn ())
this.Post ("/logon", fun p -> this.DoLogOn (downcast p)) this.Post ("/log-on", fun p -> this.DoLogOn (downcast p))
this.Get ("/logoff", fun _ -> this.LogOff ()) this.Get ("/log-off", fun _ -> this.LogOff ())
/// Show the log on page /// Show the log on page
member this.ShowLogOn () : obj = member this.ShowLogOn () : obj =
let model = LogOnModel (this.Context, this.WebLog) let model = LogOnModel (this.Context, this.WebLog)
let query = this.Request.Query :?> DynamicDictionary let query = this.Request.Query :?> DynamicDictionary
model.Form.ReturnUrl <- match query.ContainsKey "returnUrl" with true -> query.["returnUrl"].ToString () | _ -> "" model.Form.ReturnUrl <- match query.ContainsKey "returnUrl" with true -> query.["returnUrl"].ToString () | _ -> ""
upcast this.View.["admin/user/logon", model] model.PageTitle <- Strings.get "LogOn"
upcast this.View.["admin/user/log-on", model]
/// Process a user log on /// Process a user log on
member this.DoLogOn (parameters : DynamicDictionary) : obj = member this.DoLogOn (parameters : DynamicDictionary) : obj =
@ -52,12 +53,10 @@ type UserModule (data : IMyWebLogData, cfg : AppConfig) as this =
Level = Level.Error Level = Level.Error
Message = Strings.get "ErrBadLogOnAttempt" } Message = Strings.get "ErrBadLogOnAttempt" }
|> model.AddMessage |> model.AddMessage
this.Redirect (sprintf "/user/logon?returnUrl=%s" form.ReturnUrl) model this.Redirect (sprintf "/user/log-on?returnUrl=%s" form.ReturnUrl) model
/// Log a user off /// Log a user off
member this.LogOff () : obj = member this.LogOff () : obj =
// FIXME: why are we getting the user here if we don't do anything with it?
let user = this.Request.PersistableSession.GetOrDefault<User> (Keys.User, User.Empty)
this.Session.DeleteAll () this.Session.DeleteAll ()
let model = MyWebLogModel (this.Context, this.WebLog) let model = MyWebLogModel (this.Context, this.WebLog)
model.AddMessage { UserMessage.Empty with Message = Strings.get "MsgLogOffSuccess" } model.AddMessage { UserMessage.Empty with Message = Strings.get "MsgLogOffSuccess" }

View File

@ -176,7 +176,7 @@ type IndentedCategory =
Selected : bool } Selected : bool }
with with
/// Create an indented category /// Create an indented category
static member Create (cat : Category * int) (isSelected : string -> bool) = static member Create cat isSelected =
{ Category = fst cat { Category = fst cat
Indent = snd cat Indent = snd cat
Selected = isSelected (fst cat).Id } Selected = isSelected (fst cat).Id }
@ -304,6 +304,17 @@ type EditPageModel (ctx, webLog, page, revision) =
// ---- Post models ---- // ---- Post models ----
/// Formatter for comment information
type CommentForDisplay (comment : Comment, tz) =
/// The comment on which this model is based
member this.Comment = comment
/// The commentor (linked with a URL if there is one)
member this.Commentor =
match comment.Url with Some url -> sprintf "<a href=\"%s\">%s</a>" url comment.Name | _ -> comment.Name
/// The date/time this comment was posted
member this.CommentedOn =
sprintf "%s / %s" (FormatDateTime.longDate tz comment.PostedOn) (FormatDateTime.time tz comment.PostedOn)
/// Model for single post display /// Model for single post display
type PostModel (ctx, webLog, post) = type PostModel (ctx, webLog, post) =
inherit MyWebLogModel (ctx, webLog) inherit MyWebLogModel (ctx, webLog)
@ -317,8 +328,19 @@ type PostModel (ctx, webLog, post) =
member this.PublishedDate = this.DisplayLongDate this.Post.PublishedOn member this.PublishedDate = this.DisplayLongDate this.Post.PublishedOn
/// The time the post was published /// The time the post was published
member this.PublishedTime = this.DisplayTime this.Post.PublishedOn member this.PublishedTime = this.DisplayTime this.Post.PublishedOn
/// The number of comments
member this.CommentCount =
match post.Comments |> List.length with
| 0 -> Strings.get "NoComments"
| 1 -> Strings.get "OneComment"
| x -> String.Format (Strings.get "XComments", x)
/// The comments for display
member this.Comments = post.Comments
|> List.filter (fun c -> c.Status = CommentStatus.Approved)
|> List.map (fun c -> CommentForDisplay (c, webLog.TimeZone))
/// Does the post have tags? /// Does the post have tags?
member this.HasTags = not (List.isEmpty post.Tags) member this.HasTags = not <| List.isEmpty post.Tags
/// Get the tags sorted /// Get the tags sorted
member this.Tags = post.Tags member this.Tags = post.Tags
|> List.sort |> List.sort
@ -347,6 +369,12 @@ type PostForDisplay (webLog : WebLog, post : Post) =
match this.Post.Status with match this.Post.Status with
| PostStatus.Published -> FormatDateTime.time this.TimeZone this.Post.PublishedOn | PostStatus.Published -> FormatDateTime.time this.TimeZone this.Post.PublishedOn
| _ -> FormatDateTime.time this.TimeZone this.Post.UpdatedOn | _ -> FormatDateTime.time this.TimeZone this.Post.UpdatedOn
/// The number of comments
member this.CommentCount =
match post.Comments |> List.length with
| 0 -> Strings.get "NoComments"
| 1 -> Strings.get "OneComment"
| x -> String.Format (Strings.get "XComments", x)
/// Tags /// Tags
member this.Tags = member this.Tags =
match List.length this.Post.Tags with match List.length this.Post.Tags with

View File

@ -6,9 +6,18 @@ open RethinkDb.Driver.Ast
let private r = RethinkDb.Driver.RethinkDB.R let private r = RethinkDb.Driver.RethinkDB.R
/// Shorthand to select all published posts for a web log /// Shorthand to select all published posts for a web log
let private publishedPosts (webLogId : string)= let private publishedPosts (webLogId : string) =
r.Table(Table.Post) r.Table(Table.Post)
.GetAll(r.Array (webLogId, PostStatus.Published)).OptArg("index", "WebLogAndStatus") .GetAll(r.Array (webLogId, PostStatus.Published)).OptArg("index", "WebLogAndStatus")
.Without("Revisions")
// This allows us to count comments without retrieving them all
.Merge(ReqlFunction1 (fun p ->
upcast r.HashMap(
"Comments", r.Table(Table.Comment)
.GetAll(p.["id"]).OptArg("index", "PostId")
.Pluck("id")
.CoerceTo("array"))))
/// Shorthand to sort posts by published date, slice for the given page, and return a list /// Shorthand to sort posts by published date, slice for the given page, and return a list
let private toPostList conn pageNbr nbrPerPage (filter : ReqlExpr) = let private toPostList conn pageNbr nbrPerPage (filter : ReqlExpr) =

View File

@ -45,7 +45,6 @@ let private createIndex cfg table (index : string * (ReqlExpr -> obj) option) =
| Some f -> (tbl cfg table).IndexCreate(idxName, f) | Some f -> (tbl cfg table).IndexCreate(idxName, f)
| None -> (tbl cfg table).IndexCreate(idxName)) | None -> (tbl cfg table).IndexCreate(idxName))
.RunResultAsync cfg.Conn .RunResultAsync cfg.Conn
do! (tbl cfg table).IndexWait(idxName).RunResultAsync cfg.Conn
logStepDone () logStepDone ()
} }

View File

@ -2,6 +2,7 @@
"Action": "Action", "Action": "Action",
"Added": "Added", "Added": "Added",
"AddNew": "Add New", "AddNew": "Add New",
"AddNewCategory": "Add New Category",
"AddNewPage": "Add New Page", "AddNewPage": "Add New Page",
"AddNewPost": "Add New Post", "AddNewPost": "Add New Post",
"Admin": "Admin", "Admin": "Admin",
@ -15,11 +16,13 @@
"Category": "Category", "Category": "Category",
"CategoryDeleteWarning": "Are you sure you wish to delete the category", "CategoryDeleteWarning": "Are you sure you wish to delete the category",
"Close": "Close", "Close": "Close",
"Comments": "Comments",
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Date": "Date", "Date": "Date",
"Delete": "Delete", "Delete": "Delete",
"Description": "Description", "Description": "Description",
"Edit": "Edit", "Edit": "Edit",
"EditCategory": "Edit Category",
"EditPage": "Edit Page", "EditPage": "Edit Page",
"EditPost": "Edit Post", "EditPost": "Edit Post",
"EmailAddress": "E-mail Address", "EmailAddress": "E-mail Address",
@ -44,8 +47,10 @@
"Name": "Name", "Name": "Name",
"NewerPosts": "Newer Posts", "NewerPosts": "Newer Posts",
"NextPost": "Next Post", "NextPost": "Next Post",
"NoComments": "No Comments",
"NoParent": "No Parent", "NoParent": "No Parent",
"OlderPosts": "Older Posts", "OlderPosts": "Older Posts",
"OneComment": "1 Comment",
"PageDeleteWarning": "Are you sure you wish to delete the page", "PageDeleteWarning": "Are you sure you wish to delete the page",
"PageDetails": "Page Details", "PageDetails": "Page Details",
"PageHash": "Page #", "PageHash": "Page #",
@ -73,5 +78,6 @@
"Title": "Title", "Title": "Title",
"Updated": "Updated", "Updated": "Updated",
"View": "View", "View": "View",
"Warning": "Warning" "Warning": "Warning",
"XComments": "{0} Comments"
} }

View File

@ -1,7 +1,7 @@
@Master['admin/admin-layout'] @Master['admin/admin-layout']
@Section['Content'] @Section['Content']
<form action="/user/logon" method="post"> <form action="/user/log-on" method="post">
@AntiForgeryToken @AntiForgeryToken
<input type="hidden" name="ReturnUrl" value="@Model.Form.ReturnUrl" /> <input type="hidden" name="ReturnUrl" value="@Model.Form.ReturnUrl" />
<div class="row"> <div class="row">

View File

@ -0,0 +1,4 @@
<h4>
@Model.Commentor &nbsp; &nbsp;<small>@Model.CommentedOn</small>
</h4>
@Model.Comment.Text

View File

@ -22,10 +22,10 @@
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
@If.IsAuthenticated @If.IsAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li> <li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li> <li><a href="/user/log-off">@Translate.LogOff</a></li>
@EndIf @EndIf
@IfNot.IsAuthenticated @IfNot.IsAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li> <li><a href="/user/log-on">@Translate.LogOn</a></li>
@EndIf @EndIf
</ul> </ul>
</div> </div>

View File

@ -32,7 +32,7 @@
@EndEach @EndEach
</table> </table>
</div> </div>
<form method="delete" id="deleteForm"> <form method="post" id="deleteForm">
@AntiForgeryToken @AntiForgeryToken
</form> </form>
@EndSection @EndSection

View File

@ -16,7 +16,8 @@
</h1> </h1>
<p> <p>
<i class="fa fa-calendar" title="@Translate.Date"></i> @Current.PublishedDate &nbsp; <i class="fa fa-calendar" title="@Translate.Date"></i> @Current.PublishedDate &nbsp;
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.PublishedTime <i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.PublishedTime &nbsp;
<i class="fa fa-comments-o" title="@Translate.Comments"></i> @Current.CommentCount
</p> </p>
@Current.Post.Text @Current.Post.Text
</article> </article>

View File

@ -27,11 +27,11 @@
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
@If.IsAuthenticated @If.IsAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li> <li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li> <li><a href="/user/log-off">@Translate.LogOff</a></li>
@EndIf @EndIf
@IfNot.IsAuthenticated @IfNot.IsAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li> <li><a href="/user/log-on">@Translate.LogOn</a></li>
@EndIf @EndIf
</ul> </ul>
</div> </div>

View File

@ -6,7 +6,8 @@
<div class="col-xs-12"> <div class="col-xs-12">
<h4> <h4>
<i class="fa fa-calendar" title="@Translate.Date"></i> @Model.PublishedDate &nbsp; <i class="fa fa-calendar" title="@Translate.Date"></i> @Model.PublishedDate &nbsp;
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.PublishedTime &nbsp; &nbsp; &nbsp; <i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.PublishedTime &nbsp;
<i class="fa fa-comments-o" title="@Translate.Comments"></i> @Model.CommentCount &nbsp; &nbsp; &nbsp;
@Each.Post.Categories @Each.Post.Categories
<span style="white-space:nowrap;"> <span style="white-space:nowrap;">
<i class="fa fa-folder-open-o" title="@Translate.Category"></i> <i class="fa fa-folder-open-o" title="@Translate.Category"></i>
@ -37,20 +38,13 @@
<div class="row"> <div class="row">
<div class="col-xs-12"><hr /></div> <div class="col-xs-12"><hr /></div>
</div> </div>
<!-- // TODO: format comments --> <div class="row">
<!-- .row <div class="col-xs-12">
.col-xs-12 @Each.Comments
each comment in post.comments @Partial['themes/default/comment', @Current]
if 'Approved' == comment.status @EndEach
h4 </div>
if comment.url </div>
a(href=comment.url)= comment.name
else
= comment.name
| &nbsp; &nbsp;
small
=moment(comment.postedDate).format('MMMM Do, YYYY / h:mma')
!= comment.text -->
<div class="row"> <div class="row">
<div class="col-xs-12"><hr /></div> <div class="col-xs-12"><hr /></div>
</div> </div>