Skip to content

Commit

Permalink
interview review (#39)
Browse files Browse the repository at this point in the history
* model renaming and use of current_year in views

* added InterviewAssignment model/table, (and added index on ApplicationReview.submitted)

* complete support for interview reviews
** interview assignments are listed on dashboard
** and link to interview review page

resolves #18
resolves #19
part of #20
  • Loading branch information
jesteria authored Feb 17, 2018
1 parent 0985364 commit 057d337
Show file tree
Hide file tree
Showing 12 changed files with 492 additions and 86 deletions.
29 changes: 29 additions & 0 deletions src/review/migrations/0012_auto_20180215_1829.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 2.0 on 2018-02-15 18:29
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('review', '0011_auto_20180214_2043'),
]

operations = [
migrations.RenameModel('Review', 'ApplicationReview'),
migrations.AlterModelOptions(
name='applicationreview',
options={'ordering': ('-submitted',)},
),
migrations.AlterField(
model_name='applicationreview',
name='application',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='application_reviews', to='review.Application'),
),
migrations.AlterField(
model_name='applicationreview',
name='reviewer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='application_reviews', to=settings.AUTH_USER_MODEL),
),
]
40 changes: 40 additions & 0 deletions src/review/migrations/0013_auto_20180215_1924.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 2.0 on 2018-02-15 19:24

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


class Migration(migrations.Migration):

dependencies = [
('review', '0012_auto_20180215_1829'),
]

operations = [
migrations.CreateModel(
name='InterviewAssignment',
fields=[
('interview_assignment_id', models.AutoField(primary_key=True, serialize=False)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_assignments', to='review.Application')),
('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_assignments', to=settings.AUTH_USER_MODEL)),
('interview_round', review.models.EnumIntegerField([('round_one', '1'), ('round_two', '2')])), # FIXME
('assigned', models.DateTimeField(auto_now_add=True, db_index=True)),
('notified', models.DateTimeField(db_index=True, null=True)),
],
options={
'db_table': 'interview_assignment',
'ordering': ('-assigned',),
},
),
migrations.AlterField(
model_name='applicationreview',
name='submitted',
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AlterUniqueTogether(
name='interviewassignment',
unique_together={('application', 'reviewer', 'interview_round')},
),
]
17 changes: 17 additions & 0 deletions src/review/migrations/0014_auto_20180216_0116.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 2.0 on 2018-02-16 01:16

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('review', '0013_auto_20180215_1924'),
]

operations = [
migrations.AlterModelOptions(
name='application',
options={'ordering': ('-created',)},
),
]
35 changes: 35 additions & 0 deletions src/review/migrations/0015_interviewreview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 2.0 on 2018-02-16 02:57

from django.db import migrations, models
import django.db.models.deletion
import review.models


class Migration(migrations.Migration):

dependencies = [
('review', '0014_auto_20180216_0116'),
]

