V2 #1

Merged
danieljsummers merged 102 commits from v2 into main 2022-06-23 00:35:12 +00:00
8 changed files with 96 additions and 32 deletions
Showing only changes of commit fa20122f20 - Show all commits

View File

@ -204,14 +204,14 @@ module Category =
} }
/// Get a category ID -> name dictionary for the given category IDs /// Get a category ID -> name dictionary for the given category IDs
let findNames (catIds : CategoryId list) (webLogId : WebLogId) conn = backgroundTask { let findNames (webLogId : WebLogId) conn (catIds : CategoryId list) = backgroundTask {
let! cats = rethink<Category list> { let! cats = rethink<Category list> {
withTable Table.Category withTable Table.Category
getAll (catIds |> List.map (fun it -> it :> obj)) getAll (catIds |> List.map (fun it -> it :> obj))
filter "webLogId" webLogId filter "webLogId" webLogId
result; withRetryDefault conn result; withRetryDefault conn
} }
return cats |> List.map (fun c -> CategoryId.toString c.id, c.name) |> dict return cats |> List.map (fun c -> { name = CategoryId.toString c.id; value = c.name})
} }
/// Update a category /// Update a category
@ -504,12 +504,12 @@ module WebLogUser =
|> tryFirst |> tryFirst
/// Get a user ID -> name dictionary for the given user IDs /// Get a user ID -> name dictionary for the given user IDs
let findNames (userIds : WebLogUserId list) (webLogId : WebLogId) conn = backgroundTask { let findNames (webLogId : WebLogId) conn (userIds : WebLogUserId list) = backgroundTask {
let! users = rethink<WebLogUser list> { let! users = rethink<WebLogUser list> {
withTable Table.WebLogUser withTable Table.WebLogUser
getAll (userIds |> List.map (fun it -> it :> obj)) getAll (userIds |> List.map (fun it -> it :> obj))
filter "webLogId" webLogId filter "webLogId" webLogId
result; withRetryDefault conn result; withRetryDefault conn
} }
return users |> List.map (fun u -> WebLogUserId.toString u.id, WebLogUser.displayName u) |> dict return users |> List.map (fun u -> { name = WebLogUserId.toString u.id; value = WebLogUser.displayName u })
} }

View File

@ -88,6 +88,17 @@ module MarkupText =
| text -> invalidOp $"Cannot derive type of text ({text})" | text -> invalidOp $"Cannot derive type of text ({text})"
/// An item of metadata
[<CLIMutable; NoComparison; NoEquality>]
type MetaItem =
{ /// The name of the metadata value
name : string
/// The metadata value
value : string
}
/// A revision of a page or post /// A revision of a page or post
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Revision = type Revision =

View File

