Complete public search (#5)

- Bump version
- Define nav between profile and continent/skills
- Remove redundant code
This commit is contained in:
Daniel J. Summers 2021-06-14 21:49:20 -04:00
parent fb147888c5
commit 60ed7e1e79
14 changed files with 74 additions and 91 deletions

View File

@ -22,7 +22,7 @@ namespace JobsJobsJobs.Client
/// <summary> /// <summary>
/// The application version, as a nice display string /// The application version, as a nice display string
/// </summary> /// </summary>
public static Lazy<string> Version => new Lazy<string>(() => public static Lazy<string> Version => new(() =>
{ {
var version = Assembly.GetExecutingAssembly().GetName().Version!; var version = Assembly.GetExecutingAssembly().GetName().Version!;
var display = $"v{version.Major}.{version.Minor}"; var display = $"v{version.Major}.{version.Minor}";

View File

@ -11,7 +11,7 @@
{ {
<p> <p>
Your employment profile was last updated <FullDateTime TheDate=@Profile.LastUpdatedOn />. Your profile currently 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>
<p><a href="/profile/view/@state.User.Id">View Your Employment Profile</a></p> <p><a href="/profile/view/@state.User.Id">View Your Employment Profile</a></p>
@if (Profile.SeekingEmployment) @if (Profile.SeekingEmployment)
@ -41,7 +41,6 @@
</Loading> </Loading>
<hr> <hr>
<p> <p>
To see what is currently done, and how this application works, check out &ldquo;How It Works&rdquo; in the sidebar. To see how this application works, check out &ldquo;How It Works&rdquo; in the sidebar (last updated June
The application now has 4 of 5 phases complete towards version 1.0; the documentation was last updated January 14<sup>th</sup>, 2021).
31<sup>st</sup>, 2021.
</p> </p>

View File

@ -4,18 +4,6 @@
<h3>How It Works</h3> <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> <h4>Completing Your Profile</h4>
<ul> <ul>
<li> <li>
@ -44,11 +32,10 @@
would like it presented to fellow citizens. would like it presented to fellow citizens.
</li> </li>
<li> <li>
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/5" target="_blank">Phase 5</a> includes allowing If you check the &ldquo;Allow my profile to be searched publicly&rdquo; checkbox, <strong>and</strong> you are
public access to the continent, region, and skills fields of Gitmo Nation citizens who indicate that they are both seeking employment, your continent, region, and skills fields will be searchable and displayed to public users of
seeking employment <strong>and</strong> want their information disclosed to public users. The &ldquo;Allow my the site. They will not be tied to your No Agenda Social handle or real name; they are there to let people peek
profile to be searched publicly&rdquo; checkbox, at the bottom of the page where you edit your employment profile, behind the curtain a bit, and hopefully inspire them to join us.
is how you opt your profile in to this list.
</li> </li>
</ul> </ul>
@ -77,19 +64,18 @@
to read it; if you submitted the story, there will also be an &ldquo;Edit&rdquo; link. to read it; if you submitted the story, there will also be an &ldquo;Edit&rdquo; link.
</p> </p>
<h4> <h4>Publicly Available Information</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>
<p> <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> </p>
<ul>
<li>A count of profiles where the citizen is seeking employment</li> <h4>Help / Suggestions</h4>
<li>The citizen&rsquo;s continent and region</li>
<li>The citizen&rsquo;s skills and notes</li>
</ul>
<p> <p>
This information will be pullled only from profiles where citizens have said can it be publicly available This is open-source software
<strong>and</strong> are currently seeking employment. <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> </p>

View File

@ -23,10 +23,13 @@
<br> <br>
@if (SearchResults.Any()) @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"> <table class="table table-sm table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col">Profile</th>
<th scope="col">Continent</th> <th scope="col">Continent</th>
<th scope="col" class="text-center">Region</th> <th scope="col" class="text-center">Region</th>
<th scope="col" class="text-center">Remote?</th> <th scope="col" class="text-center">Remote?</th>
@ -37,12 +40,15 @@
@foreach (var profile in SearchResults) @foreach (var profile in SearchResults)
{ {
<tr> <tr>
<td><a href="/profile/bio/@profile.CitizenId">View</a></td> <td>@profile.Continent</td>
<td class=@IsSeeking(profile)>@profile.DisplayName</td> <td>@profile.Region</td>
<td class="text-center">@YesOrNo(profile.SeekingEmployment)</td>
<td class="text-center">@YesOrNo(profile.RemoteWork)</td> <td class="text-center">@YesOrNo(profile.RemoteWork)</td>
<td class="text-center">@YesOrNo(profile.FullTime)</td> <td>
<td><FullDate TheDate=@profile.LastUpdated /></td> @foreach (var skill in profile.Skills)
{
@skill.Replace(" ()", "")<br>
}
</td>
</tr> </tr>
} }
</tbody> </tbody>

View File

@ -39,7 +39,7 @@ namespace JobsJobsJobs.Client.Pages.Profiles
/// <summary> /// <summary>
/// The search results /// The search results
/// </summary> /// </summary>
private IEnumerable<ProfileSearchResult> SearchResults { get; set; } = Enumerable.Empty<ProfileSearchResult>(); private IEnumerable<PublicSearchResult> SearchResults { get; set; } = Enumerable.Empty<PublicSearchResult>();
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -72,7 +72,7 @@ namespace JobsJobsJobs.Client.Pages.Profiles
{ {
var query = SearchQuery(); var query = SearchQuery();
query.Add("Searched", "True"); query.Add("Searched", "True");
nav.NavigateTo(QueryHelpers.AddQueryString("/profile/search", query)); nav.NavigateTo(QueryHelpers.AddQueryString("/profile/seeking", query));
await RetrieveProfiles(); await RetrieveProfiles();
} }
@ -83,8 +83,8 @@ namespace JobsJobsJobs.Client.Pages.Profiles
{ {
Searching = true; Searching = true;
var searchResult = await ServerApi.RetrieveMany<ProfileSearchResult>(http, var searchResult = await ServerApi.RetrieveMany<PublicSearchResult>(http,
QueryHelpers.AddQueryString("profile/search", SearchQuery())); QueryHelpers.AddQueryString("profile/public-search", SearchQuery()));
if (searchResult.IsOk) if (searchResult.IsOk)
{ {

View File

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

View File

@ -2,7 +2,6 @@
using JobsJobsJobs.Shared.Api; using JobsJobsJobs.Shared.Api;
using NodaTime; using NodaTime;
using NodaTime.Serialization.SystemTextJson; using NodaTime.Serialization.SystemTextJson;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyVersion>0.9.1.0</AssemblyVersion> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>0.9.1.0</FileVersion> <FileVersion>1.0.0.0</FileVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -97,10 +97,6 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
return Ok(); return Ok();
} }
[HttpGet("skills")]
public async Task<IActionResult> GetSkills() =>
Ok(await _db.FindSkillsByCitizen(CurrentCitizenId));
[HttpGet("count")] [HttpGet("count")]
public async Task<IActionResult> GetProfileCount() => public async Task<IActionResult> GetProfileCount() =>
Ok(new Count(await _db.CountProfiles())); Ok(new Count(await _db.CountProfiles()));

View File

@ -83,8 +83,12 @@ namespace JobsJobsJobs.Server.Data
m.Property(e => e.LastUpdatedOn).HasColumnName("last_updated_on").IsRequired(); m.Property(e => e.LastUpdatedOn).HasColumnName("last_updated_on").IsRequired();
m.Property(e => e.Experience).HasColumnName("experience") m.Property(e => e.Experience).HasColumnName("experience")
.HasConversion(Converters.OptionalMarkdownStringConverter); .HasConversion(Converters.OptionalMarkdownStringConverter);
m.Ignore(e => e.Continent); m.HasOne(e => e.Continent)
m.Ignore(e => e.Skills); .WithMany()
.HasForeignKey(e => e.ContinentId);
m.HasMany(e => e.Skills)
.WithOne()
.HasForeignKey(e => e.CitizenId);
}); });
modelBuilder.Entity<Skill>(m => modelBuilder.Entity<Skill>(m =>

View File

@ -18,24 +18,13 @@ namespace JobsJobsJobs.Server.Data
/// </summary> /// </summary>
/// <param name="citizenId">The ID of the citizen whose profile should be retrieved</param> /// <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> /// <returns>The profile, or null if it does not exist</returns>
public static async Task<Profile?> FindProfileByCitizen(this JobsDbContext db, CitizenId citizenId) public static async Task<Profile?> FindProfileByCitizen(this JobsDbContext db, CitizenId citizenId) =>
{ await db.Profiles.AsNoTracking()
var profile = await db.Profiles.AsNoTracking() .Include(p => p.Continent)
.Include(p => p.Skills)
.SingleOrDefaultAsync(p => p.Id == citizenId) .SingleOrDefaultAsync(p => p.Id == citizenId)
.ConfigureAwait(false); .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> /// <summary>
/// Save a profile /// Save a profile
/// </summary> /// </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> /// <summary>
/// Save a skill /// Save a skill
/// </summary> /// </summary>
@ -166,29 +145,30 @@ namespace JobsJobsJobs.Server.Data
/// </summary> /// </summary>
/// <param name="search">The search parameters</param> /// <param name="search">The search parameters</param>
/// <returns>The information for profiles matching the criteria</returns> /// <returns>The information for profiles matching the criteria</returns>
public static async Task<IEnumerable<ProfileSearchResult>> SearchPublicProfiles(this JobsDbContext db, public static async Task<IEnumerable<PublicSearchResult>> SearchPublicProfiles(this JobsDbContext db,
PublicSearch search) PublicSearch search)
{ {
var query = db.Profiles var query = db.Profiles
.Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c }) .Include(it => it.Continent)
.Where(it => it.Profile.IsPublic); .Include(it => it.Skills)
.Where(it => it.IsPublic);
var useIds = false; var useIds = false;
var citizenIds = new List<CitizenId>(); var citizenIds = new List<CitizenId>();
if (!string.IsNullOrEmpty(search.ContinentId)) if (!string.IsNullOrEmpty(search.ContinentId))
{ {
query = query.Where(it => it.Profile.ContinentId == ContinentId.Parse(search.ContinentId)); query = query.Where(it => it.ContinentId == ContinentId.Parse(search.ContinentId));
} }
if (!string.IsNullOrEmpty(search.Region)) if (!string.IsNullOrEmpty(search.Region))
{ {
query = query.Where(it => it.Profile.Region.ToLower().Contains(search.Region.ToLower())); query = query.Where(it => it.Region.ToLower().Contains(search.Region.ToLower()));
} }
if (!string.IsNullOrEmpty(search.RemoteWork)) if (!string.IsNullOrEmpty(search.RemoteWork))
{ {
query = query.Where(it => it.Profile.RemoteWork == (search.RemoteWork == "yes")); query = query.Where(it => it.RemoteWork == (search.RemoteWork == "yes"));
} }
if (!string.IsNullOrEmpty(search.Skill)) if (!string.IsNullOrEmpty(search.Skill))
@ -202,11 +182,11 @@ namespace JobsJobsJobs.Server.Data
if (useIds) if (useIds)
{ {
query = query.Where(it => citizenIds.Contains(it.Citizen.Id)); query = query.Where(it => citizenIds.Contains(it.Id));
} }
return await query.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.CitizenName, return await query.Select(x => new PublicSearchResult(x.Continent!.Name, x.Region, x.RemoteWork,
x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn)) x.Skills.Select(sk => $"{sk.Description} ({sk.Notes})")))
.ToListAsync().ConfigureAwait(false); .ToListAsync().ConfigureAwait(false);
} }

View File

@ -6,7 +6,7 @@
<ActiveDebugProfile>JobsJobsJobs.Server</ActiveDebugProfile> <ActiveDebugProfile>JobsJobsJobs.Server</ActiveDebugProfile>
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID> <Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath> <Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
<NameOfLastUsedPublishProfile>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile> <NameOfLastUsedPublishProfile>FolderProfile</NameOfLastUsedPublishProfile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor> <DebuggerFlavor>ProjectDebugger</DebuggerFlavor>

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 NodaTime;
using System; using System.Collections.Generic;
namespace JobsJobsJobs.Shared namespace JobsJobsJobs.Shared
{ {
@ -26,6 +26,6 @@ namespace JobsJobsJobs.Shared
/// <summary> /// <summary>
/// Convenience property for skills associated with a profile /// Convenience property for skills associated with a profile
/// </summary> /// </summary>
public Skill[] Skills { get; set; } = Array.Empty<Skill>(); public ICollection<Skill> Skills { get; set; } = new List<Skill>();
} }
} }