Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Find optimal interview slot #1271

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
fcc650b
adds occupied time slot seed script
Snorre98 Jul 22, 2024
3bbaee1
ruff 🐕‍🦺🐕‍🦺
Snorre98 Jul 22, 2024
47bcf4d
adds seed script for interviewers. Seeds at least three interviewers …
Snorre98 Jul 22, 2024
4a6973d
adds InterviewTimeblock model
Snorre98 Jul 22, 2024
b1cdae1
working BAD solution. Generate blocks using API call
Snorre98 Jul 22, 2024
61caacb
adds "generate and rate blocks" script
Snorre98 Jul 23, 2024
53623a4
Merge branch 'master' into find-optimal-interview-slot
Snorre98 Sep 23, 2024
a7b4398
something generated
Snorre98 Sep 23, 2024
c0f0bb6
working, but without interviewers
Snorre98 Sep 23, 2024
80b3aeb
code creates correct blocks with rating
Snorre98 Sep 24, 2024
e433ddb
adds assign interview logic
Snorre98 Sep 24, 2024
216a31f
Merge branch 'master' into find-optimal-interview-slot
Snorre98 Sep 25, 2024
8c3062f
good working interview allocation for Samfundet
Snorre98 Sep 25, 2024
94eb43c
adds ecceptions
Snorre98 Sep 25, 2024
fe934ab
adds comments for pre-preview
Snorre98 Sep 25, 2024
553d568
Merge branch 'master' into find-optimal-interview-slot
Snorre98 Oct 1, 2024
e2af7fe
Merge branch 'master' into find-optimal-interview-slot
Snorre98 Oct 17, 2024
c44ca6b
starts removing InterviewTimeBlocks table
Snorre98 Oct 18, 2024
b4c72e9
adds automatic interview allocation, which work without InterviewTime…
Snorre98 Oct 18, 2024
a4e2232
adds more logging
Snorre98 Oct 18, 2024
c8afe3f
removes logging
Snorre98 Oct 18, 2024
6a91d43
adds comments
Snorre98 Oct 18, 2024
2a05fe6
adds python module for interview allocation
Snorre98 Oct 18, 2024
fec04c4
moves exceptions to pythin package
Snorre98 Oct 18, 2024
dec54f3
fixes automatic interview allocation place and adds type to view
Snorre98 Oct 18, 2024
c001181
removes logger from views
Snorre98 Oct 18, 2024
5d04cb8
removes commeted out code
Snorre98 Oct 18, 2024
5b89e35
reafactor script structure
Snorre98 Oct 18, 2024
db46498
fixed imports
Snorre98 Oct 18, 2024
18a886e
adds default params to generate_position_interview_schedule
Snorre98 Oct 18, 2024
5d2a226
completes adding types
Snorre98 Oct 20, 2024
642c994
🦮🦮 ruff
Snorre98 Oct 20, 2024
ceb4e5a
Improved doc string for InterviewBlock
Snorre98 Oct 20, 2024
a08f179
adds consideration for shared interviews in block rating
Snorre98 Oct 20, 2024
8f227ab
improved typing in util functions
Snorre98 Oct 20, 2024
d115008
improved doc strings and typing
Snorre98 Oct 20, 2024
5305afb
refactor naming of functions
Snorre98 Oct 20, 2024
7099f8c
large refactor, to implement more efficient checks, as well as improv…
Snorre98 Oct 20, 2024
ebc49e4
naming refactor
Snorre98 Oct 20, 2024
1cadd19
renaming
Snorre98 Oct 20, 2024
277fc27
remove commented code
Snorre98 Oct 20, 2024
b4226bb
removes check not needed
Snorre98 Oct 20, 2024
71dfac6
adds tests
Snorre98 Oct 20, 2024
cbd3c33
renamed test file and fixed interview allocation view
Snorre98 Oct 20, 2024
8a2cfac
ruff
Snorre98 Oct 20, 2024
e7b83ff
deltes my migration babies
Snorre98 Oct 22, 2024
262c920
fixes test, works in Docker on my maching
Snorre98 Oct 22, 2024
a99a632
ruff you dirty dog!
Snorre98 Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/root/management/commands/seed_scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
recruitment_position,
recruitment_applications,
recruitment_occupied_time,
recruitment_occupied_time,
recruitment_separate_position,
recruitment_interviewavailability,
recruitment_position_interviewers,
)

