Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
5 changed files with 122 additions and 8 deletions
Showing only changes of commit 90bca34be3 - Show all commits

View File

@ -3,6 +3,7 @@
open System open System
open MyWebLog open MyWebLog
open NodaTime open NodaTime
open NodaTime.Text
/// Helper functions for view models /// Helper functions for view models
[<AutoOpen>] [<AutoOpen>]
@ -71,6 +72,45 @@ type DisplayCategory = {
PostCount: int PostCount: int
} }
/// A display version of an episode chapter
type DisplayChapter = {
/// The start time of the chapter (HH:MM:SS.FF format)
StartTime: string
/// The title of the chapter
Title: string
/// An image to display for this chapter
ImageUrl: string
/// Whether this chapter should be displayed in podcast players
IsHidden: bool
/// The end time of the chapter (HH:MM:SS.FF format)
EndTime: string
/// The name of a location
LocationName: string
/// The geographic coordinates of the location
LocationGeo: string
/// An OpenStreetMap query for this location
LocationOsm: string
} with
/// Create a display chapter from a chapter
static member FromChapter (chapter: Chapter) =
let pattern = DurationPattern.CreateWithInvariantCulture("H:mm:ss.ff")
{ StartTime = pattern.Format(chapter.StartTime)
Title = defaultArg chapter.Title ""
ImageUrl = defaultArg chapter.ImageUrl ""
IsHidden = defaultArg chapter.IsHidden false
EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue ""
LocationName = chapter.Location |> Option.map (fun l -> l.Name) |> Option.defaultValue ""
LocationGeo = chapter.Location |> Option.map (fun l -> l.Geo) |> Option.flatten |> Option.defaultValue ""
LocationOsm = chapter.Location |> Option.map (fun l -> l.Osm) |> Option.flatten |> Option.defaultValue "" }
/// A display version of a custom feed definition /// A display version of a custom feed definition
type DisplayCustomFeed = { type DisplayCustomFeed = {
@ -984,6 +1024,25 @@ type LogOnModel = {
{ EmailAddress = ""; Password = ""; ReturnTo = None } { EmailAddress = ""; Password = ""; ReturnTo = None }
/// View model to manage chapters
[<CLIMutable; NoComparison; NoEquality>]
type ManageChaptersModel = {
/// The post ID for the chapters being edited
Id: string
/// The title of the post for which chapters are being edited
Title: string
Chapters: Chapter array
} with
/// Create a model from a post and its episode's chapters
static member Create (post: Post) =
{ Id = string post.Id
Title = post.Title
Chapters = Array.ofList post.Episode.Value.Chapters.Value }
/// View model to manage permalinks /// View model to manage permalinks
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type ManagePermalinksModel = { type ManagePermalinksModel = {

View File

@ -227,14 +227,15 @@ let register () =
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page> typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog> typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
// View models // View models
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed> typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayChapter>
typeof<DisplayPage>; typeof<DisplayRevision>; typeof<DisplayTheme> typeof<DisplayCustomFeed>; typeof<DisplayPage>; typeof<DisplayRevision>
typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditCategoryModel> typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>
typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel> typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>
typeof<EditPostModel>; typeof<EditRedirectRuleModel>; typeof<EditRssModel> typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRedirectRuleModel>
typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel> typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>
typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>; typeof<PostDisplay> typeof<LogOnModel>; typeof<ManageChaptersModel>; typeof<ManagePermalinksModel>
typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage> typeof<ManageRevisionsModel>; typeof<PostDisplay>; typeof<PostListItem>
typeof<SettingsModel>; typeof<UserMessage>
// Framework types // Framework types
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair> typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list> typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>

View File

@ -371,6 +371,21 @@ let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fu
| _, None -> return! Error.notFound next ctx | _, None -> return! Error.notFound next ctx
} }
// GET /admin/post/{id}/chapters
let chapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with
| Some post
when Option.isSome post.Episode
&& Option.isSome post.Episode.Value.Chapters
&& canEdit post.AuthorId ctx ->
return!
hashForPage "Manage Chapters"
|> withAntiCsrf ctx
|> addToHash ViewContext.Model (ManageChaptersModel.Create post)
|> adminView "chapters" next ctx
| Some _ | None -> return! Error.notFound next ctx
}
// POST /admin/post/save // POST /admin/post/save
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditPostModel>() let! model = ctx.BindFormAsync<EditPostModel>()

View File

@ -129,6 +129,7 @@ let router : HttpHandler = choose [
routef "/%s/permalinks" Post.editPermalinks routef "/%s/permalinks" Post.editPermalinks
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/chapters" Post.chapters
]) ])
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [ subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
route "" >=> Admin.WebLog.settings route "" >=> Admin.WebLog.settings

View File

@ -0,0 +1,38 @@
<h2 class=my-3>{{ page_title }}</h2>
<article>
<form method=post hx-target=body>
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type=hidden name=Id value="{{ model.id }}">
<div class="container mb-3">
<div class=row>
<div class=col>
<p style="line-height:1.2rem;">
<strong>{{ model.title }}</strong><br>
<small class=text-muted>
<a href="{{ "admin/post/" | append: model.id | append: "/edit" | relative_link }}">
&laquo; Back to Edit Post
</a>
</small>
</div>
</div>
<div class="row mwl-table-heading">
<div class=col>Start</div>
<div class=col>Title</div>
<div class=col>Image?</div>
<div class=col>Location?</div>
</div>
{% for chapter in model.chapters %}
<div class="row pb-3 mwl-table-detail">
<div class=col>{{ chapter.start_time }}</div>
<div class=col>{{ chapter.title }}</div>
<div class=col>{% if chapter.image_url == "" %}N{% else %}Y{% endif %}</div>
<div class=col>{% if chapter.location %}Y{% else %}N{% endif %}</div>
</div>
{% endfor %}
<div class="row pb-3 mwl-table-detail" id=chapter_new>
{% assign new_link = "admin/post/" | append: model.id | append: "/chapter/new" | relative_link %}
<a href="{{ new_link }}" hx-get="{{ new_link }}" hx-target=#chapter_new>Add a New Chapter</a>
</div>
</div>
</form>
</article>