Skip to content

Commit

Permalink
Merge branch 'master' into robin/query-keys
Browse files Browse the repository at this point in the history
  • Loading branch information
robines authored Nov 6, 2024
2 parents d7f83eb + 0fa76f0 commit ec17aa5
Show file tree
Hide file tree
Showing 37 changed files with 232 additions and 84 deletions.
18 changes: 18 additions & 0 deletions backend/samfundet/migrations/0010_recruitment_promo_media.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.1 on 2024-10-31 19:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0009_recruitmentpositionsharedinterviewgroup_name_en_and_more'),
]

operations = [
migrations.AddField(
model_name='recruitment',
name='promo_media',
field=models.CharField(blank=True, default=None, help_text='Youtube video id', max_length=11, null=True),
),
]
1 change: 1 addition & 0 deletions backend/samfundet/models/recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Recruitment(CustomBaseModel):
organization = models.ForeignKey(null=False, blank=False, to=Organization, on_delete=models.CASCADE, help_text='The organization that is recruiting')

max_applications = models.PositiveIntegerField(null=True, blank=True, verbose_name='Max applications per applicant')
promo_media = models.CharField(max_length=11, help_text='Youtube video id', null=True, default=None, blank=True)

def resolve_org(self, *, return_id: bool = False) -> Organization | int:
if return_id:
Expand Down
12 changes: 12 additions & 0 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
import itertools
from typing import TYPE_CHECKING
from collections import defaultdict
Expand Down Expand Up @@ -739,11 +740,22 @@ class Meta:

class RecruitmentSerializer(CustomBaseSerializer):
separate_positions = RecruitmentSeparatePositionSerializer(many=True, read_only=True)
promo_media = serializers.CharField(max_length=100, allow_blank=True, allow_null=True)

class Meta:
model = Recruitment
fields = '__all__'

def validate_promo_media(self, value: str | None) -> str | None:
if value is None or value == '':
return None
match = re.search(r'(youtu.*be.*)\/(watch\?v=|embed\/|v|shorts|)(.*?((?=[&#?])|$))', value)
if match:
return match.group(3)
if len(value) == 11:
return value
raise ValidationError('Invalid youtube url')

def to_representation(self, instance: Recruitment) -> dict:
data = super().to_representation(instance)
data['organization'] = OrganizationSerializer(instance.organization).data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Table } from '~/Components/Table';
import type { RecruitmentUserDto } from '~/dto';
import { KEY } from '~/i18n/constants';
import { reverse } from '~/named-urls';
import { ROUTES } from '~/routes';
import { dbT } from '~/utils';
import { InputField } from '../InputField';
Expand All @@ -24,7 +25,7 @@ export function RecruitmentWithoutInterviewTable({ applicants }: RecruitmentWith
{ content: t(KEY.common_phonenumber), sortable: true },
{ content: t(KEY.recruitment_applicant_top_position), sortable: true },
{ content: t(KEY.recruitment_number_of_applications), sortable: true },
{ content: t(KEY.common_processed), sortable: true },
{ content: t(KEY.recruitment_interview_planned), sortable: true },
];

