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
let findNames (catIds : CategoryId list) (webLogId : WebLogId) conn = backgroundTask {
let findNames (webLogId : WebLogId) conn (catIds : CategoryId list) = backgroundTask {
let! cats = rethink<Category list> {
withTable Table.Category
getAll (catIds |> List.map (fun it -> it :> obj))
filter "webLogId" webLogId
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
@ -504,12 +504,12 @@ module WebLogUser =
|> tryFirst
/// 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> {
withTable Table.WebLogUser
getAll (userIds |> List.map (fun it -> it :> obj))
filter "webLogId" webLogId
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})"
/// 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
[<CLIMutable; NoComparison; NoEquality>]
type Revision =

View File

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

View File

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

View File

@ -58,6 +58,14 @@ module DotLiquidBespoke =
"</ul>"
}
|> 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
@ -192,16 +200,18 @@ let main args =
// Set up DotLiquid
Template.RegisterFilter typeof<DotLiquidBespoke.NavLinkFilter>
Template.RegisterFilter typeof<DotLiquidBespoke.ValueFilter>
Template.RegisterTag<DotLiquidBespoke.UserLinksTag> "user_links"
[ // Domain types
typeof<Page>; typeof<WebLog>
typeof<MetaItem>; typeof<Page>; typeof<WebLog>
// View models
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayPage>; typeof<EditCategoryModel>
typeof<EditPageModel>; typeof<EditPostModel>; typeof<PostDisplay>; typeof<PostListItem>
typeof<SettingsModel>; typeof<UserMessage>
typeof<EditPageModel>; typeof<EditPostModel>; typeof<LogOnModel>; typeof<PostDisplay>
typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
// 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, [| "*" |]))

View File

@ -2,6 +2,9 @@
<article class="py-3">
<form action="/user/log-on" method="post">
<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="row pb-3">
<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>
</small>
</td>
<td>{{ post.author_name }}</td>
<td>{{ model.authors | value: post.author_id }}</td>
<td>{{ post.status }}</td>
<td>{{ post.tags | join: ", " }}</td>
</tr>

View File

@ -14,10 +14,30 @@
<p>
Published on {{ post.published_on | date: "MMMM d, yyyy" }}
at {{ post.published_on | date: "h:mmtt" | downcase }}
by {{ model.authors | value: post.author_id }}
</p>
{{ 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>
</div>
</div>
{% endfor %}
</section>
</section>