WIP on person table, podcast/ep assignment (#17)

This commit is contained in:
2026-08-02 17:33:38 -04:00
parent 05a810dfc6
commit d9e7c771ba
18 changed files with 471 additions and 101 deletions
+9
View File
@@ -108,6 +108,14 @@ module Json =
override _.ReadJson(reader: JsonReader, _: Type, _: PersonGroup, _: bool, _: JsonSerializer) =
(string >> PersonGroup.Parse) reader.Value
/// <summary>Converter for the <see cref="PersonId" /> type</summary>
type PersonIdConverter() =
inherit JsonConverter<PersonId>()
override _.WriteJson(writer: JsonWriter, value: PersonId, _: JsonSerializer) =
writer.WriteValue(string value)
override _.ReadJson(reader: JsonReader, _: Type, _: PersonId, _: bool, _: JsonSerializer) =
(string >> PersonId) reader.Value
/// <summary>Converter for the <see cref="PersonRole" /> type</summary>
type PersonRoleConverter() =
inherit JsonConverter<PersonRole>()
@@ -199,6 +207,7 @@ module Json =
PageIdConverter()
PermalinkConverter()
PersonGroupConverter()
PersonIdConverter()
PersonRoleConverter()
PodcastMediumConverter()
PostIdConverter()
+28
View File
@@ -97,6 +97,31 @@ type IPageData =
abstract member UpdatePriorPermalinks : PageId -> WebLogId -> Permalink list -> Task<bool>
/// Data functions to support manipulating persons
type IPersonData =
/// Add a person
abstract member Add : Person -> Task<unit>
/// Delete a person
abstract member Delete : PersonId -> WebLogId -> Task<bool>
/// Find all persons by their given assignments
abstract member FindByAssignment : PersonAssignment list -> Task<Person list>
/// Find a person by their ID
abstract member FindById : PersonId -> WebLogId -> Task<Person option>
/// Find persons for the given web log
abstract member FindByWebLog : WebLogId -> Task<Person list>
/// Restore persons from a backup
abstract member Restore : Person list -> Task<unit>
/// Update a person's information
abstract member Update : Person -> Task<unit>
/// 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
+2
View File
@@ -26,6 +26,7 @@
<Compile Include="SQLite\SQLiteHelpers.fs" />
<Compile Include="SQLite\SQLiteCategoryData.fs" />
<Compile Include="SQLite\SQLitePageData.fs" />
<Compile Include="SQLite\SQLitePersonData.fs" />
<Compile Include="SQLite\SQLitePostData.fs" />
<Compile Include="SQLite\SQLiteTagMapData.fs" />
<Compile Include="SQLite\SQLiteThemeData.fs" />
@@ -37,6 +38,7 @@
<Compile Include="Postgres\PostgresCache.fs" />
<Compile Include="Postgres\PostgresCategoryData.fs" />
<Compile Include="Postgres\PostgresPageData.fs" />
<Compile Include="Postgres\PostgresPersonData.fs" />
<Compile Include="Postgres\PostgresPostData.fs" />
<Compile Include="Postgres\PostgresTagMapData.fs" />
<Compile Include="Postgres\PostgresThemeData.fs" />
@@ -22,6 +22,10 @@ module Table =
[<Literal>]
let PageRevision = "page_revision"
/// Persons
[<Literal>]
let Person = "person"
/// Posts
[<Literal>]
let Post = "post"
@@ -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<Person>
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<Person> 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<Person> 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
+7
View File
@@ -66,6 +66,12 @@ type PostgresData(log: ILogger<PostgresData>, 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<PostgresData>, 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
+72 -1
View File
@@ -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
@@ -616,6 +620,72 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger<Rethi
}
}
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<Model.Result> {
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<Person list> {
withTable Table.Person
getAll [ personId ]
filter (nameof Person.Empty.WebLogId) webLogId
result; withRetryDefault
}
|> tryFirst <| conn
member _.FindByWebLog webLogId = rethink<Person list> {
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<Rethi
do! ensureIndexes Table.Category [ nameof Category.Empty.WebLogId ]
do! ensureIndexes Table.Comment [ nameof Comment.Empty.PostId ]
do! ensureIndexes Table.Page [ nameof Page.Empty.WebLogId; nameof Page.Empty.AuthorId ]
do! ensureIndexes Table.Person [ nameof Person.Empty.WebLogId ]
do! ensureIndexes Table.Post [ nameof Post.Empty.WebLogId; nameof Post.Empty.AuthorId ]
do! ensureIndexes Table.TagMap []
do! ensureIndexes Table.Upload []
@@ -22,6 +22,9 @@ module Table =
[<Literal>]
let PageRevision = "page_revision"
[<Literal>]
let Person = "person"
/// Posts
[<Literal>]
let Post = "post"
@@ -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<Person>
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<Person> 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<Person>
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
+12 -3
View File
@@ -74,6 +74,12 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, 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<SQLiteData>, 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<SQLiteData>, 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<SQLiteData>, 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<SQLiteData>, 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)
+30
View File
@@ -143,6 +143,36 @@ type Page = {
OpenGraph = None }
/// <summary>A person (associated with podcasting)</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Person = {
/// <summary>The ID of this person</summary>
Id: PersonId
/// <summary>The ID of the web log to which this person belongs</summary>
WebLogId: WebLogId
/// <summary>The person's name</summary>
Name: string
/// <summary>A URL for more information about this person</summary>
/// <remarks>Non-absolute URLs will be treated as a permalink</remarks>
Url: string option
/// <summary>A URL with an image / avatar for this person</summary>
/// <remarks>Non-absolute URLs will be treated as a permalink</remarks>
ImageUrl: string option
} with
/// <summary>An empty person entry</summary>
static member Empty =
{ Id = PersonId.Empty
WebLogId = WebLogId.Empty
Name = ""
Url = None
ImageUrl = None }
/// <summary>A web log post</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Post = {
+116 -1
View File
@@ -402,6 +402,37 @@ type PersonRole =
PersonRole.GroupXref |> List.find (fun it -> fst it = this) |> snd
open System
/// <summary>An identifier for a person</summary>
[<Struct>]
type PersonId =
| PersonId of string
/// <summary>An empty person ID</summary>
static member Empty = PersonId ""
/// <summary>Create a new person ID</summary>
/// <returns>A new person ID</returns>
static member Create =
Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-')[..21] |> PersonId
/// <inheritdoc />
override this.ToString() =
match this with PersonId it -> it
/// <summary>The combination of a person and a role</summary>
[<CLIMutable>]
type PersonAssignment = {
/// <summary>The ID of the person to whom this assignment applies</summary>
PersonId: PersonId
/// <summary>The role to which this person is assigned</summary>
Role: PersonRole
}
open NodaTime.Text
/// <summary>A podcast episode</summary>
@@ -462,6 +493,9 @@ type Episode = {
/// <summary>A description of the episode</summary>
EpisodeDescription: string option
/// <summary>The people and roles for this episode</summary>
People: PersonAssignment list option
} with
/// <summary>An empty episode</summary>
@@ -484,7 +518,8 @@ type Episode = {
SeasonNumber = None
SeasonDescription = None
EpisodeNumber = None
EpisodeDescription = None }
EpisodeDescription = None
People = None }
/// <summary>Format a duration for an episode</summary>
/// <returns>A duration formatted in hours, minutes, and seconds</returns>
@@ -529,3 +564,83 @@ type PodcastMedium =
| Newsletter -> "newsletter"
| Blog -> "blog"
/// <summary>Options for a feed that describes a podcast</summary>
[<CLIMutable; NoComparison; NoEquality>]
type PodcastOptions = {
/// <summary>The title of the podcast</summary>
Title: string
/// <summary>A subtitle for the podcast</summary>
Subtitle: string option
/// <summary>The number of items in the podcast feed</summary>
ItemsInFeed: int
/// <summary>A summary of the podcast (iTunes field)</summary>
Summary: string
/// <summary>The display name of the podcast author (iTunes field)</summary>
DisplayedAuthor: string
/// <summary>The e-mail address of the user who registered the podcast at iTunes</summary>
Email: string
/// <summary>The link to the image for the podcast</summary>
/// <remarks>Non-absolute URLs will be treated as a permalink</remarks>
ImageUrl: string
/// <summary>The category from Apple Podcasts (iTunes) under which this podcast is categorized</summary>
AppleCategory: string
/// <summary>
/// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values)
/// </summary>
AppleSubcategory: string option
/// <summary>The explictness rating (iTunes field)</summary>
Explicit: ExplicitRating
/// <summary>The default media type for files in this podcast</summary>
DefaultMediaType: string option
/// <summary>
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
/// </summary>
MediaBaseUrl: string option
/// <summary>A GUID for this podcast</summary>
PodcastGuid: Guid option
/// <summary>A URL at which information on supporting the podcast may be found (supports permalinks)</summary>
FundingUrl: string option
/// <summary>The text to be displayed in the funding item within the feed</summary>
FundingText: string option
/// <summary>The medium (what the podcast IS, not what it is ABOUT)</summary>
Medium: PodcastMedium option
/// <summary>The people and roles for the podcast overall</summary>
People: PersonAssignment list option
} with
/// <summary>A default set of podcast options</summary>
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 }
+13 -89
View File
@@ -552,19 +552,6 @@ type OpenGraphProperties = {
}
/// <summary>A permanent link</summary>
[<Struct>]
type Permalink =
| Permalink of string
/// <summary>An empty permalink</summary>
static member Empty = Permalink ""
/// <inheritdoc />
override this.ToString() =
match this with Permalink it -> it
/// <summary>An identifier for a page</summary>
[<Struct>]
type PageId =
@@ -583,6 +570,19 @@ type PageId =
match this with PageId it -> it
/// <summary>A permanent link</summary>
[<Struct>]
type Permalink =
| Permalink of string
/// <summary>An empty permalink</summary>
static member Empty = Permalink ""
/// <inheritdoc />
override this.ToString() =
match this with Permalink it -> it
/// <summary>Statuses for posts</summary>
[<Struct>]
type PostStatus =
@@ -683,82 +683,6 @@ type CustomFeedSource =
match this with | Category (CategoryId catId) -> $"category:{catId}" | Tag tag -> $"tag:{tag}"
/// <summary>Options for a feed that describes a podcast</summary>
[<CLIMutable; NoComparison; NoEquality>]
type PodcastOptions = {
/// <summary>The title of the podcast</summary>
Title: string
/// <summary>A subtitle for the podcast</summary>
Subtitle: string option
/// <summary>The number of items in the podcast feed</summary>
ItemsInFeed: int
/// <summary>A summary of the podcast (iTunes field)</summary>
Summary: string
/// <summary>The display name of the podcast author (iTunes field)</summary>
DisplayedAuthor: string
/// <summary>The e-mail address of the user who registered the podcast at iTunes</summary>
Email: string
/// <summary>The link to the image for the podcast</summary>
ImageUrl: Permalink
/// <summary>The category from Apple Podcasts (iTunes) under which this podcast is categorized</summary>
AppleCategory: string
/// <summary>
/// A further refinement of the categorization of this podcast (Apple Podcasts/iTunes field / values)
/// </summary>
AppleSubcategory: string option
/// <summary>The explictness rating (iTunes field)</summary>
Explicit: ExplicitRating
/// <summary>The default media type for files in this podcast</summary>
DefaultMediaType: string option
/// <summary>
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
/// </summary>
MediaBaseUrl: string option
/// <summary>A GUID for this podcast</summary>
PodcastGuid: Guid option
/// <summary>A URL at which information on supporting the podcast may be found (supports permalinks)</summary>
FundingUrl: string option
/// <summary>The text to be displayed in the funding item within the feed</summary>
FundingText: string option
/// <summary>The medium (what the podcast IS, not what it is ABOUT)</summary>
Medium: PodcastMedium option
} with
/// <summary>A default set of podcast options</summary>
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 }
/// <summary>A custom feed</summary>
[<CLIMutable; NoComparison; NoEquality>]
type CustomFeed = {
+4 -2
View File
@@ -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 }
@@ -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<PersonId>("\"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<PageIdConverter>) "Page ID converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<PermalinkConverter>) "Permalink converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<PersonGroupConverter>) "PersonGroup converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<PersonIdConverter>) "Person ID converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<PersonRoleConverter>) "PersonRole converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<PodcastMediumConverter>) "Podcast medium converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<PostIdConverter>) "Post ID converter not found"
@@ -351,6 +366,7 @@ let all = testList "Converters" [
pageIdConverterTests
permalinkConverterTests
personGroupConverterTests
personIdConverterTests
personRoleConverterTests
podcastMediumConverterTests
postIdConverterTests
+1 -1
View File
@@ -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"
+3 -2
View File
@@ -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"
+2 -2
View File
@@ -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()