Skip to content

Commit

Permalink
Merge branch 'master' into feature/positionfororganization
Browse files Browse the repository at this point in the history
  • Loading branch information
magsyg authored Dec 20, 2024
2 parents c26dd35 + ce08b37 commit 51dd569
Show file tree
Hide file tree
Showing 32 changed files with 378 additions and 161 deletions.
6 changes: 3 additions & 3 deletions backend/samfundet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,17 @@ class RoleAdmin(admin.ModelAdmin):

@admin.register(UserOrgRole)
class UserOrgRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')
list_display = ('user', 'role', 'obj', 'created_at', 'created_by')


@admin.register(UserGangRole)
class UserGangRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')
list_display = ('user', 'role', 'obj', 'created_at', 'created_by')


@admin.register(UserGangSectionRole)
class UserGangSectionRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')
list_display = ('user', 'role', 'obj', 'created_at', 'created_by')


@admin.register(Permission)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.1.1 on 2024-12-07 12:20

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("samfundet", "0010_recruitment_promo_media"),
]

operations = [
migrations.AddField(
model_name="recruitmentposition",
name="file_description_en",
field=models.TextField(
blank=True, help_text="Description of file needed (EN)", null=True
),
),
migrations.AddField(
model_name="recruitmentposition",
name="file_description_nb",
field=models.TextField(
blank=True, help_text="Description of file needed (NB)", null=True
),
),
migrations.AddField(
model_name="recruitmentposition",
name="has_file_upload",
field=models.BooleanField(
default=False, help_text="Does position have file upload"
),
),
]
32 changes: 23 additions & 9 deletions backend/samfundet/models/recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ class RecruitmentPosition(CustomBaseModel):
gang = models.ForeignKey(to=Gang, on_delete=models.CASCADE, help_text='The gang that is recruiting', null=True, blank=True)
section = models.ForeignKey(GangSection, on_delete=models.CASCADE, help_text='The section that is recruiting', null=True, blank=True)

has_file_upload = models.BooleanField(help_text='Does position have file upload', default=False)
file_description_nb = models.TextField(help_text='Description of file needed (NB)', null=True, blank=True)
file_description_en = models.TextField(help_text='Description of file needed (EN)', null=True, blank=True)

# TODO: Implement tag functionality
tags = models.CharField(max_length=100, help_text='Tags for the position')

# TODO: Implement interviewer functionality
interviewers = models.ManyToManyField(to=User, help_text='Interviewers for the position', blank=True, related_name='interviewers')

recruitment = models.ForeignKey(
Recruitment,
on_delete=models.CASCADE,
Expand All @@ -161,12 +171,6 @@ class RecruitmentPosition(CustomBaseModel):
help_text='Shared interviewgroup for position',
)

# TODO: Implement tag functionality
tags = models.CharField(max_length=100, help_text='Tags for the position')

# TODO: Implement interviewer functionality
interviewers = models.ManyToManyField(to=User, help_text='Interviewers for the position', blank=True, related_name='interviewers')

def resolve_section(self, *, return_id: bool = False) -> GangSection | int:
if return_id:
# noinspection PyTypeChecker
Expand All @@ -189,19 +193,29 @@ def __str__(self) -> str:
ONLY_ONE_OWNER_ERROR = 'Position must be owned by either gang or section, not both'
NO_OWNER_ERROR = 'Position must have an owner, either a gang or a gang section'
POSITION_NOT_IN_RECRUITMENTORGANIZATION_ERROR = 'Position must be of the organization which hosts the recruitment'
FILE_DESCRIPTION_REQUIRED_ERROR = 'Description of file is needed, if position has file upload'

def clean(self) -> None:
def clean(self) -> None: # noqa: C901
super().clean()
errors: dict[str, list[ValidationError]] = defaultdict(list)