# Insert seed scripts here (in order of priority)
Expand Down Expand Up @@ -53,6 +55,7 @@
('recruitment_separate_position', recruitment_separate_position.seed),
('recruitment_applications', recruitment_applications.seed),
('recruitment_occupied_time', recruitment_occupied_time.seed),
('recruitment_position_interviewers', recruitment_position_interviewers.seed),
# Example seed (not run unless targeted specifically)
('example', example.seed),
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from __future__ import annotations
robines marked this conversation as resolved.
Show resolved Hide resolved

from random import sample, randint

from samfundet.models.general import User
from samfundet.models.recruitment import RecruitmentPosition, RecruitmentApplication


def seed():
yield 0, 'recruitment_interviewers'
# Clear any existing interviewers
for position in RecruitmentPosition.objects.all():
position.interviewers.clear()
yield 0, 'Cleared old interviewers'

positions = RecruitmentPosition.objects.all()
applicants_by_position = RecruitmentApplication.objects.values_list('recruitment_position_id', 'user_id')

created_count = 0

for position_index, position in enumerate(positions):
# Get the users who have applied for this position
applicants_for_position = set(user_id for pos_id, user_id in applicants_by_position if pos_id == position.id)
# Get the list of interviewers excluding those who applied for this position
interviewers = User.objects.exclude(id__in=applicants_for_position)

available_interviewers = list(interviewers)
if len(available_interviewers) < 3:
print(f'Skipping position {position.id} due to insufficient interviewers')
continue # Skip this position if there are not enough interviewers

# Select 3-6 random interviewers for each position
num_interviewers = randint(3, min(6, len(available_interviewers)))
selected_interviewers = sample(available_interviewers, num_interviewers)

# Assign interviewers to the position
position.interviewers.set(selected_interviewers)
created_count += num_interviewers

yield (position_index + 1) / len(positions), 'recruitment_interviewers'

yield 100, f'Assigned {created_count} interviewers to recruitment positions'
43 changes: 43 additions & 0 deletions backend/samfundet/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations


class InterviewAllocationError(Exception):
"""Base exception for interview allocation errors."""

pass


class NoTimeBlocksAvailableError(InterviewAllocationError):
"""Raised when there are no available time blocks for interviews."""

pass


class NoApplicationsWithoutInterviewsError(InterviewAllocationError):
"""Raised when there are no applications without interviews."""

pass


class NoAvailableInterviewersError(InterviewAllocationError):
"""Raised when there are no available interviewers for a given time slot."""

pass


class AllApplicantsUnavailableError(InterviewAllocationError):
"""Raised when all applicants are unavailable for the remaining time slots."""

pass


class InsufficientTimeBlocksError(InterviewAllocationError):
"""Raised when there are not enough time blocks to accommodate all applications."""

pass


class NoFutureTimeSlotsError(InterviewAllocationError):
"""Raised when there are no time slots available at least 24 hours in the future."""

pass
27 changes: 27 additions & 0 deletions backend/samfundet/migrations/0002_interviewtimeblock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.0.7 on 2024-07-22 13:18
robines marked this conversation as resolved.
Show resolved Hide resolved

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='InterviewTimeblock',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(help_text='Block date')),
('start_dt', models.DateTimeField(help_text='Block start time')),
('end_dt', models.DateTimeField(help_text='Block end time')),
('rating', models.FloatField(help_text='Rating used for optimizing interview time')),
('available_interviewers', models.ManyToManyField(blank=True, help_text='Interviewers in this time block', related_name='interview_timeblocks', to=settings.AUTH_USER_MODEL)),
('recruitment_position', models.ForeignKey(help_text='The position which is recruiting', on_delete=django.db.models.deletion.CASCADE, related_name='interview_timeblocks', to='samfundet.recruitmentposition')),
],
),
]
14 changes: 14 additions & 0 deletions backend/samfundet/migrations/0004_merge_20240924_0058.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 5.1.1 on 2024-09-23 22:58
robines marked this conversation as resolved.
Show resolved Hide resolved

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0002_interviewtimeblock'),
('samfundet', '0003_remove_gang_event_admin_group_and_more'),
]

