Skip to content

Commit

Permalink
ACADEMYDEV-3329,3330: cheater logic & fake sending (#793)
Browse files Browse the repository at this point in the history
  • Loading branch information
sokovis authored Jun 7, 2023
1 parent c26c156 commit 5a0524a
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 112 deletions.
1 change: 1 addition & 0 deletions apps/admission/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ApplicantStatuses(DjangoChoices):
REJECTED_BY_TEST = C("rejected_test", _("Rejected by test"))
PERMIT_TO_EXAM = C("permit_to_exam", _("Permitted to the exam"))
REJECTED_BY_EXAM = C("rejected_exam", _("Rejected by exam"))
REJECTED_BY_EXAM_CHEATING = C("reject_exam_cheater", _("Rejected by exam cheating"))
REJECTED_BY_CHEATING = C("rejected_cheating", _("Cheating"))
PENDING = C("pending", _("Pending"))
# TODO: rename interview codes here and in DB.
Expand Down
128 changes: 80 additions & 48 deletions apps/admission/management/commands/stage3_exam_email_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,70 +12,98 @@

class ExamResultStatus:
FAIL = "fail"
CHEATER = "cheater"
SUCCESS = "success"


def get_exam_results_template_pattern(applicant, patterns):
if applicant.status == Applicant.INTERVIEW_TOBE_SCHEDULED:
return patterns[ExamResultStatus.SUCCESS]
else:
return patterns[ExamResultStatus.FAIL]
def get_exam_results_template_pattern(status: str, initial_pattern: str, **kwargs):
status_mapping = {
Applicant.INTERVIEW_TOBE_SCHEDULED: ExamResultStatus.SUCCESS,
Applicant.REJECTED_BY_EXAM_CHEATING: ExamResultStatus.CHEATER,
Applicant.REJECTED_BY_EXAM: ExamResultStatus.FAIL,
}
status = status_mapping.get(status)
if status is None:
raise NotImplementedError(f"Mapping from {status} to pattern status is not implemented.")
return initial_pattern.format(status=status, **kwargs)


class Command(EmailTemplateMixin, CurrentCampaignMixin, BaseCommand):
help = """Generate emails about exam results."""

TEMPLATE_PATTERN = "admission-{year}-{branch_code}-exam-{status}"
TEMPLATE_PATTERN = "shad-admission-{year}-exam-{status}-{branch_code}"

def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
"--fail-only",
"--commit",
action="store_true",
dest="fail_only",
help="Send emails only to those who didn't pass to the next stage",
default=False,
dest="commit",
help="Actually send emails."
)
parser.add_argument(
"--applicant_id",
dest="applicant_id",
help="Send email only for applicant.id == applicant_id"
)

def handle(self, *args, **options):
campaigns = self.get_current_campaigns(options, confirm=False)
# Collect all template patterns, then validate them
template_name_patterns = {}
result_statuses = [ExamResultStatus.FAIL]
if not options["fail_only"]:
result_statuses.append(ExamResultStatus.SUCCESS)
for status in result_statuses:
pattern = options["template_pattern"] or self.TEMPLATE_PATTERN
pattern = pattern.replace("{status}", status)
template_name_patterns[status] = pattern
self.validate_templates(campaigns, template_name_patterns.values())

for campaign in campaigns:
self.stdout.write("{}:".format(campaign))
email_from = get_email_from(campaign)

statuses = [Applicant.REJECTED_BY_EXAM]
if not options["fail_only"]:
statuses.append(Applicant.INTERVIEW_TOBE_SCHEDULED)
commit = options['commit']
applicant_id = options.get('applicant_id')
campaigns = self.get_current_campaigns(
options, branch_is_required=True, confirm=False
)
assert len(campaigns) == 1
campaign = campaigns[0]

pattern = options["template_pattern"] or self.TEMPLATE_PATTERN
statuses = [
Applicant.REJECTED_BY_EXAM,
Applicant.INTERVIEW_TOBE_SCHEDULED,
Applicant.REJECTED_BY_EXAM_CHEATING,
]

self.stdout.write(f"Templates for campaign: {campaign}")
status_to_pattern = {}
for status in statuses:
status_pattern = get_exam_results_template_pattern(
status,
pattern,
year=campaign.year,
branch_code=campaign.branch.code,
)
self.stdout.write(f"\t {status_pattern}")
status_to_pattern[status] = get_email_template(status_pattern)
email_from = get_email_from(campaign)

if applicant_id:
applicants = Applicant.objects.filter(id=applicant_id)
else:
applicants = (
Applicant.objects.filter(campaign=campaign.pk, status__in=statuses)
.select_related("exam")
.only("email", "status")
)
succeed = 0
total = 0
generated = 0
for a in applicants.iterator():
total += 1
succeed += int(a.status == Applicant.INTERVIEW_TOBE_SCHEDULED)
pattern = get_exam_results_template_pattern(a, template_name_patterns)
template_name = self.get_template_name(campaign, pattern)
template = get_email_template(template_name)
recipients = [a.email]
if not Email.objects.filter(to=recipients, template=template).exists():
context = {
"BRANCH": campaign.branch.name,
"CONTEST_ID": a.exam_id and a.exam.yandex_contest_id,
}