if self.gang and self.gang.organization != self.recruitment.organization:
errors['gang'].append(self.POSITION_NOT_IN_RECRUITMENTORGANIZATION_ERROR)
if self.gang and self.section:
# Both gang and section provide
errors['gang'].append(self.ONLY_ONE_OWNER_ERROR)
errors['section'].append(self.ONLY_ONE_OWNER_ERROR)
elif not (self.gang or self.section):
# neither gang nor section provided
errors['gang'].append(self.NO_OWNER_ERROR)
errors['section'].append(self.NO_OWNER_ERROR)
if self.gang and self.gang.organization != self.recruitment.organization:
errors['gang'].append(self.POSITION_NOT_IN_RECRUITMENTORGANIZATION_ERROR)
if self.has_file_upload:
# Check Norwegian file description
if not self.file_description_nb or len(self.file_description_nb) == 0:
errors['file_description_nb'].append(self.FILE_DESCRIPTION_REQUIRED_ERROR)
# Check English file description
if not self.file_description_en or len(self.file_description_en) == 0:
errors['file_description_en'].append(self.FILE_DESCRIPTION_REQUIRED_ERROR)
raise ValidationError(errors)

def save(self, *args: tuple, **kwargs: dict) -> None:
Expand Down
42 changes: 41 additions & 1 deletion backend/samfundet/models/tests/test_recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class TestRecruitmentPosition:
'norwegian_applicants_only': False,
'tags': 'tag1, tag2, tag3',
}

