Skip to content

Commit

Permalink
Merge branch 'master' into robin/update-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
robines authored Nov 1, 2024
2 parents 1aa27c3 + fb67b56 commit b8c8ca2
Show file tree
Hide file tree
Showing 127 changed files with 2,364 additions and 963 deletions.
2 changes: 2 additions & 0 deletions backend/root/management/commands/seed_scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
recruitment_separate_position,
recruitment_interviewavailability,
recruitment_position_interviewers,
recruitment_sharedinterviewgroups,
)

# Insert seed scripts here (in order of priority)
Expand All @@ -50,6 +51,7 @@
('merch', merch.seed),
('recruitment', recruitment.seed),
('recruitment_position', recruitment_position.seed),
('recruitment_position_shared_interview', recruitment_sharedinterviewgroups.seed),
('recruitment_interviewavailability', recruitment_interviewavailability.seed),
('recruitment_separate_position', recruitment_separate_position.seed),
('recruitment_applications', recruitment_applications.seed),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from __future__ import annotations

import random

from root.utils.samfundet_random import words

from samfundet.models.recruitment import Recruitment, RecruitmentPosition, RecruitmentPositionSharedInterviewGroup


def seed():
yield 0, 'recruitment_positions_shared_interview'
RecruitmentPositionSharedInterviewGroup.objects.all().delete()
yield 0, 'Deleted old recruitmentpositionsharedgroup'

recruitments = Recruitment.objects.all()
created_count = 0
for recruitment in recruitments:
for i in range(3):
RecruitmentPositionSharedInterviewGroup.objects.create(recruitment=recruitment, name_nb=f'{words(2)} {i}', name_en=f'{words(2)} {i}')
created_count += 1
shared_groups = list(RecruitmentPositionSharedInterviewGroup.objects.filter(recruitment=recruitment))
positions = random.sample(list(RecruitmentPosition.objects.filter(recruitment=recruitment)), 6)
for pos in positions:
pos.shared_interview_group = random.choice(shared_groups)
pos.save()

yield 100, f'Created {created_count} recruitment_position_shared_groups'
12 changes: 12 additions & 0 deletions backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,15 @@
admin__samfundet_recruitmentapplication_delete = 'admin:samfundet_recruitmentapplication_delete'
admin__samfundet_recruitmentapplication_change = 'admin:samfundet_recruitmentapplication_change'
adminsamfundetrecruitmentapplication__objectId = ''
admin__samfundet_recruitmentpositionsharedinterviewgroup_permissions = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_permissions'
admin__samfundet_recruitmentpositionsharedinterviewgroup_permissions_manage_user = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_permissions_manage_user'
admin__samfundet_recruitmentpositionsharedinterviewgroup_permissions_manage_group = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_permissions_manage_group'
admin__samfundet_recruitmentpositionsharedinterviewgroup_changelist = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_changelist'
admin__samfundet_recruitmentpositionsharedinterviewgroup_add = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_add'
admin__samfundet_recruitmentpositionsharedinterviewgroup_history = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_history'
admin__samfundet_recruitmentpositionsharedinterviewgroup_delete = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_delete'
admin__samfundet_recruitmentpositionsharedinterviewgroup_change = 'admin:samfundet_recruitmentpositionsharedinterviewgroup_change'
adminsamfundetrecruitmentpositionsharedinterviewgroup__objectId = ''
admin__samfundet_organization_permissions = 'admin:samfundet_organization_permissions'
admin__samfundet_organization_permissions_manage_user = 'admin:samfundet_organization_permissions_manage_user'
admin__samfundet_organization_permissions_manage_group = 'admin:samfundet_organization_permissions_manage_group'
Expand Down Expand Up @@ -502,6 +511,8 @@
samfundet__saksdokument_detail = 'samfundet:saksdokument-detail'
samfundet__profile_list = 'samfundet:profile-list'
samfundet__profile_detail = 'samfundet:profile-detail'
samfundet__permissions_list = 'samfundet:permissions-list'
samfundet__permissions_detail = 'samfundet:permissions-detail'
samfundet__menu_list = 'samfundet:menu-list'
samfundet__menu_detail = 'samfundet:menu-detail'
samfundet__menu_items_list = 'samfundet:menu_items-list'
Expand Down Expand Up @@ -575,6 +586,7 @@
samfundet__recruitment_positions = 'samfundet:recruitment_positions'
samfundet__recruitment_show_unprocessed_applicants = 'samfundet:recruitment_show_unprocessed_applicants'
samfundet__recruitment_positions_gang_for_applicants = 'samfundet:recruitment_positions_gang_for_applicants'
samfundet__recruitment_shared_interviews = 'samfundet:recruitment_shared_interviews'
samfundet__recruitment_positions_gang_for_gangs = 'samfundet:recruitment_positions_gang_for_gangs'
samfundet__recruitment_set_interview = 'samfundet:recruitment_set_interview'
samfundet__recruitment_application_states_choices = 'samfundet:recruitment_application_states_choices'
Expand Down
23 changes: 23 additions & 0 deletions backend/samfundet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
RecruitmentApplication,
RecruitmentSeparatePosition,
RecruitmentInterviewAvailability,
RecruitmentPositionSharedInterviewGroup,
)

