Merge pull request #205 from EETagent/application_list_ordering

Application list ordering
This commit is contained in:
Vojtěch Jungmann 2023-02-15 16:21:01 +01:00 committed by GitHub
commit 94015aa042
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 79 additions and 15 deletions

View file

@ -120,12 +120,13 @@ pub async fn create_candidate(
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[get("/candidates?<field>&<page>")] #[get("/candidates?<field>&<page>&<sort>")]
pub async fn list_candidates( pub async fn list_candidates(
conn: Connection<'_, Db>, conn: Connection<'_, Db>,
session: AdminAuth, session: AdminAuth,
field: Option<String>, field: Option<String>,
page: Option<u64>, page: Option<u64>,
sort: Option<String>,
) -> Result<Json<Vec<ApplicationResponse>>, Custom<String>> { ) -> Result<Json<Vec<ApplicationResponse>>, Custom<String>> {
let db = conn.into_inner(); let db = conn.into_inner();
let private_key = session.get_private_key(); let private_key = session.get_private_key();
@ -135,7 +136,7 @@ pub async fn list_candidates(
} }
} }
let candidates = ApplicationService::list_applications(&private_key, db, field, page) let candidates = ApplicationService::list_applications(&private_key, db, field, page, sort)
.await.map_err(to_custom_error)?; .await.map_err(to_custom_error)?;
Ok( Ok(

View file

@ -1,3 +1,4 @@
use chrono::NaiveDateTime;
use entity::{application, candidate}; use entity::{application, candidate};
use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait}; use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait};
@ -13,10 +14,32 @@ pub struct ApplicationCandidateJoin {
pub email: Option<String>, pub email: Option<String>,
pub telephone: Option<String>, pub telephone: Option<String>,
pub field_of_study: Option<String>, pub field_of_study: Option<String>,
pub created_at: NaiveDateTime,
} }
use crate::{Query}; use crate::{Query};
fn get_ordering(sort: String) -> (application::Column, sea_orm::Order)
{
let mut split = sort.split("_");
let column = split.next();
let order = split.next();
let column = match column {
Some("id") => application::Column::Id,
Some("createdAt") => application::Column::CreatedAt,
_ => application::Column::Id
};
let order = match order {
Some("asc") => sea_orm::Order::Asc,
Some("desc") => sea_orm::Order::Desc,
_ => sea_orm::Order::Asc,
};
(column, order)
}
impl Query { impl Query {
pub async fn find_application_by_id( pub async fn find_application_by_id(
db: &DbConn, db: &DbConn,
@ -41,14 +64,20 @@ impl Query {
db: &DbConn, db: &DbConn,
field_of_study: Option<String>, field_of_study: Option<String>,
page: Option<u64>, page: Option<u64>,
sort: Option<String>,
) -> Result<Vec<ApplicationCandidateJoin>, DbErr> { ) -> Result<Vec<ApplicationCandidateJoin>, DbErr> {
let select = application::Entity::find(); let select = application::Entity::find();
let (column, order) = if let Some(sort) = sort {
get_ordering(sort)
} else {
(application::Column::Id, sea_orm::Order::Asc)
};
let query = if let Some(field) = field_of_study { let query = if let Some(field) = field_of_study {
select.filter(application::Column::FieldOfStudy.eq(field)) select.filter(application::Column::FieldOfStudy.eq(field))
} else { } else {
select select
} }
.order_by(application::Column::Id, sea_orm::Order::Asc) .order_by(column, order)
.join(JoinType::InnerJoin, application::Relation::Candidate.def()) .join(JoinType::InnerJoin, application::Relation::Candidate.def())
.column_as(application::Column::Id, "application_id") .column_as(application::Column::Id, "application_id")
.column_as(candidate::Column::Id, "candidate_id") .column_as(candidate::Column::Id, "candidate_id")
@ -56,6 +85,7 @@ impl Query {
.column_as(candidate::Column::Surname, "surname") .column_as(candidate::Column::Surname, "surname")
.column_as(candidate::Column::Email, "email") .column_as(candidate::Column::Email, "email")
.column_as(candidate::Column::Telephone, "telephone") .column_as(candidate::Column::Telephone, "telephone")
.column_as(application::Column::CreatedAt, "created_at")
.into_model::<ApplicationCandidateJoin>(); .into_model::<ApplicationCandidateJoin>();
if let Some(page) = page { if let Some(page) = page {

View file

@ -1,3 +1,4 @@
use chrono::NaiveDateTime;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::{database::query::application::ApplicationCandidateJoin, error::ServiceError}; use crate::{database::query::application::ApplicationCandidateJoin, error::ServiceError};
@ -16,6 +17,7 @@ pub struct ApplicationResponse {
pub email: String, pub email: String,
pub telephone: String, pub telephone: String,
pub field_of_study: Option<String>, pub field_of_study: Option<String>,
pub created_at: NaiveDateTime,
} }
impl ApplicationResponse { impl ApplicationResponse {
@ -40,6 +42,7 @@ impl ApplicationResponse {
email: email.unwrap_or_default(), email: email.unwrap_or_default(),
telephone: telephone.unwrap_or_default(), telephone: telephone.unwrap_or_default(),
field_of_study: c.field_of_study, field_of_study: c.field_of_study,
created_at: c.created_at,
} }
) )
} }

View file

@ -267,8 +267,9 @@ impl ApplicationService {
db: &DbConn, db: &DbConn,
field_of_study: Option<String>, field_of_study: Option<String>,
page: Option<u64>, page: Option<u64>,
sort: Option<String>,
) -> Result<Vec<ApplicationResponse>, ServiceError> { ) -> Result<Vec<ApplicationResponse>, ServiceError> {
let applications = Query::list_applications(db, field_of_study, page).await?; let applications = Query::list_applications(db, field_of_study, page, sort).await?;
futures::future::try_join_all( futures::future::try_join_all(
applications applications

View file

@ -79,12 +79,12 @@ pub mod tests {
let db = get_memory_sqlite_connection().await; let db = get_memory_sqlite_connection().await;
let admin = create_admin(&db).await; let admin = create_admin(&db).await;
let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap();
let candidates = ApplicationService::list_applications(&private_key, &db, None, None).await.unwrap(); let candidates = ApplicationService::list_applications(&private_key, &db, None, None, None).await.unwrap();
assert_eq!(candidates.len(), 0); assert_eq!(candidates.len(), 0);
put_user_data(&db).await; put_user_data(&db).await;
let candidates = ApplicationService::list_applications(&private_key, &db, None, None).await.unwrap(); let candidates = ApplicationService::list_applications(&private_key, &db, None, None, None).await.unwrap();
assert_eq!(candidates.len(), 1); assert_eq!(candidates.len(), 1);
} }

View file

@ -101,15 +101,18 @@ export const apiLogout = async (fetchSsr?: Fetch) => {
// List all candidates /admin/list/candidates // List all candidates /admin/list/candidates
export const apiListCandidates = async ( export const apiListCandidates = async (
fetchSsr?: Fetch, fetchSsr?: Fetch,
field?: string params: { field?: string; column?: 'createdAt' | 'application', order?: 'asc' | 'desc' } = {column: 'createdAt', order: 'desc'}
): Promise<Array<CandidatePreview>> => { ): Promise<Array<CandidatePreview>> => {
const apiFetch = fetchSsr || fetch; const apiFetch = fetchSsr || fetch;
const params = new URLSearchParams(); const searchParams = new URLSearchParams();
if (field) { if (params.field) {
params.append('field', field); searchParams.append('field', params.field);
}
if (params.column) {
searchParams.append('sort', `${params.column}_${params.order}`);
} }
try { try {
const res = await apiFetch(API_URL + '/admin/list/candidates?' + params.toString(), { const res = await apiFetch(API_URL + '/admin/list/candidates?' + searchParams.toString(), {
method: 'GET', method: 'GET',
credentials: 'include' credentials: 'include'
}); });

View file

@ -3,6 +3,15 @@
import type { CandidatePreview } from '$lib/stores/candidate'; import type { CandidatePreview } from '$lib/stores/candidate';
export let candidates: Array<CandidatePreview> = []; export let candidates: Array<CandidatePreview> = [];
export let showCreatedAt: boolean;
const formatRustChronoDateTime = (date?: string) => {
if (!date) return '';
const [datePart, timePart] = date.split('T');
const [_, month, day] = datePart.split('-');
const [hour, minute, second] = timePart.split(':');
return `${day}. ${month}. ${hour}:${minute}:${Number(second).toFixed(0).padStart(2, '0')}`;
};
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
@ -16,6 +25,9 @@
<th scope="col"> Obor </th> <th scope="col"> Obor </th>
<th scope="col"> Rodné číslo </th> <th scope="col"> Rodné číslo </th>
<th scope="col"> Link </th> <th scope="col"> Link </th>
{#if showCreatedAt}
<th scope="col"> Vytvořeno </th>
{/if}
<th scope="col" /> <th scope="col" />
</tr> </tr>
</thead> </thead>
@ -38,6 +50,11 @@
<td class="text-gray-900"> <td class="text-gray-900">
{candidate.relatedApplications?.filter((a) => a !== candidate.applicationId)} {candidate.relatedApplications?.filter((a) => a !== candidate.applicationId)}
</td> </td>
{#if showCreatedAt}
<td class="text-gray-900">
{formatRustChronoDateTime(candidate.createdAt)}
</td>
{/if}
<td class="text-sm"> <td class="text-sm">
<Delete id={candidate.applicationId} on:delete value="Odstranit" /> <Delete id={candidate.applicationId} on:delete value="Odstranit" />
</td> </td>

View file

@ -47,6 +47,7 @@ export interface CandidatePreview {
surname?: string; surname?: string;
email?: string; email?: string;
fieldOfStudy?: string; fieldOfStudy?: string;
createdAt?: string;
} }
export interface CandidateLogin { export interface CandidateLogin {

View file

@ -20,13 +20,17 @@
const getCandidates = async () => { const getCandidates = async () => {
try { try {
candidates = await apiListCandidates(undefined, activeFilter.filter); // TODO: more generic implementation
candidates = await apiListCandidates(
undefined,
activeFilter.filter !== undefined ? { field: activeFilter.filter } : undefined
);
} catch { } catch {
pushErrorText('Nepodařilo se načíst uchazeče'); pushErrorText('Nepodařilo se načíst uchazeče');
} }
}; };
type Class = 'Vše' | 'KBB' | 'IT' | 'GYM'; type Class = 'Chronologicky' | 'KBB' | 'IT' | 'GYM';
type Filter = { type Filter = {
class: Class; class: Class;
@ -35,7 +39,7 @@
let filters: Array<Filter> = [ let filters: Array<Filter> = [
{ {
class: 'Vše', class: 'Chronologicky',
filter: undefined filter: undefined
}, },
{ {
@ -155,7 +159,11 @@
</div> </div>
{/if} {/if}
<Table candidates={candidatesTable} on:delete={(event) => deleteCandidate(event.detail.id)} /> <Table
showCreatedAt={activeFilter.class === 'Chronologicky'}
candidates={candidatesTable}
on:delete={(event) => deleteCandidate(event.detail.id)}
/>
</div> </div>
</div> </div>
</div> </div>