operations = [
]
14 changes: 14 additions & 0 deletions backend/samfundet/migrations/0006_merge_20240925_1842.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 5.1.1 on 2024-09-25 16:42
robines marked this conversation as resolved.
Show resolved Hide resolved

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0004_merge_20240924_0058'),
('samfundet', '0005_role_content_type_role_created_at_role_created_by_and_more'),
]

operations = [
]
11 changes: 11 additions & 0 deletions backend/samfundet/models/recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,17 @@ def resolve_org(self, *, return_id: bool = False) -> Organization | int:
return self.recruitment.resolve_org(return_id=return_id)


class InterviewTimeblock(models.Model):
recruitment_position = models.ForeignKey(
'RecruitmentPosition', on_delete=models.CASCADE, help_text='The position which is recruiting', related_name='interview_timeblocks'
)
date = models.DateField(help_text='Block date', null=False, blank=False)
start_dt = models.DateTimeField(help_text='Block start time', null=False, blank=False)
end_dt = models.DateTimeField(help_text='Block end time', null=False, blank=False)
rating = models.FloatField(help_text='Rating used for optimizing interview time')
available_interviewers = models.ManyToManyField('User', help_text='Interviewers in this time block', blank=True, related_name='interview_timeblocks')


class RecruitmentStatistics(FullCleanSaveMixin):
recruitment = models.OneToOneField(Recruitment, on_delete=models.CASCADE, blank=True, null=True, related_name='statistics')

Expand Down
7 changes: 7 additions & 0 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
Recruitment,
InterviewRoom,
OccupiedTimeslot,
InterviewTimeblock,
RecruitmentDateStat,
RecruitmentGangStat,
RecruitmentPosition,
Expand Down Expand Up @@ -901,6 +902,12 @@ class Meta:
fields = '__all__'


class InterviewTimeblockSerializer(serializers.ModelSerializer):
class Meta:
model = InterviewTimeblock
fields = '__all__'


class ApplicantInfoSerializer(CustomBaseSerializer):
occupied_timeslots = OccupiedTimeslotSerializer(many=True)

Expand Down
3 changes: 3 additions & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
router.register('recruitment-applications-for-gang', views.RecruitmentApplicationForGangView, 'recruitment_applications_for_gang')
router.register('recruitment-applications-for-position', views.RecruitmentApplicationForRecruitmentPositionView, 'recruitment_applications_for_position')
router.register('interview', views.InterviewView, 'interview')
router.register('interview-timeblocks', views.InterviewTimeblockView, 'interview_timeblock')

app_name = 'samfundet'

Expand Down Expand Up @@ -137,5 +138,7 @@
path('recruitment-interview-availability/', views.RecruitmentInterviewAvailabilityView.as_view(), name='recruitment_interview_availability'),
path('recruitment/<int:id>/availability/', views.RecruitmentAvailabilityView.as_view(), name='recruitment_availability'),
path('feedback/', views.UserFeedbackView.as_view(), name='feedback'),
path('generate-interview-blocks/<int:pk>', views.GenerateInterviewTimeblocksView.as_view(), name='generate_interview_blocks'),
path('purchase-feedback/', views.PurchaseFeedbackView.as_view(), name='purchase_feedback'),
path('allocate-interviews/<int:pk>', views.AllocateInterviewsForPositionView.as_view(), name='allocated_interviews'),
]
Loading