# Common fields:
Expand Down Expand Up @@ -715,6 +716,28 @@ class RecruitmentApplicationAdmin(CustomBaseAdmin):
list_select_related = True


@admin.register(RecruitmentPositionSharedInterviewGroup)
class RecruitmentPositionSharedInterviewGroupAdmin(CustomBaseAdmin):
sortable_by = [
'recruitment',
'name_en',
'name_nb',
'__str__',
]
list_display = [
'recruitment',
'name_en',
'name_nb',
'__str__',
]
search_fields = [
'recruitment',
'name_en',
'name_nb',
'__str__',
]


@admin.register(Organization)
class OrganizationAdmin(CustomBaseAdmin):
sortable_by = ['id', 'name']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.1.1 on 2024-10-29 17:39

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("samfundet", "0008_event_entrance_en_event_entrance_nb"),
]

operations = [
migrations.AddField(
model_name="recruitmentpositionsharedinterviewgroup",
name="name_en",
field=models.CharField(
default="hi",
help_text="Name of the recruitmentgroup (EN)",
max_length=100,
),
preserve_default=False,
),
migrations.AddField(
model_name="recruitmentpositionsharedinterviewgroup",
name="name_nb",
field=models.CharField(
default="hi",
help_text="Name of the recruitmentgroup (NB)",
max_length=100,
),
preserve_default=False,
),
]
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),
),
]
7 changes: 6 additions & 1 deletion 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 Expand Up @@ -115,8 +116,11 @@ class RecruitmentPositionSharedInterviewGroup(CustomBaseModel):
blank=True,
)

name_nb = models.CharField(max_length=100, null=False, blank=False, help_text='Name of the recruitmentgroup (NB)')
name_en = models.CharField(max_length=100, null=False, blank=False, help_text='Name of the recruitmentgroup (EN)')

def __str__(self) -> str:
return f'{self.recruitment} Interviewgroup {self.id}'
return f'{self.recruitment} Interviewgroup {self.name_nb} {", ".join(list(self.positions.values_list("name_nb", flat=True)))}'


class RecruitmentPosition(CustomBaseModel):
Expand Down Expand Up @@ -261,6 +265,7 @@ class Interview(CustomBaseModel):
help_text='Room where the interview is held',
related_name='interviews',
)

interviewers = models.ManyToManyField(to='samfundet.User', help_text='Interviewers for this interview', blank=True, related_name='interviews')
notes = models.TextField(help_text='Notes for the interview', null=True, blank=True)

Expand Down
4 changes: 2 additions & 2 deletions backend/samfundet/models/tests/test_recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def test_interview_group_autoadd_on_create(
assert fixture_recruitment_position2.shared_interview_group is None

# setup interview group
shared_group = RecruitmentPositionSharedInterviewGroup.objects.create(recruitment=fixture_recruitment)
shared_group = RecruitmentPositionSharedInterviewGroup.objects.create(recruitment=fixture_recruitment, name_en='name', name_nb='navn')
fixture_recruitment_position.shared_interview_group = shared_group
fixture_recruitment_position2.shared_interview_group = shared_group
fixture_recruitment_position.save()
Expand Down Expand Up @@ -288,7 +288,7 @@ def test_interview_group_autoset_on_set(
assert fixture_recruitment_application2.recruitment_position == fixture_recruitment_position2

# setup interview group
shared_group = RecruitmentPositionSharedInterviewGroup.objects.create(recruitment=fixture_recruitment)
shared_group = RecruitmentPositionSharedInterviewGroup.objects.create(recruitment=fixture_recruitment, name_en='name', name_nb='navn')
fixture_recruitment_position.shared_interview_group = shared_group
fixture_recruitment_position2.shared_interview_group = shared_group
fixture_recruitment_position.save()
Expand Down
64 changes: 49 additions & 15 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 @@ -64,6 +65,7 @@
RecruitmentApplication,
RecruitmentSeparatePosition,
RecruitmentInterviewAvailability,
RecruitmentPositionSharedInterviewGroup,
)
from .models.model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices

Expand Down Expand Up @@ -738,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 All @@ -769,6 +782,7 @@ class RecruitmentPositionSerializer(CustomBaseSerializer):

gang = GangSerializer(read_only=True)
interviewers = InterviewerSerializer(many=True, read_only=True)
interviewer_ids = serializers.ListField(child=serializers.IntegerField(), write_only=True, required=False)

class Meta:
model = RecruitmentPosition
Expand All @@ -778,37 +792,43 @@ def _update_interviewers(
self,
*,
recruitment_position: RecruitmentPosition,
interviewer_objects: list[dict],
interviewer_ids: list[int],
) -> None:
try:
interviewers = []
if interviewer_objects:
interviewer_ids = [interviewer.get('id') for interviewer in interviewer_objects]
if interviewer_ids:
interviewers = User.objects.filter(id__in=interviewer_ids)
recruitment_position.interviewers.set(interviewers)
except (TypeError, KeyError):
raise ValidationError('Invalid data for interviewers.') from None
if interviewer_ids:
try:
interviewers = User.objects.filter(id__in=interviewer_ids)
found_ids = set(interviewers.values_list('id', flat=True))
invalid_ids = set(interviewer_ids) - found_ids

if invalid_ids:
raise ValidationError(f'Invalid interviewer IDs: {invalid_ids}')

recruitment_position.interviewers.set(interviewers)
except (TypeError, ValueError):
raise ValidationError('Invalid interviewer IDs format.') from None
else:
recruitment_position.interviewers.clear()

def validate(self, data: dict) -> dict:
gang_id = self.initial_data.get('gang').get('id')
gang_id = self.initial_data.get('gang', {}).get('id')
if gang_id:
try:
data['gang'] = Gang.objects.get(id=gang_id)
except Gang.DoesNotExist:
raise serializers.ValidationError('Invalid gang id') from None

self.interviewer_ids = data.pop('interviewer_ids', [])

return super().validate(data)

def create(self, validated_data: dict) -> RecruitmentPosition:
recruitment_position = super().create(validated_data)
interviewer_objects = self.initial_data.get('interviewers', [])
self._update_interviewers(recruitment_position=recruitment_position, interviewer_objects=interviewer_objects)
self._update_interviewers(recruitment_position=recruitment_position, interviewer_ids=self.interviewer_ids)
return recruitment_position

def update(self, instance: RecruitmentPosition, validated_data: dict) -> RecruitmentPosition:
updated_instance = super().update(instance, validated_data)
interviewer_objects = self.initial_data.get('interviewers', [])
self._update_interviewers(recruitment_position=updated_instance, interviewer_objects=interviewer_objects)
self._update_interviewers(recruitment_position=updated_instance, interviewer_ids=self.interviewer_ids)
return updated_instance

def get_total_applicants(self, recruitment_position: RecruitmentPosition) -> int:
Expand Down Expand Up @@ -858,6 +878,20 @@ class Meta:
]


