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>
<p class="fst-italic">
Changes on August 30<sup>th</sup>, 2022 &ndash;
Changes on August 30<sup>th</sup>, 2022
</p>
<ul>
<li class="fst-italic">Removed references to Mastodon</li>

View File

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

View File

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

View File

@ -78,10 +78,23 @@ module MarkdownString =
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
type OtherContact =
{ /// The name of the contact (Email, No Agenda Social, LinkedIn, etc.)
Name : string
{ /// The type of contact
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.)
Value : string

View File

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

View File

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