Add meta item
- Use meta item for category/author lookups - Add Liquid filter to get values from meta item lists - Implement log on "returnUrl" parameter
This commit is contained in:
parent
d0e016fd28
commit
fa20122f20
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 [
|
||||||
|
|
|
@ -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, [| "*" |]))
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user