WIP on person table, podcast/ep assignment (#17)
This commit is contained in:
@@ -108,6 +108,14 @@ module Json =
|
|||||||
override _.ReadJson(reader: JsonReader, _: Type, _: PersonGroup, _: bool, _: JsonSerializer) =
|
override _.ReadJson(reader: JsonReader, _: Type, _: PersonGroup, _: bool, _: JsonSerializer) =
|
||||||
(string >> PersonGroup.Parse) reader.Value
|
(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>
|
/// <summary>Converter for the <see cref="PersonRole" /> type</summary>
|
||||||
type PersonRoleConverter() =
|
type PersonRoleConverter() =
|
||||||
inherit JsonConverter<PersonRole>()
|
inherit JsonConverter<PersonRole>()
|
||||||
@@ -199,6 +207,7 @@ module Json =
|
|||||||
PageIdConverter()
|
PageIdConverter()
|
||||||
PermalinkConverter()
|
PermalinkConverter()
|
||||||
PersonGroupConverter()
|
PersonGroupConverter()
|
||||||
|
PersonIdConverter()
|
||||||
PersonRoleConverter()
|
PersonRoleConverter()
|
||||||
PodcastMediumConverter()
|
PodcastMediumConverter()
|
||||||
PostIdConverter()
|
PostIdConverter()
|
||||||
|
|||||||
@@ -97,6 +97,31 @@ type IPageData =
|
|||||||
abstract member UpdatePriorPermalinks : PageId -> WebLogId -> Permalink list -> Task<bool>
|
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
|
/// Data functions to support manipulating posts
|
||||||
type IPostData =
|
type IPostData =
|
||||||
|
|
||||||
@@ -310,6 +335,9 @@ type IData =
|
|||||||
/// Page data functions
|
/// Page data functions
|
||||||
abstract member Page : IPageData
|
abstract member Page : IPageData
|
||||||
|
|
||||||
|
/// Person data functions
|
||||||
|
abstract member Person : IPersonData
|
||||||
|
|
||||||
/// Post data functions
|
/// Post data functions
|
||||||
abstract member Post : IPostData
|
abstract member Post : IPostData
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<Compile Include="SQLite\SQLiteHelpers.fs" />
|
<Compile Include="SQLite\SQLiteHelpers.fs" />
|
||||||
<Compile Include="SQLite\SQLiteCategoryData.fs" />
|
<Compile Include="SQLite\SQLiteCategoryData.fs" />
|
||||||
<Compile Include="SQLite\SQLitePageData.fs" />
|
<Compile Include="SQLite\SQLitePageData.fs" />
|
||||||
|
<Compile Include="SQLite\SQLitePersonData.fs" />
|
||||||
<Compile Include="SQLite\SQLitePostData.fs" />
|
<Compile Include="SQLite\SQLitePostData.fs" />
|
||||||
<Compile Include="SQLite\SQLiteTagMapData.fs" />
|
<Compile Include="SQLite\SQLiteTagMapData.fs" />
|
||||||
<Compile Include="SQLite\SQLiteThemeData.fs" />
|
<Compile Include="SQLite\SQLiteThemeData.fs" />
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
<Compile Include="Postgres\PostgresCache.fs" />
|
<Compile Include="Postgres\PostgresCache.fs" />
|
||||||
<Compile Include="Postgres\PostgresCategoryData.fs" />
|
<Compile Include="Postgres\PostgresCategoryData.fs" />
|
||||||
<Compile Include="Postgres\PostgresPageData.fs" />
|
<Compile Include="Postgres\PostgresPageData.fs" />
|
||||||
|
<Compile Include="Postgres\PostgresPersonData.fs" />
|
||||||
<Compile Include="Postgres\PostgresPostData.fs" />
|
<Compile Include="Postgres\PostgresPostData.fs" />
|
||||||
<Compile Include="Postgres\PostgresTagMapData.fs" />
|
<Compile Include="Postgres\PostgresTagMapData.fs" />
|
||||||
<Compile Include="Postgres\PostgresThemeData.fs" />
|
<Compile Include="Postgres\PostgresThemeData.fs" />
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ module Table =
|
|||||||
[<Literal>]
|
[<Literal>]
|
||||||
let PageRevision = "page_revision"
|
let PageRevision = "page_revision"
|
||||||
|
|
||||||
|
/// Persons
|
||||||
|
[<Literal>]
|
||||||
|
let Person = "person"
|
||||||
|
|
||||||
/// Posts
|
/// Posts
|
||||||
[<Literal>]
|
[<Literal>]
|
||||||
let Post = "post"
|
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
|
||||||
@@ -66,6 +66,12 @@ type PostgresData(log: ILogger<PostgresData>, ser: JsonSerializer) =
|
|||||||
revision_text TEXT NOT NULL,
|
revision_text TEXT NOT NULL,
|
||||||
PRIMARY KEY (page_id, as_of))"
|
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
|
// Post tables
|
||||||
if needsTable Table.Post then
|
if needsTable Table.Post then
|
||||||
Query.Definition.ensureTable Table.Post
|
Query.Definition.ensureTable Table.Post
|
||||||
@@ -268,6 +274,7 @@ type PostgresData(log: ILogger<PostgresData>, ser: JsonSerializer) =
|
|||||||
|
|
||||||
member _.Category = PostgresCategoryData log
|
member _.Category = PostgresCategoryData log
|
||||||
member _.Page = PostgresPageData log
|
member _.Page = PostgresPageData log
|
||||||
|
member _.Person = PostgresPersonData log
|
||||||
member _.Post = PostgresPostData log
|
member _.Post = PostgresPostData log
|
||||||
member _.TagMap = PostgresTagMapData log
|
member _.TagMap = PostgresTagMapData log
|
||||||
member _.Theme = PostgresThemeData log
|
member _.Theme = PostgresThemeData log
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ module private RethinkHelpers =
|
|||||||
/// The page table
|
/// The page table
|
||||||
let Page = "Page"
|
let Page = "Page"
|
||||||
|
|
||||||
|
/// The person table
|
||||||
|
let Person = "Person"
|
||||||
|
|
||||||
/// The post table
|
/// The post table
|
||||||
let Post = "Post"
|
let Post = "Post"
|
||||||
|
|
||||||
@@ -45,7 +48,8 @@ module private RethinkHelpers =
|
|||||||
let WebLogUser = "WebLogUser"
|
let WebLogUser = "WebLogUser"
|
||||||
|
|
||||||
/// A list of all tables
|
/// 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
|
/// 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 = {
|
member _.Post = {
|
||||||
new IPostData with
|
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.Category [ nameof Category.Empty.WebLogId ]
|
||||||
do! ensureIndexes Table.Comment [ nameof Comment.Empty.PostId ]
|
do! ensureIndexes Table.Comment [ nameof Comment.Empty.PostId ]
|
||||||
do! ensureIndexes Table.Page [ nameof Page.Empty.WebLogId; nameof Page.Empty.AuthorId ]
|
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.Post [ nameof Post.Empty.WebLogId; nameof Post.Empty.AuthorId ]
|
||||||
do! ensureIndexes Table.TagMap []
|
do! ensureIndexes Table.TagMap []
|
||||||
do! ensureIndexes Table.Upload []
|
do! ensureIndexes Table.Upload []
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ module Table =
|
|||||||
[<Literal>]
|
[<Literal>]
|
||||||
let PageRevision = "page_revision"
|
let PageRevision = "page_revision"
|
||||||
|
|
||||||
|
[<Literal>]
|
||||||
|
let Person = "person"
|
||||||
|
|
||||||
/// Posts
|
/// Posts
|
||||||
[<Literal>]
|
[<Literal>]
|
||||||
let Post = "post"
|
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
|
||||||
@@ -74,6 +74,12 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
|
|||||||
revision_text TEXT NOT NULL,
|
revision_text TEXT NOT NULL,
|
||||||
PRIMARY KEY (page_id, as_of))" []
|
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
|
// Post tables
|
||||||
if needsTable Table.Post then
|
if needsTable Table.Post then
|
||||||
log.LogInformation(creatingTable, Table.Post)
|
log.LogInformation(creatingTable, Table.Post)
|
||||||
@@ -184,7 +190,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
|
|||||||
Summary = Map.getString "summary" podcastRdr
|
Summary = Map.getString "summary" podcastRdr
|
||||||
DisplayedAuthor = Map.getString "displayed_author" podcastRdr
|
DisplayedAuthor = Map.getString "displayed_author" podcastRdr
|
||||||
Email = Map.getString "email" 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
|
AppleCategory = Map.getString "apple_category" podcastRdr
|
||||||
AppleSubcategory = Map.tryString "apple_subcategory" podcastRdr
|
AppleSubcategory = Map.tryString "apple_subcategory" podcastRdr
|
||||||
Explicit = Map.getString "explicit" podcastRdr |> ExplicitRating.Parse
|
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
|
FundingUrl = Map.tryString "funding_url" podcastRdr
|
||||||
FundingText = Map.tryString "funding_text" podcastRdr
|
FundingText = Map.tryString "funding_text" podcastRdr
|
||||||
Medium = Map.tryString "medium" podcastRdr
|
Medium = Map.tryString "medium" podcastRdr
|
||||||
|> Option.map PodcastMedium.Parse }
|
|> Option.map PodcastMedium.Parse
|
||||||
|
People = None } // TODO: retrieve person / role assignments
|
||||||
} |> List.ofSeq
|
} |> List.ofSeq
|
||||||
podcastRdr.Close()
|
podcastRdr.Close()
|
||||||
podcasts
|
podcasts
|
||||||
@@ -231,7 +238,8 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
|
|||||||
SeasonNumber = Map.tryInt "season_number" epRdr
|
SeasonNumber = Map.tryInt "season_number" epRdr
|
||||||
SeasonDescription = Map.tryString "season_description" epRdr
|
SeasonDescription = Map.tryString "season_description" epRdr
|
||||||
EpisodeNumber = Map.tryString "episode_number" epRdr |> Option.map Double.Parse
|
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
|
} |> List.ofSeq
|
||||||
epRdr.Close()
|
epRdr.Close()
|
||||||
episodes
|
episodes
|
||||||
@@ -503,6 +511,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
|
|||||||
|
|
||||||
member _.Category = SQLiteCategoryData (conn, ser, log)
|
member _.Category = SQLiteCategoryData (conn, ser, log)
|
||||||
member _.Page = SQLitePageData (conn, log)
|
member _.Page = SQLitePageData (conn, log)
|
||||||
|
member _.Person = SQLitePersonData (conn, log)
|
||||||
member _.Post = SQLitePostData (conn, log)
|
member _.Post = SQLitePostData (conn, log)
|
||||||
member _.TagMap = SQLiteTagMapData (conn, log)
|
member _.TagMap = SQLiteTagMapData (conn, log)
|
||||||
member _.Theme = SQLiteThemeData (conn, log)
|
member _.Theme = SQLiteThemeData (conn, log)
|
||||||
|
|||||||
@@ -143,6 +143,36 @@ type Page = {
|
|||||||
OpenGraph = None }
|
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>
|
/// <summary>A web log post</summary>
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Post = {
|
type Post = {
|
||||||
|
|||||||
@@ -402,6 +402,37 @@ type PersonRole =
|
|||||||
PersonRole.GroupXref |> List.find (fun it -> fst it = this) |> snd
|
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
|
open NodaTime.Text
|
||||||
|
|
||||||
/// <summary>A podcast episode</summary>
|
/// <summary>A podcast episode</summary>
|
||||||
@@ -462,6 +493,9 @@ type Episode = {
|
|||||||
|
|
||||||
/// <summary>A description of the episode</summary>
|
/// <summary>A description of the episode</summary>
|
||||||
EpisodeDescription: string option
|
EpisodeDescription: string option
|
||||||
|
|
||||||
|
/// <summary>The people and roles for this episode</summary>
|
||||||
|
People: PersonAssignment list option
|
||||||
} with
|
} with
|
||||||
|
|
||||||
/// <summary>An empty episode</summary>
|
/// <summary>An empty episode</summary>
|
||||||
@@ -484,7 +518,8 @@ type Episode = {
|
|||||||
SeasonNumber = None
|
SeasonNumber = None
|
||||||
SeasonDescription = None
|
SeasonDescription = None
|
||||||
EpisodeNumber = None
|
EpisodeNumber = None
|
||||||
EpisodeDescription = None }
|
EpisodeDescription = None
|
||||||
|
People = None }
|
||||||
|
|
||||||
/// <summary>Format a duration for an episode</summary>
|
/// <summary>Format a duration for an episode</summary>
|
||||||
/// <returns>A duration formatted in hours, minutes, and seconds</returns>
|
/// <returns>A duration formatted in hours, minutes, and seconds</returns>
|
||||||
@@ -529,3 +564,83 @@ type PodcastMedium =
|
|||||||
| Newsletter -> "newsletter"
|
| Newsletter -> "newsletter"
|
||||||
| Blog -> "blog"
|
| 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 }
|
||||||
|
|||||||
@@ -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>
|
/// <summary>An identifier for a page</summary>
|
||||||
[<Struct>]
|
[<Struct>]
|
||||||
type PageId =
|
type PageId =
|
||||||
@@ -583,6 +570,19 @@ type PageId =
|
|||||||
match this with PageId it -> it
|
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>
|
/// <summary>Statuses for posts</summary>
|
||||||
[<Struct>]
|
[<Struct>]
|
||||||
type PostStatus =
|
type PostStatus =
|
||||||
@@ -683,82 +683,6 @@ type CustomFeedSource =
|
|||||||
match this with | Category (CategoryId catId) -> $"category:{catId}" | Tag tag -> $"tag:{tag}"
|
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>
|
/// <summary>A custom feed</summary>
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type CustomFeed = {
|
type CustomFeed = {
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ type EditCustomFeedModel = {
|
|||||||
Summary = this.Summary
|
Summary = this.Summary
|
||||||
DisplayedAuthor = this.DisplayedAuthor
|
DisplayedAuthor = this.DisplayedAuthor
|
||||||
Email = this.Email
|
Email = this.Email
|
||||||
ImageUrl = Permalink this.ImageUrl
|
ImageUrl = this.ImageUrl
|
||||||
AppleCategory = this.AppleCategory
|
AppleCategory = this.AppleCategory
|
||||||
AppleSubcategory = noneIfBlank this.AppleSubcategory
|
AppleSubcategory = noneIfBlank this.AppleSubcategory
|
||||||
Explicit = ExplicitRating.Parse this.Explicit
|
Explicit = ExplicitRating.Parse this.Explicit
|
||||||
@@ -693,7 +693,8 @@ type EditCustomFeedModel = {
|
|||||||
PodcastGuid = noneIfBlank this.PodcastGuid |> Option.map Guid.Parse
|
PodcastGuid = noneIfBlank this.PodcastGuid |> Option.map Guid.Parse
|
||||||
FundingUrl = noneIfBlank this.FundingUrl
|
FundingUrl = noneIfBlank this.FundingUrl
|
||||||
FundingText = noneIfBlank this.FundingText
|
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
|
else
|
||||||
None }
|
None }
|
||||||
|
|
||||||
@@ -974,6 +975,7 @@ type EditPostModel() =
|
|||||||
| Some it -> Some (double it)
|
| Some it -> Some (double it)
|
||||||
| None -> None
|
| None -> None
|
||||||
EpisodeDescription = noneIfBlank this.EpisodeDescription
|
EpisodeDescription = noneIfBlank this.EpisodeDescription
|
||||||
|
People = None // TODO: add this to UI or handle differently
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
None }
|
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
|
/// Unit tests for the PersonRoleConverter type
|
||||||
let personRoleConverterTests = testList "PersonRoleConverter" [
|
let personRoleConverterTests = testList "PersonRoleConverter" [
|
||||||
let opts = JsonSerializerSettings()
|
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<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<PermalinkConverter>) "Permalink converter not found"
|
||||||
Expect.hasCountOf ser.Converters 1u (has typeof<PersonGroupConverter>) "PersonGroup 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<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<PodcastMediumConverter>) "Podcast medium converter not found"
|
||||||
Expect.hasCountOf ser.Converters 1u (has typeof<PostIdConverter>) "Post ID 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
|
pageIdConverterTests
|
||||||
permalinkConverterTests
|
permalinkConverterTests
|
||||||
personGroupConverterTests
|
personGroupConverterTests
|
||||||
|
personIdConverterTests
|
||||||
personRoleConverterTests
|
personRoleConverterTests
|
||||||
podcastMediumConverterTests
|
podcastMediumConverterTests
|
||||||
postIdConverterTests
|
postIdConverterTests
|
||||||
|
|||||||
@@ -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.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.DisplayedAuthor "Podcaster Extraordinaire" "Podcast author is incorrect"
|
||||||
Expect.equal pod.Email "podcaster@example.com" "Podcast e-mail 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.AppleCategory "Fiction" "Podcast Apple category is incorrect"
|
||||||
Expect.equal pod.AppleSubcategory (Some "Drama") "Podcast Apple subcategory is incorrect"
|
Expect.equal pod.AppleSubcategory (Some "Drama") "Podcast Apple subcategory is incorrect"
|
||||||
Expect.equal pod.Explicit No "Podcast explicit rating is incorrect"
|
Expect.equal pod.Explicit No "Podcast explicit rating is incorrect"
|
||||||
|
|||||||
@@ -282,7 +282,8 @@ let testFullPost =
|
|||||||
SeasonNumber = Some 3
|
SeasonNumber = Some 3
|
||||||
SeasonDescription = Some "Season Three"
|
SeasonDescription = Some "Season Three"
|
||||||
EpisodeNumber = Some 322.
|
EpisodeNumber = Some 322.
|
||||||
EpisodeDescription = Some "Episode 322" } }
|
EpisodeDescription = Some "Episode 322"
|
||||||
|
People = None } } // TODO: add some people
|
||||||
|
|
||||||
/// Unit tests for the EditCommonModel type
|
/// Unit tests for the EditCommonModel type
|
||||||
let editCommonModelTests = testList "EditCommonModel" [
|
let editCommonModelTests = testList "EditCommonModel" [
|
||||||
@@ -524,7 +525,7 @@ let editCustomFeedModelTests = testList "EditCustomFeedModel" [
|
|||||||
Summary = "As little as possible"
|
Summary = "As little as possible"
|
||||||
DisplayedAuthor = "The Tester"
|
DisplayedAuthor = "The Tester"
|
||||||
Email = "thetester@example.com"
|
Email = "thetester@example.com"
|
||||||
ImageUrl = Permalink "upload/my-image.png"
|
ImageUrl = "upload/my-image.png"
|
||||||
AppleCategory = "News"
|
AppleCategory = "News"
|
||||||
Explicit = Clean }
|
Explicit = Clean }
|
||||||
// A GUID with all zeroes, ending in "a"
|
// A GUID with all zeroes, ending in "a"
|
||||||
|
|||||||
@@ -253,8 +253,8 @@ let private addPodcast (webLog: WebLog) (rssFeed: SyndicationFeed) (feed: Custom
|
|||||||
let feedUrl = webLog.AbsoluteUrl feed.Path
|
let feedUrl = webLog.AbsoluteUrl feed.Path
|
||||||
let imageUrl =
|
let imageUrl =
|
||||||
match podcast.ImageUrl with
|
match podcast.ImageUrl with
|
||||||
| Permalink link when link.StartsWith "http" -> link
|
| link when link.StartsWith "http" -> link
|
||||||
| Permalink _ -> webLog.AbsoluteUrl podcast.ImageUrl
|
| _ -> webLog.AbsoluteUrl (Permalink podcast.ImageUrl)
|
||||||
|
|
||||||
let xmlDoc = XmlDocument()
|
let xmlDoc = XmlDocument()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user