function filterUsers(): RecruitmentUserDto[] {
Expand Down Expand Up @@ -54,7 +55,19 @@ export function RecruitmentWithoutInterviewTable({ applicants }: RecruitmentWith
return [
{
value: `${user.first_name} ${user.last_name}`,
content: <Link url={ROUTES.frontend.recruitment_application}>{`${user.first_name} ${user.last_name}`}</Link>,
content: (
<Link
url={reverse({
pattern: ROUTES.frontend.admin_recruitment_applicant,
urlParams: {
applicationID: user.top_application.id,
},
})}
className={styles.text}
>
{user.first_name} {user.last_name}
</Link>
),
},
user.email,
user.phone_number,
Expand All @@ -64,7 +77,8 @@ export function RecruitmentWithoutInterviewTable({ applicants }: RecruitmentWith
value: user.applications_without_interview ? user.applications_without_interview.length : 0,
content: (
<WithoutInterviewModal
applications_without_interview={user.applications_without_interview}
user={user}
applicationsWithoutInterview={user.applications_without_interview}
applications={user.applications}
/>
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Icon } from '@iconify/react';
import { useTranslation } from 'react-i18next';
import { Link } from '~/Components';
import { Link, Text } from '~/Components';
import { Table } from '~/Components/Table';
import type { RecruitmentApplicationDto } from '~/dto';
import type { RecruitmentApplicationDto, RecruitmentUserDto } from '~/dto';
import { KEY } from '~/i18n/constants';
import { reverse } from '~/named-urls';
import { ROUTES } from '~/routes';
Expand All @@ -10,40 +11,65 @@ import styles from './WithoutInterview.module.scss';

type WithoutInterviewListProps = {
applications: RecruitmentApplicationDto[];
user: RecruitmentUserDto;
applicationsWithoutInterview: RecruitmentApplicationDto[];
};

export function WithoutInterviewList({ applications }: WithoutInterviewListProps) {
export function WithoutInterviewList({ applications, user, applicationsWithoutInterview }: WithoutInterviewListProps) {
const { t } = useTranslation();

const tableColumns = [
{ content: t(KEY.recruitment_position), sortable: true },

{ content: t(KEY.recruitment_interview_planned), sortable: true },
{ content: t(KEY.recruitment_priority), sortable: true },
];

function applicationToRow(application: RecruitmentApplicationDto) {
const hasInterview = !applicationsWithoutInterview.some((app) => app.id === application.id);

return {
cells: [
{
value: dbT(application.recruitment_position, 'name'),
content: (
<Link
url={reverse({
pattern: ROUTES.frontend.admin_recruitment_applicant,
pattern: ROUTES.frontend.admin_recruitment_gang_position_applicants_overview,
urlParams: {
applicationID: application.id,
recruitmentId: application.recruitment,
gangId: application.recruitment_position.gang.id,
positionId: application.recruitment_position.id,
},
})}
>
{dbT(application.recruitment_position, 'name')}
</Link>
),
},

{
value: hasInterview ? 1 : 0,
content: (
<Icon
icon={
hasInterview
? 'material-symbols:check-circle-outline-rounded'
: 'material-symbols:close-small-outline-rounded'
}
/>
),
},
application.applicant_priority,
],
};
}

return (
<div className={styles.container}>
<Text size="l">
{user.first_name} {user.last_name}
</Text>
<Table columns={tableColumns} data={applications.map((application) => applicationToRow(application))} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { useState } from 'react';
import { Button, IconButton, Modal } from '~/Components';
import type { RecruitmentApplicationDto } from '~/dto';
import type { RecruitmentApplicationDto, RecruitmentUserDto } from '~/dto';
import styles from './WithoutInterview.module.scss';
import { WithoutInterviewList } from './WithoutInterviewList';

type WithoutInterviewModalProps = {
user: RecruitmentUserDto;
applications: RecruitmentApplicationDto[];
applications_without_interview: RecruitmentApplicationDto[];
applicationsWithoutInterview: RecruitmentApplicationDto[];
};

export function WithoutInterviewModal({ applications, applications_without_interview }: WithoutInterviewModalProps) {
export function WithoutInterviewModal({
applications,
applicationsWithoutInterview,
user,
}: WithoutInterviewModalProps) {
const [withoutInterviewModal, setWithoutInterviewModal] = useState(false);

return (
<>
<Button theme="text" onClick={() => setWithoutInterviewModal(true)}>
{applications.length - applications_without_interview.length} / {applications.length}
<Button theme="outlined" display="pill" onClick={() => setWithoutInterviewModal(true)}>
{applications.length - applicationsWithoutInterview.length} / {applications.length}
</Button>
<Modal isOpen={withoutInterviewModal}>
<IconButton
Expand All @@ -24,7 +29,12 @@ export function WithoutInterviewModal({ applications, applications_without_inter
icon="mdi:close"
onClick={() => setWithoutInterviewModal(false)}
/>
<WithoutInterviewList applications={applications_without_interview} />

<WithoutInterviewList
applicationsWithoutInterview={applicationsWithoutInterview}
user={user}
applications={applications}
/>
</Modal>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,7 @@ import darkAvatar from '~/assets/contributors/default_dark.svg';
import { THEME } from '~/constants';
import { useGlobalContext } from '~/context/GlobalContextProvider';
import { KEY } from '~/i18n/constants';

export type Contributor = {
name: string;
from: string;
to?: string;
github?: string;
picture?: string;
websjef?: {
from: string;
to: string;
};
};
import type { Contributor } from './types';

type Props = {
contributor: Contributor;
Expand Down
21 changes: 5 additions & 16 deletions frontend/src/Pages/ContributorsPage/ContributorsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,14 @@ import robines from '~/assets/contributors/robines.jpg';
import snorre98 from '~/assets/contributors/snorre98.jpg';
import sygard from '~/assets/contributors/sygard.jpg';
import { KEY } from '~/i18n/constants';
import { ContributorItem } from './ContributorItem';
import styles from './ContributorsPage.module.scss';
import { type Contributor, ContributorItem } from './components';
import type { Contributor } from './types';

// biome-ignore format: array should not be formatted
const CONTRIBUTORS: Contributor[] = [
// H17
{
name: 'Kevin Kristiansen',
github: 'KevinKristiansen',
from: 'H17',
to: 'V20',
websjef: { from: 'V18', to: 'H18' },
},
{ name: 'Kevin Kristiansen', github: 'KevinKristiansen', from: 'H17', to: 'V20', websjef: { from: 'V18', to: 'H18' } },
// H19
{ name: 'Emil Telstad', github: 'emilte', from: 'H19' },
{ name: 'Sigve Røkenes', github: 'evgiz', from: 'H19', to: 'H23', websjef: { from: 'H20', to: 'V21' } },
Expand All @@ -39,15 +34,9 @@ const CONTRIBUTORS: Contributor[] = [
{ name: 'Eirik Hoem', github: 'eiriksho', from: 'H22', to: 'V23' },
{ name: 'Simen Seeberg-Rommetveit', github: 'simensee', from: 'H22' },
// V23
{
name: 'Robin Espinosa Jelle',
github: 'robines',
from: 'V23',
picture: robines,
websjef: { from: 'H23', to: 'V24' },
},
{ name: 'Robin Espinosa Jelle', github: 'robines', from: 'V23', picture: robines, websjef: { from: 'H23', to: 'V24' } },
{ name: 'Johanne Grønlien Gjedrem', github: 'johannegg', from: 'V23' },
{ name: 'Tinius Presterud', github: 'tiniuspre', from: 'V23' },
{ name: 'Tinius Presterud', github: 'tiniuspre', from: 'V23', to: 'V24' },
// H23
{ name: 'Amalie Johansen Vik', github: 'amaliejvik', from: 'H23', picture: amaliejvik },
{ name: 'Marion Lystad', github: 'marionlys', from: 'H23' },
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion frontend/src/Pages/ContributorsPage/components/index.ts

This file was deleted.

11 changes: 11 additions & 0 deletions frontend/src/Pages/ContributorsPage/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type Contributor = {
name: string;
from: string;
to?: string;
github?: string;
picture?: string;
websjef?: {
from: string;
to: string;
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function GangPosition({ type, recruitmentPositions }: GangItemProps) {
<Link
url={reverse({
pattern: ROUTES.frontend.recruitment_application,
urlParams: { positionId: pos.id, gangId: gang.id },
urlParams: { positionId: pos.id, recruitmentId: pos.recruitment },
})}
className={styles.position_short_desc}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import styles from './GangPositionDropdown.module.scss';

type GangItemProps = {
type: GangTypeDto;
recruitmentId?: string;
recruitmentPositions?: RecruitmentPositionDto[];
};

//TODO: DO IN ISSUE #1121, only get gang types recruiting from backend
// TODO: so the filtering should be done from the backend
export function GangPositionDropdown({ type, recruitmentPositions }: GangItemProps) {
export function GangPositionDropdown({ type, recruitmentPositions, recruitmentId }: GangItemProps) {
const filteredGangs = type.gangs
.map((gang) => {
const filteredPositions = recruitmentPositions?.filter((pos) => pos.gang.id === gang.id);
Expand All @@ -30,7 +31,7 @@ export function GangPositionDropdown({ type, recruitmentPositions }: GangItemPro
<Link
url={reverse({
pattern: ROUTES.frontend.recruitment_application,
urlParams: { positionId: pos.id, gangId: gang.id },
urlParams: { positionId: pos.id, recruitmentId: recruitmentId },
})}
className={styles.position_name}
>
Expand All @@ -39,7 +40,7 @@ export function GangPositionDropdown({ type, recruitmentPositions }: GangItemPro
<Link
url={reverse({
pattern: ROUTES.frontend.recruitment_application,
urlParams: { positionId: pos.id, gangId: gang.id },
urlParams: { positionId: pos.id, recruitmentId: recruitmentId },
})}
className={styles.position_short_desc}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { SamfundetLogoSpinner } from '~/Components';
import { getGangList, getRecruitmentPositions } from '~/api';
import type { GangTypeDto, RecruitmentPositionDto } from '~/dto';
import { GangPositionDropdown } from '../GangPositionDropdown';

type GangTypeContainerProps = {
recruitmentId: string;
};

export function GangTypeContainer({ recruitmentId = '-1' }: GangTypeContainerProps) {
export function GangTypeContainer() {
const [recruitmentPositions, setRecruitmentPositions] = useState<RecruitmentPositionDto[]>();
const [recruitingGangTypes, setRecruitingGangs] = useState<GangTypeDto[]>();
const [loading, setLoading] = useState<boolean>(true);

const { recruitmentId } = useParams();
useEffect(() => {
if (!recruitmentId) return;
Promise.all([getRecruitmentPositions(recruitmentId), getGangList()])
.then(([recruitmentRes, gangsRes]) => {
setRecruitmentPositions(recruitmentRes.data);
Expand All @@ -31,7 +29,12 @@ export function GangTypeContainer({ recruitmentId = '-1' }: GangTypeContainerPro
) : (
<>
{recruitingGangTypes?.map((gangType) => (
<GangPositionDropdown key={gangType.id} type={gangType} recruitmentPositions={recruitmentPositions} />
<GangPositionDropdown
key={gangType.id}
type={gangType}
recruitmentPositions={recruitmentPositions}
recruitmentId={recruitmentId}
/>
))}
</>
);
Expand Down
Loading

0 comments on commit ec17aa5

Please sign in to comment.