Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b98d28adb4 | |||
| 60ed7e1e79 | |||
| fb147888c5 | |||
| 4a73927e64 |
@@ -1,3 +1,5 @@
|
||||
# jobs-jobs-jobs
|
||||
# Jobs, Jobs, Jobs <small>_(and Jobs - Let's Vote for Jobs!)_</small>
|
||||
|
||||
Repository for the development of No Agenda Jobs - currently parked [here](http://jobs.bitbadger.solutions)
|
||||
Source repository for **Jobs, Jobs, Jobs**, the jobs and career site for No Agenda nation.
|
||||
|
||||
What is No Agenda? [So glad you asked!](https://noagendashow.net)
|
||||
|
||||
2
src/.dockerignore
Normal file
2
src/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
**/bin
|
||||
**/obj
|
||||
10
src/Dockerfile
Normal file
10
src/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
WORKDIR /jjj
|
||||
COPY . ./
|
||||
WORKDIR /jjj/JobsJobsJobs/Server
|
||||
RUN dotnet publish JobsJobsJobs.Server.csproj -c Release /p:PublishProfile=Properties/PublishProfiles/FolderProfile.xml
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0
|
||||
WORKDIR /jjj
|
||||
COPY --from=build /jjj/JobsJobsJobs/Server/bin/Release/net5.0/linux-x64/publish/ ./
|
||||
ENTRYPOINT [ "/jjj/JobsJobsJobs.Server" ]
|
||||
@@ -11,8 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsJobsJobs.Shared", "Jobs
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{50B51580-9F09-41E2-BC78-DAD38C37B583}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.dockerignore = .dockerignore
|
||||
database\12-add-real-name.sql = database\12-add-real-name.sql
|
||||
JobsJobsJobs\Directory.Build.props = JobsJobsJobs\Directory.Build.props
|
||||
Dockerfile = Dockerfile
|
||||
database\tables.sql = database\tables.sql
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace JobsJobsJobs.Client
|
||||
/// <summary>
|
||||
/// The application version, as a nice display string
|
||||
/// </summary>
|
||||
public static Lazy<string> Version => new Lazy<string>(() =>
|
||||
public static Lazy<string> Version => new(() =>
|
||||
{
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||
var display = $"v{version.Major}.{version.Minor}";
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
<p>
|
||||
Your employment profile was last updated <FullDateTime TheDate=@Profile.LastUpdatedOn />. Your profile currently
|
||||
lists @Profile.Skills.Length skill@(Profile.Skills.Length != 1 ? "s" : "").
|
||||
lists @Profile.Skills.Count skill@(Profile.Skills.Count != 1 ? "s" : "").
|
||||
</p>
|
||||
<p><a href="/profile/view/@state.User.Id">View Your Employment Profile</a></p>
|
||||
@if (Profile.SeekingEmployment)
|
||||
@@ -41,7 +41,6 @@
|
||||
</Loading>
|
||||
<hr>
|
||||
<p>
|
||||
To see what is currently done, and how this application works, check out “How It Works” in the sidebar.
|
||||
The application now has 4 of 5 phases complete towards version 1.0; the documentation was last updated January
|
||||
31<sup>st</sup>, 2021.
|
||||
To see how this application works, check out “How It Works” in the sidebar (last updated June
|
||||
14<sup>th</sup>, 2021).
|
||||
</p>
|
||||
|
||||
@@ -4,18 +4,6 @@
|
||||
|
||||
<h3>How It Works</h3>
|
||||
|
||||
<p>
|
||||
Phase 4 is complete, which means that the entire logged-in version of the application is now available. There are
|
||||
GitHub issues for each one
|
||||
(<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/1" target="_blank">Phase 1</a> •
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/2" target="_blank">Phase 2</a> •
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/3" target="_blank">Phase 3</a> •
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/4" target="_blank">Phase 4</a>), and if you run into any
|
||||
issues with it, feel free to
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues" target="_blank">let us know on GitHub</a>, or look up
|
||||
@("@")danieljsummers on No Agenda Social.
|
||||
</p>
|
||||
|
||||
<h4>Completing Your Profile</h4>
|
||||
<ul>
|
||||
<li>
|
||||
@@ -44,11 +32,10 @@
|
||||
would like it presented to fellow citizens.
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/5" target="_blank">Phase 5</a> includes allowing
|
||||
public access to the continent, region, and skills fields of Gitmo Nation citizens who indicate that they are both
|
||||
seeking employment <strong>and</strong> want their information disclosed to public users. The “Allow my
|
||||
profile to be searched publicly” checkbox, at the bottom of the page where you edit your employment profile,
|
||||
is how you opt your profile in to this list.
|
||||
If you check the “Allow my profile to be searched publicly” checkbox, <strong>and</strong> you are
|
||||
seeking employment, your continent, region, and skills fields will be searchable and displayed to public users of
|
||||
the site. They will not be tied to your No Agenda Social handle or real name; they are there to let people peek
|
||||
behind the curtain a bit, and hopefully inspire them to join us.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -77,19 +64,18 @@
|
||||
to read it; if you submitted the story, there will also be an “Edit” link.
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
Publicly Available Information
|
||||
<em><small>(coming in <a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/5" target="_blank">Phase 5</a>)</small></em>
|
||||
</h4>
|
||||
<h4>Publicly Available Information</h4>
|
||||
<p>
|
||||
The “public” page for profile information will display the following information:
|
||||
The “Job Seekers” page for profile information will allow users to search for and display the continent,
|
||||
region, skills, and notes of users who are seeking employment <strong>and</strong> have opted in to their information
|
||||
being publicly searchable. If you are a public user, this information is always the latest we have; check out the link
|
||||
at the top of the search results for how you can learn more about these fine human resources!
|
||||
</p>
|
||||
<ul>
|
||||
<li>A count of profiles where the citizen is seeking employment</li>
|
||||
<li>The citizen’s continent and region</li>
|
||||
<li>The citizen’s skills and notes</li>
|
||||
</ul>
|
||||
|
||||
<h4>Help / Suggestions</h4>
|
||||
<p>
|
||||
This information will be pullled only from profiles where citizens have said can it be publicly available
|
||||
<strong>and</strong> are currently seeking employment.
|
||||
This is open-source software
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs" _target="_blank">developed on Github</a>; feel free to
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues" target="_blank">create an issue there</a>, or look up
|
||||
@("@")danieljsummers on No Agenda Social.
|
||||
</p>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<PageTitle Title="Welcome!" />
|
||||
|
||||
<p>
|
||||
Future home of No Agenda Jobs, where citizens of Gitmo Nation can assist one another in finding or enhancing their
|
||||
employment. This will enable them to continue providing value for value to Adam and John, as they continue their work
|
||||
Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in finding
|
||||
employment. This will enable them to continue providing value-for-value to Adam and John, as they continue their work
|
||||
deconstructing the misinformation that passes for news on a day-to-day basis.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -5,7 +5,6 @@ using Microsoft.AspNetCore.WebUtilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Client.Pages.Profiles
|
||||
@@ -56,10 +55,10 @@ namespace JobsJobsJobs.Client.Pages.Profiles
|
||||
{
|
||||
if (query.TryGetValue(part, out var partValue)) func(partValue);
|
||||
}
|
||||
setPart("ContinentId", x => Criteria.ContinentId = x);
|
||||
setPart("Skill", x => Criteria.Skill = x);
|
||||
setPart("BioExperience", x => Criteria.BioExperience = x);
|
||||
setPart("RemoteWork", x => Criteria.RemoteWork = x);
|
||||
setPart(nameof(Criteria.ContinentId), x => Criteria.ContinentId = x);
|
||||
setPart(nameof(Criteria.Skill), x => Criteria.Skill = x);
|
||||
setPart(nameof(Criteria.BioExperience), x => Criteria.BioExperience = x);
|
||||
setPart(nameof(Criteria.RemoteWork), x => Criteria.RemoteWork = x);
|
||||
|
||||
await RetrieveProfiles();
|
||||
}
|
||||
@@ -100,6 +99,11 @@ namespace JobsJobsJobs.Client.Pages.Profiles
|
||||
Searching = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a CSS class if the user is actively seeking work
|
||||
/// </summary>
|
||||
/// <param name="profile">The result in question</param>
|
||||
/// <returns>A string with the appropriate CSS class, if actively seeking work</returns>
|
||||
private static string? IsSeeking(ProfileSearchResult profile) =>
|
||||
profile.SeekingEmployment ? "font-weight-bold" : null;
|
||||
|
||||
@@ -124,10 +128,10 @@ namespace JobsJobsJobs.Client.Pages.Profiles
|
||||
if (!string.IsNullOrEmpty(func(Criteria))) dict.Add(name, func(Criteria));
|
||||
}
|
||||
|
||||
part("ContinentId", it => it.ContinentId);
|
||||
part("Skill", it => it.Skill);
|
||||
part("BioExperience", it => it.BioExperience);
|
||||
part("RemoteWork", it => it.RemoteWork);
|
||||
part(nameof(Criteria.ContinentId), it => it.ContinentId);
|
||||
part(nameof(Criteria.Skill), it => it.Skill);
|
||||
part(nameof(Criteria.BioExperience), it => it.BioExperience);
|
||||
part(nameof(Criteria.RemoteWork), it => it.RemoteWork);
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
62
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor
Normal file
62
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor
Normal file
@@ -0,0 +1,62 @@
|
||||
@page "/profile/seeking"
|
||||
@inject HttpClient http
|
||||
@inject NavigationManager nav
|
||||
@inject AppState state
|
||||
|
||||
<PageTitle Title="People Seeking Work" />
|
||||
<h3>People Seeking Work</h3>
|
||||
|
||||
<ErrorList Errors=@ErrorMessages>
|
||||
@if (Searching)
|
||||
{
|
||||
<p>Searching profiles...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Searched)
|
||||
{
|
||||
<p>Enter one or more criteria to filter results, or just click “Search” to list all profiles.</p>
|
||||
}
|
||||
<Collapsible HeaderText="Search Criteria" Collapsed=@(Searched && SearchResults.Any())>
|
||||
<PublicSearchForm Criteria=@Criteria OnSearch=@DoSearch Continents=@Continents />
|
||||
</Collapsible>
|
||||
<br>
|
||||
@if (SearchResults.Any())
|
||||
{
|
||||
<p>
|
||||
These profiles match your search criteria. To learn more about these people, join the merry band of human
|
||||
resources in the <a href="https://noagendashow.net" target="_blank">No Agenda</a> tribe!
|
||||
</p>
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Continent</th>
|
||||
<th scope="col" class="text-center">Region</th>
|
||||
<th scope="col" class="text-center">Remote?</th>
|
||||
<th scope="col" class="text-center">Skills</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var profile in SearchResults)
|
||||
{
|
||||
<tr>
|
||||
<td>@profile.Continent</td>
|
||||
<td>@profile.Region</td>
|
||||
<td class="text-center">@YesOrNo(profile.RemoteWork)</td>
|
||||
<td>
|
||||
@foreach (var skill in profile.Skills)
|
||||
{
|
||||
@skill.Replace(" ()", "")<br>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else if (Searched)
|
||||
{
|
||||
<p>No results found for the specified criteria</p>
|
||||
}
|
||||
}
|
||||
</ErrorList>
|
||||
134
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs
Normal file
134
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Client.Pages.Profiles
|
||||
{
|
||||
public partial class Seeking : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a search has been performed
|
||||
/// </summary>
|
||||
private bool Searched { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether a request for matching profiles is in progress
|
||||
/// </summary>
|
||||
private bool Searching { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The search criteria
|
||||
/// </summary>
|
||||
private PublicSearch Criteria { get; set; } = new PublicSearch();
|
||||
|
||||
/// <summary>
|
||||
/// Error messages encountered while searching for profiles
|
||||
/// </summary>
|
||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// All continents
|
||||
/// </summary>
|
||||
private IEnumerable<Continent> Continents { get; set; } = Enumerable.Empty<Continent>();
|
||||
|
||||
/// <summary>
|
||||
/// The search results
|
||||
/// </summary>
|
||||
private IEnumerable<PublicSearchResult> SearchResults { get; set; } = Enumerable.Empty<PublicSearchResult>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Continents = await state.GetContinents(http);
|
||||
|
||||
// Determine if we have searched before
|
||||
var query = QueryHelpers.ParseQuery(nav.ToAbsoluteUri(nav.Uri).Query);
|
||||
|
||||
if (query.TryGetValue("Searched", out var searched))
|
||||
{
|
||||
Searched = Convert.ToBoolean(searched);
|
||||
void setPart(string part, Action<string> func)
|
||||
{
|
||||
if (query.TryGetValue(part, out var partValue)) func(partValue);
|
||||
}
|
||||
setPart(nameof(Criteria.ContinentId), x => Criteria.ContinentId = x);
|
||||
setPart(nameof(Criteria.Region), x => Criteria.Region = x);
|
||||
setPart(nameof(Criteria.Skill), x => Criteria.Skill = x);
|
||||
setPart(nameof(Criteria.RemoteWork), x => Criteria.RemoteWork = x);
|
||||
|
||||
await RetrieveProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do a search
|
||||
/// </summary>
|
||||
/// <remarks>This navigates with the parameters in the URL; this should trigger a search</remarks>
|
||||
private async Task DoSearch()
|
||||
{
|
||||
var query = SearchQuery();
|
||||
query.Add("Searched", "True");
|
||||
nav.NavigateTo(QueryHelpers.AddQueryString("/profile/seeking", query));
|
||||
await RetrieveProfiles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retreive profiles matching the current search criteria
|
||||
/// </summary>
|
||||
private async Task RetrieveProfiles()
|
||||
{
|
||||
Searching = true;
|
||||
|
||||
var searchResult = await ServerApi.RetrieveMany<PublicSearchResult>(http,
|
||||
QueryHelpers.AddQueryString("profile/public-search", SearchQuery()));
|
||||
|
||||
if (searchResult.IsOk)
|
||||
{
|
||||
SearchResults = searchResult.Ok;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(searchResult.Error);
|
||||
}
|
||||
|
||||
Searched = true;
|
||||
Searching = false;
|
||||
}
|
||||
|
||||
private static string? IsSeeking(ProfileSearchResult profile) =>
|
||||
profile.SeekingEmployment ? "font-weight-bold" : null;
|
||||
|
||||
/// <summary>
|
||||
/// Return "Yes" for true and "No" for false
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition in question</param>
|
||||
/// <returns>"Yes" for true, "No" for false</returns>
|
||||
private static string YesOrNo(bool condition) => condition ? "Yes" : "No";
|
||||
|
||||
/// <summary>
|
||||
/// Create a search query string from the currently-entered criteria
|
||||
/// </summary>
|
||||
/// <returns>The query string for the currently-entered criteria</returns>
|
||||
private IDictionary<string, string?> SearchQuery()
|
||||
{
|
||||
var dict = new Dictionary<string, string?>();
|
||||
if (Criteria.IsEmptySearch) return dict;
|
||||
|
||||
void part(string name, Func<PublicSearch, string?> func)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(func(Criteria))) dict.Add(name, func(Criteria));
|
||||
}
|
||||
|
||||
part(nameof(Criteria.ContinentId), it => it.ContinentId);
|
||||
part(nameof(Criteria.Region), it => it.Region);
|
||||
part(nameof(Criteria.Skill), it => it.Skill);
|
||||
part(nameof(Criteria.RemoteWork), it => it.RemoteWork);
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
@if (Profile.Skills.Length > 0)
|
||||
@if (Profile.Skills.Count > 0)
|
||||
{
|
||||
<hr>
|
||||
<h4>Skills</h4>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/profile/seeking">
|
||||
<span class="oi oi-spreadsheet" aria-hidden="true"></span> Job Seekers
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<a class="nav-link" href="@AuthUrl">
|
||||
<span class="oi oi-account-login" aria-hidden="true"></span> Log On
|
||||
|
||||
60
src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor
Normal file
60
src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor
Normal file
@@ -0,0 +1,60 @@
|
||||
@using JobsJobsJobs.Shared.Api
|
||||
|
||||
<EditForm Model=@Criteria OnValidSubmit=@OnSearch>
|
||||
<div class="form-row">
|
||||
<div class="col col-12 col-sm-6 col-md-4 col-lg-3">
|
||||
<label for="continentId" class="jjj-label">Continent</label>
|
||||
<InputSelect id="continentId" @bind-Value=@Criteria.ContinentId class="form-control form-control-sm">
|
||||
<option value="">– Any –</option>
|
||||
@foreach (var (id, name) in Continents)
|
||||
{
|
||||
<option value="@id">@name</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="col col-12 col-sm-6 col-md-4 col-lg-3">
|
||||
<label for="region" class="jjj-label">Region</label>
|
||||
<InputText id="region" @bind-Value=@Criteria.Region class="form-control form-control-sm"
|
||||
placeholder="(free-form text)" />
|
||||
</div>
|
||||
<div class="col col-12 col-sm-6 offset-md-2 col-lg-3 offset-lg-0">
|
||||
<label class="jjj-label">Seeking Remote Work?</label><br>
|
||||
<InputRadioGroup @bind-Value=@Criteria.RemoteWork>
|
||||
<div class="form-check form-check-inline">
|
||||
<InputRadio id="remoteNull" Value=@("") class="form-check-input" />
|
||||
<label for="remoteNull" class="form-check-label">No Selection</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<InputRadio id="remoteYes" Value=@("yes") class="form-check-input" />
|
||||
<label for="remoteYes" class="form-check-label">Yes</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<InputRadio id="remoteNo" Value=@("no") class="form-check-input" />
|
||||
<label for="remoteNo" class="form-check-label">No</label>
|
||||
</div>
|
||||
</InputRadioGroup>
|
||||
</div>
|
||||
<div class="col col-12 col-sm-6 col-lg-3">
|
||||
<label for="skillSearch" class="jjj-label">Skill</label>
|
||||
<InputText id="skillSearch" @bind-Value=@Criteria.Skill class="form-control form-control-sm"
|
||||
placeholder="(free-form text)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<br>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public PublicSearch Criteria { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnSearch { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public IEnumerable<Continent> Continents { get; set; } = default!;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyVersion>0.9.1.0</AssemblyVersion>
|
||||
<FileVersion>0.9.1.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using JobsJobsJobs.Server.Data;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -9,7 +8,6 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
/// API endpoint for continent information
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class ContinentController : ControllerBase
|
||||
{
|
||||
|
||||
@@ -97,10 +97,6 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("skills")]
|
||||
public async Task<IActionResult> GetSkills() =>
|
||||
Ok(await _db.FindSkillsByCitizen(CurrentCitizenId));
|
||||
|
||||
[HttpGet("count")]
|
||||
public async Task<IActionResult> GetProfileCount() =>
|
||||
Ok(new Count(await _db.CountProfiles()));
|
||||
@@ -120,6 +116,11 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
public async Task<IActionResult> Search([FromQuery] ProfileSearch search) =>
|
||||
Ok(await _db.SearchProfiles(search));
|
||||
|
||||
[HttpGet("public-search")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> SearchPublic([FromQuery] PublicSearch search) =>
|
||||
Ok(await _db.SearchPublicProfiles(search));
|
||||
|
||||
[HttpPatch("employment-found")]
|
||||
public async Task<IActionResult> EmploymentFound()
|
||||
{
|
||||
|
||||
@@ -12,38 +12,36 @@ namespace JobsJobsJobs.Server.Data
|
||||
/// Citizen ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<CitizenId, string> CitizenIdConverter =
|
||||
new ValueConverter<CitizenId, string>(v => v.ToString(), v => CitizenId.Parse(v));
|
||||
new(v => v.ToString(), v => CitizenId.Parse(v));
|
||||
|
||||
/// <summary>
|
||||
/// Continent ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<ContinentId, string> ContinentIdConverter =
|
||||
new ValueConverter<ContinentId, string>(v => v.ToString(), v => ContinentId.Parse(v));
|
||||
new(v => v.ToString(), v => ContinentId.Parse(v));
|
||||
|
||||
/// <summary>
|
||||
/// Markdown converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<MarkdownString, string> MarkdownStringConverter =
|
||||
new ValueConverter<MarkdownString, string>(v => v.Text, v => new MarkdownString(v));
|
||||
new(v => v.Text, v => new MarkdownString(v));
|
||||
|
||||
/// <summary>
|
||||
/// Markdown converter for possibly-null values
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<MarkdownString?, string?> OptionalMarkdownStringConverter =
|
||||
new ValueConverter<MarkdownString?, string?>(
|
||||
v => v == null ? null : v.Text,
|
||||
v => v == null ? null : new MarkdownString(v));
|
||||
new(v => v == null ? null : v.Text, v => v == null ? null : new MarkdownString(v));
|
||||
|
||||
/// <summary>
|
||||
/// Skill ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<SkillId, string> SkillIdConverter =
|
||||
new ValueConverter<SkillId, string>(v => v.ToString(), v => SkillId.Parse(v));
|
||||
new(v => v.ToString(), v => SkillId.Parse(v));
|
||||
|
||||
/// <summary>
|
||||
/// Success ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<SuccessId, string> SuccessIdConverter =
|
||||
new ValueConverter<SuccessId, string>(v => v.ToString(), v => SuccessId.Parse(v));
|
||||
new(v => v.ToString(), v => SuccessId.Parse(v));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,12 @@ namespace JobsJobsJobs.Server.Data
|
||||
m.Property(e => e.LastUpdatedOn).HasColumnName("last_updated_on").IsRequired();
|
||||
m.Property(e => e.Experience).HasColumnName("experience")
|
||||
.HasConversion(Converters.OptionalMarkdownStringConverter);
|
||||
m.Ignore(e => e.Continent);
|
||||
m.Ignore(e => e.Skills);
|
||||
m.HasOne(e => e.Continent)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.ContinentId);
|
||||
m.HasMany(e => e.Skills)
|
||||
.WithOne()
|
||||
.HasForeignKey(e => e.CitizenId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Skill>(m =>
|
||||
|
||||
@@ -18,24 +18,13 @@ namespace JobsJobsJobs.Server.Data
|
||||
/// </summary>
|
||||
/// <param name="citizenId">The ID of the citizen whose profile should be retrieved</param>
|
||||
/// <returns>The profile, or null if it does not exist</returns>
|
||||
public static async Task<Profile?> FindProfileByCitizen(this JobsDbContext db, CitizenId citizenId)
|
||||
{
|
||||
var profile = await db.Profiles.AsNoTracking()
|
||||
public static async Task<Profile?> FindProfileByCitizen(this JobsDbContext db, CitizenId citizenId) =>
|
||||
await db.Profiles.AsNoTracking()
|
||||
.Include(p => p.Continent)
|
||||
.Include(p => p.Skills)
|
||||
.SingleOrDefaultAsync(p => p.Id == citizenId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
return profile with
|
||||
{
|
||||
Continent = await db.FindContinentById(profile.ContinentId).ConfigureAwait(false),
|
||||
Skills = (await db.FindSkillsByCitizen(citizenId).ConfigureAwait(false)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a profile
|
||||
/// </summary>
|
||||
@@ -52,16 +41,6 @@ namespace JobsJobsJobs.Server.Data
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all skills for the given citizen
|
||||
/// </summary>
|
||||
/// <param name="citizenId">The ID of the citizen whose skills should be retrieved</param>
|
||||
/// <returns>The skills defined for this citizen</returns>
|
||||
public static async Task<IEnumerable<Skill>> FindSkillsByCitizen(this JobsDbContext db, CitizenId citizenId) =>
|
||||
await db.Skills.AsNoTracking()
|
||||
.Where(s => s.CitizenId == citizenId)
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Save a skill
|
||||
/// </summary>
|
||||
@@ -111,7 +90,7 @@ namespace JobsJobsJobs.Server.Data
|
||||
/// <summary>
|
||||
/// Search profiles by the given criteria
|
||||
/// </summary>
|
||||
// TODO: A criteria parameter!
|
||||
/// <param name="search">The search parameters</param>
|
||||
/// <returns>The information for profiles matching the criteria</returns>
|
||||
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db,
|
||||
ProfileSearch search)
|
||||
@@ -161,6 +140,56 @@ namespace JobsJobsJobs.Server.Data
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search public profiles by the given criteria
|
||||
/// </summary>
|
||||
/// <param name="search">The search parameters</param>
|
||||
/// <returns>The information for profiles matching the criteria</returns>
|
||||
public static async Task<IEnumerable<PublicSearchResult>> SearchPublicProfiles(this JobsDbContext db,
|
||||
PublicSearch search)
|
||||
{
|
||||
var query = db.Profiles
|
||||
.Include(it => it.Continent)
|
||||
.Include(it => it.Skills)
|
||||
.Where(it => it.IsPublic);
|
||||
|
||||
var useIds = false;
|
||||
var citizenIds = new List<CitizenId>();
|
||||
|
||||
if (!string.IsNullOrEmpty(search.ContinentId))
|
||||
{
|
||||
query = query.Where(it => it.ContinentId == ContinentId.Parse(search.ContinentId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.Region))
|
||||
{
|
||||
query = query.Where(it => it.Region.ToLower().Contains(search.Region.ToLower()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.RemoteWork))
|
||||
{
|
||||
query = query.Where(it => it.RemoteWork == (search.RemoteWork == "yes"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.Skill))
|
||||
{
|
||||
useIds = true;
|
||||
citizenIds.AddRange(await db.Skills
|
||||
.Where(s => s.Description.ToLower().Contains(search.Skill.ToLower()))
|
||||
.Select(s => s.CitizenId)
|
||||
.ToListAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (useIds)
|
||||
{
|
||||
query = query.Where(it => citizenIds.Contains(it.Id));
|
||||
}
|
||||
|
||||
return await query.Select(x => new PublicSearchResult(x.Continent!.Name, x.Region, x.RemoteWork,
|
||||
x.Skills.Select(sk => $"{sk.Description} ({sk.Notes})")))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete skills and profile for the given citizen
|
||||
/// </summary>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<ActiveDebugProfile>JobsJobsJobs.Server</ActiveDebugProfile>
|
||||
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
|
||||
<NameOfLastUsedPublishProfile>FolderProfile</NameOfLastUsedPublishProfile>
|
||||
<NameOfLastUsedPublishProfile>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
||||
|
||||
@@ -5,6 +5,6 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<_PublishTargetUrl>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\bin\Release\net5.0\publish\</_PublishTargetUrl>
|
||||
<History>True|2021-03-16T23:34:57.2747439Z;</History>
|
||||
<History>True|2021-06-15T02:08:33.4261507Z;True|2021-06-14T21:58:04.2622487-04:00;True|2021-03-16T19:34:57.2747439-04:00;</History>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
37
src/JobsJobsJobs/Shared/Api/PublicSearch.cs
Normal file
37
src/JobsJobsJobs/Shared/Api/PublicSearch.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace JobsJobsJobs.Shared.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// The parameters for a public job search
|
||||
/// </summary>
|
||||
public class PublicSearch
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve citizens from this continent
|
||||
/// </summary>
|
||||
public string? ContinentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve citizens from this region
|
||||
/// </summary>
|
||||
public string? Region { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Text for a search within a citizen's skills
|
||||
/// </summary>
|
||||
public string? Skill { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to retrieve citizens who do or do not want remote work
|
||||
/// </summary>
|
||||
public string RemoteWork { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Is the search empty?
|
||||
/// </summary>
|
||||
public bool IsEmptySearch =>
|
||||
string.IsNullOrEmpty(ContinentId)
|
||||
&& string.IsNullOrEmpty(Region)
|
||||
&& string.IsNullOrEmpty(Skill)
|
||||
&& string.IsNullOrEmpty(RemoteWork);
|
||||
}
|
||||
}
|
||||
13
src/JobsJobsJobs/Shared/Api/PublicSearchResult.cs
Normal file
13
src/JobsJobsJobs/Shared/Api/PublicSearchResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace JobsJobsJobs.Shared.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// A public profile search result
|
||||
/// </summary>
|
||||
public record PublicSearchResult(
|
||||
string Continent,
|
||||
string Region,
|
||||
bool RemoteWork,
|
||||
IEnumerable<string> Skills);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using NodaTime;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace JobsJobsJobs.Shared
|
||||
{
|
||||
@@ -26,6 +26,6 @@ namespace JobsJobsJobs.Shared
|
||||
/// <summary>
|
||||
/// Convenience property for skills associated with a profile
|
||||
/// </summary>
|
||||
public Skill[] Skills { get; set; } = Array.Empty<Skill>();
|
||||
public ICollection<Skill> Skills { get; set; } = new List<Skill>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user