Add group, WIP on role (#17)

This commit is contained in:
2026-08-01 22:45:18 -04:00
parent 2241742ab8
commit e7261f6019
13 changed files with 539 additions and 72 deletions
+25 -7
View File
@@ -84,6 +84,14 @@ module Json =
override _.ReadJson(reader: JsonReader, _: Type, _: OpenGraphType, _: bool, _: JsonSerializer) =
(string >> OpenGraphType.Parse) reader.Value
/// <summary>Converter for the <see cref="PageId" /> type</summary>
type PageIdConverter() =
inherit JsonConverter<PageId>()
override _.WriteJson(writer: JsonWriter, value: PageId, _: JsonSerializer) =
writer.WriteValue(string value)
override _.ReadJson(reader: JsonReader, _: Type, _: PageId, _: bool, _: JsonSerializer) =
(string >> PageId) reader.Value
/// <summary>Converter for the <see cref="Permalink" /> type</summary>
type PermalinkConverter() =
inherit JsonConverter<Permalink>()
@@ -92,13 +100,21 @@ module Json =
override _.ReadJson(reader: JsonReader, _: Type, _: Permalink, _: bool, _: JsonSerializer) =
(string >> Permalink) reader.Value
/// <summary>Converter for the <see cref="PageId" /> type</summary>
type PageIdConverter() =
inherit JsonConverter<PageId>()
override _.WriteJson(writer: JsonWriter, value: PageId, _: JsonSerializer) =
/// <summary>Converter for the <see cref="PersonGroup" /> type</summary>
type PersonGroupConverter() =
inherit JsonConverter<PersonGroup>()
override _.WriteJson(writer: JsonWriter, value: PersonGroup, _: JsonSerializer) =
writer.WriteValue(string value)
override _.ReadJson(reader: JsonReader, _: Type, _: PageId, _: bool, _: JsonSerializer) =
(string >> PageId) reader.Value
override _.ReadJson(reader: JsonReader, _: Type, _: PersonGroup, _: bool, _: JsonSerializer) =
(string >> PersonGroup.Parse) reader.Value
/// <summary>Converter for the <see cref="PersonRole" /> type</summary>
type PersonRoleConverter() =
inherit JsonConverter<PersonRole>()
override _.WriteJson(writer: JsonWriter, value: PersonRole, _: JsonSerializer) =
writer.WriteValue(string value)
override _.ReadJson(reader: JsonReader, _: Type, _: PersonRole, _: bool, _: JsonSerializer) =
(string >> PersonRole.Parse) reader.Value
/// <summary>Converter for the <see cref="PodcastMedium" /> type</summary>
type PodcastMediumConverter() =
@@ -180,8 +196,10 @@ module Json =
ExplicitRatingConverter()
MarkupTextConverter()
OpenGraphTypeConverter()
PermalinkConverter()
PageIdConverter()
PermalinkConverter()
PersonGroupConverter()
PersonRoleConverter()
PodcastMediumConverter()
PostIdConverter()
TagMapIdConverter()
+1 -1
View File
@@ -457,7 +457,7 @@ type WebLogUser = {
PreferredName = ""
PasswordHash = ""
Url = None
AccessLevel = Author
AccessLevel = ContentAuthor
CreatedOn = Noda.epoch
LastSeenOn = None }
+333 -4
View File
@@ -61,18 +61,22 @@ module Noda =
/// <summary>A user's access level</summary>
[<Struct>]
type AccessLevel =
/// <summary>The user may create and publish posts and edit the ones they have created</summary>
| Author
| ContentAuthor
/// <summary>The user may edit posts they did not create, but may not delete them</summary>
| Editor
/// <summary>The user may delete posts and configure web log settings</summary>
| WebLogAdmin
/// <summary>The user may manage themes (which affects all web logs for an installation)</summary>
| Administrator
/// <summary>Weights applied to each access level</summary>
static member private Weights =
[ Author, 10
[ ContentAuthor, 10
Editor, 20
WebLogAdmin, 30
Administrator, 40 ]
@@ -84,7 +88,7 @@ type AccessLevel =
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse level =
match level with
| "Author" -> Author
| "Author" -> ContentAuthor
| "Editor" -> Editor
| "WebLogAdmin" -> WebLogAdmin
| "Administrator" -> Administrator
@@ -93,7 +97,7 @@ type AccessLevel =
/// <inheritdoc />
override this.ToString() =
match this with
| Author -> "Author"
| ContentAuthor -> "Author"
| Editor -> "Editor"
| WebLogAdmin -> "WebLogAdmin"
| Administrator -> "Administrator"
@@ -269,6 +273,331 @@ type Chapter = {
EndTime = None
Location = None }
/// <summary>A group to which a podcast person can belong</summary>
type PersonGroup =
| Administration
| AudioPostProduction
| AudioProduction
| Cast
| Community
| CreativeDirection
| Misc
| VideoPostProduction
| VideoProduction
| Visuals
| Writing
/// <summary>Parse a string into a group</summary>
/// <param name="group">The string representation of the group</param>
/// <returns>The <c>PersonGroup</c> instance parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse group =
match group with
| "Administration" -> Administration
| "Audio Post-Production" -> AudioPostProduction
| "Audio Production" -> AudioProduction
| "Cast" -> Cast
| "Community" -> Community
| "Creative Direction" -> CreativeDirection
| "Misc." -> Misc
| "Video Post-Production" -> VideoPostProduction
| "Video Production" -> VideoProduction
| "Visuals" -> Visuals
| "Writing" -> Writing
| _ -> invalidArg (nameof group) $"{group} is not a valid group"
/// <inheritdoc />
override this.ToString() =
match this with
| Administration -> "Administration"
| AudioPostProduction -> "Audio Post-Production"
| AudioProduction -> "Audio Production"
| Cast -> "Cast"
| Community -> "Community"
| CreativeDirection -> "Creative Direction"
| Misc -> "Misc."
| VideoPostProduction -> "Video Post-Production"
| VideoProduction -> "Video Production"
| Visuals -> "Visuals"
| Writing -> "Writing"
/// <summary>The role a person plays for a podcast or a podcast episode</summary>
type PersonRole =
| Announcer
| AssistantCamera
| AssistantDirector
| AssistantEditor
| AssociateProducer
| AudioEditor
| AudioEngineer
| Author
| BookingCoordinator
| CameraGrip
| CameraOperator
| CoHost
| Composer
| Consultant
| ContentManager
| CoverArtDesigner
| CoWriter
| CreativeDirector
| DevelopmentProducer
| Director
| EditorialDirector
| ExecutiveProducer
| FactChecker
| FoleyArtist
| GraphicDesigner
| Guest
| GuestHost
| GuestWriter
| Host
| Intern
| LightingDesigner
| Logger
| ManagingEditor
| MarketingManager
| MusicContributor
| MusicProduction
| Narrator
| PostProductionEngineer
| Producer
| ProductionAssistant
| ProductionCoordinator
| RemoteRecordingEngineer
| Reporter
| Researcher
| SalesManager
| SalesRepresentative
| ScriptCoordinator
| ScriptEditor
| SeniorProducer
| SocialMediaManager
| Songwriter
| SoundDesigner
| StoryEditor
| StudioCoordinator
| TechnicalDirector
| TechnicalManager
| ThemeMusic
| Transcriber
| Translator
| VideoEditor
| VoiceActor
| Writer
| WritingEditor
/// <summary>Parse a string into a role</summary>
/// <param name="role">The string representation of the role</param>
/// <returns>The <c>PersonRole</c> instance parsed from the string</returns>
/// <exception cref="InvalidArgumentException">If the string is not valid</exception>
static member Parse role =
match role with
| "Announcer" -> Announcer
| "Assistant Camera" -> AssistantCamera
| "Assistant Director" -> AssistantDirector
| "Assistant Editor" -> AssistantEditor
| "Associate Producer" -> AssociateProducer
| "Audio Editor" -> AudioEditor
| "Audio Engineer" -> AudioEngineer
| "Author" -> Author
| "Booking Coordinator" -> BookingCoordinator
| "Camera Grip" -> CameraGrip
| "Camera Operator" -> CameraOperator
| "Co-Host" -> CoHost
| "Composer" -> Composer
| "Consultant" -> Consultant
| "Content Manager" -> ContentManager
| "Cover Art Designer" -> CoverArtDesigner
| "Co-Writer" -> CoWriter
| "Creative Director" -> CreativeDirector
| "Development Producer" -> DevelopmentProducer
| "Director" -> Director
| "Editorial Director" -> EditorialDirector
| "Executive Producer" -> ExecutiveProducer
| "Fact Checker" -> FactChecker
| "Foley Artist" -> FoleyArtist
| "Graphic Designer" -> GraphicDesigner
| "Guest" -> Guest
| "Guest Host" -> GuestHost
| "Guest Writer" -> GuestWriter
| "Host" -> Host
| "Intern" -> Intern
| "Lighting Designer" -> LightingDesigner
| "Logger" -> Logger
| "Managing Editor" -> ManagingEditor
| "Marketing Manager" -> MarketingManager
| "Music Contributor" -> MusicContributor
| "Music Production" -> MusicProduction
| "Narrator" -> Narrator
| "Post Production Engineer" -> PostProductionEngineer
| "Producer" -> Producer
| "Production Assistant" -> ProductionAssistant
| "Production Coordinator" -> ProductionCoordinator
| "Remote Recording Engineer" -> RemoteRecordingEngineer
| "Reporter" -> Reporter
| "Researcher" -> Researcher
| "Sales Manager" -> SalesManager
| "Sales Representative" -> SalesRepresentative
| "Script Coordinator" -> ScriptCoordinator
| "Script Editor" -> ScriptEditor
| "Senior Producer" -> SeniorProducer
| "Social Media Manager" -> SocialMediaManager
| "Songwriter" -> Songwriter
| "Sound Designer" -> SoundDesigner
| "Story Editor" -> StoryEditor
| "Studio Coordinator" -> StudioCoordinator
| "Technical Director" -> TechnicalDirector
| "Technical Manager" -> TechnicalManager
| "Theme Music" -> ThemeMusic
| "Transcriber" -> Transcriber
| "Translator" -> Translator
| "Editor (Video)" -> VideoEditor
| "Voice Actor" -> VoiceActor
| "Writer" -> Writer
| "Editor (Writing)" -> WritingEditor
| _ -> invalidArg (nameof role) $"{role} is not a valid role"
/// <inheritdoc />
override this.ToString() =
match this with
| Announcer -> "Announcer"
| AssistantCamera -> "Assistant Camera"
| AssistantDirector -> "Assistant Director"
| AssistantEditor -> "Assistant Editor"
| AssociateProducer -> "Associate Producer"
| AudioEditor -> "Audio Editor"
| AudioEngineer -> "Audio Engineer"
| Author -> "Author"
| BookingCoordinator -> "Booking Coordinator"
| CameraGrip -> "Camera Grip"
| CameraOperator -> "Camera Operator"
| CoHost -> "Co-Host"
| Composer -> "Composer"
| Consultant -> "Consultant"
| ContentManager -> "Content Manager"
| CoverArtDesigner -> "Cover Art Designer"
| CoWriter -> "Co-Writer"
| CreativeDirector -> "Creative Director"
| DevelopmentProducer -> "Development Producer"
| Director -> "Director"
| EditorialDirector -> "Editorial Director"
| ExecutiveProducer -> "Executive Producer"
| FactChecker -> "Fact Checker"
| FoleyArtist -> "Foley Artist"
| GraphicDesigner -> "Graphic Designer"
| Guest -> "Guest"
| GuestHost -> "Guest Host"
| GuestWriter -> "Guest Writer"
| Host -> "Host"
| Intern -> "Intern"
| LightingDesigner -> "Lighting Designer"
| Logger -> "Logger"
| ManagingEditor -> "Managing Editor"
| MarketingManager -> "Marketing Manager"
| MusicContributor -> "Music Contributor"
| MusicProduction -> "Music Production"
| Narrator -> "Narrator"
| PostProductionEngineer -> "Post Production Engineer"
| Producer -> "Producer"
| ProductionAssistant -> "Production Assistant"
| ProductionCoordinator -> "Production Coordinator"
| RemoteRecordingEngineer -> "Remote Recording Engineer"
| Reporter -> "Reporter"
| Researcher -> "Researcher"
| SalesManager -> "Sales Manager"
| SalesRepresentative -> "Sales Representative"
| ScriptCoordinator -> "Script Coordinator"
| ScriptEditor -> "Script Editor"
| SeniorProducer -> "Senior Producer"
| SocialMediaManager -> "Social Media Manager"
| Songwriter -> "Songwriter"
| SoundDesigner -> "Sound Designer"
| StoryEditor -> "Story Editor"
| StudioCoordinator -> "Studio Coordinator"
| TechnicalDirector -> "Technical Director"
| TechnicalManager -> "Technical Manager"
| ThemeMusic -> "Theme Music"
| Transcriber -> "Transcriber"
| Translator -> "Translator"
| VideoEditor -> "Editor (Video)"
| VoiceActor -> "Voice Actor"
| Writer -> "Writer"
| WritingEditor -> "Editor (Writing)"
/// <summary>Get the display name of the role</summary>
/// <remarks>Both Video Production and Writing have an "Editor"; this is how it should be rendered in RSS feeds and
/// in the UI</remarks>
member this.ToDisplay() =
match this with VideoEditor | WritingEditor -> "Editor" | _ -> string this
/// <summary>Role / group cross-reference</summary>
static member GroupXref =
[ Announcer, Cast
AssistantCamera, VideoProduction
AssistantDirector, CreativeDirection
AssistantEditor, VideoPostProduction
AssociateProducer, CreativeDirection
AudioEditor, AudioPostProduction
AudioEngineer, AudioProduction
Author, Writing
BookingCoordinator, Administration
CameraGrip, VideoProduction
CameraOperator, VideoProduction
CoHost, Cast
Composer, AudioPostProduction
ContentManager, Administration
Consultant, Misc
CoverArtDesigner, Visuals
CoWriter, Writing
CreativeDirector, CreativeDirection
DevelopmentProducer, CreativeDirection
Director, CreativeDirection
EditorialDirector, Writing
ExecutiveProducer, CreativeDirection
FactChecker, Writing
FoleyArtist, AudioPostProduction
GraphicDesigner, Visuals
Guest, Cast
GuestHost, Cast
GuestWriter, Writing
Host, Cast
Intern, Misc
LightingDesigner, VideoProduction
Logger, Writing
ManagingEditor, Writing
MarketingManager, Administration
MusicContributor, AudioPostProduction
MusicProduction, AudioPostProduction
Narrator, Cast
PostProductionEngineer, AudioProduction
Producer, CreativeDirection
ProductionAssistant, Administration
ProductionCoordinator, Administration
RemoteRecordingEngineer, AudioProduction
Reporter, Cast
Researcher, Writing
SalesManager, Administration
SalesRepresentative, Administration
ScriptCoordinator, Writing
ScriptEditor, Writing
SeniorProducer, CreativeDirection
SocialMediaManager, Community
Songwriter, Writing
SoundDesigner, AudioPostProduction
StoryEditor, Writing
StudioCoordinator, AudioProduction
TechnicalDirector, AudioProduction
TechnicalManager, AudioProduction
ThemeMusic, AudioPostProduction
Transcriber, Writing
Translator, Writing
VideoEditor, VideoPostProduction
VoiceActor, Cast
Writer, Writing
WritingEditor, Writing ]
open NodaTime.Text
+41 -9
View File
@@ -136,6 +136,20 @@ let openGraphTypeConverterTests = testList "OpenGraphTypeConverter" [
}
]
/// Unit tests for the PageIdConverter type
let pageIdConverterTests = testList "PageIdConverter" [
let opts = JsonSerializerSettings()
opts.Converters.Add(PageIdConverter())
test "succeeds when serializing" {
let after = JsonConvert.SerializeObject(PageId "test-page", opts)
Expect.equal after "\"test-page\"" "Page ID serialized incorrectly"
}
test "succeeds when deserializing" {
let after = JsonConvert.DeserializeObject<PageId>("\"page-test\"", opts)
Expect.equal after (PageId "page-test") "Page ID deserialized incorrectly"
}
]
/// Unit tests for the PermalinkConverter type
let permalinkConverterTests = testList "PermalinkConverter" [
let opts = JsonSerializerSettings()
@@ -150,17 +164,31 @@ let permalinkConverterTests = testList "PermalinkConverter" [
}
]
/// Unit tests for the PageIdConverter type
let pageIdConverterTests = testList "PageIdConverter" [
/// Unit tests for the PersonGroupConverter type
let personGroupConverterTests = testList "PersonGroupConverter" [
let opts = JsonSerializerSettings()
opts.Converters.Add(PageIdConverter())
opts.Converters.Add(PersonGroupConverter())
test "succeeds when serializing" {
let after = JsonConvert.SerializeObject(PageId "test-page", opts)
Expect.equal after "\"test-page\"" "Page ID serialized incorrectly"
let after = JsonConvert.SerializeObject(Visuals, opts)
Expect.equal after "\"Visuals\"" "PersonGroup serialized incorrectly"
}
test "succeeds when deserializing" {
let after = JsonConvert.DeserializeObject<PageId>("\"page-test\"", opts)
Expect.equal after (PageId "page-test") "Page ID deserialized incorrectly"
let after = JsonConvert.DeserializeObject<PersonGroup>("\"Cast\"", opts)
Expect.equal after Cast "PersonGroup deserialized incorrectly"
}
]
/// Unit tests for the PersonRoleConverter type
let personRoleConverterTests = testList "PersonRoleConverter" [
let opts = JsonSerializerSettings()
opts.Converters.Add(PersonRoleConverter())
test "succeeds when serializing" {
let after = JsonConvert.SerializeObject(Host, opts)
Expect.equal after "\"Host\"" "PersonRole serialized incorrectly"
}
test "succeeds when deserializing" {
let after = JsonConvert.DeserializeObject<PersonRole>("\"Co-Host\"", opts)
Expect.equal after CoHost "PersonRole deserialized incorrectly"
}
]
@@ -291,8 +319,10 @@ let configureTests = test "Json.configure succeeds" {
Expect.hasCountOf ser.Converters 1u (has typeof<ExplicitRatingConverter>) "Explicit rating converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<MarkupTextConverter>) "Markup text converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<OpenGraphTypeConverter>) "OpenGraph type converter not found"
Expect.hasCountOf ser.Converters 1u (has typeof<PermalinkConverter>) "Permalink 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<PersonGroupConverter>) "PersonGroup 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"
Expect.hasCountOf ser.Converters 1u (has typeof<TagMapIdConverter>) "Tag map ID converter not found"
@@ -318,8 +348,10 @@ let all = testList "Converters" [
explicitRatingConverterTests
markupTextConverterTests
openGraphTypeConverterTests
permalinkConverterTests
pageIdConverterTests
permalinkConverterTests
personGroupConverterTests
personRoleConverterTests
podcastMediumConverterTests
postIdConverterTests
tagMapIdConverterTests
@@ -33,7 +33,7 @@ let ``Add succeeds`` (data: IData) = task {
PreferredName = "n00b"
PasswordHash = "hashed-password"
Url = Some "https://example.com/~new"
AccessLevel = Author
AccessLevel = ContentAuthor
CreatedOn = Noda.epoch + Duration.FromDays 365
LastSeenOn = None }
let! user = data.WebLogUser.FindById newId rootId
@@ -47,7 +47,7 @@ let ``Add succeeds`` (data: IData) = task {
Expect.equal it.PreferredName "n00b" "Preferred name is incorrect"
Expect.equal it.PasswordHash "hashed-password" "Password hash is incorrect"
Expect.equal it.Url (Some "https://example.com/~new") "URL is incorrect"
Expect.equal it.AccessLevel Author "Access level is incorrect"
Expect.equal it.AccessLevel ContentAuthor "Access level is incorrect"
Expect.equal it.CreatedOn (Noda.epoch + Duration.FromDays 365) "Created on is incorrect"
Expect.isNone it.LastSeenOn "Last seen on should not have had a value"
}
+105 -17
View File
@@ -31,7 +31,7 @@ let nodaTests = testList "Noda" [
let accessLevelTests = testList "AccessLevel" [
testList "Parse" [
test "succeeds for \"Author\"" {
Expect.equal (AccessLevel.Parse "Author") Author "Author not parsed correctly"
Expect.equal (AccessLevel.Parse "Author") AccessLevel.ContentAuthor "Author not parsed correctly"
}
test "succeeds for \"Editor\"" {
Expect.equal (AccessLevel.Parse "Editor") Editor "Editor not parsed correctly"
@@ -48,8 +48,8 @@ let accessLevelTests = testList "AccessLevel" [
}
]
testList "ToString" [
test "Author succeeds" {
Expect.equal (string Author) "Author" "Author string incorrect"
test "ContentAuthor succeeds" {
Expect.equal (string AccessLevel.ContentAuthor) "Author" "ContentAuthor string incorrect"
}
test "Editor succeeds" {
Expect.equal (string Editor) "Editor" "Editor string incorrect"
@@ -62,20 +62,20 @@ let accessLevelTests = testList "AccessLevel" [
}
]
testList "HasAccess" [
test "Author has Author access" {
Expect.isTrue (Author.HasAccess Author) "Author should have Author access"
test "ContentAuthor has ContentAuthor access" {
Expect.isTrue (ContentAuthor.HasAccess ContentAuthor) "ContentAuthor should have ContentAuthor access"
}
test "Author does not have Editor access" {
Expect.isFalse (Author.HasAccess Editor) "Author should not have Editor access"
test "ContentAuthor does not have Editor access" {
Expect.isFalse (ContentAuthor.HasAccess Editor) "ContentAuthor should not have Editor access"
}
test "Author does not have WebLogAdmin access" {
Expect.isFalse (Author.HasAccess WebLogAdmin) "Author should not have WebLogAdmin access"
test "ContentAuthor does not have WebLogAdmin access" {
Expect.isFalse (ContentAuthor.HasAccess WebLogAdmin) "ContentAuthor should not have WebLogAdmin access"
}
test "Author does not have Administrator access" {
Expect.isFalse (Author.HasAccess Administrator) "Author should not have Administrator access"
test "ContentAuthor does not have Administrator access" {
Expect.isFalse (ContentAuthor.HasAccess Administrator) "ContentAuthor should not have Administrator access"
}
test "Editor has Author access" {
Expect.isTrue (Editor.HasAccess Author) "Editor should have Author access"
test "Editor has ContentAuthor access" {
Expect.isTrue (Editor.HasAccess ContentAuthor) "Editor should have ContentAuthor access"
}
test "Editor has Editor access" {
Expect.isTrue (Editor.HasAccess Editor) "Editor should have Editor access"
@@ -86,8 +86,8 @@ let accessLevelTests = testList "AccessLevel" [
test "Editor does not have Administrator access" {
Expect.isFalse (Editor.HasAccess Administrator) "Editor should not have Administrator access"
}
test "WebLogAdmin has Author access" {
Expect.isTrue (WebLogAdmin.HasAccess Author) "WebLogAdmin should have Author access"
test "WebLogAdmin has ContentAuthor access" {
Expect.isTrue (WebLogAdmin.HasAccess ContentAuthor) "WebLogAdmin should have ContentAuthor access"
}
test "WebLogAdmin has Editor access" {
Expect.isTrue (WebLogAdmin.HasAccess Editor) "WebLogAdmin should have Editor access"
@@ -98,8 +98,8 @@ let accessLevelTests = testList "AccessLevel" [
test "WebLogAdmin does not have Administrator access" {
Expect.isFalse (WebLogAdmin.HasAccess Administrator) "WebLogAdmin should not have Administrator access"
}
test "Administrator has Author access" {
Expect.isTrue (Administrator.HasAccess Author) "Administrator should have Author access"
test "Administrator has ContentAuthor access" {
Expect.isTrue (Administrator.HasAccess ContentAuthor) "Administrator should have ContentAuthor access"
}
test "Administrator has Editor access" {
Expect.isTrue (Administrator.HasAccess Editor) "Administrator should have Editor access"
@@ -684,6 +684,93 @@ let openGraphPropertiesTests = testList "OpenGraphProperties" [
]
]
/// Tests for the PersonGroup type
let personGroupTests = testList "PersonGroup" [
testList "Parse" [
test "succeeds for \"Administration\"" {
Expect.equal (PersonGroup.Parse "Administration") Administration "Administration not parsed correctly"
}
test "succeeds for \"Audio Post-Production\"" {
Expect.equal
(PersonGroup.Parse "Audio Post-Production")
AudioPostProduction
"Audio Post-Production not parsed correctly"
}
test "succeeds for \"Audio Production\"" {
Expect.equal
(PersonGroup.Parse "Audio Production") AudioProduction "Audio Production not parsed correctly"
}
test "succeeds for \"Cast\"" {
Expect.equal (PersonGroup.Parse "Cast") Cast "Cast not parsed correctly"
}
test "succeeds for \"Community\"" {
Expect.equal (PersonGroup.Parse "Community") Community "Community not parsed correctly"
}
test "succeeds for \"Creative Direction\"" {
Expect.equal
(PersonGroup.Parse "Creative Direction") CreativeDirection "Creative Direction not parsed correctly"
}
test "succeeds for \"Misc.\"" {
Expect.equal (PersonGroup.Parse "Misc.") Misc "Misc. not parsed correctly"
}
test "succeeds for \"Video Post-Production\"" {
Expect.equal
(PersonGroup.Parse "Video Post-Production")
VideoPostProduction
"Video Post-Production not parsed correctly"
}
test "succeeds for \"Video Production\"" {
Expect.equal
(PersonGroup.Parse "Video Production") VideoProduction "Video Production not parsed correctly"
}
test "succeeds for \"Visuals\"" {
Expect.equal (PersonGroup.Parse "Visuals") Visuals "Visuals not parsed correctly"
}
test "succeeds for \"Writing\"" {
Expect.equal (PersonGroup.Parse "Writing") Writing "Writing not parsed correctly"
}
test "fails for unrecognized value" {
Expect.throwsT<ArgumentException>
(fun () -> ignore (PersonGroup.Parse "Fan")) "Invalid value should have raised an exception"
}
]
testList "ToString" [
test "Administration succeeds" {
Expect.equal (string Administration) "Administration" "Administration string incorrect"
}
test "Audio Post-Production succeeds" {
Expect.equal (string AudioPostProduction) "Audio Post-Production" "Audio Post-Production string incorrect"
}
test "Audio Production succeeds" {
Expect.equal (string AudioProduction) "Audio Production" "Audio Production string incorrect"
}
test "Cast succeeds" {
Expect.equal (string Cast) "Cast" "Cast string incorrect"
}
test "Community succeeds" {
Expect.equal (string Community) "Community" "Community string incorrect"
}
test "Creative Direction succeeds" {
Expect.equal (string CreativeDirection) "Creative Direction" "Creative Direction string incorrect"
}
test "Misc succeeds" {
Expect.equal (string Misc) "Misc." "Misc string incorrect"
}
test "Video Post-Production succeeds" {
Expect.equal (string VideoPostProduction) "Video Post-Production" "Video Post-Production string incorrect"
}
test "Video Production succeeds" {
Expect.equal (string VideoProduction) "Video Production" "Video Production string incorrect"
}
test "Visuals succeeds" {
Expect.equal (string Visuals) "Visuals" "Visuals string incorrect"
}
test "Writing succeeds" {
Expect.equal (string Writing) "Writing" "Writing string incorrect"
}
]
]
/// Unit tests for the PodcastMedium type
let podcastMediumTests = testList "PodcastMedium" [
testList "Parse" [
@@ -840,6 +927,7 @@ let all = testList "SupportTypes" [
openGraphVideoTests
openGraphTypeTests
openGraphPropertiesTests
personGroupTests
podcastMediumTests
postStatusTests
customFeedSourceTests
+1 -1
View File
@@ -13,7 +13,7 @@ module Dashboard =
// GET /admin/dashboard
let user : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let getCount (f: WebLogId -> Task<int>) = f ctx.WebLog.Id
let data = ctx.Data
+1 -1
View File
@@ -88,7 +88,7 @@ let private generateViewContext messages viewCtx (ctx: HttpContext) =
CurrentPage = ctx.Request.Path.Value[1..]
Messages = messages
Generator = ctx.Generator
IsAuthor = ctx.HasAccessLevel Author
IsAuthor = ctx.HasAccessLevel ContentAuthor
IsEditor = ctx.HasAccessLevel Editor
IsWebLogAdmin = ctx.HasAccessLevel WebLogAdmin
IsAdministrator = ctx.HasAccessLevel Administrator }
+10 -10
View File
@@ -8,7 +8,7 @@ open MyWebLog.ViewModels
// GET /admin/pages
// GET /admin/pages/page/{pageNbr}
let all pageNbr : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let! pages = ctx.Data.Page.FindPageOfPages ctx.WebLog.Id pageNbr
let displayPages =
@@ -24,7 +24,7 @@ let all pageNbr : HttpHandler =
// GET /admin/page/{id}/edit
let edit pgId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let! result = task {
match pgId with
@@ -57,7 +57,7 @@ let delete pgId : HttpHandler =
// GET /admin/page/{id}/permalinks
let editPermalinks pgId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
| Some pg when canEdit pg.AuthorId ctx ->
@@ -71,7 +71,7 @@ let editPermalinks pgId : HttpHandler =
// POST /admin/page/permalinks
let savePermalinks : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
let! model = ctx.BindFormAsync<ManagePermalinksModel>()
@@ -90,7 +90,7 @@ let savePermalinks : HttpHandler =
// GET /admin/page/{id}/revisions
let editRevisions pgId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
| Some pg when canEdit pg.AuthorId ctx ->
@@ -104,7 +104,7 @@ let editRevisions pgId : HttpHandler =
// DELETE /admin/page/{id}/revisions
let purgeRevisions pgId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let data = ctx.Data
match! data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
@@ -128,7 +128,7 @@ let private findPageRevision pgId revDate (ctx: HttpContext) = task {
// GET /admin/page/{id}/revision/{revision-date}/preview
let previewRevision (pgId, revDate) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! findPageRevision pgId revDate ctx with
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
@@ -139,7 +139,7 @@ let previewRevision (pgId, revDate) : HttpHandler =
// POST /admin/page/{id}/revision/{revision-date}/restore
let restoreRevision (pgId, revDate) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
match! findPageRevision pgId revDate ctx with
@@ -157,7 +157,7 @@ let restoreRevision (pgId, revDate) : HttpHandler =
// DELETE /admin/page/{id}/revision/{revision-date}
let deleteRevision (pgId, revDate) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! findPageRevision pgId revDate ctx with
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
@@ -171,7 +171,7 @@ let deleteRevision (pgId, revDate) : HttpHandler =
// POST /admin/page/save
let save : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditPageModel>()
+14 -14
View File
@@ -253,7 +253,7 @@ let chapters (post: Post) : HttpHandler = fun next ctx ->
// GET /admin/posts
// GET /admin/posts/page/{pageNbr}
let all pageNbr : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let data = ctx.Data
let! posts = data.Post.FindPageOfPosts ctx.WebLog.Id pageNbr 25
@@ -263,7 +263,7 @@ let all pageNbr : HttpHandler =
// GET /admin/post/{id}/edit
let edit postId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let data = ctx.Data
let! result = task {
@@ -302,7 +302,7 @@ let delete postId : HttpHandler =
// GET /admin/post/{id}/permalinks
let editPermalinks postId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
| Some post when canEdit post.AuthorId ctx ->
@@ -316,7 +316,7 @@ let editPermalinks postId : HttpHandler =
// POST /admin/post/permalinks
let savePermalinks : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
let! model = ctx.BindFormAsync<ManagePermalinksModel>()
@@ -335,7 +335,7 @@ let savePermalinks : HttpHandler =
// GET /admin/post/{id}/revisions
let editRevisions postId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
| Some post when canEdit post.AuthorId ctx ->
@@ -349,7 +349,7 @@ let editRevisions postId : HttpHandler =
// DELETE /admin/post/{id}/revisions
let purgeRevisions postId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let data = ctx.Data
match! data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
@@ -374,7 +374,7 @@ let private findPostRevision postId revDate (ctx: HttpContext) = task {
// GET /admin/post/{id}/revision/{revision-date}/preview
let previewRevision (postId, revDate) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! findPostRevision postId revDate ctx with
| Some post, Some rev when canEdit post.AuthorId ctx ->
@@ -385,7 +385,7 @@ let previewRevision (postId, revDate) : HttpHandler =
// POST /admin/post/{id}/revision/{revision-date}/restore
let restoreRevision (postId, revDate) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
match! findPostRevision postId revDate ctx with
@@ -403,7 +403,7 @@ let restoreRevision (postId, revDate) : HttpHandler =
// DELETE /admin/post/{id}/revision/{revision-date}
let deleteRevision (postId, revDate) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! findPostRevision postId revDate ctx with
| Some post, Some rev when canEdit post.AuthorId ctx ->
@@ -417,7 +417,7 @@ let deleteRevision (postId, revDate) : HttpHandler =
// GET /admin/post/{id}/chapters
let manageChapters postId : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with
| Some post
@@ -432,7 +432,7 @@ let manageChapters postId : HttpHandler =
// GET /admin/post/{id}/chapter/{idx}
let editChapter (postId, index) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with
| Some post
@@ -455,7 +455,7 @@ let editChapter (postId, index) : HttpHandler =
// POST /admin/post/{id}/chapter/{idx}
let saveChapter (postId, index) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
let data = ctx.Data
@@ -488,7 +488,7 @@ let saveChapter (postId, index) : HttpHandler =
// DELETE /admin/post/{id}/chapter/{idx}
let deleteChapter (postId, index) : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let data = ctx.Data
match! data.Post.FindById (PostId postId) ctx.WebLog.Id with
@@ -512,7 +512,7 @@ let deleteChapter (postId, index) : HttpHandler =
// POST /admin/post/save
let save : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditPostModel>()
+3 -3
View File
@@ -91,7 +91,7 @@ let makeSlug it = (Regex """\s+""").Replace((Regex "[^A-z0-9 -]").Replace(it, ""
// GET /admin/uploads
let list : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
let webLog = ctx.WebLog
let! dbUploads = ctx.Data.Upload.FindByWebLog webLog.Id
@@ -127,12 +127,12 @@ let list : HttpHandler =
// GET /admin/upload/new
let showNew : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> adminPage "Upload a File" next ctx Views.WebLog.uploadNew
// POST /admin/upload/save
let save : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
if ctx.Request.HasFormContentType && ctx.Request.Form.Files.Count > 0 then
+2 -2
View File
@@ -142,7 +142,7 @@ let delete userId : HttpHandler =
// GET /admin/my-info
let myInfo : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> fun next ctx -> task {
match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
| Some user ->
@@ -154,7 +154,7 @@ let myInfo : HttpHandler =
// POST /admin/my-info
let saveMyInfo : HttpHandler =
requireAccess Author
requireAccess ContentAuthor
>=> validateCsrf
>=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditMyInfoModel>()
+1 -1
View File
@@ -133,7 +133,7 @@ let userList (model: WebLogUser list) app =
| Administrator -> span [ _class $"{badge}-success" ] [ raw "ADMINISTRATOR" ]
| WebLogAdmin -> span [ _class $"{badge}-primary" ] [ raw "WEB LOG ADMIN" ]
| Editor -> span [ _class $"{badge}-secondary" ] [ raw "EDITOR" ]
| Author -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
| ContentAuthor -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
br []
if app.IsAdministrator || (app.IsWebLogAdmin && not (user.AccessLevel = Administrator)) then
let userUrl = relUrl app $"admin/settings/user/{user.Id}"