operations = [
migrations.CreateModel(
name='InterviewReview',
fields=[
('interview_assignment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='review.InterviewAssignment')),
('programming_rating', models.IntegerField(null=True, verbose_name='Programming')),
('machine_learning_rating', models.IntegerField(null=True, verbose_name='Stats & Machine Learning')),
('data_handling_rating', models.IntegerField(null=True, verbose_name='Data Handling & Manipulation')),
('social_science', models.IntegerField(null=True, verbose_name='Social Science')),
('interest_in_good_rating', models.IntegerField(null=True, verbose_name='Interest in Social Good')),
('communication_rating', models.IntegerField(null=True, verbose_name='Communication Ability')),
('teamwork_rating', models.IntegerField(null=True, verbose_name='Teamwork and Collaboration')),
('overall_recommendation', review.models.EnumCharField([('accept', 'Accept'), ('reject', 'Reject'), ('only_if', 'Accept <em>only</em> if you need a certain type of fellow (explain below)')], help_text='Overall recommendation')),
('comments', models.TextField(blank=True, help_text='Any comments?')),
('submitted', models.DateTimeField(auto_now_add=True, db_index=True)),
],
options={
'db_table': 'interview_review',
'ordering': ('-submitted',),
},
),
]
140 changes: 119 additions & 21 deletions src/review/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import collections
import enum
import re

Expand All @@ -19,9 +20,24 @@ def __str__(self):
return self.value


class EnumCharField(fields.CharField):
class SafeStrEnum(StrEnum):

def __init__(self, enum, **kws):
def __str__(self):
return safestring.mark_safe(self.value)


class IntEnum(int, enum.Enum):

def __str__(self):
return str(self.value)


class EnumFieldMixin(object):

member_cast = str

@classmethod
def prepare_init_kwargs(cls, enum, kws):
if 'choices' in kws:
raise TypeError("Unexpected keyword argument 'choices'")

Expand All @@ -30,12 +46,32 @@ def __init__(self, enum, **kws):
except AttributeError:
choices = enum

kws.setdefault(
return dict(kws, choices=choices)

def __init__(self, enum, **kws):
super().__init__(**self.prepare_init_kwargs(enum, kws))

def deconstruct(self):
"""Return a suitable description of this field for migrations.
"""
(name, path, args, kwargs) = super().deconstruct()
choices = [(key, self.member_cast(member))
for (key, member) in kwargs.pop('choices')]
return (name, path, [choices] + args, kwargs)


class EnumCharField(EnumFieldMixin, fields.CharField):

@classmethod
def prepare_init_kwargs(cls, enum, kws):
prepared = super().prepare_init_kwargs(enum, kws)
choices = prepared['choices']
prepared.setdefault(
'max_length',
max((len(name) for (name, _value) in choices), default=0)
)

super().__init__(choices=choices, **kws)
return prepared

def deconstruct(self):
"""Return a suitable description of this field for migrations.
Expand All @@ -44,11 +80,12 @@ def deconstruct(self):
(name, path, args, kwargs) = super().deconstruct()

del kwargs['max_length']
choices = [
(key, str(member)) for (key, member) in kwargs.pop('choices')
]

return (name, path, [choices] + args, kwargs)
return (name, path, args, kwargs)


class EnumIntegerField(EnumFieldMixin, fields.IntegerField):
pass


#
Expand Down Expand Up @@ -387,34 +424,36 @@ class Meta:
def rating_fields():
# Note: not appropriate to refer to `cls`, as we only want
# AbstractRating's fields
return [field.name for field in AbstractRating._meta.fields]
return collections.OrderedDict(
(field.name, field.verbose_name)
for field in AbstractRating._meta.fields
)


class ReviewQuerySet(models.QuerySet):
class ApplicationLinkedQuerySet(models.QuerySet):

def current_year(self):
return self.filter(
application__program_year=settings.REVIEW_PROGRAM_YEAR,
)


class Review(AbstractRating):
class ApplicationReview(AbstractRating):

class OverallRecommendation(StrEnum):
class OverallRecommendation(SafeStrEnum):

interview = "Interview"
reject = "Reject"
only_if = "Interview <em>only</em> if you need a certain type of fellow (explain below)"

def __str__(self):
return safestring.mark_safe(self.value)

review_id = models.AutoField(primary_key=True)
reviewer = models.ForeignKey('review.Reviewer',
on_delete=models.CASCADE,
related_name='reviews')
application = models.ForeignKey('review.Application', on_delete=models.CASCADE)
submitted = models.DateTimeField(auto_now_add=True)
related_name='application_reviews')
application = models.ForeignKey('review.Application',
on_delete=models.CASCADE,
related_name='application_reviews')
submitted = models.DateTimeField(auto_now_add=True, db_index=True)

overall_recommendation = EnumCharField(
OverallRecommendation,
Expand All @@ -431,12 +470,14 @@ def __str__(self):
"judge from the application and references?",
)
would_interview = models.BooleanField(
help_text="If this applicant moves to the interview round, would you like to interview them?",
help_text="If this applicant moves to the interview round, would "
"you like to interview them?",
)

objects = ReviewQuerySet.as_manager()
objects = ApplicationLinkedQuerySet.as_manager()

class Meta:
# TODO: migrate db table to "application_review"
db_table = 'review'
ordering = ('-submitted',)
unique_together = (
Expand All @@ -446,3 +487,60 @@ class Meta:
def __str__(self):
return (f'{self.reviewer} regarding {self.application}: '
f'{self.overall_recommendation}')


class InterviewReview(AbstractRating):

class OverallRecommendation(SafeStrEnum):

accept = "Accept"
reject = "Reject"
only_if = "Accept <em>only</em> if you need a certain type of fellow (explain below)"

interview_assignment = models.OneToOneField('review.InterviewAssignment',
primary_key=True,
on_delete=models.CASCADE,
related_name='interview_review')

overall_recommendation = EnumCharField(
OverallRecommendation,
help_text="Overall recommendation",
)
comments = models.TextField(
blank=True,
help_text="Any comments?",
)

submitted = models.DateTimeField(auto_now_add=True, db_index=True)

class Meta:
db_table = 'interview_review'
ordering = ('-submitted',)


class InterviewAssignment(models.Model):

class InterviewRound(IntEnum):

round_one = 1
round_two = 2

interview_assignment_id = models.AutoField(primary_key=True)
application = models.ForeignKey('review.Application',
on_delete=models.CASCADE,
related_name='interview_assignments')
reviewer = models.ForeignKey('review.Reviewer',
on_delete=models.CASCADE,
related_name='interview_assignments')
interview_round = EnumIntegerField(InterviewRound) # FIXME: key/value backwards in choices?
assigned = models.DateTimeField(auto_now_add=True, db_index=True)
notified = models.DateTimeField(null=True, db_index=True)

objects = ApplicationLinkedQuerySet.as_manager()

class Meta:
db_table = 'interview_assignment'
ordering = ('-assigned',)
unique_together = (
('application', 'reviewer', 'interview_round'),
)
6 changes: 3 additions & 3 deletions src/review/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ def apps_to_review(reviewer, *, application_id=None, limit=None,
LEFT OUTER JOIN "review" USING ("application_id")
LEFT OUTER JOIN "review" "positive_review" ON (
"application"."application_id" = "positive_review"."application_id" AND
"positive_review"."overall_recommendation" = '{models.Review.OverallRecommendation.interview.name}'
"positive_review"."overall_recommendation" = '{models.ApplicationReview.OverallRecommendation.interview.name}'
)
LEFT OUTER JOIN "review" "unknown_review" ON (
"application"."application_id" = "unknown_review"."application_id" AND
"unknown_review"."overall_recommendation" = '{models.Review.OverallRecommendation.only_if.name}'
"unknown_review"."overall_recommendation" = '{models.ApplicationReview.OverallRecommendation.only_if.name}'
)
LEFT OUTER JOIN "review" "negative_review" ON (
"application"."application_id" = "negative_review"."application_id" AND
"negative_review"."overall_recommendation" = '{models.Review.OverallRecommendation.reject.name}'
"negative_review"."overall_recommendation" = '{models.ApplicationReview.OverallRecommendation.reject.name}'
)
-- only consider applications ...
Expand Down
Loading

0 comments on commit 057d337

Please sign in to comment.