Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/npm_and_yarn/frontend/cross-spa…
Browse files Browse the repository at this point in the history
…wn-7.0.6
  • Loading branch information
Snorre98 authored Jan 9, 2025
2 parents bf01be2 + 1dd5622 commit 5a78c17
Show file tree
Hide file tree
Showing 54 changed files with 1,089 additions and 279 deletions.
4 changes: 2 additions & 2 deletions backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,15 +564,14 @@
samfundet__recruitment_applications_for_position_detail = 'samfundet:recruitment_applications_for_position-detail'
samfundet__interview_list = 'samfundet:interview-list'
samfundet__interview_detail = 'samfundet:interview-detail'
samfundet__api_root = 'samfundet:api-root'
samfundet__api_root = 'samfundet:api-root'
samfundet__schema = 'samfundet:schema'
samfundet__swagger_ui = 'samfundet:swagger_ui'
samfundet__redoc = 'samfundet:redoc'
samfundet__csrf = 'samfundet:csrf'
samfundet__login = 'samfundet:login'
samfundet__register = 'samfundet:register'
samfundet__logout = 'samfundet:logout'
samfundet__change_password = 'samfundet:change-password'
samfundet__user = 'samfundet:user'
samfundet__groups = 'samfundet:groups'
samfundet__users = 'samfundet:users'
Expand Down Expand Up @@ -603,6 +602,7 @@
samfundet__applicants_without_interviews = 'samfundet:applicants_without_interviews'
samfundet__applicants_without_three_interview_criteria = 'samfundet:applicants_without_three_interview_criteria'
samfundet__recruitment_recruiter_dashboard = 'samfundet:recruitment_recruiter_dashboard'
samfundet__recruitment_download_applications_csv = 'samfundet:recruitment_download_applications_csv'
samfundet__recruitment_download_gang_application_csv = 'samfundet:recruitment_download_gang_application_csv'
samfundet__occupied_timeslots = 'samfundet:occupied_timeslots'
samfundet__recruitment_interview_availability = 'samfundet:recruitment_interview_availability'
Expand Down
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"
),
),
]
57 changes: 49 additions & 8 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,11 +171,10 @@ 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 get_section_name(self, language: str = 'nb') -> str | None:
if not self.section:
return None
return self.section.name_nb if language == 'nb' else self.section.name_en

def resolve_section(self, *, return_id: bool = False) -> GangSection | int:
if return_id:
Expand All @@ -185,11 +194,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 Expand Up @@ -401,6 +430,18 @@ def save(self, *args: tuple, **kwargs: dict) -> None: # noqa: C901

super().save(*args, **kwargs)

def get_total_interviews_for_gang(self) -> int:
return (
RecruitmentApplication.objects.filter(user=self.user, recruitment=self.recruitment, recruitment_position__gang=self.resolve_gang(), withdrawn=False)
.exclude(interview=None)
.count()
)

def get_total_applications_for_gang(self) -> int:
return RecruitmentApplication.objects.filter(
user=self.user, recruitment=self.recruitment, withdrawn=False, recruitment_position__gang=self.resolve_gang()
).count()

def get_total_interviews(self) -> int:
return RecruitmentApplication.objects.filter(user=self.user, recruitment=self.recruitment, withdrawn=False).exclude(interview=None).count()

Expand Down
208 changes: 208 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 Expand Up @@ -420,6 +481,153 @@ def test_check_withdraw_sets_unwanted(self, fixture_recruitment_application: Rec
assert fixture_recruitment_application.recruiter_status == RecruitmentStatusChoices.AUTOMATIC_REJECTION
assert fixture_recruitment_application.recruiter_priority == RecruitmentPriorityChoices.NOT_WANTED

def test_recruitmentapplication_total_applications_two_gangs(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_gang2: Gang,
fixture_recruitment: Recruitment,
):
fixture_recruitment_position2.gang = fixture_gang2
fixture_recruitment_position2.save()

assert fixture_recruitment_position2.gang != fixture_recruitment_position.gang

test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
test_application1.save()
test_application2.save()
assert test_application1.get_total_applications() > 0
assert test_application1.get_total_applications() == test_application2.get_total_applications()
assert test_application1.get_total_applications_for_gang() == 1
assert test_application2.get_total_applications_for_gang() == 1
assert test_application1.get_total_applications_for_gang() != test_application1.get_total_applications()
assert test_application2.get_total_applications_for_gang() != test_application2.get_total_applications()

def test_recruitmentapplication_total_applications_single_gang(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_recruitment: Recruitment,
):
test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
test_application1.save()
test_application2.save()
assert test_application1.get_total_applications() > 0
assert test_application1.get_total_applications() == test_application2.get_total_applications()
assert test_application1.get_total_applications_for_gang() == test_application2.get_total_applications_for_gang()
assert test_application1.get_total_applications_for_gang() == test_application1.get_total_applications()

def test_recruitmentapplication_total_interviews_two_gangs(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_gang2: Gang,
fixture_recruitment: Recruitment,
):
fixture_recruitment_position2.gang = fixture_gang2
fixture_recruitment_position2.save()
# Create two interviews with separate gangs
assert fixture_recruitment_position2.gang != fixture_recruitment_position.gang

test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
# assign 1 interview to one of them
test_application1.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application1.get_total_interviews()
assert test_application2.get_total_interviews_for_gang() != test_application2.get_total_interviews()

# test with both having an interview each
test_application2.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application2.get_total_interviews_for_gang()
assert test_application1.get_total_interviews_for_gang() != test_application1.get_total_interviews()
assert test_application2.get_total_interviews_for_gang() != test_application2.get_total_interviews()

def test_recruitmentapplication_total_interviews_single_gang(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_recruitment: Recruitment,
):
test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
test_application1.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application2.get_total_interviews_for_gang()
assert test_application1.get_total_interviews_for_gang() == test_application1.get_total_interviews()

test_application2.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application2.get_total_interviews_for_gang()
assert test_application1.get_total_interviews_for_gang() == test_application1.get_total_interviews()


class TestRecruitmentApplicationStatus:
def test_check_called_accepted_sets_auto_rejection(
Expand Down
Loading

0 comments on commit 5a78c17

Please sign in to comment.