Skip to content

Commit 72d168d

Browse files
tegiozcynthia-sg
andauthored
Job postings can now include desired certifications (#294)
Signed-off-by: Sergio Castaño Arteaga <[email protected]> Signed-off-by: Cintia Sánchez García <[email protected]> Co-authored-by: Cintia Sánchez García <[email protected]>
1 parent 4a9dc6e commit 72d168d

File tree

13 files changed

+478
-70
lines changed

13 files changed

+478
-70
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
create table certification (
2+
certification_id uuid primary key default gen_random_uuid(),
3+
4+
name text not null unique check (name <> ''),
5+
provider text not null check (provider <> ''),
6+
short_name text not null unique check (short_name <> ''),
7+
8+
description text check (description <> ''),
9+
logo_url text check (logo_url <> ''),
10+
url text check (url <> '')
11+
);
12+
13+
insert into certification (name, provider, short_name, description, url, logo_url) values
14+
('Certified Kubernetes Administrator', 'CNCF', 'CKA', 'Performance-based exam where candidates interact with the command line to solve real-world challenges', 'https://www.cncf.io/training/certification/cka/', 'https://www.cncf.io/wp-content/uploads/2021/09/kubernetes-cka-color.svg'),
15+
('Certified Kubernetes Application Developer', 'CNCF', 'CKAD', 'Proves you can design, build, and manage cloud-native applications on Kubernetes', 'https://www.cncf.io/training/certification/ckad/', 'https://www.cncf.io/wp-content/uploads/2021/09/kubernetes-ckad-color.svg'),
16+
('Certified Kubernetes Security Specialist', 'CNCF', 'CKS', 'Covers best practices for securing container-based applications and Kubernetes platforms', 'https://www.cncf.io/training/certification/cks/', 'https://www.cncf.io/wp-content/uploads/2020/11/kubernetes-security-specialist-logo.svg'),
17+
('Kubernetes and Cloud Native Associate', 'CNCF', 'KCNA', 'Entry-level credential focusing on Kubernetes and broader cloud native ecosystem', 'https://www.cncf.io/training/certification/kcna/', 'https://www.cncf.io/wp-content/uploads/2021/09/kcna_color.svg'),
18+
('Kubernetes and Cloud Security Associate', 'CNCF', 'KCSA', 'Validates skills in evaluating Kubernetes cluster security configurations and compliance', 'https://www.cncf.io/training/certification/kcsa/', 'https://www.cncf.io/wp-content/uploads/2024/03/kubernetes-kcsa-color.svg'),
19+
('Prometheus Certified Associate', 'CNCF', 'PCA', 'Entry-level certification demonstrating foundational knowledge of observability and Prometheus monitoring', 'https://www.cncf.io/training/certification/pca/', 'https://www.cncf.io/wp-content/uploads/2023/11/PCA-Prometheus-Certified-Associate-logo-color.svg'),
20+
('Istio Certified Associate', 'CNCF', 'ICA', 'Pre-professional certification demonstrating foundational knowledge of Istio principles, terminology, and best practices', 'https://www.cncf.io/training/certification/ica/', 'https://www.cncf.io/wp-content/uploads/2024/03/ica-icon-color.svg'),
21+
('Cilium Certified Associate', 'CNCF', 'CCA', 'Entry-level certification designed for platform or cloud engineers interested in networking, security, and observability', 'https://www.cncf.io/training/certification/cca/', 'https://www.cncf.io/wp-content/uploads/2024/03/cca-icon-color.svg'),
22+
('Certified Argo Project Associate', 'CNCF', 'CAPA', 'Designed for engineers, data scientists, and others interested in demonstrating their understanding of the Argo Project ecosystem', 'https://www.cncf.io/training/certification/capa/', 'https://www.cncf.io/wp-content/uploads/2024/03/capa-icon-color.svg'),
23+
('GitOps Certified Associate', 'CNCF', 'CGOA', 'For DevOps engineers and team members, platform and software engineers, CI/CD practitioners interested in GitOps', 'https://www.cncf.io/training/certification/cgoa/', 'https://www.cncf.io/wp-content/uploads/2024/03/gitops_associate.svg'),
24+
('Certified Backstage Associate', 'CNCF', 'CBA', 'Proves you have the skills & the mindset to work with Backstage to advance your career, your team & your organization', 'https://www.cncf.io/training/certification/cba/', 'https://www.cncf.io/wp-content/uploads/2024/11/lft_badge_backstage_associate1.svg'),
25+
('OpenTelemetry Certified Associate', 'CNCF', 'OTCA', 'Prove your expertise in OpenTelemetry – the industry standard for tracing, metrics & logs', 'https://www.cncf.io/training/certification/otca/', 'https://www.cncf.io/wp-content/uploads/2024/11/lft_badge_opentelemetry_associate1.svg'),
26+
('Kyverno Certified Associate', 'CNCF', 'KCA', 'Position yourself as an expert in managing and securing Kubernetes environments. Kyverno expertise shows you understand the advanced aspects of cloud management and security', 'https://www.cncf.io/training/certification/kca/', 'https://www.cncf.io/wp-content/uploads/2024/11/lft_badge_kyverno_associate1.svg');
27+
28+
create table job_certification (
29+
job_id uuid not null references job on delete cascade,
30+
certification_id uuid not null references certification on delete restrict,
31+
32+
primary key (job_id, certification_id)
33+
);
34+
35+
create index job_certification_job_id_idx on job_certification (job_id);
36+
create index job_certification_certification_id_idx on job_certification (certification_id);
37+
38+
---- create above / drop below ----
39+
40+
drop table job_certification;
41+
drop table certification;

gitjobs-server/src/db/dashboard/employer.rs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
team::{TeamInvitation, TeamMember},
1818
},
1919
helpers::normalize_salary,
20-
misc::Foundation,
20+
misc::{Certification, Foundation},
2121
},
2222
};
2323