@ -209,7 +209,14 @@ type LogOnModel =
/// The user's password /// The user's password
password : string password : string
/// Where the user should be redirected once they have logged on
returnTo : string option
} }
/// An empty log on model
static member empty =
{ emailAddress = ""; password = ""; returnTo = None }
/// View model for posts in a list /// View model for posts in a list
@ -221,9 +228,6 @@ type PostListItem =
/// The ID of the user who authored the post /// The ID of the user who authored the post
authorId : string authorId : string
/// The name of the user who authored the post
authorName : string
/// The status of the post /// The status of the post
status : string status : string
@ -243,25 +247,24 @@ type PostListItem =
text : string text : string
/// The IDs of the categories for this post /// The IDs of the categories for this post
categoryIds : string[] categoryIds : string list
/// Tags for the post /// Tags for the post
tags : string[] tags : string list
} }
/// Create a post list item from a post /// Create a post list item from a post
static member fromPost (post : Post) = static member fromPost (post : Post) =
{ id = PostId.toString post.id { id = PostId.toString post.id
authorId = WebLogUserId.toString post.authorId authorId = WebLogUserId.toString post.authorId
authorName = ""
status = PostStatus.toString post.status status = PostStatus.toString post.status
title = post.title title = post.title
permalink = Permalink.toString post.permalink permalink = Permalink.toString post.permalink
publishedOn = Option.toNullable post.publishedOn publishedOn = Option.toNullable post.publishedOn
updatedOn = post.updatedOn updatedOn = post.updatedOn
text = post.text text = post.text
categoryIds = post.categoryIds |> List.map CategoryId.toString |> Array.ofList categoryIds = post.categoryIds |> List.map CategoryId.toString
tags = Array.ofList post.tags tags = post.tags
} }
@ -270,8 +273,11 @@ type PostDisplay =
{ /// The posts to be displayed { /// The posts to be displayed
posts : PostListItem[] posts : PostListItem[]
/// Author ID -> name lookup
authors : MetaItem list
/// Category ID -> name lookup /// Category ID -> name lookup
categories : IDictionary<string, string> categories : MetaItem list
/// A subtitle for the page /// A subtitle for the page
subtitle : string option subtitle : string option

View File

@ -424,19 +424,25 @@ module Post =
/// Convert a list of posts into items ready to be displayed /// Convert a list of posts into items ready to be displayed
let private preparePostList (webLog : WebLog) (posts : Post list) pageNbr perPage conn = task { let private preparePostList (webLog : WebLog) (posts : Post list) pageNbr perPage conn = task {
let! authors = let! authors =
Data.WebLogUser.findNames (posts |> List.map (fun p -> p.authorId) |> List.distinct) webLog.id conn posts
|> List.map (fun p -> p.authorId)
|> List.distinct
|> Data.WebLogUser.findNames webLog.id conn
let! cats = let! cats =
Data.Category.findNames (posts |> List.map (fun c -> c.categoryIds) |> List.concat |> List.distinct) posts
webLog.id conn |> List.map (fun c -> c.categoryIds)
|> List.concat
|> List.distinct
|> Data.Category.findNames webLog.id conn
let postItems = let postItems =
posts posts
|> Seq.ofList |> Seq.ofList
|> Seq.truncate perPage |> Seq.truncate perPage
|> Seq.map PostListItem.fromPost |> Seq.map PostListItem.fromPost
|> Seq.map (fun pi -> { pi with authorName = authors[pi.authorId] })
|> Array.ofSeq |> Array.ofSeq
let model = let model =
{ posts = postItems { posts = postItems
authors = authors
categories = cats categories = cats
subtitle = None subtitle = None
hasNewer = pageNbr <> 1 hasNewer = pageNbr <> 1
@ -606,9 +612,20 @@ module User =
Convert.ToBase64String (alg.GetBytes 64) Convert.ToBase64String (alg.GetBytes 64)
// GET /user/log-on // GET /user/log-on
let logOn : HttpHandler = fun next ctx -> task { let logOn returnUrl : HttpHandler = fun next ctx -> task {
let returnTo =
match returnUrl with
| Some _ -> returnUrl
| None ->
match ctx.Request.Query.ContainsKey "returnUrl" with
| true -> Some ctx.Request.Query["returnUrl"].[0]
| false -> None
return! return!
Hash.FromAnonymousObject {| page_title = "Log On"; csrf = (csrfToken ctx) |} Hash.FromAnonymousObject {|
model = { LogOnModel.empty with returnTo = returnTo }
page_title = "Log On"
csrf = csrfToken ctx
|}
|> viewForTheme "admin" "log-on" next ctx |> viewForTheme "admin" "log-on" next ctx
} }
@ -629,14 +646,11 @@ module User =
do! ctx.SignInAsync (identity.AuthenticationType, ClaimsPrincipal identity, do! ctx.SignInAsync (identity.AuthenticationType, ClaimsPrincipal identity,
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow)) AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
do! addMessage ctx do! addMessage ctx
{ UserMessage.success with { UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" }
message = "Logged on successfully" return! redirectToGet (match model.returnTo with Some url -> url | None -> "/admin") next ctx
detail = Some $"Welcome to {webLog.name}!"
}
return! redirectToGet "/admin" next ctx
| _ -> | _ ->
do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" } do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" }
return! logOn next ctx return! logOn model.returnTo next ctx
} }
// GET /user/log-off // GET /user/log-off
@ -696,7 +710,7 @@ let endpoints = [
] ]
subRoute "/user" [ subRoute "/user" [
GET [ GET [
route "/log-on" User.logOn route "/log-on" (User.logOn None)
route "/log-off" User.logOff route "/log-off" User.logOff
] ]
POST [ POST [

View File

@ -58,6 +58,14 @@ module DotLiquidBespoke =
"</ul>" "</ul>"
} }
|> Seq.iter result.WriteLine |> Seq.iter result.WriteLine
/// A filter to retrieve the value of a meta item from a list
// (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`)
type ValueFilter () =
static member Value (_ : Context, items : MetaItem list, name : string) =
match items |> List.tryFind (fun it -> it.name = name) with
| Some item -> item.value
| None -> $"-- {name} not found --"
/// Create the default information for a new web log /// Create the default information for a new web log
@ -192,16 +200,18 @@ let main args =
// Set up DotLiquid // Set up DotLiquid
Template.RegisterFilter typeof<DotLiquidBespoke.NavLinkFilter> Template.RegisterFilter typeof<DotLiquidBespoke.NavLinkFilter>
Template.RegisterFilter typeof<DotLiquidBespoke.ValueFilter>
Template.RegisterTag<DotLiquidBespoke.UserLinksTag> "user_links" Template.RegisterTag<DotLiquidBespoke.UserLinksTag> "user_links"
[ // Domain types [ // Domain types
typeof<Page>; typeof<WebLog> typeof<MetaItem>; typeof<Page>; typeof<WebLog>
// View models // View models
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayPage>; typeof<EditCategoryModel> typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayPage>; typeof<EditCategoryModel>
typeof<EditPageModel>; typeof<EditPostModel>; typeof<PostDisplay>; typeof<PostListItem> typeof<EditPageModel>; typeof<EditPostModel>; typeof<LogOnModel>; typeof<PostDisplay>
typeof<SettingsModel>; typeof<UserMessage> typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
// Framework types // Framework types
typeof<AntiforgeryTokenSet>; typeof<string option>; typeof<KeyValuePair> typeof<AntiforgeryTokenSet>; typeof<KeyValuePair>; typeof<MetaItem list>; typeof<string list>
typeof<string option>
] ]
|> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |])) |> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))

