From d9e7c771ba0ba40841c3bd43b6923ff17053be5b Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 2 Aug 2026 17:33:38 -0400 Subject: [PATCH] WIP on person table, podcast/ep assignment (#17) --- src/MyWebLog.Data/Converters.fs | 9 ++ src/MyWebLog.Data/Interfaces.fs | 28 +++++ src/MyWebLog.Data/MyWebLog.Data.fsproj | 2 + src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 4 + .../Postgres/PostgresPersonData.fs | 78 ++++++++++++ src/MyWebLog.Data/PostgresData.fs | 7 ++ src/MyWebLog.Data/RethinkDbData.fs | 73 ++++++++++- src/MyWebLog.Data/SQLite/SQLiteHelpers.fs | 3 + src/MyWebLog.Data/SQLite/SQLitePersonData.fs | 71 +++++++++++ src/MyWebLog.Data/SQLiteData.fs | 15 ++- src/MyWebLog.Domain/DataTypes.fs | 30 +++++ src/MyWebLog.Domain/PodcastTypes.fs | 117 +++++++++++++++++- src/MyWebLog.Domain/SupportTypes.fs | 102 ++------------- src/MyWebLog.Domain/ViewModels.fs | 6 +- src/MyWebLog.Tests/Data/ConvertersTests.fs | 16 +++ src/MyWebLog.Tests/Data/WebLogDataTests.fs | 2 +- src/MyWebLog.Tests/Domain/ViewModelsTests.fs | 5 +- src/MyWebLog/Handlers/Feed.fs | 4 +- 18 files changed, 471 insertions(+), 101 deletions(-) create mode 100644 src/MyWebLog.Data/Postgres/PostgresPersonData.fs create mode 100644 src/MyWebLog.Data/SQLite/SQLitePersonData.fs diff --git a/src/MyWebLog.Data/Converters.fs b/src/MyWebLog.Data/Converters.fs index 4c779c9..d93f8de 100644 --- a/src/MyWebLog.Data/Converters.fs +++ b/src/MyWebLog.Data/Converters.fs @@ -108,6 +108,14 @@ module Json = override _.ReadJson(reader: JsonReader, _: Type, _: PersonGroup, _: bool, _: JsonSerializer) = (string >> PersonGroup.Parse) reader.Value + /// Converter for the type + type PersonIdConverter() = + inherit JsonConverter() + override _.WriteJson(writer: JsonWriter, value: PersonId, _: JsonSerializer) = + writer.WriteValue(string value) + override _.ReadJson(reader: JsonReader, _: Type, _: PersonId, _: bool, _: JsonSerializer) = + (string >> PersonId) reader.Value + /// Converter for the type type PersonRoleConverter() = inherit JsonConverter() @@ -199,6 +207,7 @@ module Json = PageIdConverter() PermalinkConverter() PersonGroupConverter() + PersonIdConverter() PersonRoleConverter() PodcastMediumConverter() PostIdConverter() diff --git a/src/MyWebLog.Data/Interfaces.fs b/src/MyWebLog.Data/Interfaces.fs index 6fd9207..9362212 100644 --- a/src/MyWebLog.Data/Interfaces.fs +++ b/src/MyWebLog.Data/Interfaces.fs @@ -97,6 +97,31 @@ type IPageData = abstract member UpdatePriorPermalinks : PageId -> WebLogId -> Permalink list -> Task +/// Data functions to support manipulating persons +type IPersonData = + + /// Add a person + abstract member Add : Person -> Task + + /// Delete a person + abstract member Delete : PersonId -> WebLogId -> Task + + /// Find all persons by their given assignments + abstract member FindByAssignment : PersonAssignment list -> Task + + /// Find a person by their ID + abstract member FindById : PersonId -> WebLogId -> Task + + /// Find persons for the given web log + abstract member FindByWebLog : WebLogId -> Task + + /// Restore persons from a backup + abstract member Restore : Person list -> Task + + /// Update a person's information + abstract member Update : Person -> Task + + /// Data functions to support manipulating posts type IPostData = @@ -310,6 +335,9 @@ type IData = /// Page data functions abstract member Page : IPageData + /// Person data functions + abstract member Person : IPersonData + /// Post data functions abstract member Post : IPostData diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj index cd13951..61d2b01 100644 --- a/src/MyWebLog.Data/MyWebLog.Data.fsproj +++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj @@ -26,6 +26,7 @@ + @@ -37,6 +38,7 @@ + diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 4972ff3..7c65cf2 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -22,6 +22,10 @@ module Table = [] let PageRevision = "page_revision" + /// Persons + [] + let Person = "person" + /// Posts [] let Post = "post" diff --git a/src/MyWebLog.Data/Postgres/PostgresPersonData.fs b/src/MyWebLog.Data/Postgres/PostgresPersonData.fs new file mode 100644 index 0000000..28ae298 --- /dev/null +++ b/src/MyWebLog.Data/Postgres/PostgresPersonData.fs @@ -0,0 +1,78 @@ +namespace MyWebLog.Data.Postgres + +open BitBadger.Documents +open BitBadger.Documents.Postgres +open Microsoft.Extensions.Logging +open MyWebLog +open MyWebLog.Data +open NodaTime +open Npgsql.FSharp + +/// PostgreSQL myWebLog person data implementation +type PostgresPersonData(log: ILogger) = + + /// Add a person + let add (person : Person) = + log.LogTrace "Person.add" + insert Table.Person person + + /// Find persons for the given assignments + let findByAssignment (assignments: PersonAssignment list) = + log.LogTrace "Person.findByAssignment" + Find.byFieldsOrdered + Table.Person + All + [ Field.In (nameof Person.Empty.Id) (assignments |> List.map _.PersonId) ] + [ Field.Named (nameof Person.Empty.Name) ] + + /// Find a person by their ID for the given web log + let findById (personId: PersonId) (webLogId: WebLogId) = + log.LogTrace "Person.findById" + Find.firstByContains Table.Person {| Id = personId; WebLogId = webLogId |} + + /// Delete a person by their ID for the given web log + let delete personId webLogId = backgroundTask { + log.LogTrace "Person.delete" + match! findById personId webLogId with + | Some _ -> + // TODO: also remove this person from assignments in episodes and podcasts + do! Custom.nonQuery + $"""{Query.delete Table.Person} WHERE {Query.whereById "@id"}""" + [ idParam personId ] + return true + | None -> return false + } + + /// Find all persons for the given web log + let findByWebLog webLogId = + log.LogTrace "Person.findByWebLog" + Find.byContainsOrdered Table.Person (webLogContains webLogId) [ Field.Named (nameof Person.Empty.Name) ] + + + /// Restore persons from a backup + let restore persons = backgroundTask { + log.LogTrace "Person.restore" + let! _ = + Configuration.dataSource () + |> Sql.fromDataSource + |> Sql.executeTransactionAsync + [ Query.insert Table.Person, persons |> List.map (fun person -> [ jsonParam "@data" person ]) ] + () + } + + /// Update a person + let update (person: Person) = backgroundTask { + log.LogTrace "Person.update" + match! findById person.Id person.WebLogId with + | Some _ -> do! Update.byId Table.Person person.Id person + | None -> () + } + + interface IPersonData with + member _.Add person = add person + member _.Delete personId webLogId = delete personId webLogId + member _.FindByAssignment assignments = findByAssignment assignments + member _.FindById personId webLogId = findById personId webLogId + member _.FindByWebLog webLogId = findByWebLog webLogId + member _.Restore persons = restore persons + member _.Update person = update person diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index fa02e1a..5b1d9aa 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -66,6 +66,12 @@ type PostgresData(log: ILogger, ser: JsonSerializer) = revision_text TEXT NOT NULL, PRIMARY KEY (page_id, as_of))" + // Person table + if needsTable Table.Person then + Query.Definition.ensureTable Table.Person + Query.Definition.ensureKey Table.Person PostgreSQL + Query.Definition.ensureDocumentIndex Table.Person Optimized + // Post tables if needsTable Table.Post then Query.Definition.ensureTable Table.Post @@ -268,6 +274,7 @@ type PostgresData(log: ILogger, ser: JsonSerializer) = member _.Category = PostgresCategoryData log member _.Page = PostgresPageData log + member _.Person = PostgresPersonData log member _.Post = PostgresPostData log member _.TagMap = PostgresTagMapData log member _.Theme = PostgresThemeData log diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index 20b48ce..ed793b8 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -23,6 +23,9 @@ module private RethinkHelpers = /// The page table let Page = "Page" + /// The person table + let Person = "Person" + /// The post table let Post = "Post" @@ -45,7 +48,8 @@ module private RethinkHelpers = let WebLogUser = "WebLogUser" /// A list of all tables - let all = [ Category; Comment; DbVersion; Page; Post; TagMap; Theme; ThemeAsset; Upload; WebLog; WebLogUser ] + let all = + [ Category; Comment; DbVersion; Page; Person; Post; TagMap; Theme; ThemeAsset; Upload; WebLog; WebLogUser ] /// Index names for indexes not on a data item's name @@ -615,6 +619,72 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger return false } } + + member _.Person = { + new IPersonData with + + member _.Add person = rethink { + withTable Table.Person + insert person + write; withRetryDefault; ignoreResult conn + } + + member _.Delete personId webLogId = backgroundTask { + // TODO: also remove this person from assignments in episodes and podcasts + let! result = rethink { + withTable Table.Person + getAll [ personId ] + filter (fun row -> row[nameof Person.Empty.WebLogId].Eq webLogId :> obj) + delete + write; withRetryDefault conn + } + return result.Deleted > 0UL + } + + member _.FindByAssignment assignments = rethink { + withTable Table.Person + getAll (assignments |> List.map _.PersonId) + orderBy (nameof Person.Empty.Name) + result; withRetryDefault conn + } + + member _.FindById personId webLogId = + rethink { + withTable Table.Person + getAll [ personId ] + filter (nameof Person.Empty.WebLogId) webLogId + result; withRetryDefault + } + |> tryFirst <| conn + + member _.FindByWebLog webLogId = rethink { + withTable Table.Person + getAll [ webLogId ] (nameof Person.Empty.WebLogId) + orderBy (nameof Person.Empty.Name) + result; withRetryDefault conn + } + + member _.Restore persons = backgroundTask { + for batch in persons |> List.chunkBySize restoreBatchSize do + do! rethink { + withTable Table.Person + insert batch + write; withRetryOnce; ignoreResult conn + } + } + + member this.Update person = backgroundTask { + match! this.FindById person.Id person.WebLogId with + | Some _ -> + do! rethink { + withTable Table.Person + get person.Id + replace person + write; withRetryDefault; ignoreResult conn + } + | None -> () + } + } member _.Post = { new IPostData with @@ -1281,6 +1351,7 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger] let PageRevision = "page_revision" + [] + let Person = "person" + /// Posts [] let Post = "post" diff --git a/src/MyWebLog.Data/SQLite/SQLitePersonData.fs b/src/MyWebLog.Data/SQLite/SQLitePersonData.fs new file mode 100644 index 0000000..04ed271 --- /dev/null +++ b/src/MyWebLog.Data/SQLite/SQLitePersonData.fs @@ -0,0 +1,71 @@ +namespace MyWebLog.Data.SQLite + +open BitBadger.Documents +open BitBadger.Documents.Sqlite +open Microsoft.Data.Sqlite +open Microsoft.Extensions.Logging +open MyWebLog +open MyWebLog.Data + +/// SQLite myWebLog person data implementation +type SQLitePersonData(conn: SqliteConnection, log: ILogger) = + + /// Add a person + let add (person: Person) = + log.LogTrace "Person.add" + conn.insert Table.Person person + + /// Find persons by their assignments + let findByAssignment (assignments: PersonAssignment list) = + log.LogTrace "Person.findByAssignment" + conn.findByFieldsOrdered + Table.Person All + [ Field.In "PersonId" (assignments |> List.map _.PersonId) ] + [ Field.Named (nameof Person.Empty.Name) ] + + /// Find a person by their ID + let findById (personId: PersonId) webLogId = + log.LogTrace "Person.findById" + conn.findFirstByFields Table.Person All [ idField personId; webLogField webLogId ] + + /// Delete a person by their ID + let delete personId webLogId = backgroundTask { + log.LogTrace "Person.delete" + match! findById personId webLogId with + | Some _ -> + // TODO: also remove this person from assignments in episodes and podcasts + do! conn.customNonQuery + $"{Query.byId (Query.delete Table.Person) (string personId)}" + [ idParam personId ] + return true + | None -> return false + } + + /// Find a page by its permalink for the given web log + let findByWebLog webLogId = + log.LogTrace "Person.findByWebLog" + conn.findByFieldsOrdered + Table.Person All [ webLogField webLogId ] [ Field.Named (nameof Person.Empty.Name) ] + + /// Restore persons from a backup + let restore persons = backgroundTask { + log.LogTrace "Person.restore" + for person in persons do do! add person + } + + /// Update a page + let update (person: Person) = backgroundTask { + log.LogTrace "Person.update" + match! findById person.Id person.WebLogId with + | Some _ -> do! conn.updateById Table.Person person.Id person + | None -> () + } + + interface IPersonData with + member _.Add person = add person + member _.Delete personId webLogId = delete personId webLogId + member _.FindByAssignment assignments = findByAssignment assignments + member _.FindById personId webLogId = findById personId webLogId + member _.FindByWebLog webLogId = findByWebLog webLogId + member _.Restore pages = restore pages + member _.Update page = update page diff --git a/src/MyWebLog.Data/SQLiteData.fs b/src/MyWebLog.Data/SQLiteData.fs index 2906ebf..318bac0 100644 --- a/src/MyWebLog.Data/SQLiteData.fs +++ b/src/MyWebLog.Data/SQLiteData.fs @@ -74,6 +74,12 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria revision_text TEXT NOT NULL, PRIMARY KEY (page_id, as_of))" [] + // Person table + if needsTable Table.Person then + log.LogInformation(creatingTable, Table.Person) + do! conn.ensureTable Table.Person + do! conn.ensureFieldIndex Table.Person "web_log" [ nameof Person.Empty.WebLogId ] + // Post tables if needsTable Table.Post then log.LogInformation(creatingTable, Table.Post) @@ -184,7 +190,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria Summary = Map.getString "summary" podcastRdr DisplayedAuthor = Map.getString "displayed_author" podcastRdr Email = Map.getString "email" podcastRdr - ImageUrl = Map.getString "image_url" podcastRdr |> Permalink + ImageUrl = Map.getString "image_url" podcastRdr AppleCategory = Map.getString "apple_category" podcastRdr AppleSubcategory = Map.tryString "apple_subcategory" podcastRdr Explicit = Map.getString "explicit" podcastRdr |> ExplicitRating.Parse @@ -194,7 +200,8 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria FundingUrl = Map.tryString "funding_url" podcastRdr FundingText = Map.tryString "funding_text" podcastRdr Medium = Map.tryString "medium" podcastRdr - |> Option.map PodcastMedium.Parse } + |> Option.map PodcastMedium.Parse + People = None } // TODO: retrieve person / role assignments } |> List.ofSeq podcastRdr.Close() podcasts @@ -231,7 +238,8 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria SeasonNumber = Map.tryInt "season_number" epRdr SeasonDescription = Map.tryString "season_description" epRdr EpisodeNumber = Map.tryString "episode_number" epRdr |> Option.map Double.Parse - EpisodeDescription = Map.tryString "episode_description" epRdr } + EpisodeDescription = Map.tryString "episode_description" epRdr + People = None } // TODO: get person / role assignments } |> List.ofSeq epRdr.Close() episodes @@ -503,6 +511,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger, ser: JsonSeria member _.Category = SQLiteCategoryData (conn, ser, log) member _.Page = SQLitePageData (conn, log) + member _.Person = SQLitePersonData (conn, log) member _.Post = SQLitePostData (conn, log) member _.TagMap = SQLiteTagMapData (conn, log) member _.Theme = SQLiteThemeData (conn, log) diff --git a/src/MyWebLog.Domain/DataTypes.fs b/src/MyWebLog.Domain/DataTypes.fs index bb25151..3972bb7 100644 --- a/src/MyWebLog.Domain/DataTypes.fs +++ b/src/MyWebLog.Domain/DataTypes.fs @@ -143,6 +143,36 @@ type Page = { OpenGraph = None } +/// A person (associated with podcasting) +[] +type Person = { + /// The ID of this person + Id: PersonId + + /// The ID of the web log to which this person belongs + WebLogId: WebLogId + + /// The person's name + Name: string + + /// A URL for more information about this person + /// Non-absolute URLs will be treated as a permalink + Url: string option + + /// A URL with an image / avatar for this person + /// Non-absolute URLs will be treated as a permalink + ImageUrl: string option +} with + + /// An empty person entry + static member Empty = + { Id = PersonId.Empty + WebLogId = WebLogId.Empty + Name = "" + Url = None + ImageUrl = None } + + /// A web log post [] type Post = { diff --git a/src/MyWebLog.Domain/PodcastTypes.fs b/src/MyWebLog.Domain/PodcastTypes.fs index 252be03..68dd9e8 100644 --- a/src/MyWebLog.Domain/PodcastTypes.fs +++ b/src/MyWebLog.Domain/PodcastTypes.fs @@ -402,6 +402,37 @@ type PersonRole = PersonRole.GroupXref |> List.find (fun it -> fst it = this) |> snd +open System + +/// An identifier for a person +[] +type PersonId = + | PersonId of string + + /// An empty person ID + static member Empty = PersonId "" + + /// Create a new person ID + /// A new person ID + static member Create = + Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-')[..21] |> PersonId + + /// + override this.ToString() = + match this with PersonId it -> it + + +/// The combination of a person and a role +[] +type PersonAssignment = { + /// The ID of the person to whom this assignment applies + PersonId: PersonId + + /// The role to which this person is assigned + Role: PersonRole +} + + open NodaTime.Text /// A podcast episode @@ -462,6 +493,9 @@ type Episode = { /// A description of the episode EpisodeDescription: string option + + /// The people and roles for this episode + People: PersonAssignment list option } with /// An empty episode @@ -484,7 +518,8 @@ type Episode = { SeasonNumber = None SeasonDescription = None EpisodeNumber = None - EpisodeDescription = None } + EpisodeDescription = None + People = None } /// Format a duration for an episode /// A duration formatted in hours, minutes, and seconds @@ -529,3 +564,83 @@ type PodcastMedium = | Newsletter -> "newsletter" | Blog -> "blog" + +/// Options for a feed that describes a podcast +[] +type PodcastOptions = { + /// The title of the podcast + Title: string + + /// A subtitle for the podcast + Subtitle: string option + + /// The number of items in the podcast feed + ItemsInFeed: int + + /// A summary of the podcast (iTunes field) + Summary: string + + /// The display name of the podcast author (iTunes field) + DisplayedAuthor: string + + /// The e-mail address of the user who registered the podcast at iTunes + Email: string + + /// The link to the image for the podcast + /// Non-absolute URLs will be treated as a permalink + ImageUrl: string + + /// The category from Apple Podcasts (iTunes) under which this podcast is categorized + AppleCategory: string + + /// + /// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values) + /// + AppleSubcategory: string option + + /// The explictness rating (iTunes field) + Explicit: ExplicitRating + + /// The default media type for files in this podcast + DefaultMediaType: string option + + /// + /// The base URL for relative URL media files for this podcast (optional; defaults to web log base) + /// + MediaBaseUrl: string option + + /// A GUID for this podcast + PodcastGuid: Guid option + + /// A URL at which information on supporting the podcast may be found (supports permalinks) + FundingUrl: string option + + /// The text to be displayed in the funding item within the feed + FundingText: string option + + /// The medium (what the podcast IS, not what it is ABOUT) + Medium: PodcastMedium option + + /// The people and roles for the podcast overall + People: PersonAssignment list option +} with + + /// A default set of podcast options + static member Empty = + { Title = "" + Subtitle = None + ItemsInFeed = 0 + Summary = "" + DisplayedAuthor = "" + Email = "" + ImageUrl = "" + AppleCategory = "" + AppleSubcategory = None + Explicit = No + DefaultMediaType = None + MediaBaseUrl = None + PodcastGuid = None + FundingUrl = None + FundingText = None + Medium = None + People = None } diff --git a/src/MyWebLog.Domain/SupportTypes.fs b/src/MyWebLog.Domain/SupportTypes.fs index 39f7a4a..84da758 100644 --- a/src/MyWebLog.Domain/SupportTypes.fs +++ b/src/MyWebLog.Domain/SupportTypes.fs @@ -552,19 +552,6 @@ type OpenGraphProperties = { } -/// A permanent link -[] -type Permalink = - | Permalink of string - - /// An empty permalink - static member Empty = Permalink "" - - /// - override this.ToString() = - match this with Permalink it -> it - - /// An identifier for a page [] type PageId = @@ -583,6 +570,19 @@ type PageId = match this with PageId it -> it +/// A permanent link +[] +type Permalink = + | Permalink of string + + /// An empty permalink + static member Empty = Permalink "" + + /// + override this.ToString() = + match this with Permalink it -> it + + /// Statuses for posts [] type PostStatus = @@ -683,82 +683,6 @@ type CustomFeedSource = match this with | Category (CategoryId catId) -> $"category:{catId}" | Tag tag -> $"tag:{tag}" -/// Options for a feed that describes a podcast -[] -type PodcastOptions = { - /// The title of the podcast - Title: string - - /// A subtitle for the podcast - Subtitle: string option - - /// The number of items in the podcast feed - ItemsInFeed: int - - /// A summary of the podcast (iTunes field) - Summary: string - - /// The display name of the podcast author (iTunes field) - DisplayedAuthor: string - - /// The e-mail address of the user who registered the podcast at iTunes - Email: string - - /// The link to the image for the podcast - ImageUrl: Permalink - - /// The category from Apple Podcasts (iTunes) under which this podcast is categorized - AppleCategory: string - - /// - /// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values) - /// - AppleSubcategory: string option - - /// The explictness rating (iTunes field) - Explicit: ExplicitRating - - /// The default media type for files in this podcast - DefaultMediaType: string option - - /// - /// The base URL for relative URL media files for this podcast (optional; defaults to web log base) - /// - MediaBaseUrl: string option - - /// A GUID for this podcast - PodcastGuid: Guid option - - /// A URL at which information on supporting the podcast may be found (supports permalinks) - FundingUrl: string option - - /// The text to be displayed in the funding item within the feed - FundingText: string option - - /// The medium (what the podcast IS, not what it is ABOUT) - Medium: PodcastMedium option -} with - - /// A default set of podcast options - static member Empty = - { Title = "" - Subtitle = None - ItemsInFeed = 0 - Summary = "" - DisplayedAuthor = "" - Email = "" - ImageUrl = Permalink.Empty - AppleCategory = "" - AppleSubcategory = None - Explicit = No - DefaultMediaType = None - MediaBaseUrl = None - PodcastGuid = None - FundingUrl = None - FundingText = None - Medium = None } - - /// A custom feed [] type CustomFeed = { diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index c94f899..8f4ea05 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -684,7 +684,7 @@ type EditCustomFeedModel = { Summary = this.Summary DisplayedAuthor = this.DisplayedAuthor Email = this.Email - ImageUrl = Permalink this.ImageUrl + ImageUrl = this.ImageUrl AppleCategory = this.AppleCategory AppleSubcategory = noneIfBlank this.AppleSubcategory Explicit = ExplicitRating.Parse this.Explicit @@ -693,7 +693,8 @@ type EditCustomFeedModel = { PodcastGuid = noneIfBlank this.PodcastGuid |> Option.map Guid.Parse FundingUrl = noneIfBlank this.FundingUrl FundingText = noneIfBlank this.FundingText - Medium = noneIfBlank this.Medium |> Option.map PodcastMedium.Parse } + Medium = noneIfBlank this.Medium |> Option.map PodcastMedium.Parse + People = None } // TODO: add this to UI or handle differently else None } @@ -974,6 +975,7 @@ type EditPostModel() = | Some it -> Some (double it) | None -> None EpisodeDescription = noneIfBlank this.EpisodeDescription + People = None // TODO: add this to UI or handle differently } else None } diff --git a/src/MyWebLog.Tests/Data/ConvertersTests.fs b/src/MyWebLog.Tests/Data/ConvertersTests.fs index 274d749..6283a3c 100644 --- a/src/MyWebLog.Tests/Data/ConvertersTests.fs +++ b/src/MyWebLog.Tests/Data/ConvertersTests.fs @@ -178,6 +178,20 @@ let personGroupConverterTests = testList "PersonGroupConverter" [ } ] +/// Unit tests for the PersonIdConverter type +let personIdConverterTests = testList "PersonIdConverter" [ + let opts = JsonSerializerSettings() + opts.Converters.Add(PersonIdConverter()) + test "succeeds when serializing" { + let after = JsonConvert.SerializeObject(PersonId "person-x", opts) + Expect.equal after "\"person-x\"" "Person ID serialized incorrectly" + } + test "succeeds when deserializing" { + let after = JsonConvert.DeserializeObject("\"masked\"", opts) + Expect.equal after (PersonId "masked") "Person ID deserialized incorrectly" + } +] + /// Unit tests for the PersonRoleConverter type let personRoleConverterTests = testList "PersonRoleConverter" [ let opts = JsonSerializerSettings() @@ -322,6 +336,7 @@ let configureTests = test "Json.configure succeeds" { Expect.hasCountOf ser.Converters 1u (has typeof) "Page ID converter not found" Expect.hasCountOf ser.Converters 1u (has typeof) "Permalink converter not found" Expect.hasCountOf ser.Converters 1u (has typeof) "PersonGroup converter not found" + Expect.hasCountOf ser.Converters 1u (has typeof) "Person ID converter not found" Expect.hasCountOf ser.Converters 1u (has typeof) "PersonRole converter not found" Expect.hasCountOf ser.Converters 1u (has typeof) "Podcast medium converter not found" Expect.hasCountOf ser.Converters 1u (has typeof) "Post ID converter not found" @@ -351,6 +366,7 @@ let all = testList "Converters" [ pageIdConverterTests permalinkConverterTests personGroupConverterTests + personIdConverterTests personRoleConverterTests podcastMediumConverterTests postIdConverterTests diff --git a/src/MyWebLog.Tests/Data/WebLogDataTests.fs b/src/MyWebLog.Tests/Data/WebLogDataTests.fs index aca500e..3dcbd85 100644 --- a/src/MyWebLog.Tests/Data/WebLogDataTests.fs +++ b/src/MyWebLog.Tests/Data/WebLogDataTests.fs @@ -112,7 +112,7 @@ let ``FindById succeeds when a web log is found`` (data: IData) = task { Expect.equal pod.Summary "All things that happen in the domain root" "Podcast summary is incorrect" Expect.equal pod.DisplayedAuthor "Podcaster Extraordinaire" "Podcast author is incorrect" Expect.equal pod.Email "podcaster@example.com" "Podcast e-mail is incorrect" - Expect.equal pod.ImageUrl (Permalink "images/cover-art.png") "Podcast image URL is incorrect" + Expect.equal pod.ImageUrl "images/cover-art.png" "Podcast image URL is incorrect" Expect.equal pod.AppleCategory "Fiction" "Podcast Apple category is incorrect" Expect.equal pod.AppleSubcategory (Some "Drama") "Podcast Apple subcategory is incorrect" Expect.equal pod.Explicit No "Podcast explicit rating is incorrect" diff --git a/src/MyWebLog.Tests/Domain/ViewModelsTests.fs b/src/MyWebLog.Tests/Domain/ViewModelsTests.fs index 3608040..870ed93 100644 --- a/src/MyWebLog.Tests/Domain/ViewModelsTests.fs +++ b/src/MyWebLog.Tests/Domain/ViewModelsTests.fs @@ -282,7 +282,8 @@ let testFullPost = SeasonNumber = Some 3 SeasonDescription = Some "Season Three" EpisodeNumber = Some 322. - EpisodeDescription = Some "Episode 322" } } + EpisodeDescription = Some "Episode 322" + People = None } } // TODO: add some people /// Unit tests for the EditCommonModel type let editCommonModelTests = testList "EditCommonModel" [ @@ -524,7 +525,7 @@ let editCustomFeedModelTests = testList "EditCustomFeedModel" [ Summary = "As little as possible" DisplayedAuthor = "The Tester" Email = "thetester@example.com" - ImageUrl = Permalink "upload/my-image.png" + ImageUrl = "upload/my-image.png" AppleCategory = "News" Explicit = Clean } // A GUID with all zeroes, ending in "a" diff --git a/src/MyWebLog/Handlers/Feed.fs b/src/MyWebLog/Handlers/Feed.fs index aa08e52..8383dff 100644 --- a/src/MyWebLog/Handlers/Feed.fs +++ b/src/MyWebLog/Handlers/Feed.fs @@ -253,8 +253,8 @@ let private addPodcast (webLog: WebLog) (rssFeed: SyndicationFeed) (feed: Custom let feedUrl = webLog.AbsoluteUrl feed.Path let imageUrl = match podcast.ImageUrl with - | Permalink link when link.StartsWith "http" -> link - | Permalink _ -> webLog.AbsoluteUrl podcast.ImageUrl + | link when link.StartsWith "http" -> link + | _ -> webLog.AbsoluteUrl (Permalink podcast.ImageUrl) let xmlDoc = XmlDocument()