succeed = 0
cheater = 0
failed = 0
total = 0
generated = 0
for a in applicants.iterator():
total += 1
succeed += int(a.status == Applicant.INTERVIEW_TOBE_SCHEDULED)
cheater += int(a.status == Applicant.REJECTED_BY_EXAM_CHEATING)
failed += int(a.status == Applicant.REJECTED_BY_EXAM)
template = status_to_pattern[a.status]
recipients = [a.email]
if not Email.objects.filter(to=recipients, template=template).exists():
context = {
"BRANCH": campaign.branch.name,
"name": a.first_name,
}
if commit:
mail.send(
recipients,
sender=email_from,
Expand All @@ -88,8 +116,12 @@ def handle(self, *args, **options):
backend="ses",
)
generated += 1
self.stdout.write("Total: {}".format(total))
self.stdout.write("Succeed: {}".format(succeed))
self.stdout.write("Fail: {}".format(total - succeed))
self.stdout.write("Emails generated: {}".format(generated))
self.stdout.write("Done")
self.stdout.write("Total: {}".format(total))
self.stdout.write("Succeed: {}".format(succeed))
self.stdout.write("Cheater: {}".format(cheater))
self.stdout.write("Fail: {}".format(failed))
self.stdout.write("Emails generated: {}".format(generated))
if commit:
self.stdout.write("Done")
else:
self.stdout.write("Emails is not sent. Use --commit to send them.")
168 changes: 104 additions & 64 deletions apps/admission/management/commands/stage3_exam_step4_statuses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from decimal import Decimal

from django.core.management import BaseCommand, CommandError
from django.db import transaction
from django.db.models import Q

from admission.models import Applicant
Expand All @@ -25,11 +26,34 @@ def add_arguments(self, parser):
parser.add_argument(
"--reject_value",
type=Decimal,
help="Set `rejected by exam` to applicants with exam score equal or"
" below this value.",
help="Set `rejected by exam` to applicants with exam score equal or below this value.",
)
parser.add_argument(
"--passing_score",
type=Decimal,
help="Applicant with score: "
"passing_score <= score < cheater_score will be updated to INTERVIEW_TOBE_SCHEDULED status",
dest='passing_score',
)
parser.add_argument(
"--cheater_score",
type=Decimal,
required=True,
dest='cheater_score',
help="Users with a score >= cheater_score will have CHEATER status.",
)
parser.add_argument(
"--commit",
action="store_true",
default=False,
dest="commit",
help="Commit changes to database."
)

def handle(self, *args, **options):
passing_score = options.get('passing_score')
cheater_score = options['cheater_score']
commit = options['commit']
campaigns = self.get_current_campaigns(
options, branch_is_required=True, confirm=False
)
Expand All @@ -46,9 +70,10 @@ def handle(self, *args, **options):
f"There are {in_pending_state_total} applicants with PENDING status."
)

if not campaign.exam_passing_score:
raise CommandError(f"Set exam passing score for {campaign}")
exam_score_pass = Decimal(campaign.exam_passing_score)
passing_score = passing_score or campaign.exam_passing_score
if not passing_score:
raise CommandError(f"Set exam passing score for {campaign} or provide --passing_score")
exam_score_pass = Decimal(passing_score)
self.stdout.write(f"{exam_score_pass} и больше - прошёл на собеседование.")

reject_value = options["reject_value"]
Expand Down Expand Up @@ -77,7 +102,7 @@ def handle(self, *args, **options):

self.stdout.write("Total applicants: {}".format(applicants.count()))

cheaters_total = applicants.filter(
cheaters_test = applicants.filter(
status=Applicant.REJECTED_BY_CHEATING
).count()

Expand All @@ -86,71 +111,86 @@ def handle(self, *args, **options):
status=Applicant.REJECTED_BY_TEST
).count()