View File

@ -2,6 +2,9 @@
<article class="py-3"> <article class="py-3">
<form action="/user/log-on" method="post"> <form action="/user/log-on" method="post">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
{% if model.return_to %}
<input type="hidden" name="returnTo" value="{{ model.return_to.value }}">
{% endif %}
<div class="container"> <div class="container">
<div class="row pb-3"> <div class="row pb-3">
<div class="col col-md-6 col-lg-4 offset-lg-2"> <div class="col col-md-6 col-lg-4 offset-lg-2">

View File

@ -31,7 +31,7 @@
<a href="#" class="text-danger">Delete</a> <a href="#" class="text-danger">Delete</a>
</small> </small>
</td> </td>
<td>{{ post.author_name }}</td> <td>{{ model.authors | value: post.author_id }}</td>
<td>{{ post.status }}</td> <td>{{ post.status }}</td>
<td>{{ post.tags | join: ", " }}</td> <td>{{ post.tags | join: ", " }}</td>
</tr> </tr>

View File

@ -14,10 +14,30 @@
<p> <p>
Published on {{ post.published_on | date: "MMMM d, yyyy" }} Published on {{ post.published_on | date: "MMMM d, yyyy" }}
at {{ post.published_on | date: "h:mmtt" | downcase }} at {{ post.published_on | date: "h:mmtt" | downcase }}
by {{ model.authors | value: post.author_id }}
</p> </p>
{{ post.text }} {{ post.text }}
{%- assign category_count = post.category_ids | size -%}
{%- assign tag_count = post.tags | size -%}
{% if category_count > 0 or tag_count > 0 %}
<footer>
<p>
{%- if category_count > 0 -%}
{%- for cat in post.category_ids -%}
{%- assign cat_names = model.categories | value: cat | split: "," | concat: cat_names -%}
{%- endfor -%}
Categorized under: {{ cat_names | reverse | join: ", " }}<br>
{%- assign cat_names = "" -%}
{% endif -%}
{%- if tag_count > 0 %}
Tagged: {{ post.tags | join: ", " }}
{% endif -%}
</p>
</footer>
{% endif %}
<hr>
</article> </article>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</section> </section>