Version 3 #40

Merged
danieljsummers merged 67 commits from version-2-3 into main 2023-02-02 23:47:28 +00:00
6 changed files with 170 additions and 167 deletions
Showing only changes of commit 45b115418f - Show all commits

View File

@ -410,7 +410,7 @@
<hr> <hr>
<p class="fst-italic"> <p class="fst-italic">
Changes on August 30<sup>th</sup>, 2022 &ndash; Changes on August 30<sup>th</sup>, 2022
</p> </p>
<ul> <ul>
<li class="fst-italic">Removed references to Mastodon</li> <li class="fst-italic">Removed references to Mastodon</li>

View File

@ -335,7 +335,7 @@ module Listings =
/// Map a result for a listing view /// Map a result for a listing view
let private toListingForView row = let private toListingForView row =
{ listing = toDocument<Listing> row; continent = toDocumentFrom<Continent> "cont_data" row } { Listing = toDocument<Listing> row; Continent = toDocumentFrom<Continent> "cont_data" row }
/// Find all job listings posted by the given citizen /// Find all job listings posted by the given citizen
let findByCitizen citizenId = let findByCitizen citizenId =
@ -369,15 +369,15 @@ module Listings =
/// Search job listings /// Search job listings
let search (search : ListingSearch) = let search (search : ListingSearch) =
let searches = [ let searches = [
match search.continentId with match search.ContinentId with
| Some contId -> "l.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ] | Some contId -> "l.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ]
| None -> () | None -> ()
match search.region with match search.Region with
| Some region -> "l.data ->> 'region' ILIKE @region", [ "@region", like region ] | Some region -> "l.data ->> 'region' ILIKE @region", [ "@region", like region ]
| None -> () | None -> ()
if search.remoteWork <> "" then if search.RemoteWork <> "" then
"l.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.remoteWork = "yes") ] "l.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ]
match search.text with match search.Text with
| Some text -> "l.data ->> 'text' ILIKE @text", [ "@text", like text ] | Some text -> "l.data ->> 'text' ILIKE @text", [ "@text", like text ]
| None -> () | None -> ()
] ]
@ -431,9 +431,9 @@ module Profiles =
AND p.data ->> 'isLegacy' = 'false'" AND p.data ->> 'isLegacy' = 'false'"
|> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ] |> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ]
|> Sql.executeAsync (fun row -> |> Sql.executeAsync (fun row ->
{ profile = toDocument<Profile> row { Profile = toDocument<Profile> row
citizen = toDocumentFrom<Citizen> "cit_data" row Citizen = toDocumentFrom<Citizen> "cit_data" row
continent = toDocumentFrom<Continent> "cont_data" row Continent = toDocumentFrom<Continent> "cont_data" row
}) })
return List.tryHead tryCitizen return List.tryHead tryCitizen
} }
@ -445,15 +445,15 @@ module Profiles =
/// Search profiles (logged-on users) /// Search profiles (logged-on users)
let search (search : ProfileSearch) = backgroundTask { let search (search : ProfileSearch) = backgroundTask {
let searches = [ let searches = [
match search.continentId with match search.ContinentId with
| Some contId -> "p.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ] | Some contId -> "p.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ]
| None -> () | None -> ()
if search.remoteWork <> "" then if search.RemoteWork <> "" then
"p.data ->> 'remoteWork' = @remote", [ "@remote", jsonBool (search.remoteWork = "yes") ] "p.data ->> 'remoteWork' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ]
match search.skill with match search.Skill with
| Some skl -> "p.data -> 'skills' ->> 'description' ILIKE @description", [ "@description", like skl ] | Some skl -> "p.data -> 'skills' ->> 'description' ILIKE @description", [ "@description", like skl ]
| None -> () | None -> ()
match search.bioExperience with match search.BioExperience with
| Some text -> | Some text ->
"(p.data ->> 'biography' ILIKE @text OR p.data ->> 'experience' ILIKE @text)", "(p.data ->> 'biography' ILIKE @text OR p.data ->> 'experience' ILIKE @text)",
[ "@text", Sql.string text ] [ "@text", Sql.string text ]
@ -471,28 +471,28 @@ module Profiles =
|> Sql.executeAsync (fun row -> |> Sql.executeAsync (fun row ->
let profile = toDocument<Profile> row let profile = toDocument<Profile> row
let citizen = toDocumentFrom<Citizen> "cit_data" row let citizen = toDocumentFrom<Citizen> "cit_data" row
{ citizenId = profile.Id { CitizenId = profile.Id
displayName = Citizen.name citizen DisplayName = Citizen.name citizen
seekingEmployment = profile.IsSeekingEmployment SeekingEmployment = profile.IsSeekingEmployment
remoteWork = profile.IsRemote RemoteWork = profile.IsRemote
fullTime = profile.IsFullTime FullTime = profile.IsFullTime
lastUpdatedOn = profile.LastUpdatedOn LastUpdatedOn = profile.LastUpdatedOn
}) })
return results |> List.sortBy (fun psr -> psr.displayName.ToLowerInvariant ()) return results |> List.sortBy (fun psr -> psr.DisplayName.ToLowerInvariant ())
} }
// Search profiles (public) // Search profiles (public)
let publicSearch (search : PublicSearch) = let publicSearch (search : PublicSearch) =
let searches = [ let searches = [
match search.continentId with match search.ContinentId with
| Some contId -> "p.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ] | Some contId -> "p.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ]
| None -> () | None -> ()
match search.region with match search.Region with
| Some region -> "p.data ->> 'region' ILIKE @region", [ "@region", like region ] | Some region -> "p.data ->> 'region' ILIKE @region", [ "@region", like region ]
| None -> () | None -> ()
if search.remoteWork <> "" then if search.RemoteWork <> "" then
"p.data ->> 'remoteWork' = @remote", [ "@remote", jsonBool (search.remoteWork = "yes") ] "p.data ->> 'remoteWork' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ]
match search.skill with match search.Skill with
| Some skl -> | Some skl ->
"p.data -> 'skills' ->> 'description' ILIKE @description", [ "@description", like skl ] "p.data -> 'skills' ->> 'description' ILIKE @description", [ "@description", like skl ]
| None -> () | None -> ()
@ -508,10 +508,10 @@ module Profiles =
|> Sql.executeAsync (fun row -> |> Sql.executeAsync (fun row ->
let profile = toDocument<Profile> row let profile = toDocument<Profile> row
let continent = toDocumentFrom<Continent> "cont_data" row let continent = toDocumentFrom<Continent> "cont_data" row
{ continent = continent.Name { Continent = continent.Name
region = profile.Region Region = profile.Region
remoteWork = profile.IsRemote RemoteWork = profile.IsRemote
skills = profile.Skills Skills = profile.Skills
|> List.map (fun s -> |> List.map (fun s ->
let notes = match s.Notes with Some n -> $" ({n})" | None -> "" let notes = match s.Notes with Some n -> $" ({n})" | None -> ""
$"{s.Description}{notes}") $"{s.Description}{notes}")
@ -532,12 +532,12 @@ module Successes =
|> Sql.executeAsync (fun row -> |> Sql.executeAsync (fun row ->
let success = toDocument<Success> row let success = toDocument<Success> row
let citizen = toDocumentFrom<Citizen> "cit_data" row let citizen = toDocumentFrom<Citizen> "cit_data" row
{ id = success.Id { Id = success.Id
citizenId = success.CitizenId CitizenId = success.CitizenId
citizenName = Citizen.name citizen CitizenName = Citizen.name citizen
recordedOn = success.RecordedOn RecordedOn = success.RecordedOn
fromHere = success.IsFromHere FromHere = success.IsFromHere
hasStory = Option.isSome success.Story HasStory = Option.isSome success.Story
}) })
/// Find a success story by its ID /// Find a success story by its ID

View File

@ -5,8 +5,6 @@ open JobsJobsJobs.Domain
open Microsoft.Extensions.Options open Microsoft.Extensions.Options
open NodaTime open NodaTime
// fsharplint:disable FieldNames
/// The data required to register a new citizen (user) /// The data required to register a new citizen (user)
type CitizenRegistrationForm = type CitizenRegistrationForm =
{ /// The first name of the new citizen { /// The first name of the new citizen
@ -31,45 +29,45 @@ type CitizenRegistrationForm =
/// The data required to add or edit a job listing /// The data required to add or edit a job listing
type ListingForm = type ListingForm =
{ /// The ID of the listing { /// The ID of the listing
id : string Id : string
/// The listing title /// The listing title
title : string Title : string
/// The ID of the continent on which this opportunity exists /// The ID of the continent on which this opportunity exists
continentId : string ContinentId : string
/// The region in which this opportunity exists /// The region in which this opportunity exists
region : string Region : string
/// Whether this is a remote work opportunity /// Whether this is a remote work opportunity
remoteWork : bool RemoteWork : bool
/// The text of the job listing /// The text of the job listing
text : string Text : string
/// The date by which this job listing is needed /// The date by which this job listing is needed
neededBy : string option NeededBy : string option
} }
/// The data needed to display a listing /// The data needed to display a listing
type ListingForView = type ListingForView =
{ /// The listing itself { /// The listing itself
listing : Listing Listing : Listing
/// The continent for that listing /// The continent for that listing
continent : Continent Continent : Continent
} }
/// The form submitted to expire a listing /// The form submitted to expire a listing
type ListingExpireForm = type ListingExpireForm =
{ /// Whether the job was filled from here { /// Whether the job was filled from here
fromHere : bool FromHere : bool
/// The success story written by the user /// The success story written by the user
successStory : string option SuccessStory : string option
} }
@ -77,46 +75,39 @@ type ListingExpireForm =
[<CLIMutable>] [<CLIMutable>]
type ListingSearch = type ListingSearch =
{ /// Retrieve job listings for this continent { /// Retrieve job listings for this continent
continentId : string option ContinentId : string option
/// Text for a search within a region /// Text for a search within a region
region : string option Region : string option
/// Whether to retrieve job listings for remote work /// Whether to retrieve job listings for remote work
remoteWork : string RemoteWork : string
/// Text for a search with the job listing description /// Text for a search with the job listing description
text : string option Text : string option
} }
/// The fields needed to log on to Jobs, Jobs, Jobs /// The fields needed to log on to Jobs, Jobs, Jobs
type LogOnForm = type LogOnForm =
{ /// The e-mail address for the citizen { /// The e-mail address for the citizen
email : string Email : string
/// The password provided by the user /// The password provided by the user
password : string Password : string
} }
/// A successful logon /// A successful logon
type LogOnSuccess = type LogOnSuccess =
{ /// The JSON Web Token (JWT) to use for API access { /// The JSON Web Token (JWT) to use for API access
jwt : string Jwt : string
/// The ID of the logged-in citizen (as a string) /// The ID of the logged-in citizen (as a string)
citizenId : string CitizenId : string
/// The name of the logged-in citizen /// The name of the logged-in citizen
name : string Name : string
}
/// A count
type Count =
{ // The count being returned
count : int64
} }
@ -133,44 +124,44 @@ type AuthOptions () =
/// The fields required for a skill /// The fields required for a skill
type SkillForm = type SkillForm =
{ /// The ID of this skill { /// The ID of this skill
id : string Id : string
/// The description of the skill /// The description of the skill
description : string Description : string
/// Notes regarding the skill /// Notes regarding the skill
notes : string option Notes : string option
} }
/// The data required to update a profile /// The data required to update a profile
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type ProfileForm = type ProfileForm =
{ /// Whether the citizen to whom this profile belongs is actively seeking employment { /// Whether the citizen to whom this profile belongs is actively seeking employment
isSeekingEmployment : bool IsSeekingEmployment : bool
/// Whether this profile should appear in the public search /// Whether this profile should appear in the public search
isPublic : bool IsPublic : bool
/// The ID of the continent on which the citizen is located /// The ID of the continent on which the citizen is located
continentId : string ContinentId : string
/// The area within that continent where the citizen is located /// The area within that continent where the citizen is located
region : string Region : string
/// If the citizen is available for remote work /// If the citizen is available for remote work
remoteWork : bool RemoteWork : bool
/// If the citizen is seeking full-time employment /// If the citizen is seeking full-time employment
fullTime : bool FullTime : bool
/// The user's professional biography /// The user's professional biography
biography : string Biography : string
/// The user's past experience /// The user's past experience
experience : string option Experience : string option
/// The skills for the user /// The skills for the user
skills : SkillForm list Skills : SkillForm list
} }
/// Support functions for the ProfileForm type /// Support functions for the ProfileForm type
@ -178,19 +169,19 @@ module ProfileForm =
/// Create an instance of this form from the given profile /// Create an instance of this form from the given profile
let fromProfile (profile : Profile) = let fromProfile (profile : Profile) =
{ isSeekingEmployment = profile.IsSeekingEmployment { IsSeekingEmployment = profile.IsSeekingEmployment
isPublic = profile.IsPubliclySearchable IsPublic = profile.IsPubliclySearchable
continentId = string profile.ContinentId ContinentId = string profile.ContinentId
region = profile.Region Region = profile.Region
remoteWork = profile.IsRemote RemoteWork = profile.IsRemote
fullTime = profile.IsFullTime FullTime = profile.IsFullTime
biography = MarkdownString.toString profile.Biography Biography = MarkdownString.toString profile.Biography
experience = profile.Experience |> Option.map MarkdownString.toString Experience = profile.Experience |> Option.map MarkdownString.toString
skills = profile.Skills Skills = profile.Skills
|> List.map (fun s -> |> List.map (fun s ->
{ id = string s.Id { Id = string s.Id
description = s.Description Description = s.Description
notes = s.Notes Notes = s.Notes
}) })
} }
@ -199,51 +190,51 @@ module ProfileForm =
[<CLIMutable>] [<CLIMutable>]
type ProfileSearch = type ProfileSearch =
{ /// Retrieve citizens from this continent { /// Retrieve citizens from this continent
continentId : string option ContinentId : string option
/// Text for a search within a citizen's skills /// Text for a search within a citizen's skills
skill : string option Skill : string option
/// Text for a search with a citizen's professional biography and experience fields /// Text for a search with a citizen's professional biography and experience fields
bioExperience : string option BioExperience : string option
/// Whether to retrieve citizens who do or do not want remote work /// Whether to retrieve citizens who do or do not want remote work
remoteWork : string RemoteWork : string
} }
/// A user matching the profile search /// A user matching the profile search
type ProfileSearchResult = type ProfileSearchResult =
{ /// The ID of the citizen { /// The ID of the citizen
citizenId : CitizenId CitizenId : CitizenId
/// The citizen's display name /// The citizen's display name
displayName : string DisplayName : string
/// Whether this citizen is currently seeking employment /// Whether this citizen is currently seeking employment
seekingEmployment : bool SeekingEmployment : bool
/// Whether this citizen is looking for remote work /// Whether this citizen is looking for remote work
remoteWork : bool RemoteWork : bool
/// Whether this citizen is looking for full-time work /// Whether this citizen is looking for full-time work
fullTime : bool FullTime : bool
/// When this profile was last updated /// When this profile was last updated
lastUpdatedOn : Instant LastUpdatedOn : Instant
} }
/// The data required to show a viewable profile /// The data required to show a viewable profile
type ProfileForView = type ProfileForView =
{ /// The profile itself { /// The profile itself
profile : Profile Profile : Profile
/// The citizen to whom the profile belongs /// The citizen to whom the profile belongs
citizen : Citizen Citizen : Citizen
/// The continent for the profile /// The continent for the profile
continent : Continent Continent : Continent
} }
@ -251,25 +242,26 @@ type ProfileForView =
[<CLIMutable>] [<CLIMutable>]
type PublicSearch = type PublicSearch =
{ /// Retrieve citizens from this continent { /// Retrieve citizens from this continent
continentId : string option ContinentId : string option
/// Retrieve citizens from this region /// Retrieve citizens from this region
region : string option Region : string option
/// Text for a search within a citizen's skills /// Text for a search within a citizen's skills
skill : string option Skill : string option
/// Whether to retrieve citizens who do or do not want remote work /// Whether to retrieve citizens who do or do not want remote work
remoteWork : string RemoteWork : string
} }
/// Support functions for public searches /// Support functions for public searches
module PublicSearch = module PublicSearch =
/// Is the search empty? /// Is the search empty?
let isEmptySearch (search : PublicSearch) = let isEmptySearch (search : PublicSearch) =
[ search.continentId [ search.ContinentId
search.skill search.Region
match search.remoteWork with "" -> Some search.remoteWork | _ -> None search.Skill
if search.RemoteWork = "" then Some search.RemoteWork else None
] ]
|> List.exists Option.isSome |> List.exists Option.isSome
@ -277,49 +269,49 @@ module PublicSearch =
/// A public profile search result /// A public profile search result
type PublicSearchResult = type PublicSearchResult =
{ /// The name of the continent on which the citizen resides { /// The name of the continent on which the citizen resides
continent : string Continent : string
/// The region in which the citizen resides /// The region in which the citizen resides
region : string Region : string
/// Whether this citizen is seeking remote work /// Whether this citizen is seeking remote work
remoteWork : bool RemoteWork : bool
/// The skills this citizen has identified /// The skills this citizen has identified
skills : string list Skills : string list
} }
/// The data required to provide a success story /// The data required to provide a success story
type StoryForm = type StoryForm =
{ /// The ID of this story { /// The ID of this story
id : string Id : string
/// Whether the employment was obtained from Jobs, Jobs, Jobs /// Whether the employment was obtained from Jobs, Jobs, Jobs
fromHere : bool FromHere : bool
/// The success story /// The success story
story : string Story : string
} }
/// An entry in the list of success stories /// An entry in the list of success stories
type StoryEntry = type StoryEntry =
{ /// The ID of this success story { /// The ID of this success story
id : SuccessId Id : SuccessId
/// The ID of the citizen who recorded this story /// The ID of the citizen who recorded this story
citizenId : CitizenId CitizenId : CitizenId
/// The name of the citizen who recorded this story /// The name of the citizen who recorded this story
citizenName : string CitizenName : string
/// When this story was recorded /// When this story was recorded
recordedOn : Instant RecordedOn : Instant
/// Whether this story involves an opportunity that arose due to Jobs, Jobs, Jobs /// Whether this story involves an opportunity that arose due to Jobs, Jobs, Jobs
fromHere : bool FromHere : bool
/// Whether this report has a further story, or if it is simply a "found work" entry /// Whether this report has a further story, or if it is simply a "found work" entry
hasStory : bool HasStory : bool
} }

