From 2ff44c2602ba41709dfc5daeb84bb61492887568 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: Fri, 21 Jun 2024 13:45:43 +0300 Subject: [PATCH] More interview updates (#849) * Update InterviewListView * New Interview features * Update InterviewListView * New Interview features * New Interview features * test_fix --- ...viewers.py => create_delete_role_group.py} | 27 +++++++--- apps/admission/models.py | 3 ++ apps/admission/reports.py | 34 +++++++++---- apps/admission/tests/test_timezones.py | 6 ++- apps/admission/tests/test_views.py | 6 ++- apps/admission/views.py | 50 +++++++------------ lms/jinja2/lms/admission/applicant_list.html | 2 + 7 files changed, 73 insertions(+), 55 deletions(-) rename apps/admission/management/commands/{create_delete_interviewers.py => create_delete_role_group.py} (72%) diff --git a/apps/admission/management/commands/create_delete_interviewers.py b/apps/admission/management/commands/create_delete_role_group.py similarity index 72% rename from apps/admission/management/commands/create_delete_interviewers.py rename to apps/admission/management/commands/create_delete_role_group.py index ee83fe16c..6a1033bc6 100644 --- a/apps/admission/management/commands/create_delete_interviewers.py +++ b/apps/admission/management/commands/create_delete_role_group.py @@ -11,14 +11,18 @@ class Command(CurrentCampaignMixin, BaseCommand): - help = """Give or take back interviewer role from Users in csv""" + help = """ + Give or take back interviewer role from Users in csv + Example of usage: + ./manage.py create_delete_role_group --filename=interviewers.csv --default_branch=msk --role=INTERVIEWER + """ def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( "--filename", type=str, - default='interviewers.csv', + default='emails.csv', help="csv file name", ) parser.add_argument( @@ -39,12 +43,19 @@ def add_arguments(self, parser): dest="take_back", help="Take roles back" ) + parser.add_argument( + "--role", + type=str, + required=True, + help="Role to give or take back", + ) def handle(self, *args, **options): delimiter = options["delimiter"] filename = options["filename"] take_back = options["take_back"] default_branch = options["default_branch"] + role = options["role"] available = Branch.objects.filter( active=True, site_id=settings.SITE_ID ) @@ -58,14 +69,14 @@ def handle(self, *args, **options): with transaction.atomic(): headers = next(reader) for row in reader: - interviewer: User = User.objects.get(email__iexact=row[0]) - branch = interviewer.branch + user: User = User.objects.get(email__iexact=row[0]) + branch = user.branch if not branch: - self.stdout.write(self.style.WARNING(f"{interviewer} doesn't have branch. Using default one")) + self.stdout.write(self.style.WARNING(f"{user} doesn't have branch. Using default one")) branch = default_branch - role = Roles.INTERVIEWER + role = getattr(Roles, role) if take_back: - interviewer.remove_group(role, branch=branch) + user.remove_group(role, branch=branch) else: - interviewer.add_group(role, branch=branch) + user.add_group(role, branch=branch) diff --git a/apps/admission/models.py b/apps/admission/models.py index 6af240ee9..15af7fc61 100644 --- a/apps/admission/models.py +++ b/apps/admission/models.py @@ -702,6 +702,9 @@ def get_exam_record(self) -> Optional["Exam"]: except Exam.DoesNotExist: return None + def get_all_interview_score(self) -> int: + return sum(Comment.objects.filter(interview__applicant=self).values_list('score', flat=True)) + def get_university_display(self) -> Optional[str]: if self.university is not None: return self.university.name diff --git a/apps/admission/reports.py b/apps/admission/reports.py index 5a2c2c51b..4b2244816 100644 --- a/apps/admission/reports.py +++ b/apps/admission/reports.py @@ -36,10 +36,12 @@ def process(self): 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") + to_prefetch.extend(["interviews", + Prefetch("interviews__comments", + queryset=(Comment.objects.prefetch_related("interviewer")))]) applicants = applicants.prefetch_related(*to_prefetch) @@ -50,21 +52,18 @@ def process(self): "Результаты экзамена", ] ) + interview_section_indexes: dict[int,int] = {} + for index, (value, label) in enumerate(InterviewSections.choices): + self.headers.append(f"{label} / балл") + self.headers.append(f"{label} / комментарии") + interview_section_indexes[value] = 2 * index # Collect data for applicant in applicants: row = [] for field in applicant_fields: value = getattr(applicant, field.name) - if field.name == "status": - value = applicant.get_status_display() - elif field.name == "level_of_education": - value = applicant.get_level_of_education_display() - elif field.name == "has_diploma": - value = applicant.get_has_diploma_display() - elif field.name == "gender": - value = applicant.get_gender_display() - elif field.name == "diploma_degree": - value = applicant.get_diploma_degree_display() + if field.name in ("status", "level_of_education", "has_diploma", "gender", "diploma_degree"): + value = getattr(applicant, f"get_{field.name}_display")() elif field.name == "id": value = reverse("admission:applicants:detail", args=[value]) elif field.name == "created": @@ -78,6 +77,17 @@ def process(self): row.append(applicant.exam.score) else: row.append("") + interview_details = ["" for _ in range(2 * len(InterviewSections.values))] + for interview in applicant.interviews.all(): + interview_comments = "" + for c in interview.comments.all(): + author = c.interviewer.get_full_name() + interview_comments += f"{author}:\n{c.text}\n\n" + index = interview_section_indexes[interview.section] + interview_details[index] = interview.get_average_score_display() + interview_details[index + 1] = interview_comments.rstrip() + row.extend(interview_details) + assert len(row) == len(self.headers) self.data.append([force_str(x) if x is not None else "" for x in row]) def export_row(self, row): @@ -128,6 +138,8 @@ class AdmissionApplicantsYearReport(AdmissionApplicantsReport): "preferred_study_programs_cs_note", "your_future_plans", "admin_note", + "interview_format", + "miss_count" } def __init__(self, year): diff --git a/apps/admission/tests/test_timezones.py b/apps/admission/tests/test_timezones.py index 009224c07..ce63413d7 100644 --- a/apps/admission/tests/test_timezones.py +++ b/apps/admission/tests/test_timezones.py @@ -3,6 +3,7 @@ import pytest import pytz from bs4 import BeautifulSoup +from django.utils import formats from admission.constants import InterviewSections from admission.tests.factories import ( @@ -191,8 +192,9 @@ def test_interview_list(settings, client, curator): msk_interview_date_in_utc = interview.date localized = msk_interview_date_in_utc.astimezone(tz_msk) time_str = "{:02d}:{:02d}".format(localized.hour, localized.minute) + today = formats.date_format(msk_interview_date_in_utc, "SHORT_DATE_FORMAT") assert time_str == "18:00" # expected UTC+3 - url = reverse("admission:interviews:list") + "?campaign=" + url = reverse("admission:interviews:list") + f"?campaign=&status=approval&date_from={today}&date_to={today}" response = client.get(url) assert response.status_code == 200 html = BeautifulSoup(response.content, "html.parser") @@ -214,7 +216,7 @@ def test_interview_list(settings, client, curator): localized = interview_date_in_utc.astimezone(tz) time_str = "{:02d}:{:02d}".format(localized.hour, localized.minute) assert time_str == "19:00" # expected UTC + 7 - url = reverse("admission:interviews:list") + "?campaign=" + url = reverse("admission:interviews:list") + f"?campaign=&status=approval&date_from={today}&date_to={today}" response = client.get(url) assert response.status_code == 200 html = BeautifulSoup(response.content, "html.parser") diff --git a/apps/admission/tests/test_views.py b/apps/admission/tests/test_views.py index 72211477d..17ef502c1 100644 --- a/apps/admission/tests/test_views.py +++ b/apps/admission/tests/test_views.py @@ -56,6 +56,8 @@ def test_simple_interviews_list(client, curator, settings): campaign = CampaignFactory(current=True, branch=branch_nsk) today_local_nsk = now_local(branch_nsk.get_timezone()) today_local_nsk_date = formats.date_format(today_local_nsk, "SHORT_DATE_FORMAT") + date_to = datetime.datetime(today_local_nsk.year, 8, 1) + date_to = formats.date_format(date_to, "SHORT_DATE_FORMAT") interview1, interview2, interview3 = InterviewFactory.create_batch( 3, interviewers=[interviewer], @@ -71,11 +73,11 @@ def test_simple_interviews_list(client, curator, settings): response = client.get(reverse("admission:interviews:list")) # For curator set default filters and redirect assert response.status_code == 302 - assert f"campaign={campaign.pk}" in response.url + assert f"campaign=" in response.url assert f"status={Interview.COMPLETED}" in response.url assert f"status={Interview.APPROVED}" in response.url assert f"date_from={today_local_nsk_date}" in response.url - assert f"date_to={today_local_nsk_date}" in response.url + assert f"date_to={date_to}" in response.url def format_url(campaign_id, date_from: str, date_to: str): return ( diff --git a/apps/admission/views.py b/apps/admission/views.py index f747c08d9..d2a0b4a8d 100644 --- a/apps/admission/views.py +++ b/apps/admission/views.py @@ -586,7 +586,7 @@ def get_context_data(self, **kwargs): return context -class ApplicantDetailView(InterviewerOnlyMixin, TemplateResponseMixin, BaseCreateView): +class ApplicantDetailView(CuratorOnlyMixin, TemplateResponseMixin, BaseCreateView): form_class = InterviewForm template_name = "admission/applicant_detail.html" @@ -742,26 +742,22 @@ class InterviewListView(InterviewerOnlyMixin, BaseFilterView, generic.ListView): def get(self, request, *args, **kwargs): """ - Redirects curator to appropriate campaign if no any provided. + Redirects curator to page with appropriate parameters for correct work of 'download_csv'. """ user = self.request.user - if user.is_curator and "campaign" not in self.request.GET: - # Try to find user preferred current campaign id - campaign = get_default_campaign_for_user(user) - if not campaign: - return HttpResponseRedirect(reverse("admission:applicants:list")) - else: - today_local = now_local(campaign.branch.get_timezone()) - date = formats.date_format(today_local, "SHORT_DATE_FORMAT") - params = parse.urlencode( - { - "campaign": campaign.pk, + is_param_lost = any(param not in self.request.GET for param in ["status", "date_from", "date_to"]) + if is_param_lost: + today = formats.date_format(timezone.now(), "SHORT_DATE_FORMAT") + date_to = datetime(timezone.now().year, 8, 1) + date_to = formats.date_format(date_to, "SHORT_DATE_FORMAT") + params = { "status": [Interview.COMPLETED, Interview.APPROVED], - "date_from": date, - "date_to": date, - }, - doseq=True, - ) + "date_from": today, + "date_to": date_to, + } + if user.is_curator and "campaign" not in self.request.GET: + params.update({"campaign": ""}) + params = parse.urlencode(params, doseq=True) url = "{}?{}".format(reverse("admission:interviews:list"), params) return HttpResponseRedirect(redirect_to=url) if "download_csv" in request.GET: @@ -780,18 +776,6 @@ def get_filterset_class(self): return InterviewsCuratorFilter return InterviewsFilter - def get_filterset_kwargs(self, filterset_class): - kwargs = super().get_filterset_kwargs(filterset_class) - kwargs["request"] = self.request - if not kwargs["data"]: - today = formats.date_format(timezone.now(), "SHORT_DATE_FORMAT") - kwargs["data"] = { - "status": [Interview.COMPLETED, Interview.APPROVED], - "date_from": today, - "date_to": today, - } - return kwargs - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["filter"] = self.filterset @@ -841,7 +825,8 @@ class InterviewListCSVView(CuratorOnlyMixin, generic.base.View): def get(self, request, *args, **kwargs): date_from = datetime.strptime(request.GET.get("date_from"), "%d.%m.%Y") date_to = datetime.strptime(request.GET.get("date_to"), "%d.%m.%Y") - campaign = int(request.GET.get("campaign")) + campaign = request.GET.get("campaign") + campaign = int(campaign) if campaign else None response = HttpResponse(content_type="text/csv; charset=utf-8") filename = f"interviews_{date_from.strftime('%d.%m.%Y')}_{date_to.strftime('%d.%m.%Y')}.csv" response["Content-Disposition"] = f'attachment; filename="{filename}"' @@ -855,12 +840,13 @@ def get(self, request, *args, **kwargs): "applicant_name", "interviewer_name", ] + campaign_filter = Q(applicant__campaign=campaign) if campaign else Q() writer.writerow(headers) interviews = ( Interview.objects.select_related("applicant") .prefetch_related("interviewers") .filter( - applicant__campaign=campaign, + campaign_filter, date__date__gte=date_from.strftime("%Y-%m-%d"), date__date__lte=date_to.strftime("%Y-%m-%d"), ) diff --git a/lms/jinja2/lms/admission/applicant_list.html b/lms/jinja2/lms/admission/applicant_list.html index c6083dfc1..8eda1ab09 100644 --- a/lms/jinja2/lms/admission/applicant_list.html +++ b/lms/jinja2/lms/admission/applicant_list.html @@ -53,6 +53,7 @@

Анкеты поступающих / {{ paginator.count Направление Тест Экз. + Cоб. @@ -86,6 +87,7 @@

Анкеты поступающих / {{ paginator.count {% if applicant.online_test %}{{ applicant.online_test.score_display() }}{% else %}-{% endif %} {% if applicant.exam %}{{ applicant.exam.score_display() }}{% else %}-{% endif %} + {{ applicant.get_all_interview_score() }} {% else %}