From 17dbaf34dcbf63ce47162ff89f35ec7cd4b527e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5?= <39742182+Dmi4er4@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:35:29 +0300 Subject: [PATCH] add per-year application report (#833) --- apps/admission/reports.py | 102 ++++++++++++++++-------- apps/admission/tests/test_reports.py | 4 +- apps/staff/templates/staff/exports.html | 19 ++++- apps/staff/urls.py | 9 ++- apps/staff/views.py | 22 ++++- 5 files changed, 112 insertions(+), 44 deletions(-) diff --git a/apps/admission/reports.py b/apps/admission/reports.py index f52a5faa8..5a2c2c51b 100644 --- a/apps/admission/reports.py +++ b/apps/admission/reports.py @@ -1,6 +1,8 @@ import datetime +from abc import abstractmethod from typing import Callable +from django.db import models from pandas import DataFrame from django.db.models import Prefetch @@ -14,47 +16,33 @@ class AdmissionApplicantsReport(ReportFileOutput): - def __init__(self, campaign): + exclude_applicant_fields = set() + + def __init__(self): super().__init__() - self.campaign = campaign self.process() + @abstractmethod def get_queryset(self): - return ( - Applicant.objects.filter(campaign=self.campaign.pk) - .select_related("exam", "online_test") - .prefetch_related( - "university_legacy", - Prefetch( - "interviews", - queryset=( - Interview.objects.prefetch_related( - Prefetch( - "comments", - queryset=( - Comment.objects.select_related("interviewer") - ), - ) - ) - ), - ), - ) - .order_by("pk") - ) + raise NotImplementedError() def process(self): + applicants = (self.get_queryset() + .defer(*self.exclude_applicant_fields) + .select_related("exam", "online_test") + ) + self.exclude_applicant_fields.update(("modified", "meta")) # Collect headers - exclude_applicant_fields = { - "modified", - "uuid", - "yandex_login_q", - "campaign", - "user", - "meta", - } applicant_fields = [ - f for f in Applicant._meta.fields if f.name not in exclude_applicant_fields + f for f in Applicant._meta.fields if f.name not in self.exclude_applicant_fields ] + + to_prefetch = [field.name for field in applicant_fields if isinstance(field, models.ForeignKey)] + if "campaign" in to_prefetch: + to_prefetch.append("campaign__branch") + + applicants = applicants.prefetch_related(*to_prefetch) + self.headers = [force_str(f.verbose_name) for f in applicant_fields] self.headers.extend( [ @@ -62,7 +50,6 @@ def process(self): "Результаты экзамена", ] ) - applicants = self.get_queryset() # Collect data for applicant in applicants: row = [] @@ -96,6 +83,19 @@ def process(self): def export_row(self, row): return row +class AdmissionApplicantsCampaignReport(AdmissionApplicantsReport): + exclude_applicant_fields = { + "yandex_login_q", + "campaign", + "user", + } + def __init__(self, campaign): + self.campaign = campaign + super().__init__() + + def get_queryset(self): + return Applicant.objects.filter(campaign=self.campaign.pk).order_by("pk") + def get_filename(self): today = datetime.datetime.now() return "admission_{}_{}_report_{}".format( @@ -105,6 +105,42 @@ def get_filename(self): ) +class AdmissionApplicantsYearReport(AdmissionApplicantsReport): + exclude_applicant_fields = { + "user", + "yandex_login_q", + "photo", + "telegram_username", + "is_unsubscribed", + "stepic_id", + "github_login", + "graduate_work", + "online_education_experience", + "experience", + "internship_beginning", + "internship_end", + "working_hours", + "probability", + "preferred_study_programs", + "preferred_study_program_notes", + "preferred_study_programs_dm_note", + "preferred_study_programs_se_note", + "preferred_study_programs_cs_note", + "your_future_plans", + "admin_note", + } + + def __init__(self, year): + self.year = year + super().__init__() + + def get_queryset(self): + return Applicant.objects.filter(campaign__year=self.year).exclude(campaign__branch__name='Тест').order_by("pk") + + def get_filename(self): + today = datetime.datetime.now() + return f"admission_{self.year}_report_{formats.date_format(today, 'SHORT_DATE_FORMAT')}" + class AdmissionExamReport: def __init__(self, campaign): self.campaign = campaign diff --git a/apps/admission/tests/test_reports.py b/apps/admission/tests/test_reports.py index afcf16f14..e9d7b07db 100644 --- a/apps/admission/tests/test_reports.py +++ b/apps/admission/tests/test_reports.py @@ -1,7 +1,7 @@ import pytest from admission.constants import InterviewSections -from admission.reports import AdmissionApplicantsReport, AdmissionExamReport +from admission.reports import AdmissionApplicantsCampaignReport, AdmissionExamReport from admission.tests.factories import ( ApplicantFactory, CampaignFactory, @@ -32,7 +32,7 @@ def test_report_smoke(): applicant=applicant, section=InterviewSections.ALL_IN_ONE ) CommentFactory(score=1, interview=interview) - report = AdmissionApplicantsReport(campaign=campaign) + report = AdmissionApplicantsCampaignReport(campaign=campaign) assert len(report.data) == 1 diff --git a/apps/staff/templates/staff/exports.html b/apps/staff/templates/staff/exports.html index 2b1b54a95..75c1826ad 100644 --- a/apps/staff/templates/staff/exports.html +++ b/apps/staff/templates/staff/exports.html @@ -127,8 +127,8 @@

Набор

{{ campaign }} - csv, - xlsx + csv, + xlsx csv, @@ -140,6 +140,21 @@

Набор

{% endfor %} + + + + + + {% for year in years %} + + + + + {% endfor %} +
Год набораАнкеты
{{ year }} + csv, + xlsx +
diff --git a/apps/staff/urls.py b/apps/staff/urls.py index 11678e948..63539b513 100644 --- a/apps/staff/urls.py +++ b/apps/staff/urls.py @@ -8,7 +8,7 @@ ) from staff.api.views import CreateAlumniProfiles, StudentSearchJSONView from staff.views import ( - AdmissionApplicantsReportView, AdmissionExamReportView, + AdmissionApplicantsCampaignReportView, AdmissionExamReportView, AdmissionInterviewsReportView, CourseParticipantsIntersectionView, EnrollmentInvitationListView, ExportsView, FutureGraduateDiplomasCSVView, FutureGraduateDiplomasTeXView, FutureGraduateStatsView, GradeBookListView, @@ -16,7 +16,7 @@ OfficialDiplomasCSVView, OfficialDiplomasListView, OfficialDiplomasTeXView, ProgressReportForSemesterView, ProgressReportFullView, StudentFacesView, StudentSearchCSVView, StudentSearchView, SurveySubmissionsReportView, - SurveySubmissionsStatsView, WillGraduateStatsReportView + SurveySubmissionsStatsView, WillGraduateStatsReportView, AdmissionApplicantsYearReportView ) from staff.views import autograde_projects, autofail_ungraded, create_alumni_profiles @@ -91,7 +91,10 @@ def to_url(self, value): path('tex/', OfficialDiplomasTeXView.as_view(), name='exports_official_diplomas_tex'), path('csv/', OfficialDiplomasCSVView.as_view(), name='exports_official_diplomas_csv'), ])), - path('reports/admission//applicants//', AdmissionApplicantsReportView.as_view(), name='exports_report_admission_applicants'), + path('reports/admission//campain_applicants//', + AdmissionApplicantsCampaignReportView.as_view(), name='exports_report_admission_campaign_applicants'), + path('reports/admission//year_applicants//', + AdmissionApplicantsYearReportView.as_view(), name='exports_report_admission_year_applicants'), path('reports/admission//interviews//', AdmissionInterviewsReportView.as_view(), name='exports_report_admission_interviews'), path('reports/admission//exam//', AdmissionExamReportView.as_view(), name='exports_report_admission_exam'), re_path(r'^reports/surveys/(?P\d+)/(?Pcsv|xlsx)/$', SurveySubmissionsReportView.as_view(), name='exports_report_survey_submissions'), diff --git a/apps/staff/views.py b/apps/staff/views.py index 4c4ed4374..e66fe8443 100644 --- a/apps/staff/views.py +++ b/apps/staff/views.py @@ -19,12 +19,13 @@ import core.utils from admission.models import Campaign, Interview from admission.reports import ( - AdmissionApplicantsReport, + AdmissionApplicantsCampaignReport, AdmissionExamReport, - generate_admission_interviews_report, + generate_admission_interviews_report, AdmissionApplicantsYearReport, ) from core.http import HttpRequest from core.models import Branch +from core.typings import assert_never from core.urls import reverse from core.utils import bucketize from courses.constants import SemesterTypes @@ -149,6 +150,7 @@ def get_context_data(self, **kwargs): .select_related("branch") .order_by("-year", "branch__name") ), + "years": range(2020, current_term.year + 1), "branches": branches, "official_diplomas_dates": official_diplomas_dates, } @@ -448,16 +450,28 @@ def get(self, request, output_format, invitation_id, *args, **kwargs): return dataframe_to_response(report.generate(), output_format, file_name) -class AdmissionApplicantsReportView(CuratorOnlyMixin, generic.base.View): +class AdmissionApplicantsCampaignReportView(CuratorOnlyMixin, generic.base.View): def get(self, request, campaign_id, output_format, **kwargs): campaign = get_object_or_404( Campaign.objects.filter(pk=campaign_id, branch__site_id=settings.SITE_ID) ) - report = AdmissionApplicantsReport(campaign=campaign) + report = AdmissionApplicantsCampaignReport(campaign=campaign) if output_format == "csv": return report.output_csv() elif output_format == "xlsx": return report.output_xlsx() + else: + assert_never(output_format) + +class AdmissionApplicantsYearReportView(CuratorOnlyMixin, generic.base.View): + def get(self, request, output_format, year, **kwargs): + report = AdmissionApplicantsYearReport(year=year) + if output_format == "csv": + return report.output_csv() + elif output_format == "xlsx": + return report.output_xlsx() + else: + assert_never(output_format) class AdmissionInterviewsReportView(CuratorOnlyMixin, generic.base.View):