View File

@ -78,10 +78,23 @@ module MarkdownString =
let toString = function Text text -> text let toString = function Text text -> text
/// 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
/// Another way to contact a citizen from this site /// Another way to contact a citizen from this site
type OtherContact = type OtherContact =
{ /// The name of the contact (Email, No Agenda Social, LinkedIn, etc.) { /// The type of contact
Name : string ContactType : ContactType
/// The name of the contact (Email, No Agenda Social, LinkedIn, etc.)
Name : string option
/// The value for the contact (e-mail address, user name, URL, etc.) /// The value for the contact (e-mail address, user name, URL, etc.)
Value : string Value : string

View File

@ -3,8 +3,6 @@
open NodaTime open NodaTime
open System open System
// fsharplint:disable FieldNames
/// A user of Jobs, Jobs, Jobs; a citizen of Gitmo Nation /// A user of Jobs, Jobs, Jobs; a citizen of Gitmo Nation
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Citizen = type Citizen =

View File

@ -144,26 +144,26 @@ module Citizen =
let confirmToken : HttpHandler = fun next ctx -> task { let confirmToken : HttpHandler = fun next ctx -> task {
let! form = ctx.BindJsonAsync<{| token : string |}> () let! form = ctx.BindJsonAsync<{| token : string |}> ()
let! valid = Citizens.confirmAccount form.token let! valid = Citizens.confirmAccount form.token
return! json {| valid = valid |} next ctx return! json {| Valid = valid |} next ctx
} }
// DELETE: /api/citizen/deny // DELETE: /api/citizen/deny
let denyToken : HttpHandler = fun next ctx -> task { let denyToken : HttpHandler = fun next ctx -> task {
let! form = ctx.BindJsonAsync<{| token : string |}> () let! form = ctx.BindJsonAsync<{| token : string |}> ()
let! valid = Citizens.denyAccount form.token let! valid = Citizens.denyAccount form.token
return! json {| valid = valid |} next ctx return! json {| Valid = valid |} next ctx
} }
// POST: /api/citizen/log-on // POST: /api/citizen/log-on
let logOn : HttpHandler = fun next ctx -> task { let logOn : HttpHandler = fun next ctx -> task {
let! form = ctx.BindJsonAsync<LogOnForm> () let! form = ctx.BindJsonAsync<LogOnForm> ()
match! Citizens.tryLogOn form.email form.password Auth.Passwords.verify Auth.Passwords.hash (now ctx) with match! Citizens.tryLogOn form.Email form.Password Auth.Passwords.verify Auth.Passwords.hash (now ctx) with
| Ok citizen -> | Ok citizen ->
return! return!
json json
{ jwt = Auth.createJwt citizen (authConfig ctx) { Jwt = Auth.createJwt citizen (authConfig ctx)
citizenId = CitizenId.toString citizen.Id CitizenId = CitizenId.toString citizen.Id
name = Citizen.name citizen Name = Citizen.name citizen
} next ctx } next ctx
| Error msg -> return! RequestErrors.BAD_REQUEST msg next ctx | Error msg -> return! RequestErrors.BAD_REQUEST msg next ctx
} }
@ -228,14 +228,14 @@ module Listing =
Id = ListingId.create () Id = ListingId.create ()
CitizenId = currentCitizenId ctx CitizenId = currentCitizenId ctx
CreatedOn = now CreatedOn = now
Title = form.title Title = form.Title
ContinentId = ContinentId.ofString form.continentId ContinentId = ContinentId.ofString form.ContinentId
Region = form.region Region = form.Region
IsRemote = form.remoteWork IsRemote = form.RemoteWork
IsExpired = false IsExpired = false
UpdatedOn = now UpdatedOn = now
Text = Text form.text Text = Text form.Text
NeededBy = (form.neededBy |> Option.map parseDate) NeededBy = (form.NeededBy |> Option.map parseDate)
WasFilledHere = None WasFilledHere = None
IsLegacy = false IsLegacy = false
} }
@ -250,12 +250,12 @@ module Listing =
let! form = ctx.BindJsonAsync<ListingForm> () let! form = ctx.BindJsonAsync<ListingForm> ()
do! Listings.save do! Listings.save
{ listing with { listing with
Title = form.title Title = form.Title
ContinentId = ContinentId.ofString form.continentId ContinentId = ContinentId.ofString form.ContinentId
Region = form.region Region = form.Region
IsRemote = form.remoteWork IsRemote = form.RemoteWork
Text = Text form.text Text = Text form.Text
NeededBy = form.neededBy |> Option.map parseDate NeededBy = form.NeededBy |> Option.map parseDate
UpdatedOn = now ctx UpdatedOn = now ctx
} }
return! ok next ctx return! ok next ctx
@ -272,16 +272,16 @@ module Listing =
do! Listings.save do! Listings.save
{ listing with { listing with
IsExpired = true IsExpired = true
WasFilledHere = Some form.fromHere WasFilledHere = Some form.FromHere
UpdatedOn = now UpdatedOn = now
} }
match form.successStory with match form.SuccessStory with
| Some storyText -> | Some storyText ->
do! Successes.save do! Successes.save
{ Id = SuccessId.create() { Id = SuccessId.create()
CitizenId = currentCitizenId ctx CitizenId = currentCitizenId ctx
RecordedOn = now RecordedOn = now
IsFromHere = form.fromHere IsFromHere = form.FromHere
Source = "listing" Source = "listing"
Story = (Text >> Some) storyText Story = (Text >> Some) storyText
} }
@ -328,7 +328,7 @@ module Profile =
// GET: /api/profile/count // GET: /api/profile/count
let count : HttpHandler = authorize >=> fun next ctx -> task { let count : HttpHandler = authorize >=> fun next ctx -> task {
let! theCount = Profiles.count () let! theCount = Profiles.count ()
return! json { count = theCount } next ctx return! json {| Count = theCount |} next ctx
} }
// POST: /api/profile/save // POST: /api/profile/save
@ -342,21 +342,21 @@ module Profile =
} }
do! Profiles.save do! Profiles.save
{ profile with { profile with
IsSeekingEmployment = form.isSeekingEmployment IsSeekingEmployment = form.IsSeekingEmployment
IsPubliclySearchable = form.isPublic IsPubliclySearchable = form.IsPublic
ContinentId = ContinentId.ofString form.continentId ContinentId = ContinentId.ofString form.ContinentId
Region = form.region Region = form.Region
IsRemote = form.remoteWork IsRemote = form.RemoteWork
IsFullTime = form.fullTime IsFullTime = form.FullTime
Biography = Text form.biography Biography = Text form.Biography
LastUpdatedOn = now ctx LastUpdatedOn = now ctx
Experience = noneIfBlank form.experience |> Option.map Text Experience = noneIfBlank form.Experience |> Option.map Text
Skills = form.skills Skills = form.Skills
|> List.map (fun s -> |> List.map (fun s ->
{ Id = if s.id.StartsWith "new" then SkillId.create () { Id = if s.Id.StartsWith "new" then SkillId.create ()
else SkillId.ofString s.id else SkillId.ofString s.Id
Description = s.description Description = s.Description
Notes = noneIfBlank s.notes Notes = noneIfBlank s.Notes
}) })
} }
return! ok next ctx return! ok next ctx
@ -414,21 +414,21 @@ module Success =
let citizenId = currentCitizenId ctx let citizenId = currentCitizenId ctx
let! form = ctx.BindJsonAsync<StoryForm> () let! form = ctx.BindJsonAsync<StoryForm> ()
let! success = task { let! success = task {
match form.id with match form.Id with
| "new" -> | "new" ->
return Some { Id = SuccessId.create () return Some { Id = SuccessId.create ()
CitizenId = citizenId CitizenId = citizenId
RecordedOn = now ctx RecordedOn = now ctx
IsFromHere = form.fromHere IsFromHere = form.FromHere
Source = "profile" Source = "profile"
Story = noneIfEmpty form.story |> Option.map Text Story = noneIfEmpty form.Story |> Option.map Text
} }
| successId -> | successId ->
match! Successes.findById (SuccessId.ofString successId) with match! Successes.findById (SuccessId.ofString successId) with
| Some story when story.CitizenId = citizenId -> | Some story when story.CitizenId = citizenId ->
return Some { story with return Some { story with
IsFromHere = form.fromHere IsFromHere = form.FromHere
Story = noneIfEmpty form.story |> Option.map Text Story = noneIfEmpty form.Story |> Option.map Text
} }
| Some _ | None -> return None | Some _ | None -> return None
} }