From 93da2831cb58d8cfd25898d8f8212a83deb00283 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 19 Jan 2023 23:04:41 -0500 Subject: [PATCH] Add employment history to profile type (#39) - Add edit profile "menu" page - Fix build errors with migration after repo reorg --- src/JobsJobsJobs/Common/Domain.fs | 85 +++++++++++++------ .../JobsJobsJobs.V3Migration.fsproj | 2 +- .../JobsJobsJobs.V3Migration/Program.fs | 15 ++-- src/JobsJobsJobs/Profiles/Handlers.fs | 21 +++-- src/JobsJobsJobs/Profiles/Views.fs | 43 +++++++++- 5 files changed, 126 insertions(+), 40 deletions(-) diff --git a/src/JobsJobsJobs/Common/Domain.fs b/src/JobsJobsJobs/Common/Domain.fs index 858c57f..6bfa848 100644 --- a/src/JobsJobsJobs/Common/Domain.fs +++ b/src/JobsJobsJobs/Common/Domain.fs @@ -44,6 +44,55 @@ module ContinentId = let value = function ContinentId guid -> guid +/// A string of Markdown text +type MarkdownString = Text of string + +/// Support functions for Markdown strings +module MarkdownString = + + open Markdig + + /// 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 + + +/// An employment history entry +[] +type EmploymentHistory = + { /// The employer for this period of employment + Employer : string + + /// The date employment started + StartDate : LocalDate + + /// The date employment ended (None implies ongoing employment) + EndDate : LocalDate option + + /// The title / position held + Position : string option + + /// A description of the duties entailed during this employment + Description : MarkdownString option + } + +/// Support functions for employment history entries +module EmploymentHistory = + + let empty = + { Employer = "" + StartDate = LocalDate.MinIsoValue + EndDate = None + Position = None + Description = None + } + + /// The ID of a job listing type ListingId = ListingId of Guid @@ -63,24 +112,6 @@ module ListingId = let value = function ListingId guid -> guid -/// A string of Markdown text -type MarkdownString = Text of string - -/// Support functions for Markdown strings -module MarkdownString = - - open Markdig - - /// 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 - - /// Types of contacts supported by Jobs, Jobs, Jobs type ContactType = /// E-mail addresses @@ -133,7 +164,7 @@ type Skill = Description : string /// Notes regarding this skill (level, duration, etc.) - Notes : string option + Notes : string option } @@ -363,14 +394,17 @@ type Profile = /// The citizen's professional biography Biography : MarkdownString - /// When the citizen last updated their profile - LastUpdatedOn : Instant + /// Skills this citizen possesses + Skills : Skill list + /// The citizen's employment history + History : EmploymentHistory list + /// The citizen's experience (topical / chronological) Experience : MarkdownString option - /// Skills this citizen possesses - Skills : Skill list + /// When the citizen last updated their profile + LastUpdatedOn : Instant /// Whether this is a legacy profile IsLegacy : bool @@ -390,9 +424,10 @@ module Profile = IsRemote = false IsFullTime = false Biography = Text "" - LastUpdatedOn = Instant.MinValue - Experience = None Skills = [] + History = [] + Experience = None + LastUpdatedOn = Instant.MinValue IsLegacy = false } diff --git a/src/JobsJobsJobs/JobsJobsJobs.V3Migration/JobsJobsJobs.V3Migration.fsproj b/src/JobsJobsJobs/JobsJobsJobs.V3Migration/JobsJobsJobs.V3Migration.fsproj index c56b51a..f9e84bf 100644 --- a/src/JobsJobsJobs/JobsJobsJobs.V3Migration/JobsJobsJobs.V3Migration.fsproj +++ b/src/JobsJobsJobs/JobsJobsJobs.V3Migration/JobsJobsJobs.V3Migration.fsproj @@ -15,7 +15,7 @@ - + diff --git a/src/JobsJobsJobs/JobsJobsJobs.V3Migration/Program.fs b/src/JobsJobsJobs/JobsJobsJobs.V3Migration/Program.fs index d79562d..ae3148b 100644 --- a/src/JobsJobsJobs/JobsJobsJobs.V3Migration/Program.fs +++ b/src/JobsJobsJobs/JobsJobsJobs.V3Migration/Program.fs @@ -42,7 +42,8 @@ module Rethink = /// Shorthand for the RethinkDB R variable (how every command starts) let r = RethinkDb.Driver.RethinkDB.R -open JobsJobsJobs.Data +open JobsJobsJobs +open JobsJobsJobs.Common.Data open JobsJobsJobs.Domain open Newtonsoft.Json.Linq open NodaTime.Text @@ -63,8 +64,8 @@ task { // Establish database connections let cfg = ConfigurationBuilder().AddJsonFile("appsettings.json").Build () use rethinkConn = Rethink.Startup.createConnection (cfg.GetConnectionString "RethinkDB") - do! DataConnection.setUp cfg - let pgConn = DataConnection.dataSource () + do! setUp cfg + let pgConn = dataSource () let getOld table = fromTable table @@ -88,7 +89,7 @@ task { IsLegacy = true }) for citizen in newCitizens do - do! Citizens.save citizen + do! Citizens.Data.save citizen let! _ = pgConn |> Sql.executeTransactionAsync [ @@ -148,7 +149,7 @@ task { IsLegacy = true }) for profile in newProfiles do - do! Profiles.save profile + do! Profiles.Data.save profile printfn $"** Migrated {List.length newProfiles} profiles" // Migrate listings @@ -179,7 +180,7 @@ task { IsLegacy = true }) for listing in newListings do - do! Listings.save listing + do! Listings.Data.save listing printfn $"** Migrated {List.length newListings} listings" // Migrate success stories @@ -196,7 +197,7 @@ task { Story = if isNull story then None else Some (Text story) }) for success in newSuccesses do - do! Successes.save success + do! SuccessStories.Data.save success printfn $"** Migrated {List.length newSuccesses} successes" // Delete any citizens who have no profile, no listing, and no success story recorded diff --git a/src/JobsJobsJobs/Profiles/Handlers.fs b/src/JobsJobsJobs/Profiles/Handlers.fs index 8a32e05..0518deb 100644 --- a/src/JobsJobsJobs/Profiles/Handlers.fs +++ b/src/JobsJobsJobs/Profiles/Handlers.fs @@ -15,13 +15,21 @@ let delete : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task // GET: /profile/edit let edit : HttpHandler = requireUser >=> fun next ctx -> task { + let citizenId = currentCitizenId ctx + let! profile = Data.findById citizenId + let display = match profile with Some p -> p | None -> { Profile.empty with Id = citizenId } + return! Views.edit display |> render "Employment Profile" next ctx +} + +// GET: /profile/edit/general +let editGeneralInfo : HttpHandler = requireUser >=> fun next ctx -> task { let citizenId = currentCitizenId ctx let! profile = Data.findById citizenId let! continents = Common.Data.Continents.all () let isNew = Option.isNone profile let form = if isNew then EditProfileForm.empty else EditProfileForm.fromProfile profile.Value let title = $"""{if isNew then "Create" else "Edit"} Profile""" - return! Views.edit form continents isNew citizenId (csrf ctx) |> render title next ctx + return! Views.editGeneralInfo form continents isNew citizenId (csrf ctx) |> render title next ctx } // POST: /profile/save @@ -66,7 +74,7 @@ let save : HttpHandler = requireUser >=> fun next ctx -> task { do! addErrors errors ctx let! continents = Common.Data.Continents.all () return! - Views.edit form continents isNew citizenId (csrf ctx) + Views.editGeneralInfo form continents isNew citizenId (csrf ctx) |> render $"""{if isNew then "Create" else "Edit"} Profile""" next ctx } @@ -123,10 +131,11 @@ open Giraffe.EndpointRouting let endpoints = subRoute "/profile" [ GET_HEAD [ - routef "/%O/view" view - route "/edit" edit - route "/search" search - route "/seeking" seeking + routef "/%O/view" view + route "/edit" edit + route "/edit/general" editGeneralInfo + route "/search" search + route "/seeking" seeking ] POST [ route "/delete" delete diff --git a/src/JobsJobsJobs/Profiles/Views.fs b/src/JobsJobsJobs/Profiles/Views.fs index 4937f95..49c91c9 100644 --- a/src/JobsJobsJobs/Profiles/Views.fs +++ b/src/JobsJobsJobs/Profiles/Views.fs @@ -7,6 +7,47 @@ open JobsJobsJobs.Common.Views open JobsJobsJobs.Domain open JobsJobsJobs.Profiles.Domain +/// The profile edit menu page +let edit (profile : Profile) = + let hasProfile = profile.Region <> "" + pageWithTitle "Employment Profile" [ + p [] [ txt "There are three different sections to the employment profile." ] + ul [] [ + li [ _class "mb-2" ] [ + a [ _href $"/profile/edit/general" ] [ strong [] [ txt "General Information" ] ]; br [] + txt "contains your location, professional biography, and information about the type of employment you " + txt "may be seeking." + if not hasProfile then txt " Entering information here will create your profile." + ] + if hasProfile then + li [ _class "mb-2" ] [ + let skillCount = List.length profile.Skills + a [ _href $"/profile/edit/skills" ] [ strong [] [ txt "Skills" ] ]; br [] + txt "is where you can list skills you have acquired through education or experience." + em [] [ + txt $" (Your profile currently lists {skillCount} skill"; if skillCount <> 1 then txt "s" + txt ".)" + ] + ] + li [ _class "mb-2" ] [ + let historyCount = List.length profile.History + a [ _href $"/profile/edit/history" ] [ strong [] [ txt "Employment History" ] ]; br [] + txt "is where you can record a chronological history of your employment." + em [] [ + txt $" (Your profile contains {historyCount} employment history entr" + txt (if historyCount <> 1 then "ies" else "y"); txt ".)" + ] + ] + ] + if hasProfile then + p [] [ + a [ _class "btn btn-primary"; _href $"/profile/{CitizenId.toString profile.Id}/view" ] [ + i [ _class "mdi mdi-file-account-outline" ] []; txt "  View Your User Profile" + ] + ] + ] + + /// Render the skill edit template and existing skills let skillEdit (skills : SkillForm array) = let mapToInputs (idx : int) (skill : SkillForm) = @@ -38,7 +79,7 @@ let skillEdit (skills : SkillForm array) = :: (skills |> Array.mapi mapToInputs |> List.ofArray) /// The profile edit page -let edit (m : EditProfileForm) continents isNew citizenId csrf = +let editGeneralInfo (m : EditProfileForm) continents isNew citizenId csrf = pageWithTitle "My Employment Profile" [ form [ _class "row g-3"; _action "/profile/save"; _hxPost "/profile/save" ] [ antiForgery csrf