Version 3 #40
@ -17,16 +17,20 @@ let private fromShortGuid (it : string) =
|
||||
|
||||
/// Support functions for citizen IDs
|
||||
module CitizenId =
|
||||
|
||||
/// Create a new citizen ID
|
||||
let create () = (Guid.NewGuid >> CitizenId) ()
|
||||
|
||||
/// A string representation of a citizen ID
|
||||
let toString = function CitizenId it -> toShortGuid it
|
||||
|
||||
/// Parse a string into a citizen ID
|
||||
let ofString = fromShortGuid >> CitizenId
|
||||
|
||||
|
||||
/// Support functions for citizens
|
||||
module Citizen =
|
||||
|
||||
/// Get the name of the citizen (the first of real name, display name, or handle that is filled in)
|
||||
let name x =
|
||||
[ x.realName; x.displayName; Some x.mastodonUser ]
|
||||
@ -36,34 +40,46 @@ module Citizen =
|
||||
|
||||
/// Support functions for continent IDs
|
||||
module ContinentId =
|
||||
|
||||
/// Create a new continent ID
|
||||
let create () = (Guid.NewGuid >> ContinentId) ()
|
||||
|
||||
/// A string representation of a continent ID
|
||||
let toString = function ContinentId it -> toShortGuid it
|
||||
|
||||
/// Parse a string into a continent ID
|
||||
let ofString = fromShortGuid >> ContinentId
|
||||
|
||||
|
||||
/// Support functions for listing IDs
|
||||
module ListingId =
|
||||
|
||||
/// Create a new job listing ID
|
||||
let create () = (Guid.NewGuid >> ListingId) ()
|
||||
|
||||
/// A string representation of a listing ID
|
||||
let toString = function ListingId it -> toShortGuid it
|
||||
|
||||
/// Parse a string into a listing ID
|
||||
let ofString = fromShortGuid >> ListingId
|
||||
|
||||
|
||||
/// Support functions for Markdown strings
|
||||
module MarkdownString =
|
||||
|
||||
/// The Markdown conversion pipeline (enables all advanced features)
|
||||
let private pipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build ()
|
||||
|
||||
/// Convert this Markdown string to HTML
|
||||
let toHtml = function Text text -> Markdown.ToHtml (text, pipeline)
|
||||
|
||||
/// Convert a Markdown string to its string representation
|
||||
let toString = function Text text -> text
|
||||
|
||||
|
||||
/// Support functions for Profiles
|
||||
module Profile =
|
||||
|
||||
// An empty profile
|
||||
let empty =
|
||||
{ id = CitizenId Guid.Empty
|
||||
@ -79,21 +95,28 @@ module Profile =
|
||||
skills = []
|
||||
}
|
||||
|
||||
|
||||
/// Support functions for skill IDs
|
||||
module SkillId =
|
||||
|
||||
/// Create a new skill ID
|
||||
let create () = (Guid.NewGuid >> SkillId) ()
|
||||
|
||||
/// A string representation of a skill ID
|
||||
let toString = function SkillId it -> toShortGuid it
|
||||
|
||||
/// Parse a string into a skill ID
|
||||
let ofString = fromShortGuid >> SkillId
|
||||
|
||||
|
||||
/// Support functions for success report IDs
|
||||
module SuccessId =
|
||||
|
||||
/// Create a new success report ID
|
||||
let create () = (Guid.NewGuid >> SuccessId) ()
|
||||
|
||||
/// A string representation of a success report ID
|
||||
let toString = function SuccessId it -> toShortGuid it
|
||||
|
||||
/// Parse a string into a success report ID
|
||||
let ofString = fromShortGuid >> SuccessId
|
||||
|
@ -11,16 +11,22 @@ open NodaTime
|
||||
type ListingForm =
|
||||
{ /// The ID of the listing
|
||||
id : string
|
||||
|
||||
/// The listing title
|
||||
title : string
|
||||
|
||||
/// The ID of the continent on which this opportunity exists
|
||||
continentId : string
|
||||
|
||||
/// The region in which this opportunity exists
|
||||
region : string
|
||||
|
||||
/// Whether this is a remote work opportunity
|
||||
remoteWork : bool
|
||||
|
||||
/// The text of the job listing
|
||||
text : string
|
||||
|
||||
/// The date by which this job listing is needed
|
||||
neededBy : string option
|
||||
}
|
||||
@ -30,6 +36,7 @@ type ListingForm =
|
||||
type ListingForView =
|
||||
{ /// The listing itself
|
||||
listing : Listing
|
||||
|
||||
/// The continent for that listing
|
||||
continent : Continent
|
||||
}
|
||||
@ -39,6 +46,7 @@ type ListingForView =
|
||||
type ListingExpireForm =
|
||||
{ /// Whether the job was filled from here
|
||||
fromHere : bool
|
||||
|
||||
/// The success story written by the user
|
||||
successStory : string option
|
||||
}
|
||||
@ -49,10 +57,13 @@ type ListingExpireForm =
|
||||
type ListingSearch =
|
||||
{ /// Retrieve job listings for this continent
|
||||
continentId : string option
|
||||
|
||||
/// Text for a search within a region
|
||||
region : string option
|
||||
|
||||
/// Whether to retrieve job listings for remote work
|
||||
remoteWork : string
|
||||
|
||||
/// Text for a search with the job listing description
|
||||
text : string option
|
||||
}
|
||||
@ -62,8 +73,10 @@ type ListingSearch =
|
||||
type LogOnSuccess =
|
||||
{ /// The JSON Web Token (JWT) to use for API access
|
||||
jwt : string
|
||||
|
||||
/// The ID of the logged-in citizen (as a string)
|
||||
citizenId : string
|
||||
|
||||
/// The name of the logged-in citizen
|
||||
name : string
|
||||
}
|
||||
@ -78,30 +91,41 @@ type Count =
|
||||
|
||||
/// An instance of a Mastodon server which is configured to work with Jobs, Jobs, Jobs
|
||||
type MastodonInstance () =
|
||||
|
||||
/// The name of the instance
|
||||
member val Name = "" with get, set
|
||||
|
||||
/// The URL for this instance
|
||||
member val Url = "" with get, set
|
||||
|
||||
/// The abbreviation used in the URL to distinguish this instance's return codes
|
||||
member val Abbr = "" with get, set
|
||||
|
||||
/// The client ID (assigned by the Mastodon server)
|
||||
member val ClientId = "" with get, set
|
||||
|
||||
/// The cryptographic secret (provided by the Mastodon server)
|
||||
member val Secret = "" with get, set
|
||||
|
||||
/// Whether the instance is currently enabled
|
||||
member val IsEnabled = true with get, set
|
||||
|
||||
/// If an instance is disabled, the reason for it being disabled
|
||||
member val Reason = "" with get, set
|
||||
|
||||
|
||||
/// The authorization options for Jobs, Jobs, Jobs
|
||||
type AuthOptions () =
|
||||
|
||||
/// The host for the return URL for Mastodon verification
|
||||
member val ReturnHost = "" with get, set
|
||||
|
||||
/// The secret with which the server signs the JWTs for auth once we've verified with Mastodon
|
||||
member val ServerSecret = "" with get, set
|
||||
|
||||
/// The instances configured for use
|
||||
member val Instances = Array.empty<MastodonInstance> with get, set
|
||||
|
||||
interface IOptions<AuthOptions> with
|
||||
override this.Value = this
|
||||
|
||||
@ -110,14 +134,19 @@ type AuthOptions () =
|
||||
type Instance =
|
||||
{ /// The name of the instance
|
||||
name : string
|
||||
|
||||
/// The URL for this instance
|
||||
url : string
|
||||
|
||||
/// The abbreviation used in the URL to distinguish this instance's return codes
|
||||
abbr : string
|
||||
|
||||
/// The client ID (assigned by the Mastodon server)
|
||||
clientId : string
|
||||
|
||||
/// Whether this instance is currently enabled
|
||||
isEnabled : bool
|
||||
|
||||
/// If not enabled, the reason the instance is disabled
|
||||
reason : string
|
||||
}
|
||||
@ -127,8 +156,10 @@ type Instance =
|
||||
type SkillForm =
|
||||
{ /// The ID of this skill
|
||||
id : string
|
||||
|
||||
/// The description of the skill
|
||||
description : string
|
||||
|
||||
/// Notes regarding the skill
|
||||
notes : string option
|
||||
}
|
||||
@ -138,30 +169,40 @@ type SkillForm =
|
||||
type ProfileForm =
|
||||
{ /// Whether the citizen to whom this profile belongs is actively seeking employment
|
||||
isSeekingEmployment : bool
|
||||
|
||||
/// Whether this profile should appear in the public search
|
||||
isPublic : bool
|
||||
|
||||
/// The user's real name
|
||||
realName : string
|
||||
|
||||
/// The ID of the continent on which the citizen is located
|
||||
continentId : string
|
||||
|
||||
/// The area within that continent where the citizen is located
|
||||
region : string
|
||||
|
||||
/// If the citizen is available for remote work
|
||||
remoteWork : bool
|
||||
|
||||
/// If the citizen is seeking full-time employment
|
||||
fullTime : bool
|
||||
|
||||
/// The user's professional biography
|
||||
biography : string
|
||||
|
||||
/// The user's past experience
|
||||
experience : string option
|
||||
|
||||
/// The skills for the user
|
||||
skills : SkillForm list
|
||||
}
|
||||
|
||||
/// Support functions for the ProfileForm type
|
||||
module ProfileForm =
|
||||
/// Create an instance of this form from the given profile
|
||||
let fromProfile (profile : Types.Profile) =
|
||||
|
||||
/// Create an instance of this form from the given profile
|
||||
let fromProfile (profile : Types.Profile) =
|
||||
{ isSeekingEmployment = profile.seekingEmployment
|
||||
isPublic = profile.isPublic
|
||||
realName = ""
|
||||
@ -169,8 +210,8 @@ module ProfileForm =
|
||||
region = profile.region
|
||||
remoteWork = profile.remoteWork
|
||||
fullTime = profile.fullTime
|
||||
biography = match profile.biography with Text bio -> bio
|
||||
experience = profile.experience |> Option.map (fun x -> match x with Text exp -> exp)
|
||||
biography = MarkdownString.toString profile.biography
|
||||
experience = profile.experience |> Option.map MarkdownString.toString
|
||||
skills = profile.skills
|
||||
|> List.map (fun s ->
|
||||
{ id = string s.id
|
||||
@ -185,10 +226,13 @@ module ProfileForm =
|
||||
type ProfileSearch =
|
||||
{ /// Retrieve citizens from this continent
|
||||
continentId : string option
|
||||
|
||||
/// Text for a search within a citizen's skills
|
||||
skill : string option
|
||||
|
||||
/// Text for a search with a citizen's professional biography and experience fields
|
||||
bioExperience : string option
|
||||
|
||||
/// Whether to retrieve citizens who do or do not want remote work
|
||||
remoteWork : string
|
||||
}
|
||||
@ -198,14 +242,19 @@ type ProfileSearch =
|
||||
type ProfileSearchResult =
|
||||
{ /// The ID of the citizen
|
||||
citizenId : CitizenId
|
||||
|
||||
/// The citizen's display name
|
||||
displayName : string
|
||||
|
||||
/// Whether this citizen is currently seeking employment
|
||||
seekingEmployment : bool
|
||||
|
||||
/// Whether this citizen is looking for remote work
|
||||
remoteWork : bool
|
||||
|
||||
/// Whether this citizen is looking for full-time work
|
||||
fullTime : bool
|
||||
|
||||
/// When this profile was last updated
|
||||
lastUpdatedOn : Instant
|
||||
}
|
||||
@ -215,8 +264,10 @@ type ProfileSearchResult =
|
||||
type ProfileForView =
|
||||
{ /// The profile itself
|
||||
profile : Profile
|
||||
|
||||
/// The citizen to whom the profile belongs
|
||||
citizen : Citizen
|
||||
|
||||
/// The continent for the profile
|
||||
continent : Continent
|
||||
}
|
||||
@ -227,10 +278,13 @@ type ProfileForView =
|
||||
type PublicSearch =
|
||||
{ /// Retrieve citizens from this continent
|
||||
continentId : string option
|
||||
|
||||
/// Retrieve citizens from this region
|
||||
region : string option
|
||||
|
||||
/// Text for a search within a citizen's skills
|
||||
skill : string option
|
||||
|
||||
/// Whether to retrieve citizens who do or do not want remote work
|
||||
remoteWork : string
|
||||
}
|
||||
@ -250,10 +304,13 @@ module PublicSearch =
|
||||
type PublicSearchResult =
|
||||
{ /// The name of the continent on which the citizen resides
|
||||
continent : string
|
||||
|
||||
/// The region in which the citizen resides
|
||||
region : string
|
||||
|
||||
/// Whether this citizen is seeking remote work
|
||||
remoteWork : bool
|
||||
|
||||
/// The skills this citizen has identified
|
||||
skills : string list
|
||||
}
|
||||
@ -263,8 +320,10 @@ type PublicSearchResult =
|
||||
type StoryForm =
|
||||
{ /// The ID of this story
|
||||
id : string
|
||||
|
||||
/// Whether the employment was obtained from Jobs, Jobs, Jobs
|
||||
fromHere : bool
|
||||
|
||||
/// The success story
|
||||
story : string
|
||||
}
|
||||
@ -274,14 +333,19 @@ type StoryForm =
|
||||
type StoryEntry =
|
||||
{ /// The ID of this success story
|
||||
id : SuccessId
|
||||
|
||||
/// The ID of the citizen who recorded this story
|
||||
citizenId : CitizenId
|
||||
|
||||
/// The name of the citizen who recorded this story
|
||||
citizenName : string
|
||||
|
||||
/// When this story was recorded
|
||||
recordedOn : Instant
|
||||
|
||||
/// Whether this story involves an opportunity that arose due to Jobs, Jobs, Jobs
|
||||
fromHere : bool
|
||||
|
||||
/// Whether this report has a further story, or if it is simply a "found work" entry
|
||||
hasStory : bool
|
||||
}
|
||||
|
@ -14,18 +14,25 @@ type CitizenId = CitizenId of Guid
|
||||
type Citizen =
|
||||
{ /// The ID of the user
|
||||
id : CitizenId
|
||||
|
||||
/// The Mastodon instance abbreviation from which this citizen is authorized
|
||||
instance : string
|
||||
|
||||
/// The handle by which the user is known on Mastodon
|
||||
mastodonUser : string
|
||||
|
||||
/// The user's display name from Mastodon (updated every login)
|
||||
displayName : string option
|
||||
|
||||
/// The user's real name
|
||||
realName : string option
|
||||
|
||||
/// The URL for the user's Mastodon profile
|
||||
profileUrl : string
|
||||
|
||||
/// When the user joined Jobs, Jobs, Jobs
|
||||
joinedOn : Instant
|
||||
|
||||
/// When the user last logged in
|
||||
lastSeenOn : Instant
|
||||
}
|
||||
@ -39,6 +46,7 @@ type ContinentId = ContinentId of Guid
|
||||
type Continent =
|
||||
{ /// The ID of the continent
|
||||
id : ContinentId
|
||||
|
||||
/// The name of the continent
|
||||
name : string
|
||||
}
|
||||
@ -56,26 +64,37 @@ type ListingId = ListingId of Guid
|
||||
type Listing =
|
||||
{ /// The ID of the job listing
|
||||
id : ListingId
|
||||
|
||||
/// The ID of the citizen who posted the job listing
|
||||
citizenId : CitizenId
|
||||
|
||||
/// When this job listing was created
|
||||
createdOn : Instant
|
||||
|
||||
/// The short title of the job listing
|
||||
title : string
|
||||
|
||||
/// The ID of the continent on which the job is located
|
||||
continentId : ContinentId
|
||||
|
||||
/// The region in which the job is located
|
||||
region : string
|
||||
|
||||
/// Whether this listing is for remote work
|
||||
remoteWork : bool
|
||||
|
||||
/// Whether this listing has expired
|
||||
isExpired : bool
|
||||
|
||||
/// When this listing was last updated
|
||||
updatedOn : Instant
|
||||
|
||||
/// The details of this job
|
||||
text : MarkdownString
|
||||
|
||||
/// When this job needs to be filled
|
||||
neededBy : LocalDate option
|
||||
|
||||
/// Was this job filled as part of its appearance on Jobs, Jobs, Jobs?
|
||||
wasFilledHere : bool option
|
||||
}
|
||||
@ -88,8 +107,10 @@ type SkillId = SkillId of Guid
|
||||
type Skill =
|
||||
{ /// The ID of the skill
|
||||
id : SkillId
|
||||
|
||||
/// A description of the skill
|
||||
description : string
|
||||
|
||||
/// Notes regarding this skill (level, duration, etc.)
|
||||
notes : string option
|
||||
}
|
||||
@ -100,24 +121,34 @@ type Skill =
|
||||
type Profile =
|
||||
{ /// The ID of the citizen to whom this profile belongs
|
||||
id : CitizenId
|
||||
|
||||
/// Whether this citizen is actively seeking employment
|
||||
seekingEmployment : bool
|
||||
|
||||
/// Whether this citizen allows their profile to be a part of the publicly-viewable, anonymous data
|
||||
isPublic : 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
|
||||
remoteWork : bool
|
||||
|
||||
/// Whether the citizen is looking for full-time work
|
||||
fullTime : bool
|
||||
|
||||
/// The citizen's professional biography
|
||||
biography : MarkdownString
|
||||
|
||||
/// When the citizen last updated their profile
|
||||
lastUpdatedOn : Instant
|
||||
|
||||
/// The citizen's experience (topical / chronological)
|
||||
experience : MarkdownString option
|
||||
|
||||
/// Skills this citizen possesses
|
||||
skills : Skill list
|
||||
}
|
||||
@ -130,14 +161,19 @@ type SuccessId = SuccessId of Guid
|
||||
type Success =
|
||||
{ /// The ID of the success report
|
||||
id : SuccessId
|
||||
|
||||
/// The ID of the citizen who wrote this success report
|
||||
citizenId : CitizenId
|
||||
|
||||
/// When this success report was recorded
|
||||
recordedOn : Instant
|
||||
|
||||
/// Whether the success was due, at least in part, to Jobs, Jobs, Jobs
|
||||
fromHere : bool
|
||||
|
||||
/// The source of this success (listing or profile)
|
||||
source : string
|
||||
|
||||
/// The success story
|
||||
story : MarkdownString option
|
||||
}
|
||||
|
@ -31,8 +31,7 @@ module Converters =
|
||||
type MarkdownStringJsonConverter() =
|
||||
inherit JsonConverter<MarkdownString>()
|
||||
override _.WriteJson(writer : JsonWriter, value : MarkdownString, _ : JsonSerializer) =
|
||||
let (Text text) = value
|
||||
writer.WriteValue text
|
||||
writer.WriteValue (MarkdownString.toString value)
|
||||
override _.ReadJson(reader: JsonReader, _ : Type, _ : MarkdownString, _ : bool, _ : JsonSerializer) =
|
||||
(string >> Text) reader.Value
|
||||
|
||||
@ -75,16 +74,22 @@ module Converters =
|
||||
/// Table names
|
||||
[<RequireQualifiedAccess>]
|
||||
module Table =
|
||||
|
||||
/// The user (citizen of Gitmo Nation) table
|
||||
let Citizen = "citizen"
|
||||
|
||||
/// The continent table
|
||||
let Continent = "continent"
|
||||
|
||||
/// The job listing table
|
||||
let Listing = "listing"
|
||||
|
||||
/// The citizen employment profile table
|
||||
let Profile = "profile"
|
||||
|
||||
/// The success story table
|
||||
let Success = "success"
|
||||
|
||||
/// All tables
|
||||
let all () = [ Citizen; Continent; Listing; Profile; Success ]
|
||||
|
||||
@ -166,7 +171,7 @@ module Startup =
|
||||
let! userIdx = fromTable Table.Citizen |> indexList |> result<string list> conn
|
||||
if not (List.contains "instanceUser" userIdx) then
|
||||
do! fromTable Table.Citizen
|
||||
|> indexCreateFunc "instanceUser" (fun row -> r.Array (row.G "instance", row.G "mastodonUser"))
|
||||
|> indexCreateFunc "instanceUser" (fun row -> [| row.G "instance"; row.G "mastodonUser" |])
|
||||
|> write conn
|
||||
}
|
||||
|
||||
@ -175,7 +180,21 @@ open JobsJobsJobs.Domain
|
||||
open JobsJobsJobs.Domain.SharedTypes
|
||||
|
||||
/// Sanitize user input, and create a "contains" pattern for use with RethinkDB queries
|
||||
let regexContains = System.Text.RegularExpressions.Regex.Escape >> sprintf "(?i)%s"
|
||||
let private regexContains = System.Text.RegularExpressions.Regex.Escape >> sprintf "(?i)%s"
|
||||
|
||||
/// Apply filters to a query, ensuring that types all match up
|
||||
let private applyFilters (filters : (ReqlExpr -> Filter) list) query : ReqlExpr =
|
||||
if List.isEmpty filters then
|
||||
query
|
||||
else
|
||||
let first = List.head filters query
|
||||
List.fold (fun q (f : ReqlExpr -> Filter) -> f q) first (List.tail filters)
|
||||
|
||||
/// Derive a user's display name from real name, display name, or handle (in that order)
|
||||
let private deriveDisplayName (it : ReqlExpr) =
|
||||
r.Branch (it.G("realName" ).Default_("").Ne "", it.G "realName",
|
||||
it.G("displayName").Default_("").Ne "", it.G "displayName",
|
||||
it.G "mastodonUser")
|
||||
|
||||
/// Profile data access functions
|
||||
[<RequireQualifiedAccess>]
|
||||
@ -209,75 +228,62 @@ module Profile =
|
||||
|
||||
/// Search profiles (logged-on users)
|
||||
let search (search : ProfileSearch) conn =
|
||||
(seq<ReqlExpr -> ReqlExpr> {
|
||||
match search.continentId with
|
||||
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
|
||||
| None -> ()
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
|
||||
match search.skill with
|
||||
| Some skl ->
|
||||
yield (fun q -> q.Filter (ReqlFunction1(fun it ->
|
||||
it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl))))))
|
||||
| None -> ()
|
||||
match search.bioExperience with
|
||||
| Some text ->
|
||||
let txt = regexContains text
|
||||
yield (fun q -> q.Filter (ReqlFunction1(fun it ->
|
||||
it.G("biography").Match(txt).Or (it.G("experience").Match txt))))
|
||||
| None -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|> List.fold
|
||||
(fun q f -> f q)
|
||||
(r.Table(Table.Profile)
|
||||
.EqJoin("id", r.Table Table.Citizen)
|
||||
.Without(r.HashMap ("right", "id"))
|
||||
.Zip () :> ReqlExpr))
|
||||
|> mergeFunc (fun it ->
|
||||
r.HashMap("displayName",
|
||||
r.Branch (it.G("realName" ).Default_("").Ne "", it.G "realName",
|
||||
it.G("displayName").Default_("").Ne "", it.G "displayName",
|
||||
it.G "mastodonUser"))
|
||||
.With ("citizenId", it.G "id"))
|
||||
fromTable Table.Profile
|
||||
|> eqJoin "id" (fromTable Table.Citizen)
|
||||
|> without [ "right.id" ]
|
||||
|> zip
|
||||
|> applyFilters
|
||||
[ match search.continentId with
|
||||
| Some contId -> yield filter {| continentId = ContinentId.ofString contId |}
|
||||
| None -> ()
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield filter {| remoteWork = search.remoteWork = "yes" |}
|
||||
match search.skill with
|
||||
| Some skl ->
|
||||
yield filterFunc (fun it ->
|
||||
it.G("skills").Contains (ReqlFunction1 (fun s -> s.G("description").Match (regexContains skl))))
|
||||
| None -> ()
|
||||
match search.bioExperience with
|
||||
| Some text ->
|
||||
let txt = regexContains text
|
||||
yield filterFunc (fun it -> it.G("biography").Match(txt).Or (it.G("experience").Match txt))
|
||||
| None -> ()
|
||||
]
|
||||
|> mergeFunc (fun it -> {| displayName = deriveDisplayName it; citizenId = it.G "id" |})
|
||||
|> pluck [ "citizenId"; "displayName"; "seekingEmployment"; "remoteWork"; "fullTime"; "lastUpdatedOn" ]
|
||||
|> orderByFunc (fun it -> it.G("displayName").Downcase ())
|
||||
|> result<ProfileSearchResult list> conn
|
||||
|
||||
// Search profiles (public)
|
||||
let publicSearch (search : PublicSearch) conn =
|
||||
(seq<ReqlExpr -> ReqlExpr> {
|
||||
match search.continentId with
|
||||
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
|
||||
| None -> ()
|
||||
match search.region with
|
||||
| Some reg ->
|
||||
yield (fun q -> q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg))))
|
||||
| None -> ()
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
|
||||
match search.skill with
|
||||
| Some skl ->
|
||||
yield (fun q -> q.Filter (ReqlFunction1 (fun it ->
|
||||
it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl))))))
|
||||
| None -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|> List.fold
|
||||
(fun q f -> f q)
|
||||
(r.Table(Table.Profile)
|
||||
.EqJoin("continentId", r.Table Table.Continent)
|
||||
.Without(r.HashMap ("right", "id"))
|
||||
.Zip()
|
||||
.Filter(r.HashMap ("isPublic", true))))
|
||||
fromTable Table.Profile
|
||||
|> eqJoin "continentId" (fromTable Table.Continent)
|
||||
|> without [ "right.id" ]
|
||||
|> zip
|
||||
|> applyFilters
|
||||
[ yield filter {| isPublic = true |}
|
||||
match search.continentId with
|
||||
| Some contId -> yield filter {| continentId = ContinentId.ofString contId |}
|
||||
| None -> ()
|
||||
match search.region with
|
||||
| Some reg -> yield filterFunc (fun it -> it.G("region").Match (regexContains reg))
|
||||
| None -> ()
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield filter {| remoteWork = search.remoteWork = "yes" |}
|
||||
match search.skill with
|
||||
| Some skl ->
|
||||
yield filterFunc (fun it ->
|
||||
it.G("skills").Contains (ReqlFunction1 (fun s -> s.G("description").Match (regexContains skl))))
|
||||
| None -> ()
|
||||
]
|
||||
|> mergeFunc (fun it ->
|
||||
r.HashMap("skills",
|
||||
it.G("skills").Map (ReqlFunction1 (fun skill ->
|
||||
r.Branch(skill.G("notes").Default_("").Eq "", skill.G "description",
|
||||
skill.G("description").Add(" (").Add(skill.G("notes")).Add ")"))))
|
||||
.With("continent", it.G "name"))
|
||||
{| skills = it.G("skills").Map (ReqlFunction1 (fun skill ->
|
||||
r.Branch(skill.G("notes").Default_("").Eq "", skill.G "description",
|
||||
skill.G("description").Add(" (").Add(skill.G("notes")).Add ")")))
|
||||
continent = it.G "name"
|
||||
|})
|
||||
|> pluck [ "continent"; "region"; "skills"; "remoteWork" ]
|
||||
|> result<PublicSearchResult list> conn
|
||||
|
||||
@ -295,7 +301,7 @@ module Citizen =
|
||||
let findByMastodonUser (instance : string) (mastodonUser : string) conn = task {
|
||||
let! u =
|
||||
fromTable Table.Citizen
|
||||
|> getAllWithIndex [ r.Array (instance, mastodonUser) ] "instanceUser"
|
||||
|> getAllWithIndex [ [| instance; mastodonUser |] ] "instanceUser"
|
||||
|> limit 1
|
||||
|> result<Citizen list> conn
|
||||
return List.tryHead u
|
||||
@ -311,8 +317,7 @@ module Citizen =
|
||||
let logOnUpdate (citizen : Citizen) conn =
|
||||
fromTable Table.Citizen
|
||||
|> get citizen.id
|
||||
|> update (r.HashMap( nameof citizen.displayName, citizen.displayName)
|
||||
.With (nameof citizen.lastSeenOn, citizen.lastSeenOn))
|
||||
|> update {| displayName = citizen.displayName; lastSeenOn = citizen.lastSeenOn |}
|
||||
|> write conn
|
||||
|
||||
/// Delete a citizen
|
||||
@ -336,7 +341,7 @@ module Citizen =
|
||||
let realNameUpdate (citizenId : CitizenId) (realName : string option) conn =
|
||||
fromTable Table.Citizen
|
||||
|> get citizenId
|
||||
|> update (r.HashMap (nameof realName, realName))
|
||||
|> update {| realName = realName |}
|
||||
|> write conn
|
||||
|
||||
|
||||
@ -362,12 +367,15 @@ module Listing =
|
||||
|
||||
open NodaTime
|
||||
|
||||
/// Convert a joined query to the form needed for ListingForView deserialization
|
||||
let private toListingForView (it : ReqlExpr) : obj = {| listing = it.G "left"; continent = it.G "right" |}
|
||||
|
||||
/// Find all job listings posted by the given citizen
|
||||
let findByCitizen (citizenId : CitizenId) conn =
|
||||
fromTable Table.Listing
|
||||
|> getAllWithIndex [ citizenId ] (nameof citizenId)
|
||||
|> eqJoin "continentId" (fromTable Table.Continent)
|
||||
|> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))
|
||||
|> mapFunc toListingForView
|
||||
|> result<ListingForView list> conn
|
||||
|
||||
/// Find a listing by its ID
|
||||
@ -380,9 +388,9 @@ module Listing =
|
||||
let findByIdForView (listingId : ListingId) conn = task {
|
||||
let! listing =
|
||||
fromTable Table.Listing
|
||||
|> filter (r.HashMap ("id", listingId))
|
||||
|> filter {| id = listingId |}
|
||||
|> eqJoin "continentId" (fromTable Table.Continent)
|
||||
|> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))
|
||||
|> mapFunc toListingForView
|
||||
|> result<ListingForView list> conn
|
||||
return List.tryHead listing
|
||||
}
|
||||
@ -404,36 +412,29 @@ module Listing =
|
||||
let expire (listingId : ListingId) (fromHere : bool) (now : Instant) conn =
|
||||
(fromTable Table.Listing
|
||||
|> get listingId)
|
||||
.Update (r.HashMap("isExpired", true).With("wasFilledHere", fromHere).With ("updatedOn", now))
|
||||
.Update {| isExpired = true; wasFilledHere = fromHere; updatedOn = now |}
|
||||
|> write conn
|
||||
|
||||
/// Search job listings
|
||||
let search (search : ListingSearch) conn =
|
||||
(seq<ReqlExpr -> ReqlExpr> {
|
||||
match search.continentId with
|
||||
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
|
||||
| None -> ()
|
||||
match search.region with
|
||||
| Some rgn ->
|
||||
yield (fun q ->
|
||||
q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.region).Match (regexContains rgn))))
|
||||
| None -> ()
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
|
||||
match search.text with
|
||||
| Some text ->
|
||||
yield (fun q ->
|
||||
q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.text).Match (regexContains text))))
|
||||
| None -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|> List.fold
|
||||
(fun q f -> f q)
|
||||
(fromTable Table.Listing
|
||||
|> getAllWithIndex [ false ] "isExpired" :> ReqlExpr))
|
||||
fromTable Table.Listing
|
||||
|> getAllWithIndex [ false ] "isExpired"
|
||||
|> applyFilters
|
||||
[ match search.continentId with
|
||||
| Some contId -> yield filter {| continentId = ContinentId.ofString contId |}
|
||||
| None -> ()
|
||||
match search.region with
|
||||
| Some rgn -> yield filterFunc (fun it -> it.G(nameof search.region).Match (regexContains rgn))
|
||||
| None -> ()
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield filter {| remoteWork = search.remoteWork = "yes" |}
|
||||
match search.text with
|
||||
| Some text -> yield filterFunc (fun it -> it.G(nameof search.text).Match (regexContains text))
|
||||
| None -> ()
|
||||
]
|
||||
|> eqJoin "continentId" (fromTable Table.Continent)
|
||||
|> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))
|
||||
|> mapFunc toListingForView
|
||||
|> result<ListingForView list> conn
|
||||
|
||||
|
||||
@ -456,16 +457,11 @@ module Success =
|
||||
|
||||
// Retrieve all success stories
|
||||
let all conn =
|
||||
(fromTable Table.Success
|
||||
|> eqJoin "citizenId" (fromTable Table.Citizen))
|
||||
.Without(r.HashMap ("right", "id"))
|
||||
fromTable Table.Success
|
||||
|> eqJoin "citizenId" (fromTable Table.Citizen)
|
||||
|> without [ "right.id" ]
|
||||
|> zip
|
||||
|> mergeFunc (fun it ->
|
||||
r.HashMap("citizenName",
|
||||
r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName",
|
||||
it.G("displayName").Default_("").Ne "", it.G "displayName",
|
||||
it.G "mastodonUser"))
|
||||
.With ("hasStory", it.G("story").Default_("").Gt ""))
|
||||
|> mergeFunc (fun it -> {| citizenName = deriveDisplayName it; hasStory = it.G("story").Default_("").Gt "" |})
|
||||
|> pluck [ "id"; "citizenId"; "citizenName"; "recordedOn"; "fromHere"; "hasStory" ]
|
||||
|> orderByDescending "recordedOn"
|
||||
|> result<StoryEntry list> conn
|
||||
|
Loading…
Reference in New Issue
Block a user