Add profile visibility, skill edit (#39)

- Add hover style for labels
This commit is contained in:
2023-01-20 20:44:10 -05:00
parent 93da2831cb
commit 2e0bfa5524
9 changed files with 381 additions and 228 deletions

View File

@@ -25,6 +25,34 @@ module CitizenId =
let value = function CitizenId guid -> guid
/// Types of contacts supported by Jobs, Jobs, Jobs
type ContactType =
/// E-mail addresses
| Email
/// Phone numbers (home, work, cell, etc.)
| Phone
/// Websites (personal, social, etc.)
| Website
/// Functions to support contact types
module ContactType =
/// Parse a contact type from a string
let parse typ =
match typ with
| "Email" -> Email
| "Phone" -> Phone
| "Website" -> Website
| it -> invalidOp $"{it} is not a valid contact type"
/// Convert a contact type to its string representation
let toString =
function
| Email -> "Email"
| Phone -> "Phone"
| Website -> "Website"
/// The ID of a continent
type ContinentId = ContinentId of Guid
@@ -112,34 +140,6 @@ module ListingId =
let value = function ListingId guid -> guid
/// Types of contacts supported by Jobs, Jobs, Jobs
type ContactType =
/// E-mail addresses
| Email
/// Phone numbers (home, work, cell, etc.)
| Phone
/// Websites (personal, social, etc.)
| Website
/// Functions to support contact types
module ContactType =
/// Parse a contact type from a string
let parse typ =
match typ with
| "Email" -> Email
| "Phone" -> Phone
| "Website" -> Website
| it -> invalidOp $"{it} is not a valid contact type"
/// Convert a contact type to its string representation
let toString =
function
| Email -> "Email"
| Phone -> "Phone"
| Website -> "Website"
/// Another way to contact a citizen from this site
[<NoComparison; NoEquality>]
type OtherContact =
@@ -157,6 +157,34 @@ type OtherContact =
}
/// Visibility options for an employment profile
type ProfileVisibility =
/// Profile is only visible to authenticated users
| Private
/// Anonymous information is visible to public users
| Anonymous
/// The full employment profile is visible to public users
| Public
/// Support functions for profile visibility
module ProfileVisibility =
/// Parse a string into a profile visibility
let parse viz =
match viz with
| "Private" -> Private
| "Anonymous" -> Anonymous
| "Public" -> Public
| it -> invalidOp $"{it} is not a valid profile visibility value"
/// Convert a profile visibility to its string representation
let toString =
function
| Private -> "Private"
| Anonymous -> "Anonymous"
| Public -> "Public"
/// A skill the job seeker possesses
[<NoComparison; NoEquality>]
type Skill =
@@ -370,25 +398,19 @@ type Profile =
{ /// The ID of the citizen to whom this profile belongs
Id : CitizenId
/// Whether this citizen is actively seeking employment
IsSeekingEmployment : bool
/// Whether this citizen allows their profile to be a part of the publicly-viewable, anonymous data
IsPubliclySearchable : bool
/// Whether this citizen allows their profile to be viewed via a public link
IsPubliclyLinkable : bool
/// The ID of the continent on which the citizen resides
ContinentId : ContinentId
/// The region in which the citizen resides
Region : string
/// Whether the citizen is looking for remote work
/// Whether this citizen is actively seeking employment
IsSeekingEmployment : bool
/// Whether the citizen is interested in remote work
IsRemote : bool
/// Whether the citizen is looking for full-time work
/// Whether the citizen is interested in full-time work
IsFullTime : bool
/// The citizen's professional biography
@@ -403,6 +425,9 @@ type Profile =
/// The citizen's experience (topical / chronological)
Experience : MarkdownString option
/// The visibility of this profile
Visibility : ProfileVisibility
/// When the citizen last updated their profile
LastUpdatedOn : Instant
@@ -415,20 +440,19 @@ module Profile =
// An empty profile
let empty = {
Id = CitizenId Guid.Empty
IsSeekingEmployment = false
IsPubliclySearchable = false
IsPubliclyLinkable = false
ContinentId = ContinentId Guid.Empty
Region = ""
IsRemote = false
IsFullTime = false
Biography = Text ""
Skills = []
History = []
Experience = None
LastUpdatedOn = Instant.MinValue
IsLegacy = false
Id = CitizenId Guid.Empty
ContinentId = ContinentId Guid.Empty
Region = ""
IsSeekingEmployment = false
IsRemote = false
IsFullTime = false
Biography = Text ""
Skills = []
History = []
Experience = None
Visibility = Private
LastUpdatedOn = Instant.MinValue
IsLegacy = false
}

View File

@@ -20,12 +20,12 @@ module Error =
open System.Net
/// Handler that will return a status code 404 and the text "Not Found"
let notFound : HttpHandler = fun next ctx ->
let notFound : HttpHandler = fun _ ctx ->
let fac = ctx.GetService<ILoggerFactory> ()
let log = fac.CreateLogger "Handler"
let path = string ctx.Request.Path
log.LogInformation "Returning 404"
RequestErrors.NOT_FOUND $"The URL {path} was not recognized as a valid URL" next ctx
RequestErrors.NOT_FOUND $"The URL {path} was not recognized as a valid URL" earlyReturn ctx
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response
@@ -78,19 +78,10 @@ let tryUser (ctx : HttpContext) =
|> Option.ofObj
|> Option.map (fun x -> x.Value)
/// Require a user to be logged in
let authorize : HttpHandler =
fun next ctx -> match tryUser ctx with Some _ -> next ctx | None -> Error.notAuthorized next ctx
/// Get the ID of the currently logged in citizen
// NOTE: if no one is logged in, this will raise an exception
let currentCitizenId ctx = (tryUser >> Option.get >> CitizenId.ofString) ctx
/// Return an empty OK response
let ok : HttpHandler = Successful.OK ""
// -- NEW --
let antiForgerySvc (ctx : HttpContext) =
ctx.RequestServices.GetRequiredService<IAntiforgery> ()
@@ -168,6 +159,16 @@ let render pageTitle (_ : HttpFunc) (ctx : HttpContext) content = task {
return! ctx.WriteHtmlViewAsync (renderFunc renderCtx)
}
let renderBare (_ : HttpFunc) (ctx : HttpContext) content =
({ IsLoggedOn = Option.isSome (tryUser ctx)
CurrentUrl = ctx.Request.Path.Value
PageTitle = ""
Content = content
Messages = []
} : Layout.PageRenderContext)
|> Layout.bare
|> ctx.WriteHtmlViewAsync
/// Render as a composable HttpHandler
let renderHandler pageTitle content : HttpHandler = fun next ctx ->
render pageTitle next ctx content
@@ -194,3 +195,7 @@ let redirectToGet (url : string) next ctx = task {
else RequestErrors.BAD_REQUEST "Invalid redirect URL"
return! action next ctx
}
/// Shorthand for Error.notFound for use in handler functions
let notFound ctx =
Error.notFound earlyReturn ctx

View File

@@ -19,13 +19,14 @@ open NodaTime.Serialization.SystemTextJson
/// JsonSerializer options that use the custom converters
let options =
let opts = JsonSerializerOptions ()
[ WrappedJsonConverter (CitizenId.ofString, CitizenId.toString) :> JsonConverter
WrappedJsonConverter (ContactType.parse, ContactType.toString)
WrappedJsonConverter (ContinentId.ofString, ContinentId.toString)
WrappedJsonConverter (ListingId.ofString, ListingId.toString)
WrappedJsonConverter (Text, MarkdownString.toString)
WrappedJsonConverter (SuccessId.ofString, SuccessId.toString)
JsonFSharpConverter ()
[ WrappedJsonConverter (CitizenId.ofString, CitizenId.toString) :> JsonConverter
WrappedJsonConverter (ContactType.parse, ContactType.toString)
WrappedJsonConverter (ContinentId.ofString, ContinentId.toString)
WrappedJsonConverter (ListingId.ofString, ListingId.toString)
WrappedJsonConverter (Text, MarkdownString.toString)
WrappedJsonConverter (ProfileVisibility.parse, ProfileVisibility.toString)
WrappedJsonConverter (SuccessId.ofString, SuccessId.toString)
JsonFSharpConverter ()
]
|> List.iter opts.Converters.Add
let _ = opts.ConfigureForNodaTime DateTimeZoneProviders.Tzdb

View File

@@ -352,3 +352,10 @@ module Layout =
]
]
]
/// Render a bare view (used for components)
let bare ctx =
html [ _lang "en" ] [
head [] [ title [] [] ]
body [] [ ctx.Content ]
]