@@ -74,6 +74,9 @@ pub(crate) trait DBDashBoardEmployer {
7474
/// Lists all employers where the user is a team member.
7575
async fn list_employers(&self, user_id: &Uuid) -> Result<Vec<EmployerSummary>>;
7676

77+
/// Lists all available certifications.
78+
async fn list_certifications(&self) -> Result<Vec<Certification>>;
79+
7780
/// Lists all available foundations.
7881
async fn list_foundations(&self) -> Result<Vec<Foundation>>;
7982

@@ -301,6 +304,20 @@ impl DBDashBoardEmployer for PgDB {
301304
}
302305
}
303306

307+
// Insert job certifications
308+
if let Some(certifications) = &job.certifications {
309+
for certification in certifications {
310+
tx.execute(
311+
"
312+
insert into job_certification (job_id, certification_id)
313+
values ($1::uuid, $2::uuid);
314+
",
315+
&[&job_id, &certification.certification_id],
316+
)
317+
.await?;
318+
}
319+
}
320+
304321
// Commit transaction
305322
tx.commit().await?;
306323

@@ -602,7 +619,22 @@ impl DBDashBoardEmployer for PgDB {
602619
left join job_project jp using (project_id)
603620
left join job j using (job_id)
604621
where j.job_id = $1::uuid
605-
) as projects
622+
) as projects,
623+
(
624+
select json_agg(json_build_object(
625+
'certification_id', c.certification_id,
626+
'name', c.name,
627+
'provider', c.provider,
628+
'short_name', c.short_name,
629+
'description', c.description,
630+
'url', c.url,
631+
'logo_url', c.logo_url
632+
))
633+
from certification c
634+
left join job_certification jc using (certification_id)
635+
left join job j using (job_id)
636+
where j.job_id = $1::uuid
637+
) as certifications
606638
from job j
607639
left join location l using (location_id)
608640
where job_id = $1::uuid
@@ -621,6 +653,9 @@ impl DBDashBoardEmployer for PgDB {
621653
apply_instructions: row.get("apply_instructions"),
622654
apply_url: row.get("apply_url"),
623655
benefits: row.get("benefits"),
656+
certifications: row
657+
.get::<_, Option<serde_json::Value>>("certifications")
658+
.map(|v| serde_json::from_value(v).expect("certifications should be valid json")),
624659
job_id: row.get("job_id"),
625660
location: row
626661
.get::<_, Option<serde_json::Value>>("location")
@@ -786,6 +821,43 @@ impl DBDashBoardEmployer for PgDB {
786821
Ok(employers)
787822
}
788823

824+
#[instrument(skip(self), err)]
825+
async fn list_certifications(&self) -> Result<Vec<Certification>> {
826+
trace!("db: list certifications");
827+
828+
let db = self.pool.get().await?;
829+
let certifications = db
830+
.query(
831+
"
832+
select
833+
certification_id,
834+
name,
835+
provider,
836+
short_name,
837+
description,
838+
logo_url,
839+
url
840+
from certification
841+
order by name asc;
842+
",
843+
&[],
844+
)
845+
.await?
846+
.into_iter()
847+
.map(|row| Certification {
848+
certification_id: row.get("certification_id"),
849+
name: row.get("name"),
850+
provider: row.get("provider"),
851+
short_name: row.get("short_name"),
852+
description: row.get("description"),
853+
logo_url: row.get("logo_url"),
854+
url: row.get("url"),
855+
})
856+
.collect();
857+
858+
Ok(certifications)
859+
}
860+
789861
#[instrument(skip(self), err)]
790862
async fn list_foundations(&self) -> Result<Vec<Foundation>> {
791863
trace!("db: list foundations");
@@ -1060,8 +1132,8 @@ impl DBDashBoardEmployer for PgDB {
10601132
)
10611133
.await?;
10621134

1063-
// Update job projects
10641135
if rows_updated == 1 {
1136+
// Update job projects
10651137
tx.execute("delete from job_project where job_id = $1::uuid;", &[&job_id])
10661138
.await?;
10671139
if let Some(projects) = &job.projects {
@@ -1076,6 +1148,25 @@ impl DBDashBoardEmployer for PgDB {
10761148
.await?;
10771149
}
10781150
}
1151+
1152+
// Update job certifications
1153+
tx.execute(
1154+
"delete from job_certification where job_id = $1::uuid;",
1155+
&[&job_id],
1156+
)
1157+
.await?;
1158+
if let Some(certifications) = &job.certifications {
1159+
for certification in certifications {
1160+
tx.execute(
1161+
"
1162+
insert into job_certification (job_id, certification_id)
1163+
values ($1::uuid, $2::uuid);
1164+
",
1165+
&[&job_id, &certification.certification_id],
1166+
)
1167+
.await?;
1168+
}
1169+
}
10791170
}
10801171

10811172
// Commit transaction

gitjobs-server/src/db/jobboard.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,22 @@ impl DBJobBoard for PgDB {
138138
left join job_project jp using (project_id)
139139
left join job j using (job_id)
140140
where j.job_id = $1::uuid
141-
) as projects
141+
) as projects,
142+
(
143+
select json_agg(json_build_object(
144+
'certification_id', c.certification_id,
145+
'name', c.name,
146+
'provider', c.provider,
147+
'short_name', c.short_name,
148+
'description', c.description,
149+
'logo_url', c.logo_url,
150+
'url', c.url
151+
))
152+
from certification c
153+
left join job_certification jc using (certification_id)
154+
left join job j using (job_id)
155+
where j.job_id = $1::uuid
156+
) as certifications
142157
from job j
143158
join employer e on j.employer_id = e.employer_id
144159
left join location l on j.location_id = l.location_id
@@ -162,6 +177,9 @@ impl DBJobBoard for PgDB {
162177
apply_instructions: row.get("apply_instructions"),
163178
apply_url: row.get("apply_url"),
164179
benefits: row.get("benefits"),
180+
certifications: row
181+
.get::<_, Option<serde_json::Value>>("certifications")
182+
.map(|v| serde_json::from_value(v).expect("certifications should be valid json")),
165183
location: row
166184
.get::<_, Option<serde_json::Value>>("location")
167185
.map(|v| serde_json::from_value(v).expect("location should be valid json")),

gitjobs-server/src/handlers/dashboard/employer/jobs.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ use crate::{
2626
/// Renders the page to add a new job for an employer.
2727
#[instrument(skip_all, err)]
2828
pub(crate) async fn add_page(State(db): State<DynDB>) -> Result<impl IntoResponse, HandlerError> {
29-
let foundations = db.list_foundations().await?;
30-
let template = jobs::AddPage { foundations };
29+
let (certifications, foundations) = tokio::try_join!(db.list_certifications(), db.list_foundations())?;
30+
let template = jobs::AddPage {
31+
certifications,
32+
foundations,
33+
};
3134

3235
Ok(Html(template.render()?))
3336
}
@@ -87,9 +90,16 @@ pub(crate) async fn update_page(
8790
State(db): State<DynDB>,
8891
Path(job_id): Path<Uuid>,
8992
) -> Result<impl IntoResponse, HandlerError> {
90-
let foundations = db.list_foundations().await?;
91-
let job = db.get_job_dashboard(&job_id).await?;
92-
let template = jobs::UpdatePage { foundations, job };
93+
let (certifications, foundations, job) = tokio::try_join!(
94+
db.list_certifications(),
95+
db.list_foundations(),
96+
db.get_job_dashboard(&job_id)
97+
)?;
98+
let template = jobs::UpdatePage {
99+
certifications,
100+
foundations,
101+
job,
102+
};
93103

94104
Ok(Html(template.render()?).into_response())
95105
}

gitjobs-server/src/templates/dashboard/employer/jobs.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::templates::{
1111
filters,
1212
helpers::{DATE_FORMAT, build_dashboard_image_url, format_location, normalize, normalize_salary},
1313
jobboard::jobs::Seniority,
14-
misc::{Foundation, Location, Project},
14+
misc::{Certification, Foundation, Location, Project},
1515
};
1616

1717
// Pages templates.
@@ -20,6 +20,8 @@ use crate::templates::{
2020
#[derive(Debug, Clone, Template, Serialize, Deserialize)]
2121
#[template(path = "dashboard/employer/jobs/add.html")]
2222
pub(crate) struct AddPage {
23+
/// List of available certifications for job requirements.
24+
pub certifications: Vec<Certification>,
2325
/// List of available foundations for job association.
2426
pub foundations: Vec<Foundation>,
2527
}
@@ -46,6 +48,8 @@ pub(crate) struct PreviewPage {
4648
#[derive(Debug, Clone, Template, Serialize, Deserialize)]
4749
#[template(path = "dashboard/employer/jobs/update.html")]
4850
pub(crate) struct UpdatePage {
51+
/// List of available certifications for job requirements.
52+
pub certifications: Vec<Certification>,
4953
/// List of available foundations for job association.
5054
pub foundations: Vec<Foundation>,
5155
/// Job details to update.
@@ -102,6 +106,8 @@ pub(crate) struct Job {
102106
pub apply_url: Option<String>,
103107
/// List of job benefits, if any.
104108
pub benefits: Option<Vec<String>>,
109+
/// Desired certifications, if any.
110+
pub certifications: Option<Vec<Certification>>,
105111
/// Unique identifier for the job, if available.
106112
pub job_id: Option<Uuid>,
107113
/// Location details for the job, if specified.

gitjobs-server/src/templates/jobboard/jobs.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::templates::{
1212
dashboard::employer::jobs::{JobKind, SalaryKind, Workplace},
1313
filters,
1414
helpers::{DATE_FORMAT, DATE_FORMAT_3, build_jobboard_image_url, option_is_none_or_default},
15-
misc::{Foundation, Location, Member, Project},
15+
misc::{Certification, Foundation, Location, Member, Project},
1616
pagination::{NavigationLinks, Pagination},
1717
};
1818

@@ -310,6 +310,8 @@ pub(crate) struct Job {
310310
pub apply_url: Option<String>,
311311
/// List of benefits, if any.
312312
pub benefits: Option<Vec<String>>,
313+
/// Desired certifications, if any.
314+
pub certifications: Option<Vec<Certification>>,
313315
/// Location of the job, if specified.
314316
pub location: Option<Location>,
315317
/// Open source status, if specified.

gitjobs-server/src/templates/misc.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ pub(crate) struct UserMenuSection {
3636

3737
// Types.
3838

39+
/// Information about a certification.
40+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41+
pub(crate) struct Certification {
42+
/// Unique identifier for the certification.
43+
pub certification_id: Uuid,
44+
/// Full name of the certification.
45+
pub name: String,
46+
/// Provider of the certification.
47+
pub provider: String,
48+
/// Short name or abbreviation.
49+
pub short_name: String,
50+
51+
/// Description of the certification.
52+
pub description: Option<String>,
53+
/// Logo URL for the certification.
54+
pub logo_url: Option<String>,
55+
/// URL to certification information.
56+
pub url: Option<String>,
57+
}
58+
3959
/// Information about a foundation.
4060
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4161
pub(crate) struct Foundation {

gitjobs-server/src/templates/notifications.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ mod tests {
8080
apply_instructions: None,
8181
apply_url: None,
8282
benefits: None,
83+
certifications: None,
8384
projects: None,
8485
published_at: None,
8586
qualifications: None,

0 commit comments

Comments
 (0)