Version 2.1 #41
@ -531,13 +531,10 @@ module WebLog =
|
|||||||
// GET /admin/settings
|
// GET /admin/settings
|
||||||
let settings : HttpHandler = fun next ctx -> task {
|
let settings : HttpHandler = fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
match! TemplateCache.get adminTheme "user-list-body" data with
|
|
||||||
| Ok userTemplate ->
|
|
||||||
match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with
|
match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with
|
||||||
| Ok tagMapTemplate ->
|
| Ok tagMapTemplate ->
|
||||||
let! allPages = data.Page.All ctx.WebLog.Id
|
let! allPages = data.Page.All ctx.WebLog.Id
|
||||||
let! themes = data.Theme.All()
|
let! themes = data.Theme.All()
|
||||||
let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id
|
|
||||||
let! hash =
|
let! hash =
|
||||||
hashForPage "Web Log Settings"
|
hashForPage "Web Log Settings"
|
||||||
|> withAntiCsrf ctx
|
|> withAntiCsrf ctx
|
||||||
@ -560,7 +557,6 @@ module WebLog =
|
|||||||
KeyValuePair.Create(string Database, "Database")
|
KeyValuePair.Create(string Database, "Database")
|
||||||
KeyValuePair.Create(string Disk, "Disk")
|
KeyValuePair.Create(string Disk, "Disk")
|
||||||
|]
|
|]
|
||||||
|> addToHash "users" (users |> List.map (DisplayUser.FromUser ctx.WebLog) |> Array.ofList)
|
|
||||||
|> addToHash "rss_model" (EditRssModel.FromRssOptions ctx.WebLog.Rss)
|
|> addToHash "rss_model" (EditRssModel.FromRssOptions ctx.WebLog.Rss)
|
||||||
|> addToHash "custom_feeds" (
|
|> addToHash "custom_feeds" (
|
||||||
ctx.WebLog.Rss.CustomFeeds
|
ctx.WebLog.Rss.CustomFeeds
|
||||||
@ -569,11 +565,10 @@ module WebLog =
|
|||||||
|> addViewContext ctx
|
|> addViewContext ctx
|
||||||
let! hash' = TagMapping.withTagMappings ctx hash
|
let! hash' = TagMapping.withTagMappings ctx hash
|
||||||
return!
|
return!
|
||||||
addToHash "user_list" (userTemplate.Render hash') hash'
|
hash'
|
||||||
|> addToHash "tag_mapping_list" (tagMapTemplate.Render hash')
|
|> addToHash "tag_mapping_list" (tagMapTemplate.Render hash')
|
||||||
|> adminView "settings" next ctx
|
|> adminView "settings" next ctx
|
||||||
| Error message -> return! Error.server message next ctx
|
| Error message -> return! Error.server message next ctx
|
||||||
| Error message -> return! Error.server message next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/settings
|
// POST /admin/settings
|
||||||
|
@ -381,7 +381,9 @@ let adminPage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) :
|
|||||||
let adminBarePage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) : HttpHandler = fun next ctx -> task {
|
let adminBarePage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) : HttpHandler = fun next ctx -> task {
|
||||||
let! messages = getCurrentMessages ctx
|
let! messages = getCurrentMessages ctx
|
||||||
let appCtx = generateViewContext pageTitle messages includeCsrf ctx
|
let appCtx = generateViewContext pageTitle messages includeCsrf ctx
|
||||||
return! htmlString (Layout.bare content appCtx |> RenderView.AsString.htmlDocument) next ctx
|
return!
|
||||||
|
( messagesToHeaders appCtx.Messages
|
||||||
|
>=> htmlString (Layout.bare content appCtx |> RenderView.AsString.htmlDocument)) next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the anti cross-site request forgery token in the current request
|
/// Validate the anti cross-site request forgery token in the current request
|
||||||
|
@ -207,6 +207,23 @@ let home : HttpHandler = fun next ctx -> task {
|
|||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /{post-permalink}?chapters
|
||||||
|
let chapters (post: Post) : HttpHandler =
|
||||||
|
match post.Episode with
|
||||||
|
| Some ep ->
|
||||||
|
match ep.Chapters with
|
||||||
|
| Some chapters ->
|
||||||
|
|
||||||
|
json chapters
|
||||||
|
| None ->
|
||||||
|
match ep.ChapterFile with
|
||||||
|
| Some file -> redirectTo true file
|
||||||
|
| None -> Error.notFound
|
||||||
|
| None -> Error.notFound
|
||||||
|
|
||||||
|
|
||||||
|
// ~~ ADMINISTRATION ~~
|
||||||
|
|
||||||
// GET /admin/posts
|
// GET /admin/posts
|
||||||
// GET /admin/posts/page/{pageNbr}
|
// GET /admin/posts/page/{pageNbr}
|
||||||
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
@ -372,7 +389,7 @@ let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/post/{id}/chapters
|
// GET /admin/post/{id}/chapters
|
||||||
let chapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let manageChapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with
|
match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with
|
||||||
| Some post
|
| Some post
|
||||||
when Option.isSome post.Episode
|
when Option.isSome post.Episode
|
||||||
|
@ -29,9 +29,14 @@ module CatchAll =
|
|||||||
match data.Post.FindByPermalink permalink webLog.Id |> await with
|
match data.Post.FindByPermalink permalink webLog.Id |> await with
|
||||||
| Some post ->
|
| Some post ->
|
||||||
debug (fun () -> "Found post by permalink")
|
debug (fun () -> "Found post by permalink")
|
||||||
let hash = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 data |> await
|
if post.Status = Published || Option.isSome ctx.UserAccessLevel then
|
||||||
|
if ctx.Request.Query.ContainsKey "chapters" then
|
||||||
|
yield Post.chapters post
|
||||||
|
else
|
||||||
yield fun next ctx ->
|
yield fun next ctx ->
|
||||||
addToHash ViewContext.PageTitle post.Title hash
|
Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 data
|
||||||
|
|> await
|
||||||
|
|> addToHash ViewContext.PageTitle post.Title
|
||||||
|> themedView (defaultArg post.Template "single-post") next ctx
|
|> themedView (defaultArg post.Template "single-post") next ctx
|
||||||
| None -> ()
|
| None -> ()
|
||||||
// Current page
|
// Current page
|
||||||
@ -130,7 +135,7 @@ let router : HttpHandler = choose [
|
|||||||
routef "/%s/revision/%s/preview" Post.previewRevision
|
routef "/%s/revision/%s/preview" Post.previewRevision
|
||||||
routef "/%s/revisions" Post.editRevisions
|
routef "/%s/revisions" Post.editRevisions
|
||||||
routef "/%s/chapter/%i" Post.editChapter
|
routef "/%s/chapter/%i" Post.editChapter
|
||||||
routef "/%s/chapters" Post.chapters
|
routef "/%s/chapters" Post.manageChapters
|
||||||
])
|
])
|
||||||
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
||||||
route "" >=> Admin.WebLog.settings
|
route "" >=> Admin.WebLog.settings
|
||||||
@ -201,10 +206,7 @@ let router : HttpHandler = choose [
|
|||||||
route "/save" >=> Admin.TagMapping.save
|
route "/save" >=> Admin.TagMapping.save
|
||||||
routef "/%s/delete" Admin.TagMapping.delete
|
routef "/%s/delete" Admin.TagMapping.delete
|
||||||
])
|
])
|
||||||
subRoute "/user" (choose [
|
route "/user/save" >=> User.save
|
||||||
route "/save" >=> User.save
|
|
||||||
routef "/%s/delete" User.delete
|
|
||||||
])
|
|
||||||
])
|
])
|
||||||
subRoute "/theme" (choose [
|
subRoute "/theme" (choose [
|
||||||
route "/new" >=> Admin.Theme.save
|
route "/new" >=> Admin.Theme.save
|
||||||
@ -220,6 +222,9 @@ let router : HttpHandler = choose [
|
|||||||
subRoute "/post" (choose [
|
subRoute "/post" (choose [
|
||||||
routef "/%s/chapter/%i" Post.deleteChapter
|
routef "/%s/chapter/%i" Post.deleteChapter
|
||||||
])
|
])
|
||||||
|
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
||||||
|
routef "/user/%s" User.delete
|
||||||
|
])
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
GET_HEAD >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts
|
GET_HEAD >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts
|
||||||
|
@ -5,7 +5,6 @@ open System
|
|||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
open Microsoft.AspNetCore.Identity
|
open Microsoft.AspNetCore.Identity
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open NodaTime
|
|
||||||
|
|
||||||
// ~~ LOG ON / LOG OFF ~~
|
// ~~ LOG ON / LOG OFF ~~
|
||||||
|
|
||||||
@ -84,7 +83,6 @@ let logOff : HttpHandler = fun next ctx -> task {
|
|||||||
|
|
||||||
// ~~ ADMINISTRATION ~~
|
// ~~ ADMINISTRATION ~~
|
||||||
|
|
||||||
open System.Collections.Generic
|
|
||||||
open Giraffe.Htmx
|
open Giraffe.Htmx
|
||||||
|
|
||||||
/// Got no time for URL/form manipulators...
|
/// Got no time for URL/form manipulators...
|
||||||
@ -98,16 +96,7 @@ let all : HttpHandler = fun next ctx -> task {
|
|||||||
|
|
||||||
/// Show the edit user page
|
/// Show the edit user page
|
||||||
let private showEdit (model: EditUserModel) : HttpHandler = fun next ctx ->
|
let private showEdit (model: EditUserModel) : HttpHandler = fun next ctx ->
|
||||||
hashForPage (if model.IsNew then "Add a New User" else "Edit User")
|
adminBarePage (if model.IsNew then "Add a New User" else "Edit User") true (Views.User.edit model) next ctx
|
||||||
|> withAntiCsrf ctx
|
|
||||||
|> addToHash ViewContext.Model model
|
|
||||||
|> addToHash "access_levels" [|
|
|
||||||
KeyValuePair.Create(string Author, "Author")
|
|
||||||
KeyValuePair.Create(string Editor, "Editor")
|
|
||||||
KeyValuePair.Create(string WebLogAdmin, "Web Log Admin")
|
|
||||||
if ctx.HasAccessLevel Administrator then KeyValuePair.Create(string Administrator, "Administrator")
|
|
||||||
|]
|
|
||||||
|> adminBareView "user-edit" next ctx
|
|
||||||
|
|
||||||
// GET /admin/settings/user/{id}/edit
|
// GET /admin/settings/user/{id}/edit
|
||||||
let edit usrId : HttpHandler = fun next ctx -> task {
|
let edit usrId : HttpHandler = fun next ctx -> task {
|
||||||
@ -121,7 +110,7 @@ let edit usrId : HttpHandler = fun next ctx -> task {
|
|||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/settings/user/{id}/delete
|
// DELETE /admin/settings/user/{id}
|
||||||
let delete userId : HttpHandler = fun next ctx -> task {
|
let delete userId : HttpHandler = fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
match! data.WebLogUser.FindById (WebLogUserId userId) ctx.WebLog.Id with
|
match! data.WebLogUser.FindById (WebLogUserId userId) ctx.WebLog.Id with
|
||||||
@ -144,21 +133,11 @@ let delete userId : HttpHandler = fun next ctx -> task {
|
|||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display the user "my info" page, with information possibly filled in
|
|
||||||
let private showMyInfo (model: EditMyInfoModel) (user: WebLogUser) : HttpHandler = fun next ctx ->
|
|
||||||
hashForPage "Edit Your Information"
|
|
||||||
|> withAntiCsrf ctx
|
|
||||||
|> addToHash ViewContext.Model model
|
|
||||||
|> addToHash "access_level" (string user.AccessLevel)
|
|
||||||
|> addToHash "created_on" (ctx.WebLog.LocalTime user.CreatedOn)
|
|
||||||
|> addToHash "last_seen_on" (ctx.WebLog.LocalTime (defaultArg user.LastSeenOn (Instant.FromUnixTimeSeconds 0)))
|
|
||||||
|> adminView "my-info" next ctx
|
|
||||||
|
|
||||||
|
|
||||||
// GET /admin/my-info
|
// GET /admin/my-info
|
||||||
let myInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let myInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
|
match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
|
||||||
| Some user -> return! showMyInfo (EditMyInfoModel.FromUser user) user next ctx
|
| Some user ->
|
||||||
|
return! adminPage "Edit Your Information" true (Views.User.myInfo (EditMyInfoModel.FromUser user) user) next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +160,10 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||||||
return! redirectToGet "admin/my-info" next ctx
|
return! redirectToGet "admin/my-info" next ctx
|
||||||
| Some user ->
|
| Some user ->
|
||||||
do! addMessage ctx { UserMessage.Error with Message = "Passwords did not match; no updates made" }
|
do! addMessage ctx { UserMessage.Error with Message = "Passwords did not match; no updates made" }
|
||||||
return! showMyInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user next ctx
|
return!
|
||||||
|
adminPage
|
||||||
|
"Edit Your Information" true
|
||||||
|
(Views.User.myInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user) next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,19 +76,27 @@ let raw = rawText
|
|||||||
|
|
||||||
/// The pattern for a long date
|
/// The pattern for a long date
|
||||||
let longDatePattern =
|
let longDatePattern =
|
||||||
InstantPattern.CreateWithInvariantCulture "MMMM d, yyyy"
|
ZonedDateTimePattern.CreateWithInvariantCulture("MMMM d, yyyy", DateTimeZoneProviders.Tzdb)
|
||||||
|
|
||||||
/// Create a long date
|
/// Create a long date
|
||||||
let longDate =
|
let longDate app (instant: Instant) =
|
||||||
longDatePattern.Format >> txt
|
DateTimeZoneProviders.Tzdb[app.WebLog.TimeZone]
|
||||||
|
|> Option.ofObj
|
||||||
|
|> Option.map (fun tz -> longDatePattern.Format(instant.InZone(tz)))
|
||||||
|
|> Option.defaultValue "--"
|
||||||
|
|> txt
|
||||||
|
|
||||||
/// The pattern for a short time
|
/// The pattern for a short time
|
||||||
let shortTimePattern =
|
let shortTimePattern =
|
||||||
InstantPattern.CreateWithInvariantCulture "h:mmtt"
|
ZonedDateTimePattern.CreateWithInvariantCulture("h:mmtt", DateTimeZoneProviders.Tzdb)
|
||||||
|
|
||||||
/// Create a short time
|
/// Create a short time
|
||||||
let shortTime instant =
|
let shortTime app (instant: Instant) =
|
||||||
txt (shortTimePattern.Format(instant).ToLower())
|
DateTimeZoneProviders.Tzdb[app.WebLog.TimeZone]
|
||||||
|
|> Option.ofObj
|
||||||
|
|> Option.map (fun tz -> shortTimePattern.Format(instant.InZone(tz)).ToLowerInvariant())
|
||||||
|
|> Option.defaultValue "--"
|
||||||
|
|> txt
|
||||||
|
|
||||||
/// Functions for generating content in varying layouts
|
/// Functions for generating content in varying layouts
|
||||||
module Layout =
|
module Layout =
|
||||||
|
@ -5,7 +5,130 @@ open Giraffe.ViewEngine.Htmx
|
|||||||
open MyWebLog
|
open MyWebLog
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
/// Page to display the log on page
|
/// User edit form
|
||||||
|
let edit (model: EditUserModel) app =
|
||||||
|
let levelOption value name =
|
||||||
|
option [ _value value; if model.AccessLevel = value then _selected ] [ txt name ]
|
||||||
|
div [ _class "col-12" ] [
|
||||||
|
h5 [ _class "my-3" ] [ txt app.PageTitle ]
|
||||||
|
form [ _hxPost (relUrl app "admin/settings/user/save"); _method "post"; _class "container"
|
||||||
|
_hxTarget "#userList"; _hxSwap "outerHTML show:window:top" ] [
|
||||||
|
antiCsrf app
|
||||||
|
input [ _type "hidden"; _name "Id"; _value model.Id ]
|
||||||
|
div [ _class "row" ] [
|
||||||
|
div [ _class "col-12 col-md-5 col-lg-3 col-xxl-2 offset-xxl-1 mb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
select [ _name "AccessLevel"; _id "accessLevel"; _class "form-control"; _required
|
||||||
|
_autofocus ] [
|
||||||
|
levelOption (string Author) "Author"
|
||||||
|
levelOption (string Editor) "Editor"
|
||||||
|
levelOption (string WebLogAdmin) "Web Log Admin"
|
||||||
|
if app.IsAdministrator then levelOption (string Administrator) "Administrator"
|
||||||
|
]
|
||||||
|
label [ _for "accessLevel" ] [ raw "Access Level" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-7 col-lg-4 col-xxl-3 mb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "email"; _name "Email"; _id "email"; _class "form-control"; _placeholder "E-mail"
|
||||||
|
_required; _value model.Email ]
|
||||||
|
label [ _for "email" ] [ raw "E-mail Address" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-lg-5 mb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _name "Url"; _id "url"; _class "form-control"; _placeholder "URL"
|
||||||
|
_value model.Url ]
|
||||||
|
label [ _for "url" ] [ raw "User’s Personal URL" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row mb-3" ] [
|
||||||
|
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 offset-xl-1 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"
|
||||||
|
_placeholder "First"; _required; _value model.FirstName ]
|
||||||
|
label [ _for "firstName" ] [ raw "First Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"
|
||||||
|
_placeholder "Last"; _required; _value model.LastName ]
|
||||||
|
label [ _for "lastName" ] [ raw "Last Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-0 col-xl-3 offset-xl-1 pb-3" ] [
|
||||||
|
div [ _class "form-floating " ] [
|
||||||
|
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
|
||||||
|
_placeholder "Preferred"; _required; _value model.PreferredName ]
|
||||||
|
label [ _for "preferredName" ] [ raw "Preferred Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row mb-3" ] [
|
||||||
|
div [ _class "col-12 col-xl-10 offset-xl-1" ] [
|
||||||
|
fieldset [ _class "p-2" ] [
|
||||||
|
legend [ _class "ps-1" ] [
|
||||||
|
if not model.IsNew then raw "Change "
|
||||||
|
raw "Password"
|
||||||
|
]
|
||||||
|
if not model.IsNew then
|
||||||
|
div [ _class "row" ] [
|
||||||
|
div [ _class "col" ] [
|
||||||
|
p [ _class "form-text" ] [
|
||||||
|
raw "Optional; leave blank not change the user’s password"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row" ] [
|
||||||
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "password"; _name "Password"; _id "password"; _class "form-control"
|
||||||
|
_placeholder "Password"
|
||||||
|
if model.IsNew then _required ]
|
||||||
|
label [ _for "password" ] [
|
||||||
|
if not model.IsNew then raw "New "
|
||||||
|
raw "Password"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "password"; _name "PasswordConfirm"; _id "passwordConfirm"
|
||||||
|
_class "form-control"; _placeholder "Confirm"
|
||||||
|
if model.IsNew then _required ]
|
||||||
|
label [ _for "passwordConfirm" ] [
|
||||||
|
raw "Confirm"
|
||||||
|
if not model.IsNew then raw " New"
|
||||||
|
raw " Password"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row mb-3" ] [
|
||||||
|
div [ _class "col text-center" ] [
|
||||||
|
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ]; raw " "
|
||||||
|
if model.IsNew then
|
||||||
|
button [ _type "button"; _class "btn btn-sm btn-secondary ms-3"
|
||||||
|
_onclick "document.getElementById('user_new').innerHTML = ''" ] [
|
||||||
|
raw "Cancel"
|
||||||
|
]
|
||||||
|
else
|
||||||
|
a [ _href (relUrl app "admin/settings/users"); _class "btn btn-sm btn-secondary ms-3" ] [
|
||||||
|
raw "Cancel"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|> List.singleton
|
||||||
|
|
||||||
|
|
||||||
|
/// User log on form
|
||||||
let logOn (model: LogOnModel) (app: AppViewContext) = [
|
let logOn (model: LogOnModel) (app: AppViewContext) = [
|
||||||
h2 [ _class "my-3" ] [ rawText "Log On to "; encodedText app.WebLog.Name ]
|
h2 [ _class "my-3" ] [ rawText "Log On to "; encodedText app.WebLog.Name ]
|
||||||
article [ _class "py-3" ] [
|
article [ _class "py-3" ] [
|
||||||
@ -36,6 +159,7 @@ let logOn (model: LogOnModel) (app: AppViewContext) = [
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
/// The list of users for a web log (part of web log settings page)
|
/// The list of users for a web log (part of web log settings page)
|
||||||
let userList (model: WebLogUser list) app =
|
let userList (model: WebLogUser list) app =
|
||||||
let badge = "ms-2 badge bg"
|
let badge = "ms-2 badge bg"
|
||||||
@ -47,7 +171,7 @@ let userList (model: WebLogUser list) app =
|
|||||||
antiCsrf app
|
antiCsrf app
|
||||||
for user in model do
|
for user in model do
|
||||||
div [ _class "row mwl-table-detail"; _id $"user_{user.Id}" ] [
|
div [ _class "row mwl-table-detail"; _id $"user_{user.Id}" ] [
|
||||||
div [ _class $"col-12 col-md-4 col-xl-3 no-wrap" ] [
|
div [ _class "col-12 col-md-4 col-xl-3 no-wrap" ] [
|
||||||
txt user.PreferredName; raw " "
|
txt user.PreferredName; raw " "
|
||||||
match user.AccessLevel with
|
match user.AccessLevel with
|
||||||
| Administrator -> span [ _class $"{badge}-success" ] [ raw "ADMINISTRATOR" ]
|
| Administrator -> span [ _class $"{badge}-success" ] [ raw "ADMINISTRATOR" ]
|
||||||
@ -56,17 +180,16 @@ let userList (model: WebLogUser list) app =
|
|||||||
| Author -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
|
| Author -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
|
||||||
br []
|
br []
|
||||||
if app.IsAdministrator || (app.IsWebLogAdmin && not (user.AccessLevel = Administrator)) then
|
if app.IsAdministrator || (app.IsWebLogAdmin && not (user.AccessLevel = Administrator)) then
|
||||||
let urlBase = $"admin/settings/user/{user.Id}"
|
let userUrl = relUrl app $"admin/settings/user/{user.Id}"
|
||||||
small [] [
|
small [] [
|
||||||
a [ _href (relUrl app $"{urlBase}/edit"); _hxTarget $"#user_{user.Id}"
|
a [ _href $"{userUrl}/edit"; _hxTarget $"#user_{user.Id}"
|
||||||
_hxSwap $"innerHTML show:#user_{user.Id}:top" ] [
|
_hxSwap $"innerHTML show:#user_{user.Id}:top" ] [
|
||||||
raw "Edit"
|
raw "Edit"
|
||||||
]
|
]
|
||||||
if app.UserId.Value <> user.Id then
|
if app.UserId.Value <> user.Id then
|
||||||
let delLink = relUrl app $"{urlBase}/delete"
|
|
||||||
span [ _class "text-muted" ] [ raw " • " ]
|
span [ _class "text-muted" ] [ raw " • " ]
|
||||||
a [ _href delLink; _hxPost delLink; _class "text-danger"
|
a [ _href userUrl; _hxDelete userUrl; _class "text-danger"
|
||||||
_hxConfirm $"Are you sure you want to delete the user “{user.PreferredName}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)" ] [
|
_hxConfirm $"Are you sure you want to delete the user “{user.PreferredName}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)" ] [
|
||||||
raw "Delete"
|
raw "Delete"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -79,13 +202,101 @@ let userList (model: WebLogUser list) app =
|
|||||||
br []; txt user.Url.Value
|
br []; txt user.Url.Value
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "d-none d-xl-block col-xl-2" ] [ longDate user.CreatedOn ]
|
div [ _class "d-none d-xl-block col-xl-2" ] [
|
||||||
|
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
|
||||||
|
]
|
||||||
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
||||||
match user.LastSeenOn with
|
match user.LastSeenOn with
|
||||||
| Some it -> longDate it; raw " at "; shortTime it
|
| Some it -> longDate app it; raw " at "; shortTime app it
|
||||||
| None -> raw "--"
|
| None -> raw "--"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|> List.singleton
|
|> List.singleton
|
||||||
|
|
||||||
|
|
||||||
|
/// Edit My Info form
|
||||||
|
let myInfo (model: EditMyInfoModel) (user: WebLogUser) app = [
|
||||||
|
h2 [ _class "my-3" ] [ txt app.PageTitle ]
|
||||||
|
article [] [
|
||||||
|
form [ _action (relUrl app "admin/my-info"); _method "post" ] [
|
||||||
|
antiCsrf app
|
||||||
|
div [ _class "d-flex flex-row flex-wrap justify-content-around" ] [
|
||||||
|
div [ _class "text-center mb-3 lh-sm" ] [
|
||||||
|
strong [ _class "text-decoration-underline" ] [ raw "Access Level" ]; br []
|
||||||
|
raw (string user.AccessLevel)
|
||||||
|
]
|
||||||
|
div [ _class "text-center mb-3 lh-sm" ] [
|
||||||
|
strong [ _class "text-decoration-underline" ] [ raw "Created" ]; br []
|
||||||
|
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
|
||||||
|
]
|
||||||
|
div [ _class "text-center mb-3 lh-sm" ] [
|
||||||
|
strong [ _class "text-decoration-underline" ] [ raw "Last Log On" ]; br []
|
||||||
|
longDate app user.LastSeenOn.Value; raw " at "; shortTime app user.LastSeenOn.Value
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "container" ] [
|
||||||
|
div [ _class "row" ] [ div [ _class "col" ] [ hr [ _class "mt-0" ] ] ]
|
||||||
|
div [ _class "row mb-3" ] [
|
||||||
|
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"; _autofocus
|
||||||
|
_required; _placeholder "First"; _value model.FirstName ]
|
||||||
|
label [ _for "firstName" ] [ raw "First Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"; _required
|
||||||
|
_placeholder "Last"; _value model.LastName ]
|
||||||
|
label [ _for "lastName" ] [ raw "Last Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
|
||||||
|
_required; _placeholder "Preferred"; _value model.PreferredName ]
|
||||||
|
label [ _for "preferredName" ] [ raw "Preferred Name" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row mb-3" ] [
|
||||||
|
div [ _class "col" ] [
|
||||||
|
fieldset [ _class "p-2" ] [
|
||||||
|
legend [ _class "ps-1" ] [ raw "Change Password" ]
|
||||||
|
div [ _class "row" ] [
|
||||||
|
div [ _class "col" ] [
|
||||||
|
p [ _class "form-text" ] [
|
||||||
|
raw "Optional; leave blank to keep your current password"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row" ] [
|
||||||
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "password"; _name "NewPassword"; _id "newPassword"
|
||||||
|
_class "form-control"; _placeholder "Password" ]
|
||||||
|
label [ _for "newPassword" ] [ raw "New Password" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 pb-3" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "password"; _name "NewPasswordConfirm"; _id "newPasswordConfirm"
|
||||||
|
_class "form-control"; _placeholder "Confirm" ]
|
||||||
|
label [ _for "newPasswordConfirm" ] [ raw "Confirm New Password" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row" ] [
|
||||||
|
div [ _class "col text-center mb-3" ] [
|
||||||
|
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
<h2 class=my-3>{{ page_title }}</h2>
|
|
||||||
<article>
|
|
||||||
<form action="{{ "admin/my-info" | relative_link }}" method=post>
|
|
||||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
<div class="d-flex flex-row flex-wrap justify-content-around">
|
|
||||||
<div class="text-center mb-3 lh-sm">
|
|
||||||
<strong class=text-decoration-underline>Access Level</strong><br>{{ access_level }}
|
|
||||||
</div>
|
|
||||||
<div class="text-center mb-3 lh-sm">
|
|
||||||
<strong class=text-decoration-underline>Created</strong><br>{{ created_on | date: "MMMM d, yyyy" }}
|
|
||||||
</div>
|
|
||||||
<div class="text-center mb-3 lh-sm">
|
|
||||||
<strong class=text-decoration-underline>Last Log On</strong><br>
|
|
||||||
{{ last_seen_on | date: "MMMM d, yyyy" }} at {{ last_seen_on | date: "h:mmtt" | downcase }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=container>
|
|
||||||
<div class=row><div class=col><hr class=mt-0></div></div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12 col-md-6 col-lg-4 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=FirstName id=firstName class=form-control autofocus required placeholder=First
|
|
||||||
value="{{ model.first_name }}">
|
|
||||||
<label for=firstName>First Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-lg-4 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=LastName id=lastName class=form-control required placeholder="Last"
|
|
||||||
value="{{ model.last_name }}">
|
|
||||||
<label for=lastName>Last Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-lg-4 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=PreferredName id=preferredName class=form-control required placeholder="Preferred"
|
|
||||||
value="{{ model.preferred_name }}">
|
|
||||||
<label for=preferredName>Preferred Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class=col>
|
|
||||||
<fieldset class=p-2>
|
|
||||||
<legend class=ps-1>Change Password</legend>
|
|
||||||
<div class=row>
|
|
||||||
<div class=col><p class=form-text>Optional; leave blank to keep your current password</div>
|
|
||||||
</div>
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-6 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=password name=NewPassword id=newPassword class=form-control placeholder=Password>
|
|
||||||
<label for=newPassword>New Password</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=password name=NewPasswordConfirm id=newPasswordConfirm class=form-control
|
|
||||||
placeholder=Confirm>
|
|
||||||
<label for=newPasswordConfirm>Confirm New Password</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=row>
|
|
||||||
<div class="col text-center mb-3">
|
|
||||||
<button type=submit class="btn btn-primary">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
@ -122,7 +122,7 @@
|
|||||||
<div class="{{ last_col }} d-none d-md-block">Last Log On</div>
|
<div class="{{ last_col }} d-none d-md-block">Last Log On</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ user_list }}
|
<span hx-get="{{ "admin/settings/users" | relative_link }}" hx-trigger="load" hx-swap="outerHTML"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id=rss-settings class="container mb-3 pb-0">
|
<fieldset id=rss-settings class="container mb-3 pb-0">
|
||||||
<legend>RSS Settings</legend>
|
<legend>RSS Settings</legend>
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
<div class=col-12>
|
|
||||||
<h5 class=my-3>{{ page_title }}</h5>
|
|
||||||
<form hx-post="{{ "admin/settings/user/save" | relative_link }}" method=post class=container hx-target=#userList
|
|
||||||
hx-swap="outerHTML show:window:top">
|
|
||||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
<input type=hidden name=Id value="{{ model.id }}">
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-5 col-lg-3 col-xxl-2 offset-xxl-1 mb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<select name=AccessLevel id=accessLevel class=form-control required autofocus>
|
|
||||||
{%- for level in access_levels %}
|
|
||||||
<option value="{{ level[0] }}"{% if model.access_level == level[0] %} selected{% endif %}>
|
|
||||||
{{ level[1] }}
|
|
||||||
</option>
|
|
||||||
{%- endfor %}
|
|
||||||
</select>
|
|
||||||
<label for=accessLevel>Access Level</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-7 col-lg-4 col-xxl-3 mb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=email name=Email id=email class=form-control placeholder=E-mail required
|
|
||||||
value="{{ model.email | escape }}">
|
|
||||||
<label for=email>E-mail Address</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-5 mb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=Url id=url class=form-control placeholder=URL value="{{ model.url | escape }}">
|
|
||||||
<label for=url>User’s Personal URL</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12 col-md-6 col-lg-4 col-xl-3 offset-xl-1 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=FirstName id=firstName class=form-control placeholder=First required
|
|
||||||
value="{{ model.first_name | escape }}">
|
|
||||||
<label for=firstName>First Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 col-lg-4 col-xl-3 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=LastName id=lastName class=form-control placeholder=Last required
|
|
||||||
value="{{ model.last_name | escape }}">
|
|
||||||
<label for=lastName>Last Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-0 col-xl-3 offset-xl-1 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=PreferredName id=preferredName class=form-control placeholder=Preferred required
|
|
||||||
value="{{ model.preferred_name | escape }}">
|
|
||||||
<label for=preferredName>Preferred Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12 col-xl-10 offset-xl-1">
|
|
||||||
<fieldset class=p-2>
|
|
||||||
<legend class=ps-1>{% unless model.is_new %}Change {% endunless %}Password</legend>
|
|
||||||
{% unless model.is_new %}
|
|
||||||
<div class=row>
|
|
||||||
<div class=col><p class=form-text>Optional; leave blank not change the user’s password</div>
|
|
||||||
</div>
|
|
||||||
{% endunless %}
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-6 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=password name=Password id=password class=form-control placeholder=Password
|
|
||||||
{%- if model.is_new %} required{% endif %}>
|
|
||||||
<label for=password>{% unless model.is_new %}New {% endunless %}Password</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=password name=PasswordConfirm id=passwordConfirm class=form-control placeholder=Confirm
|
|
||||||
{%- if model.is_new %} required{% endif %}>
|
|
||||||
<label for=passwordConfirm>Confirm{% unless model.is_new %} New{% endunless %} Password</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col text-center">
|
|
||||||
<button type=submit class="btn btn-sm btn-primary">Save Changes</button>
|
|
||||||
{% if model.is_new %}
|
|
||||||
<button type=button class="btn btn-sm btn-secondary ms-3"
|
|
||||||
onclick="document.getElementById('user_new').innerHTML = ''">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ "admin/settings/users" | relative_link }}" class="btn btn-sm btn-secondary ms-3">Cancel</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
@ -1,61 +0,0 @@
|
|||||||
<div id=userList>
|
|
||||||
<div class="container g-0">
|
|
||||||
<div class="row mwl-table-detail" id=user_new></div>
|
|
||||||
</div>
|
|
||||||
<form method=post id=userList class="container g-0" hx-target=this hx-swap="outerHTML show:window:top">
|
|
||||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
{% include_template "_user-list-columns" %}
|
|
||||||
{%- assign badge = "ms-2 badge bg" -%}
|
|
||||||
{% for user in users -%}
|
|
||||||
<div class="row mwl-table-detail" id="user_{{ user.id }}">
|
|
||||||
<div class="{{ user_col }} no-wrap">
|
|
||||||
{{ user.preferred_name }}
|
|
||||||
{%- if user.access_level == "Administrator" %}
|
|
||||||
<span class="{{ badge }}-success">ADMINISTRATOR</span>
|
|
||||||
{%- elsif user.access_level == "WebLogAdmin" %}
|
|
||||||
<span class="{{ badge }}-primary">WEB LOG ADMIN</span>
|
|
||||||
{%- elsif user.access_level == "Editor" %}
|
|
||||||
<span class="{{ badge }}-secondary">EDITOR</span>
|
|
||||||
{%- elsif user.access_level == "Author" %}
|
|
||||||
<span class="{{ badge }}-dark">AUTHOR</span>
|
|
||||||
{%- endif %}<br>
|
|
||||||
{%- unless is_administrator == false and user.access_level == "Administrator" %}
|
|
||||||
<small>
|
|
||||||
{%- assign user_url_base = "admin/settings/user/" | append: user.id -%}
|
|
||||||
<a href="{{ user_url_base | append: "/edit" | relative_link }}" hx-target="#user_{{ user.id }}"
|
|
||||||
hx-swap="innerHTML show:#user_{{ user.id }}:top">
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
{% unless user_id == user.id %}
|
|
||||||
<span class=text-muted> • </span>
|
|
||||||
{%- assign user_del_link = user_url_base | append: "/delete" | relative_link -%}
|
|
||||||
<a href="{{ user_del_link }}" hx-post="{{ user_del_link }}" class=text-danger
|
|
||||||
hx-confirm="Are you sure you want to delete the user “{{ user.preferred_name }}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)">
|
|
||||||
Delete
|
|
||||||
</a>
|
|
||||||
{% endunless %}
|
|
||||||
</small>
|
|
||||||
{%- endunless %}
|
|
||||||
</div>
|
|
||||||
<div class="{{ email_col }}">
|
|
||||||
{{ user.first_name }} {{ user.last_name }}<br>
|
|
||||||
<small class=text-muted>
|
|
||||||
{{ user.email }}
|
|
||||||
{%- unless user.url == "" %}<br>{{ user.url }}{% endunless %}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="{{ cre8_col }}">
|
|
||||||
{{ user.created_on | date: "MMMM d, yyyy" }}
|
|
||||||
</div>
|
|
||||||
<div class="{{ last_col }}">
|
|
||||||
{% if user.last_seen_on %}
|
|
||||||
{{ user.last_seen_on | date: "MMMM d, yyyy" }} at
|
|
||||||
{{ user.last_seen_on | date: "h:mmtt" | downcase }}
|
|
||||||
{% else %}
|
|
||||||
--
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{%- endfor %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
Loading…
Reference in New Issue
Block a user