rejects_by_exam_total = (
applicants.filter(
Q(exam__score__lte=reject_value) | Q(exam__score__isnull=True)
with transaction.atomic():
rejects_by_exam_total = (
applicants.filter(
Q(exam__score__lte=reject_value) | Q(exam__score__isnull=True)
)
.filter(
status__in=[
Applicant.PENDING,
Applicant.PERMIT_TO_EXAM,
Applicant.REJECTED_BY_EXAM,
]
)
.exclude(exam__score__gte=cheater_score)
.update(status=Applicant.REJECTED_BY_EXAM)
)
.filter(

applicants.filter(
exam__score__gte=exam_score_pass,
exam__score__lt=cheater_score,
status__in=[
Applicant.PENDING,
Applicant.PERMIT_TO_EXAM,
Applicant.INTERVIEW_TOBE_SCHEDULED,
],
).update(status=Applicant.INTERVIEW_TOBE_SCHEDULED)
exam_cheaters_total = applicants.filter(
exam__score__gte=cheater_score,
status__in=[
Applicant.PENDING,
Applicant.PERMIT_TO_EXAM,
Applicant.REJECTED_BY_EXAM,
Applicant.INTERVIEW_TOBE_SCHEDULED,
],
).update(status=Applicant.REJECTED_BY_EXAM_CHEATING)
# Some applicants could have exam score < passing score, but they
# still pass to the next stage (by manual application form check)
# Also count those who passed the interview phase and waiting
# for the final decision
pass_exam_total = applicants.filter(
status__in=[
Applicant.INTERVIEW_TOBE_SCHEDULED,
Applicant.INTERVIEW_SCHEDULED,
Applicant.INTERVIEW_COMPLETED,
Applicant.REJECTED_BY_INTERVIEW,
]
)
.update(status=Applicant.REJECTED_BY_EXAM)
)
).count()

pass_exam_total = applicants.filter(
exam__score__gte=exam_score_pass,
status__in=[
Applicant.PENDING,
Applicant.PERMIT_TO_EXAM,
Applicant.INTERVIEW_TOBE_SCHEDULED,
],
).update(status=Applicant.INTERVIEW_TOBE_SCHEDULED)
# Some applicants could have exam score < passing score, but they
# still pass to the next stage (by manual application form check)
# Also count those who passed the interview phase and waiting
# for the final decision
pass_exam_total = applicants.filter(
status__in=[
Applicant.INTERVIEW_TOBE_SCHEDULED,
Applicant.INTERVIEW_SCHEDULED,
Applicant.INTERVIEW_COMPLETED,
Applicant.REJECTED_BY_INTERVIEW,
]
).count()

pending_total = (
applicants.filter(
exam__score__gt=reject_value,
exam__score__lt=exam_score_pass,
pending_total = (
applicants.filter(
exam__score__gt=reject_value,
exam__score__lt=exam_score_pass,
)
.filter(status__in=[Applicant.PERMIT_TO_EXAM, Applicant.PENDING])
.update(status=Applicant.PENDING)
)
.filter(status__in=[Applicant.PERMIT_TO_EXAM, Applicant.PENDING])
.update(status=Applicant.PENDING)
)

# Applicants who skipped the exam could be resolved
# with THEY_REFUSED or REJECTED_BY_EXAM status
refused_total = applicants.filter(status=Applicant.THEY_REFUSED).count()

total = (
cheaters_total,
rejects_by_test_total,
rejects_by_exam_total,
pass_exam_total,
pending_total,
refused_total,
)
# Applicants who skipped the exam could be resolved
# with THEY_REFUSED or REJECTED_BY_EXAM status
refused_total = applicants.filter(status=Applicant.THEY_REFUSED).count()

total = (
cheaters_test,
rejects_by_test_total,
rejects_by_exam_total,
exam_cheaters_total,
pass_exam_total,
pending_total,
refused_total,
)

self.stdout.write("Cheaters: {}".format(cheaters_total))
self.stdout.write("Rejected by test: {}".format(rejects_by_test_total))
self.stdout.write("Rejected by exam: {}".format(rejects_by_exam_total))
self.stdout.write("Pass exam stage: {}".format(pass_exam_total))
self.stdout.write("Pending status: {}".format(pending_total))
self.stdout.write("Refused status: {}".format(refused_total))
self.stdout.write(
"{exp} = {result}".format(
exp=" + ".join((str(t) for t in total)), result=sum(total)
self.stdout.write("Cheaters test: {}".format(cheaters_test))
self.stdout.write("Rejected by test: {}".format(rejects_by_test_total))
self.stdout.write("Rejected by exam: {}".format(rejects_by_exam_total))
self.stdout.write("Rejected by exam cheating: {}".format(exam_cheaters_total))
self.stdout.write("Pass exam stage: {}".format(pass_exam_total))
self.stdout.write("Pending status: {}".format(pending_total))
self.stdout.write("Refused status: {}".format(refused_total))
self.stdout.write(
"{exp} = {result}".format(
exp=" + ".join((str(t) for t in total)), result=sum(total)
)
)
)
if not commit:
raise CommandError("Use --commit to apply changes.")
18 changes: 18 additions & 0 deletions apps/admission/migrations/0049_alter_applicant_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2023-06-07 17:10

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('admission', '0048_applicant_residence_city'),
]

operations = [
migrations.AlterField(
model_name='applicant',
name='status',
field=models.CharField(blank=True, choices=[('rejected_test', 'Rejected by test'), ('permit_to_exam', 'Permitted to the exam'), ('rejected_exam', 'Rejected by exam'), ('reject_exam_cheater', 'Rejected by exam cheating'), ('rejected_cheating', 'Cheating'), ('pending', 'Pending'), ('interview_phase', 'Can be interviewed'), ('interview_assigned', 'Interview assigned'), ('interview_completed', 'Interview completed'), ('rejected_interview', 'Rejected by interview'), ('rejected_with_bonus', 'Rejected by interview. Offered a bonus'), ('accept_paid', 'Accept on paid'), ('waiting_for_payment', 'Waiting for Payment'), ('accept', 'Accept'), ('accept_if', 'Accept with condition'), ('volunteer', 'Applicant|Volunteer'), ('they_refused', 'He or she refused')], max_length=20, null=True, verbose_name='Applicant|Status'),
),
]
Loading

0 comments on commit 5a0524a

Please sign in to comment.