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) =
|
||||
(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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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<Rethi
|
||||
| None -> 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<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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user