class RecruitmentPositionSharedInterviewGroupSerializer(serializers.ModelSerializer):
positions = RecruitmentPositionForApplicantSerializer(many=True, read_only=True)

class Meta:
model = RecruitmentPositionSharedInterviewGroup
fields = [
'id',
'recruitment',
'positions',
'name_en',
'name_nb',
]


class RecruitmentApplicationForApplicantSerializer(CustomBaseSerializer):
interview = ApplicantInterviewSerializer(read_only=True)

Expand Down
5 changes: 5 additions & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@
views.RecruitmentPositionsPerGangForApplicantView.as_view(),
name='recruitment_positions_gang_for_applicants',
),
path(
'recruitment-shared-interview-groups/<int:recruitment_id>/',
views.RecruitmentInterviewGroupView.as_view(),
name='recruitment_shared_interviews',
),
path('recruitment-positions-gang-for-gangs/', views.RecruitmentPositionsPerGangForGangView.as_view(), name='recruitment_positions_gang_for_gangs'),
path('recruitment-set-interview/<slug:pk>/', views.RecruitmentApplicationSetInterviewView.as_view(), name='recruitment_set_interview'),
path(
Expand Down
16 changes: 16 additions & 0 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
RecruitmentApplicationForRecruiterSerializer,
RecruitmentApplicationUpdateForGangSerializer,
RecruitmentShowUnprocessedApplicationsSerializer,
RecruitmentPositionSharedInterviewGroupSerializer,
)
from .models.event import (
Event,
Expand Down Expand Up @@ -137,6 +138,7 @@
RecruitmentApplication,
RecruitmentSeparatePosition,
RecruitmentInterviewAvailability,
RecruitmentPositionSharedInterviewGroup,
)
from .models.model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices

Expand Down Expand Up @@ -1109,6 +1111,20 @@ def get_queryset(self) -> Response:
return Recruitment.objects.filter(visible_from__lte=timezone.now(), actual_application_deadline__gte=timezone.now())


class RecruitmentInterviewGroupView(APIView):
permission_classes = [IsAuthenticated]

def get(
self,
request: Request,
recruitment_id: int,
) -> HttpResponse:
recruitment = get_object_or_404(Recruitment, id=recruitment_id)
interview_groups = RecruitmentPositionSharedInterviewGroup.objects.filter(recruitment=recruitment)

return Response(data=RecruitmentPositionSharedInterviewGroupSerializer(interview_groups, many=True).data, status=status.HTTP_200_OK)


class DownloadRecruitmentApplicationGangCSV(APIView):
permission_classes = [IsAuthenticated]

Expand Down
2 changes: 1 addition & 1 deletion frontend/.stylelintrc
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"declaration-block-single-line-max-declarations": null,
"declaration-property-max-values": null,
"max-nesting-depth": [
1,
2,
{
"ignoreAtRules": [""],
"ignore": ["pseudo-classes", "blockless-at-rules"]
Expand Down
Loading

0 comments on commit b8c8ca2

Please sign in to comment.