diff --git a/build.fsx b/build.fsx
index 3aa6501..c3dfbbd 100644
--- a/build.fsx
+++ b/build.fsx
@@ -36,7 +36,7 @@ let zipTheme (name : string) (_ : TargetParameter) =
!! $"{path}/**/*"
|> Zip.filesAsSpecs path //$"src/{name}-theme"
|> Seq.filter (fun (_, name) -> not (name.EndsWith ".zip"))
- |> Zip.zipSpec $"{releasePath}/{name}.zip"
+ |> Zip.zipSpec $"{releasePath}/{name}-theme.zip"
/// Publish the project for the given runtime ID
let publishFor rid (_ : TargetParameter) =
@@ -45,11 +45,12 @@ let publishFor rid (_ : TargetParameter) =
/// Package published output for the given runtime ID
let packageFor (rid : string) (_ : TargetParameter) =
let path = $"{projectPath}/bin/Release/net6.0/{rid}/publish"
+ let prodSettings = $"{path}/appsettings.Production.json"
+ if File.exists prodSettings then File.delete prodSettings
[ !! $"{path}/**/*"
|> Zip.filesAsSpecs path
- |> Zip.moveToFolder "app"
- Seq.singleton ($"{releasePath}/admin.zip", "admin.zip")
- Seq.singleton ($"{releasePath}/default.zip", "default.zip")
+ Seq.singleton ($"{releasePath}/admin-theme.zip", "admin-theme.zip")
+ Seq.singleton ($"{releasePath}/default-theme.zip", "default-theme.zip")
]
|> Seq.concat
|> Zip.zipSpec $"{releasePath}/myWebLog-{version}.{rid}.zip"
@@ -86,7 +87,7 @@ Target.create "RepackageLinux" (fun _ ->
Shell.mkdir workDir
Zip.unzip workDir zipArchive
Shell.cd workDir
- sh "chmod" [ "+x"; "app/MyWebLog" ]
+ sh "chmod" [ "+x"; "./MyWebLog" ]
sh "tar" [ "cfj"; $"../myWebLog-{version}.linux-x64.tar.bz2"; "." ]
Shell.cd "../.."
Shell.rm zipArchive
@@ -96,8 +97,8 @@ Target.create "RepackageLinux" (fun _ ->
Target.create "All" ignore
Target.create "RemoveThemeArchives" (fun _ ->
- Shell.rm $"{releasePath}/admin.zip"
- Shell.rm $"{releasePath}/default.zip"
+ Shell.rm $"{releasePath}/admin-theme.zip"
+ Shell.rm $"{releasePath}/default-theme.zip"
)
Target.create "CI" ignore
diff --git a/rethink-case-fix.js b/rethink-case-fix.js
deleted file mode 100644
index 7c62dac..0000000
--- a/rethink-case-fix.js
+++ /dev/null
@@ -1,171 +0,0 @@
-
-// Category
-r.db('myWebLog').table('Category').map({
- Description: r.row('description'),
- Id: r.row('id'),
- Name: r.row('name'),
- ParentId: r.row('parentId'),
- Slug: r.row('slug'),
- WebLogId: r.row('webLogId')
-})
-
-// Page
-r.db('myWebLog').table('Page').map({
- AuthorId: r.row('authorId'),
- Id: r.row('id'),
- Metadata: r.row('metadata').map(function (meta) {
- return { Name: meta('name'), Value: meta('value') }
- }),
- Permalink: r.row('permalink'),
- PriorPermalinks: r.row('priorPermalinks'),
- PublishedOn: r.row('publishedOn'),
- Revisions: r.row('revisions').map(function (rev) {
- return {
- AsOf: rev('asOf'),
- Text: rev('text')
- }
- }),
- IsInPageList: r.row('showInPageList'),
- Template: r.row('template'),
- Text: r.row('text'),
- Title: r.row('title'),
- UpdatedOn: r.row('updatedOn'),
- WebLogId: r.row('webLogId')
-})
-
-// Post
-r.db('myWebLog').table('Post').map({
- AuthorId: r.row('authorId'),
- CategoryIds: r.row('categoryIds'),
- Episode: r.branch(r.row.hasFields('episode'), {
- Duration: r.row('episode')('duration'),
- Length: r.row('episode')('length'),
- Media: r.row('episode')('media'),
- MediaType: r.row('episode')('mediaType').default(null),
- ImageUrl: r.row('episode')('imageUrl').default(null),
- Subtitle: r.row('episode')('subtitle').default(null),
- Explicit: r.row('episode')('explicit').default(null),
- ChapterFile: r.row('episode')('chapterFile').default(null),
- ChapterType: r.row('episode')('chapterType').default(null),
- TranscriptUrl: r.row('episode')('transcriptUrl').default(null),
- TranscriptType: r.row('episode')('transcriptType').default(null),
- TranscriptLang: r.row('episode')('transcriptLang').default(null),
- TranscriptCaptions: r.row('episode')('transcriptCaptions').default(null),
- SeasonNumber: r.row('episode')('seasonNumber').default(null),
- SeasonDescription: r.row('episode')('seasonDescription').default(null),
- EpisodeNumber: r.row('episode')('episodeNumber').default(null),
- EpisodeDescription: r.row('episode')('episodeDescription').default(null)
- }, null),
- Id: r.row('id'),
- Metadata: r.row('metadata').map(function (meta) {
- return { Name: meta('name'), Value: meta('value') }
- }),
- Permalink: r.row('permalink'),
- PriorPermalinks: r.row('priorPermalinks'),
- PublishedOn: r.row('publishedOn'),
- Revisions: r.row('revisions').map(function (rev) {
- return {
- AsOf: rev('asOf'),
- Text: rev('text')
- }
- }),
- Status: r.row('status'),
- Tags: r.row('tags'),
- Template: r.row('template').default(null),
- Text: r.row('text'),
- Title: r.row('title'),
- UpdatedOn: r.row('updatedOn'),
- WebLogId: r.row('webLogId')
-})
-
-// TagMap
-r.db('myWebLog').table('TagMap').map({
- Id: r.row('id'),
- Tag: r.row('tag'),
- UrlValue: r.row('urlValue'),
- WebLogId: r.row('webLogId')
-})
-
-// Theme
-r.db('myWebLog').table('Theme').map({
- Id: r.row('id'),
- Name: r.row('name'),
- Templates: r.row('templates').map(function (tmpl) {
- return {
- Name: tmpl('name'),
- Text: tmpl('text')
- }
- }),
- Version: r.row('version')
-})
-
-// ThemeAsset
-r.db('myWebLog').table('ThemeAsset').map({
- Data: r.row('data'),
- Id: r.row('id'),
- UpdatedOn: r.row('updatedOn')
-})
-
-// WebLog
-r.db('myWebLog').table('WebLog').map(
- { AutoHtmx: r.row('autoHtmx'),
- DefaultPage: r.row('defaultPage'),
- Id: r.row('id'),
- Name: r.row('name'),
- PostsPerPage: r.row('postsPerPage'),
- Rss: {
- IsCategoryEnabled: r.row('rss')('categoryEnabled'),
- Copyright: r.row('rss')('copyright'),
- CustomFeeds: r.row('rss')('customFeeds').map(function (feed) {
- return {
- Id: feed('id'),
- Path: feed('path'),
- Podcast: {
- DefaultMediaType: feed('podcast')('defaultMediaType'),
- DisplayedAuthor: feed('podcast')('displayedAuthor'),
- Email: feed('podcast')('email'),
- Explicit: feed('podcast')('explicit'),
- FundingText: feed('podcast')('fundingText'),
- FundingUrl: feed('podcast')('fundingUrl'),
- PodcastGuid: feed('podcast')('guid'),
- AppleCategory: feed('podcast')('iTunesCategory'),
- AppleSubcategory: feed('podcast')('iTunesSubcategory'),
- ImageUrl: feed('podcast')('imageUrl'),
- ItemsInFeed: feed('podcast')('itemsInFeed'),
- MediaBaseUrl: feed('podcast')('mediaBaseUrl'),
- Medium: feed('podcast')('medium'),
- Subtitle: feed('podcast')('subtitle'),
- Summary: feed('podcast')('summary'),
- Title: feed('podcast')('title')
- },
- Source: feed('source')
- }
- }),
- IsFeedEnabled: r.row('rss')('feedEnabled'),
- FeedName: r.row('rss')('feedName'),
- ItemsInFeed: r.row('rss')('itemsInFeed'),
- IsTagEnabled: r.row('rss')('tagEnabled')
- },
- Slug: r.row('slug'),
- Subtitle: r.row('subtitle'),
- ThemeId: r.row('themePath'),
- TimeZone: r.row('timeZone'),
- Uploads: r.row('uploads'),
- UrlBase: r.row('urlBase')
- })
-
-// WebLogUser
-r.db('myWebLog').table('WebLogUser').map({
- AccessLevel: r.row('authorizationLevel'),
- FirstName: r.row('firstName'),
- Id: r.row('id'),
- LastName: r.row('lastName'),
- PasswordHash: r.row('passwordHash'),
- PreferredName: r.row('preferredName'),
- Salt: r.row('salt'),
- Url: r.row('url'),
- Email: r.row('userName'),
- WebLogId: r.row('webLogId'),
- CreatedOn: r.branch(r.row.hasFields('createdOn'), r.row('createdOn'), r.expr(new Date(0))),
- LastSeenOn: r.row('lastSeenOn').default(null)
-})
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..b50ea6d
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+ net6.0
+ embedded
+ 2.0.0.0
+ 2.0.0.0
+ 2.0.0
+ rc1
+
+
diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj
index e489f9e..558c1cf 100644
--- a/src/MyWebLog.Data/MyWebLog.Data.fsproj
+++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj
@@ -1,11 +1,5 @@
-
- net6.0
- true
- embedded
-
-
diff --git a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj
index 5d8c3e0..3414816 100644
--- a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj
+++ b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj
@@ -1,11 +1,5 @@
-
- net6.0
- true
- embedded
-
-
diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs
index e5167ce..b27d551 100644
--- a/src/MyWebLog/Handlers/Admin.fs
+++ b/src/MyWebLog/Handlers/Admin.fs
@@ -244,7 +244,10 @@ let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundT
/// Get the theme name from the file name given
let getThemeName (fileName : string) =
let themeName = fileName.Split(".").[0].ToLowerInvariant().Replace (" ", "-")
- if Regex.IsMatch (themeName, """^[a-z0-9\-]+$""") then Ok themeName else Error $"Theme name {fileName} is invalid"
+ if themeName.EndsWith "-theme" then
+ if Regex.IsMatch (themeName, """^[a-z0-9\-]+$""") then Ok (themeName.Substring (0, themeName.Length - 6))
+ else Error $"Theme name {fileName} is invalid"
+ else Error "Theme .zip file name must end in \"-theme.zip\""
/// Load a theme from the given stream, which should contain a ZIP archive
let loadThemeFromZip themeName file clean (data : IData) = backgroundTask {
@@ -260,6 +263,8 @@ let loadThemeFromZip themeName file clean (data : IData) = backgroundTask {
let! theme = updateTemplates theme zip
do! data.Theme.Save theme
do! updateAssets themeId zip data
+
+ return theme
}
// POST /admin/theme/update
@@ -271,7 +276,7 @@ let updateTheme : HttpHandler = requireAccess Administrator >=> fun next ctx ->
let data = ctx.Data
use stream = new MemoryStream ()
do! themeFile.CopyToAsync stream
- do! loadThemeFromZip themeName stream true data
+ let! _ = loadThemeFromZip themeName stream true data
do! ThemeAssetCache.refreshTheme (ThemeId themeName) data
TemplateCache.invalidateTheme themeName
do! addMessage ctx { UserMessage.success with Message = "Theme updated successfully" }
diff --git a/src/MyWebLog/Handlers/User.fs b/src/MyWebLog/Handlers/User.fs
index 828fab5..9b193b5 100644
--- a/src/MyWebLog/Handlers/User.fs
+++ b/src/MyWebLog/Handlers/User.fs
@@ -128,43 +128,6 @@ let edit usrId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> tas
| None -> return! Error.notFound next ctx
}
-// POST /admin/user/save
-let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
- let! model = ctx.BindFormAsync ()
- let data = ctx.Data
- let tryUser =
- if model.IsNew then
- { WebLogUser.empty with
- Id = WebLogUserId.create ()
- WebLogId = ctx.WebLog.Id
- CreatedOn = DateTime.UtcNow
- } |> someTask
- else data.WebLogUser.FindById (WebLogUserId model.Id) ctx.WebLog.Id
- match! tryUser with
- | Some user when model.Password = model.PasswordConfirm ->
- let updatedUser = model.UpdateUser user
- if updatedUser.AccessLevel = Administrator && not (ctx.HasAccessLevel Administrator) then
- return! goAway next ctx
- else
- let updatedUser =
- if model.Password = "" then updatedUser
- else
- let salt = Guid.NewGuid ()
- { updatedUser with PasswordHash = hashedPassword model.Password model.Email salt; Salt = salt }
- do! (if model.IsNew then data.WebLogUser.Add else data.WebLogUser.Update) updatedUser
- do! addMessage ctx
- { UserMessage.success with
- Message = $"""{if model.IsNew then "Add" else "Updat"}ed user successfully"""
- }
- return! bare next ctx
- | Some _ ->
- do! addMessage ctx { UserMessage.error with Message = "The passwords did not match; nothing saved" }
- return!
- (withHxRetarget $"#user_{model.Id}" >=> showEdit { model with Password = ""; PasswordConfirm = "" })
- next ctx
- | None -> return! Error.notFound next ctx
-}
-
// POST /admin/user/{id}/delete
let delete userId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data
@@ -237,3 +200,44 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
return! showMyInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user next ctx
| None -> return! Error.notFound next ctx
}
+
+// User save is not statically compilable; not sure why, but we'll revisit it at some point
+#nowarn "3511"
+
+// POST /admin/user/save
+let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
+ let! model = ctx.BindFormAsync ()
+ let data = ctx.Data
+ let tryUser =
+ if model.IsNew then
+ { WebLogUser.empty with
+ Id = WebLogUserId.create ()
+ WebLogId = ctx.WebLog.Id
+ CreatedOn = DateTime.UtcNow
+ } |> someTask
+ else data.WebLogUser.FindById (WebLogUserId model.Id) ctx.WebLog.Id
+ match! tryUser with
+ | Some user when model.Password = model.PasswordConfirm ->
+ let updatedUser = model.UpdateUser user
+ if updatedUser.AccessLevel = Administrator && not (ctx.HasAccessLevel Administrator) then
+ return! goAway next ctx
+ else
+ let toUpdate =
+ if model.Password = "" then updatedUser
+ else
+ let salt = Guid.NewGuid ()
+ { updatedUser with PasswordHash = hashedPassword model.Password model.Email salt; Salt = salt }
+ do! (if model.IsNew then data.WebLogUser.Add else data.WebLogUser.Update) toUpdate
+ do! addMessage ctx
+ { UserMessage.success with
+ Message = $"""{if model.IsNew then "Add" else "Updat"}ed user successfully"""
+ }
+ return! bare next ctx
+ | Some _ ->
+ do! addMessage ctx { UserMessage.error with Message = "The passwords did not match; nothing saved" }
+ return!
+ (withHxRetarget $"#user_{model.Id}" >=> showEdit { model with Password = ""; PasswordConfirm = "" })
+ next ctx
+ | None -> return! Error.notFound next ctx
+}
+
diff --git a/src/MyWebLog/Maintenance.fs b/src/MyWebLog/Maintenance.fs
index 6fe7f57..bbbeede 100644
--- a/src/MyWebLog/Maintenance.fs
+++ b/src/MyWebLog/Maintenance.fs
@@ -128,6 +128,8 @@ let importLinks args sp = task {
// Loading a theme and restoring a backup are not statically compilable; this is OK
#nowarn "3511"
+open Microsoft.Extensions.Logging
+
/// Load a theme from the given ZIP file
let loadTheme (args : string[]) (sp : IServiceProvider) = task {
if args.Length > 1 then
@@ -142,8 +144,10 @@ let loadTheme (args : string[]) (sp : IServiceProvider) = task {
use stream = File.Open (args[1], FileMode.Open)
use copy = new MemoryStream ()
do! stream.CopyToAsync copy
- do! Handlers.Admin.loadThemeFromZip themeName copy clean data
- printfn $"Theme {themeName} loaded successfully"
+ let! theme = Handlers.Admin.loadThemeFromZip themeName copy clean data
+ let fac = sp.GetRequiredService ()
+ let log = fac.CreateLogger "MyWebLog.Themes"
+ log.LogInformation $"{theme.Name} v{theme.Version} ({ThemeId.toString theme.Id}) loaded"
| Error message -> eprintfn $"{message}"
else
eprintfn "Usage: MyWebLog load-theme [theme-zip-file-name] [*clean-load]"
diff --git a/src/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj
index bdd7230..1473d53 100644
--- a/src/MyWebLog/MyWebLog.fsproj
+++ b/src/MyWebLog/MyWebLog.fsproj
@@ -2,11 +2,8 @@
Exe
- net6.0
true
false
- embedded
- 3391
diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs
index d60d9f4..5eca40c 100644
--- a/src/MyWebLog/Program.fs
+++ b/src/MyWebLog/Program.fs
@@ -82,6 +82,7 @@ let showHelp () =
Task.FromResult ()
+open System.IO
open Giraffe
open Giraffe.EndpointRouting
open Microsoft.AspNetCore.Authentication.Cookies
@@ -135,7 +136,7 @@ let rec main args =
|> ignore
builder.Services.AddScoped () |> ignore
// Use SQLite for caching as well
- let cachePath = Option.ofObj (cfg.GetConnectionString "SQLiteCachePath") |> Option.defaultValue "./session.db"
+ let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db"
builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath) |> ignore
| _ -> ()
@@ -162,7 +163,11 @@ let rec main args =
| Some it ->
printfn $"""Unrecognized command "{it}" - valid commands are:"""
showHelp ()
- | None ->
+ | None -> task {
+ // Load all themes in the application directory
+ for themeFile in Directory.EnumerateFiles (".", "*-theme.zip") do
+ do! Maintenance.loadTheme [| ""; themeFile |] app.Services
+
let _ = app.UseForwardedHeaders ()
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
let _ = app.UseMiddleware ()
@@ -172,7 +177,8 @@ let rec main args =
let _ = app.UseSession ()
let _ = app.UseGiraffe Handlers.Routes.endpoint
- Task.FromResult (app.Run ())
+ app.Run ()
+ }
|> Async.AwaitTask |> Async.RunSynchronously
0 // Exit code
diff --git a/src/MyWebLog/appsettings.json b/src/MyWebLog/appsettings.json
index ed42558..6c0b98c 100644
--- a/src/MyWebLog/appsettings.json
+++ b/src/MyWebLog/appsettings.json
@@ -1,5 +1,5 @@
{
- "Generator": "myWebLog 2.0-beta05",
+ "Generator": "myWebLog 2.0-rc1",
"Logging": {
"LogLevel": {
"MyWebLog.Handlers": "Information"
diff --git a/src/admin-theme/version.txt b/src/admin-theme/version.txt
index 72b667e..18c98a2 100644
--- a/src/admin-theme/version.txt
+++ b/src/admin-theme/version.txt
@@ -1,2 +1,2 @@
myWebLog Admin
-2.0.0-beta05
\ No newline at end of file
+2.0.0-rc1
\ No newline at end of file
diff --git a/src/default-theme/index.liquid b/src/default-theme/index.liquid
index f59b409..dac2eb5 100644
--- a/src/default-theme/index.liquid
+++ b/src/default-theme/index.liquid
@@ -1,10 +1,7 @@
{%- if is_category or is_tag %}
{{ page_title }}
- {%- if is_category %}
- {%- assign cat = categories | where: "slug", slug | first -%}
- {%- if cat.description %}{{ cat.description.value }}
{% endif -%}
- {%- endif %}
-{%- endif %}
+ {%- if subtitle %}{{ subtitle }}
{% endif -%}
+{% endif %}
{% for post in model.posts %}
@@ -27,7 +24,7 @@
{%- if category_count > 0 -%}
Categorized under:
{% for cat in post.category_ids -%}
- {%- assign this_cat = categories | where: "id", cat | first -%}
+ {%- assign this_cat = categories | where: "Id", cat | first -%}
{{ this_cat.name }}{% unless forloop.last %}, {% endunless %}
{%- assign cat_names = this_cat.name | concat: cat_names -%}
{%- endfor -%}
diff --git a/src/default-theme/single-post.liquid b/src/default-theme/single-post.liquid
index 45c98e6..77ef861 100644
--- a/src/default-theme/single-post.liquid
+++ b/src/default-theme/single-post.liquid
@@ -20,7 +20,7 @@