Skip to content

Commit

Permalink
Merge branch 'master' into robin/queryclient-404-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
robines authored Jan 7, 2025
2 parents fb56774 + 39fda68 commit d5e32a3
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 30 deletions.
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"
),
),
]
42 changes: 33 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 @@ -185,11 +189,31 @@ def resolve_org(self, *, return_id: bool = False) -> Organization | int:
def __str__(self) -> str:
return f'Position: {self.name_en} in {self.recruitment}'

def clean(self) -> None:
# Error messages
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'
FILE_DESCRIPTION_REQUIRED_ERROR = 'Description of file is needed, if position has file upload'

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

if (self.gang and self.section) or not (self.gang or self.section):
raise ValidationError('Position must be owned by either gang or section, not both')
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.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:
if self.norwegian_applicants_only:
Expand Down
61 changes: 61 additions & 0 deletions backend/samfundet/models/tests/test_recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,67 @@ def test_actual_deadline_before_shown_deadline(self, fixture_org):
assert Recruitment.SHOWN_AFTER_ACTUAL_ERROR in e['shown_application_deadline']


class TestRecruitmentPosition:
default_data = {
'name_en': 'Name_en',
'name_nb': 'Name_nb',
'short_description_nb': 'short_description_nb',
'short_description_en': 'short_description_en',
'long_description_nb': 'long_description_nb',
'long_description_en': 'long_description_en',
'is_funksjonaer_position': False,
'default_application_letter_nb': 'default_application_letter_nb',
'default_application_letter_en': 'default_application_letter_en',
'norwegian_applicants_only': False,
'tags': 'tag1, tag2, tag3',
}

def test_create_recruitmentposition_gang(self, fixture_gang: Gang):
test_position = RecruitmentPosition.objects.create(**self.default_data, gang=fixture_gang)
assert test_position.id

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):
"""Check if fixture_recruitment has the related object"""
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/Components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export function Navbar() {
});
}, []);

const showActiveRecruitments = activeRecruitments !== undefined && activeRecruitments?.length > 0;

// Return profile button for navbar if logged in.
const mobileProfileButton = (
<div className={styles.navbar_profile_button}>
Expand Down Expand Up @@ -133,7 +135,7 @@ export function Navbar() {
expandedDropdown={expandedDropdown}
route={ROUTES.frontend.recruitment}
label={t(KEY.common_volunteer)}
labelClassName={activeRecruitments && styles.active_recruitment}
labelClassName={showActiveRecruitments ? styles.active_recruitment : ''}
/>
</div>
);
Expand Down
9 changes: 3 additions & 6 deletions frontend/src/Components/Table/Table.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,13 @@ th {

.sortable_th {
cursor: pointer;
position: relative;
transition: 0.2s;
}

.sort_icons {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
float: right;

position: absolute;
right: 0;
.icon {
background-color: transparent;
padding: 2px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ interface FormProps {
positionId?: string;
recruitmentId?: string;
gangId?: string;
users?: Partial<UserDto[]>;
users?: UserDto[];
}

export function RecruitmentPositionForm({ initialData, positionId, recruitmentId, gangId, users }: FormProps) {
Expand Down Expand Up @@ -98,12 +98,10 @@ export function RecruitmentPositionForm({ initialData, positionId, recruitmentId

// Convert users array to dropdown options
const interviewerOptions =
users
?.filter((user) => user?.id && (user?.username || user?.first_name))
.map((user) => ({
value: user?.id,
label: user?.username || `${user?.first_name} ${user?.last_name}`,
})) || [];
users?.map((user) => ({
value: user.id,
label: user?.username || `${user?.first_name} ${user?.last_name}`,
})) || [];

// Get currently selected interviewers
const selectedInterviewers = form.watch('interviewer_ids') || [];
Expand Down Expand Up @@ -282,11 +280,9 @@ export function RecruitmentPositionForm({ initialData, positionId, recruitmentId
selected={interviewerOptions.filter(
(option) => option.value && selectedInterviewers.includes(option.value),
)}
onChange={(values) => {
field.onChange(values);
}}
optionsLabel="Available Interviewers"
selectedLabel="Selected Interviewers"
{...field}
/>
</FormControl>
<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ export function RecruitmentPositionFormAdminPage() {
const navigate = useNavigate();
const { recruitmentId, gangId, positionId } = useParams();
const [position, setPosition] = useState<Partial<RecruitmentPositionDto>>();
const [users, setUsers] = useState<Partial<UserDto[]>>();
const [users, setUsers] = useState<UserDto[]>();

useEffect(() => {
getUsers().then((data) => {
setUsers(data);
});
}, []);

useEffect(() => {
if (positionId) {
Expand All @@ -35,9 +41,6 @@ export function RecruitmentPositionFormAdminPage() {
);
});
}
getUsers().then((data) => {
setUsers(data);
});
}, [positionId, recruitmentId, gangId, navigate, t]);

const initialData: Partial<RecruitmentPositionDto> = {
Expand Down

0 comments on commit d5e32a3

Please sign in to comment.