Skip to content

Commit

Permalink
Merge branch 'develop' into dockerfile
Browse files Browse the repository at this point in the history
  • Loading branch information
kharann authored Apr 1, 2022
2 parents bf890a9 + 3b9db23 commit 9bf96fe
Show file tree
Hide file tree
Showing 68 changed files with 3,320 additions and 232 deletions.
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,16 @@ user-cooler:
showmigrations:
poetry run python manage.py showmigrations

.PHONY: testdata
testdata:
poetry run python manage.py generate_testdata


.PHONY: activeadmission
activeadmission:
poetry run python manage.py generate_active_admission


.PHONY: nukeadmission
nukeadmission:
poetry run python manage.py nuke_admission_data
8 changes: 7 additions & 1 deletion admissions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ class InterviewBooleanEvaluationAnswerInline(admin.TabularInline):
extra = 1


class InternalGroupPositionPriorityInline(admin.TabularInline):
model = InternalGroupPositionPriority
extra = 1


@admin.register(Interview)
class InterviewAdmin(admin.ModelAdmin):
list_display = ("applicant",)
inlines = (
InterviewAdditionalEvaluationAnswerInline,
InterviewBooleanEvaluationAnswerInline,
Expand Down Expand Up @@ -77,7 +83,7 @@ class AdmissionAdmin(admin.ModelAdmin):

@admin.register(Applicant)
class ApplicantAdmin(admin.ModelAdmin):
pass
inlines = [InternalGroupPositionPriorityInline]


@admin.register(InternalGroupPositionPriority)
Expand Down
10 changes: 6 additions & 4 deletions admissions/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ class Priority(models.TextChoices):


class AdmissionStatus(models.TextChoices):
INITIALIZATION = ("initialization", "Initialization")
INITIALIZATION = ("configuration", "Configuration")
OPEN = ("open", "Open")
IN_SESSION = ("in-session", "In session") # Fordelingsmøtet
FINALIZATION = (
"finalization",
"Finalization",
LOCKED = (
"locked",
"Locked",
) # Reviewing last step before admitting everyone
CLOSED = ("closed", "Closed")

Expand All @@ -22,6 +22,7 @@ class ApplicantStatus(models.TextChoices):
# This probably has to be revisited
EMAIL_SENT = ("email-sent", "Email sent")
HAS_REGISTERED_PROFILE = ("has-registered-profile", "Has registered profile")
HAS_SET_PRIORITIES = ("has-set-priorities", "Has set priorities")
SCHEDULED_INTERVIEW = ("scheduled-interview", "Scheduled interview")
INTERVIEW_FINISHED = ("interview-finished", "Interview finished")
DID_NOT_SHOW_UP_FOR_INTERVIEW = (
Expand All @@ -45,3 +46,4 @@ class InternalGroupStatus(models.TextChoices):
)
RESERVE = ("reserve", "Reserve")
SHOULD_BE_ADMITTED = ("should-be-admitted", "Should be admitted")
PASS_AROUND = ("pass-around", "Pass around")
Empty file.
Empty file.
9 changes: 9 additions & 0 deletions admissions/management/commands/_consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
INTERVIEW_DISCUSSION_TEXT = """
Dette var en helt middelmådig søker. Ikke noe spesielt dårlig og ingenting spesielt bra.
"""


INTERVIEW_NOTES_TEXT = """
Ny student i byen. Har lyst på venner. Liker å være sosial på fritiden.
"""
278 changes: 278 additions & 0 deletions admissions/management/commands/generate_active_admission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
from django.core.management.base import BaseCommand, CommandError
from admissions.models import (
Admission,
Applicant,
InterviewScheduleTemplate,
Interview,
InterviewLocation,
InterviewLocationAvailability,
AdmissionAvailableInternalGroupPositionData,
InterviewBooleanEvaluation,
InterviewBooleanEvaluationAnswer,
InterviewAdditionalEvaluationAnswer,
InterviewAdditionalEvaluationStatement,
)
from admissions.tests.factories import ApplicantFactory
from admissions.consts import AdmissionStatus, ApplicantStatus
from organization.models import InternalGroupPosition
from admissions.utils import generate_interviews_from_schedule
import datetime
from common.util import date_time_combiner
from common.management.commands._utils import (
chose_random_element,
get_random_model_objects,
)
from admissions.management.commands._consts import (
INTERVIEW_NOTES_TEXT,
INTERVIEW_DISCUSSION_TEXT,
)
from users.models import User


class Command(BaseCommand):
"""
Algorithm:
1. Generate default interview schedule template if it does not exist
- Should create an interview period which started a week ago and has another week left
2. Generate interviews
3. Generate available positions
4. Randomize applicants
- Their state in the interview process
- Their priorities
5. Randomize interviews
- Content
- Interviewers
"""

def handle(self, *args, **options):
try:
self.generate_interview_schedule()

except Exception as e:
self.stdout.write(self.style.ERROR(f"{e}"))
raise CommandError(e)
self.stdout.write(self.style.SUCCESS("Active admission has been generated"))

def generate_interview_schedule(self):
""""""
now = datetime.date.today()
last_week = now - datetime.timedelta(days=7)
next_week = now + datetime.timedelta(days=7)
day_start = datetime.time(hour=12, minute=0, second=0)
day_end = datetime.time(hour=20, minute=0, second=0)
interview_duration = datetime.timedelta(hours=0, minutes=30, seconds=0)
pause_duration = datetime.timedelta(hours=1, minutes=0, seconds=0)
block_size = 5
self.stdout.write(
self.style.SUCCESS(
f"Creating interview schedule from {last_week} to {next_week} with these settings:"
)
)
self.stdout.write(
self.style.SUCCESS(
f"""
> Interview start for each day is {day_start}
> Maximum interview end for each day is {day_end}
> Default interviews in a row before a break is {block_size}
> Default interview duration is {interview_duration}
> Default pause duration is {pause_duration}
"""
)
)

# We design around the model only ever having one instance
# An idea could be to have a OneToONe rel with admission and then always try to copy the defaults from 1 year ago
schedule = InterviewScheduleTemplate.get_or_create_interview_schedule_template()
schedule.interview_period_start_date = last_week
schedule.interview_period_end_date = next_week
schedule.default_interview_day_start = day_start
schedule.default_interview_day_end = day_end
schedule.default_interview_duration = interview_duration
schedule.default_pause_duration = pause_duration
schedule.default_block_size = block_size
schedule.save()

# Create two interview locations

locations = [
InterviewLocation.objects.get_or_create(name="Bodegaen")[0],
InterviewLocation.objects.get_or_create(name="Knaus")[0],
]
self.stdout.write(self.style.SUCCESS(f"Created {len(locations)} locations"))
# Make the locations available 12:00 to 20:00 each day in the interview period
self.stdout.write(
self.style.SUCCESS(
f"Making locations available from {day_start} to {day_end} for each day in the interview period"
)
)
cursor = last_week
while cursor <= next_week:
for location in locations:
datetime_from = date_time_combiner(cursor, day_start)
datetime_to = date_time_combiner(cursor, day_end)
InterviewLocationAvailability.objects.create(
interview_location=location,
datetime_from=datetime_from,
datetime_to=datetime_to,
)
cursor += datetime.timedelta(days=1)

self.stdout.write(
self.style.SUCCESS(f"Retrieving or creating admission object")
)
# Get or an admission
admission = Admission.objects.get_or_create(
status=AdmissionStatus.OPEN, date=last_week - datetime.timedelta(days=3)
)[0]

# Add some ordinarily available positions
self.stdout.write(self.style.SUCCESS("Creating available positions"))
positions = InternalGroupPosition.objects.all().filter(
available_externally=True
)
available_position_choices = [5, 10, 15]
for position in positions:
number = chose_random_element(available_position_choices)
AdmissionAvailableInternalGroupPositionData.objects.create(
admission=admission,
internal_group_position=position,
available_positions=number,
)
self.stdout.write(
self.style.SUCCESS(f"Created {position.name} with {number} spots")
)

# Interview generation
generate_interviews_from_schedule(schedule)
interview_period_datetime_start = date_time_combiner(last_week, day_start)
number_of_interviews = Interview.objects.filter(
interview_start__gte=interview_period_datetime_start
).count()
self.stdout.write(
self.style.SUCCESS(f"Generated {number_of_interviews} interviews")
)

# Applicant generation distributed randomly across the admission
self.stdout.write(self.style.SUCCESS("Creating 200 applicants "))
ApplicantFactory.create_batch(200, admission=admission)

# Applicants now need to be filtered by status and their data parsed.
# Example being purging data for those who just got an email or assigning them to interviews
self.stdout.write(
self.style.SUCCESS("Grouping applicants together based on status")
)
email_sent_applicants = Applicant.objects.all().filter(
status=ApplicantStatus.EMAIL_SENT
)
registered_profile__applicants = Applicant.objects.all().filter(
status=ApplicantStatus.HAS_REGISTERED_PROFILE
)
interview_scheduled_applicants = Applicant.objects.all().filter(
status=ApplicantStatus.SCHEDULED_INTERVIEW
)
finished_with_interview_applicants = Applicant.objects.all().filter(
status=ApplicantStatus.INTERVIEW_FINISHED
)
ghosted_applicants = Applicant.objects.all().filter(
status=ApplicantStatus.DID_NOT_SHOW_UP_FOR_INTERVIEW
)
retracted_applicants = Applicant.objects.all().filter(
status=ApplicantStatus.RETRACTED_APPLICATION
)

# Nuke details
count = email_sent_applicants.update(
first_name="",
last_name="",
date_of_birth=None,
phone="",
hometown="",
address="",
)
self.stdout.write(
self.style.SUCCESS(f"Reset personal details for {count} applicants")
)

# We assign each applicant to a random interview in the future
datetime_today = date_time_combiner(now, day_start)
self.stdout.write(
self.style.SUCCESS(
f"Assigning random future interviews to {interview_scheduled_applicants.count()} applicants"
)
)
for applicant in interview_scheduled_applicants:
random_interview = (
Interview.objects.all()
.filter(applicant__isnull=True, interview_start__lte=datetime_today)
.order_by("?")
.first()
)
applicant.interview = random_interview
applicant.save()

self.stdout.write(
self.style.SUCCESS("Adding random interviewers to interviews")
)
number_of_interviewers_choices = [3, 4, 5]
for applicant in finished_with_interview_applicants:
self.stdout.write(
self.style.SUCCESS(f"Generating interview data for {applicant}")
)
random_interview = (
Interview.objects.all()
.filter(applicant__isnull=True, interview_start__gte=datetime_today)
.order_by("?")
.first()
)
applicant.interview = random_interview
applicant.save()
number_of_interviewers = chose_random_element(
number_of_interviewers_choices
)
random_interviewers = get_random_model_objects(User, number_of_interviewers)
random_interview.interviewers.set(random_interviewers)
random_interview.discussion = INTERVIEW_DISCUSSION_TEXT
random_interview.notes = INTERVIEW_NOTES_TEXT

boolean_evaluations = InterviewBooleanEvaluation.objects.all()
additional_evaluations = (
InterviewAdditionalEvaluationStatement.objects.all()
)

for statement in boolean_evaluations:
random_answer = chose_random_element([True, False])
InterviewBooleanEvaluationAnswer.objects.create(
interview=random_interview, statement=statement, value=random_answer
)
additional_evaluation_answer_choices = (
InterviewAdditionalEvaluationAnswer.Options.values
)
for statement in additional_evaluations:
random_answer = chose_random_element(
additional_evaluation_answer_choices
)
InterviewAdditionalEvaluationAnswer.objects.create(
interview=random_interview,
statement=statement,
answer=random_answer,
)
applicant.save()
random_interview.save()

self.stdout.write(self.style.SUCCESS("Giving all applicants random priorities"))
# Now we give the applicants random priorities
number_of_priorities_choices = [2, 3]
for applicant in Applicant.objects.all():
number_of_priorities = chose_random_element(number_of_priorities_choices)
positions = (
InternalGroupPosition.objects.all()
.filter(available_externally=True)
.order_by("?")[0:number_of_priorities]
)
self.stdout.write(
self.style.SUCCESS(
f"Adding positions {positions} to {applicant} priorities"
)
)
for position in positions:
applicant.add_priority(position)
35 changes: 35 additions & 0 deletions admissions/management/commands/nuke_admission_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.core.management.base import BaseCommand, CommandError
from admissions.models import Admission, Interview, Applicant, InterviewLocation


class Command(BaseCommand):
"""
This is a fairly large model generation script. One of the main caveats with the main implementation now
is that is in no way modular and can only be run once due to factory.sequence usage which will
cause unique constraint errors when using without nuking several database tables.
Future improvements:
> Move generation into helpers that reside at at app-level instead of them all being here
> Wrap script in exception blocks so that it can be used a bit more easily
> Allow for modular usage (Lets say you just want users and not quotes or the other way around. Would
be nice if we could target specific modules of the application for data generation)
"""

help = "Nukes admission data"

def handle(self, *args, **options):
try:
# We start by setting up the base structure
self.stdout.write(self.style.SUCCESS("🧨 Admission model "))
Admission.objects.all().delete()
self.stdout.write(self.style.SUCCESS("🧨 InterviewLocation model"))
InterviewLocation.objects.all().delete()
self.stdout.write(self.style.SUCCESS("🧨 Interview model"))
Interview.objects.all().delete()
self.stdout.write(self.style.SUCCESS("🧨 Applicant model"))
Applicant.objects.all().delete()

except Exception as e:
self.stdout.write(self.style.ERROR(f"Something went wrong"))
raise CommandError(e)
self.stdout.write(self.style.SUCCESS("Nuking complete"))
Loading

0 comments on commit 9bf96fe

Please sign in to comment.