4 Commits
v0.9.1 ... v1

Author SHA1 Message Date
b98d28adb4 Update welcome page (#5) 2021-06-14 22:09:33 -04:00
60ed7e1e79 Complete public search (#5)
- Bump version
- Define nav between profile and continent/skills
- Remove redundant code
2021-06-14 21:49:20 -04:00
fb147888c5 Add public search page / initial API (#5) 2021-04-07 21:49:22 -04:00
4a73927e64 Update README 2021-03-16 20:53:09 -04:00
26 changed files with 443 additions and 98 deletions

View File

@@ -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
View File

@@ -0,0 +1,2 @@
**/bin
**/obj

10
src/Dockerfile Normal file
View 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" ]

View File

@@ -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

View File

@@ -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}";

View File

@@ -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 &ldquo;How It Works&rdquo; 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 &ldquo;How It Works&rdquo; in the sidebar (last updated June
14<sup>th</sup>, 2021).
</p>

View File

@@ -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> &bull;
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/2" target="_blank">Phase 2</a> &bull;
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/3" target="_blank">Phase 3</a> &bull;
<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 &ldquo;Allow my
profile to be searched publicly&rdquo; 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 &ldquo;Allow my profile to be searched publicly&rdquo; 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 &ldquo;Edit&rdquo; 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 &ldquo;public&rdquo; page for profile information will display the following information:
The &ldquo;Job Seekers&rdquo; 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&rsquo;s continent and region</li>
<li>The citizen&rsquo;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>

View File

@@ -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>

View File

@@ -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;
}

View 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 &ldquo;Search&rdquo; 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>

View 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;
}
}
}

View File

@@ -15,7 +15,7 @@
</div>
@if (Profile.Skills.Length > 0)
@if (Profile.Skills.Count > 0)
{
<hr>
<h4>Skills</h4>

View File

@@ -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;

View File

@@ -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

View 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="">&ndash; Any &ndash;</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!;
}

View File

@@ -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>

View File

@@ -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
{

View File

@@ -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()
{

View File

@@ -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));
}
}

View File

@@ -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 =>

View File

@@ -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)
@@ -160,7 +139,57 @@ namespace JobsJobsJobs.Server.Data
x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
.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>

View File

@@ -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>

View File

@@ -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>

View 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);
}
}

View 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);
}

View File

@@ -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>();
}
}