def test_create_recruitmentposition_gang(self, fixture_recruitment: Recruitment, fixture_gang: Gang):
test_position = RecruitmentPosition.objects.create(**self.default_data, recruitment=fixture_recruitment, gang=fixture_gang)
assert test_position.id
Expand All @@ -130,6 +130,46 @@ def test_create_recruitmentposition_gang_from_other_org(self, fixture_recruitmen
e = dict(error.value)
assert RecruitmentPosition.POSITION_NOT_IN_RECRUITMENTORGANIZATION_ERROR in e['gang']

def test_create_recruitmentposition_section(self, fixture_gang_section: GangSection):
test_position = RecruitmentPosition.objects.create(**self.default_data, section=fixture_gang_section)
assert test_position.id

def test_create_recruitmentposition_no_section(self):
with pytest.raises(ValidationError) as error:
RecruitmentPosition.objects.create(**self.default_data)
e = dict(error.value)
assert RecruitmentPosition.NO_OWNER_ERROR in e['section']
assert RecruitmentPosition.NO_OWNER_ERROR in e['gang']

def test_create_recruitmentposition_only_one_owner(self, fixture_gang_section: GangSection, fixture_gang: Gang):
with pytest.raises(ValidationError) as error:
RecruitmentPosition.objects.create(**self.default_data, section=fixture_gang_section, gang=fixture_gang)
e = dict(error.value)
assert RecruitmentPosition.ONLY_ONE_OWNER_ERROR in e['section']
assert RecruitmentPosition.ONLY_ONE_OWNER_ERROR in e['gang']

def test_create_recruitmentposition_file_upload_no_description(self, fixture_gang_section: GangSection):
with pytest.raises(ValidationError) as error:
RecruitmentPosition.objects.create(**self.default_data, section=fixture_gang_section, has_file_upload=True)
e = dict(error.value)
assert RecruitmentPosition.FILE_DESCRIPTION_REQUIRED_ERROR in e['file_description_nb']
assert RecruitmentPosition.FILE_DESCRIPTION_REQUIRED_ERROR in e['file_description_en']

with pytest.raises(ValidationError) as error:
RecruitmentPosition.objects.create(**self.default_data, section=fixture_gang_section, has_file_upload=True, file_description_en='Description')
e = dict(error.value)
assert RecruitmentPosition.FILE_DESCRIPTION_REQUIRED_ERROR in e['file_description_nb']

with pytest.raises(ValidationError) as error:
RecruitmentPosition.objects.create(**self.default_data, section=fixture_gang_section, has_file_upload=True, file_description_nb='Description')
e = dict(error.value)
assert RecruitmentPosition.FILE_DESCRIPTION_REQUIRED_ERROR in e['file_description_en']

def test_create_recruitmentposition_file_upload(self, fixture_gang_section: GangSection):
test_position = RecruitmentPosition.objects.create(
**self.default_data, section=fixture_gang_section, has_file_upload=True, file_description_en='Description', file_description_nb='Description'
)
assert test_position.id

class TestRecruitmentStats:
def test_recruitment_has_stats(self, fixture_recruitment: Recruitment):
Expand Down
6 changes: 5 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true
"recommended": true,
"correctness": {
"noUnusedImports": "error",
"useHookAtTopLevel": "error"
}
}
},
"formatter": {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function App() {

return (
<HelmetProvider>
<UserFeedback enabled={true} />
<UserFeedback />
{goatCounterCode && (
<Helmet>
{/* Helmet is linked to <head>. Used to add scripts. */}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Icon } from '@iconify/react';
import classNames from 'classnames';
import { format } from 'date-fns';
import React, { forwardRef, useMemo } from 'react';
import { forwardRef, useMemo } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MiniCalendar } from '~/Components';
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Components/Dropdown/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ComponentMeta, ComponentStory, Meta, StoryObj } from '@storybook/react';
import type { Meta, StoryObj } from '@storybook/react';
import { Dropdown } from './Dropdown';

// Local component config.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useTranslation } from 'react-i18next';
import { InputField, TimeDisplay } from '~/Components';
import { TimeDisplay } from '~/Components';
import { CrudButtons } from '~/Components/CrudButtons/CrudButtons';
import { Dropdown, type DropdownOption } from '~/Components/Dropdown/Dropdown';
import { Table } from '~/Components/Table';
import { Text } from '~/Components/Text/Text';
import { putRecruitmentApplicationForGang } from '~/api';
import type { RecruitmentApplicationDto, RecruitmentApplicationStateDto } from '~/dto';
import { useCustomNavigate } from '~/hooks';
import { KEY } from '~/i18n/constants';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default {
} as ComponentMeta<typeof UserFeedback>;

const Template: ComponentStory<typeof UserFeedback> = () => {
return <UserFeedback enabled={true} />;
return <UserFeedback />;
};

export const Primary = Template.bind({});
10 changes: 1 addition & 9 deletions frontend/src/Components/UserFeedback/UserFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,15 @@ import { useTextItem } from '~/hooks';
import { KEY } from '~/i18n/constants';
import styles from './UserFeedback.module.scss';

type UserFeedbackProps = {
enabled: boolean;
};

type FormProps = {
text: string;
contact_email?: string;
};

export function UserFeedback({ enabled }: UserFeedbackProps) {
export function UserFeedback() {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);

if (!enabled) {
return <div />;
}

const handleFormSubmit = (formData: FormProps) => {
postFeedback({
...formData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function GangSeparatePositions({ recruitmentSeparatePositions }: GangSepa
return (
<ExpandableHeader
showByDefault={true}
label={t(KEY.recruitment_gangs_with_separate_positions)}
label={t(KEY.recruitment_positions_with_separate_recruitment)}
className={styles.separate_header}
>
{recruitmentSeparatePositions.map((pos) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,40 @@ import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Button, Logo, OccupiedFormModal, Page, SamfundetLogoSpinner, Text, Video } from '~/Components';
import { PersonalRow } from '~/Pages/RecruitmentPage';
import { getOrganization, getRecruitment } from '~/api';
import { getRecruitment } from '~/api';
import { useOrganizationContext } from '~/context/OrgContextProvider';
import type { RecruitmentDto } from '~/dto';
import { useDesktop, useTitle } from '~/hooks';
import { KEY } from '~/i18n/constants';
import { OrgNameType, type OrgNameTypeValue } from '~/types';
import { dbT, getObjectFieldOrNumber } from '~/utils';
import { dbT } from '~/utils';
import { GangSeparatePositions, GangTypeContainer, RecruitmentTabs } from './Components';
import styles from './OrganizationRecruitmentPage.module.scss';

type ViewMode = 'list' | 'tab';

const ORG_STYLES = {
[OrgNameType.SAMFUNDET_NAME]: {
subHeaderStyle: styles.samfRecruitmentSubHeader,
},
[OrgNameType.UKA_NAME]: {
subHeaderStyle: styles.ukaRecruitmentSubHeader,
},
[OrgNameType.ISFIT_NAME]: {
subHeaderStyle: styles.isfitRecruitmentSubHeader,
},
[OrgNameType.FALLBACK]: {
subHeaderStyle: '',
},
};

export function OrganizationRecruitmentPage() {
const isDesktop = useDesktop();
const { recruitmentId } = useParams<{ recruitmentId: string }>();
const [viewAllPositions, setViewAllPositions] = useState<boolean>(true);
const { t } = useTranslation();
const { changeOrgTheme, organizationTheme } = useOrganizationContext();
const [recruitment, setRecruitment] = useState<RecruitmentDto>();
const [organizationName, setOrganizationName] = useState<OrgNameTypeValue>(OrgNameType.FALLBACK);
const [loading, setLoading] = useState<boolean>(true);
const [positionsViewMode, setViewMode] = useState<ViewMode>('list');

Expand All @@ -43,25 +57,10 @@ export function OrganizationRecruitmentPage() {

useEffect(() => {
if (recruitment) {
getOrganization(getObjectFieldOrNumber<number>(recruitment.organization, 'id'))
.then((response) => {
if (Object.values(OrgNameType).includes(response.name as OrgNameTypeValue)) {
setOrganizationName(response.name as OrgNameTypeValue);
}
})
.catch((error) => {
console.log(error);
setOrganizationName(OrgNameType.FALLBACK);
});
changeOrgTheme(recruitment.organization.name as OrgNameTypeValue);
}
setLoading(false);
}, [recruitment]);

useEffect(() => {
if (organizationName) {
changeOrgTheme(organizationName);
}
}, [organizationName, changeOrgTheme]);
}, [recruitment, changeOrgTheme]);

function toggleViewAll() {
const toggledValue = !viewAllPositions;
Expand All @@ -75,22 +74,24 @@ export function OrganizationRecruitmentPage() {
) : (
<div className={styles.container}>
<div className={styles.organizationHeader} style={{ backgroundColor: organizationTheme?.pagePrimaryColor }}>
<Logo organization={organizationName} color="light" size={isDesktop ? 'small' : 'xsmall'} />
<Logo
organization={(recruitment?.organization.name as OrgNameTypeValue) ?? OrgNameType.FALLBACK}
color="light"
size={isDesktop ? 'small' : 'xsmall'}
/>
<Text as="strong" size={isDesktop ? 'xl' : 'l'}>
{dbT(recruitment, 'name')}
</Text>
</div>
{recruitment?.promo_media && <Video embedId={recruitment.promo_media} className={styles.video} />}
<div
className={classNames(
organizationName === 'Samfundet' && styles.samfRecruitmentSubHeader,
organizationName === 'UKA' && styles.ukaRecruitmentSubHeader,
organizationName === 'ISFiT' && styles.isfitRecruitmentSubHeader,
ORG_STYLES[(recruitment?.organization.name as OrgNameTypeValue) ?? OrgNameType.FALLBACK].subHeaderStyle,
styles.basicRecruitmentSubHeader,
)}
>
<Text as={'strong'} size={isDesktop ? 'xl' : 'l'}>
{t(KEY.recruitment_apply_for)} {organizationName}
{t(KEY.recruitment_apply_for)} {recruitment?.organization.name}
</Text>
</div>
<div className={styles.personalRow}>
Expand All @@ -99,7 +100,7 @@ export function OrganizationRecruitmentPage() {
<OccupiedFormModal recruitmentId={+recruitmentId} />
<PersonalRow
recruitmentId={recruitmentId}
organizationName={organizationName}
organizationName={recruitment?.organization.name}
showRecruitmentBtn={false}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ROUTES } from '~/routes';

type PersonalRowProps = {
recruitmentId: string;
organizationName: string;
organizationName?: string;
showRecruitmentBtn?: boolean;
};

Expand Down
Loading

0 comments on commit 51dd569

Please sign in to comment.