Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions partner_programs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ class PartnerProgramForMemberSerializer(PartnerProgramBaseSerializerMixin):

views_count = serializers.SerializerMethodField(method_name="count_views")
links = serializers.SerializerMethodField(method_name="get_links")
is_user_manager = serializers.SerializerMethodField(
method_name="get_is_user_manager"
)
is_user_manager = serializers.SerializerMethodField(method_name="get_is_user_manager")

def count_views(self, program):
return get_views_count(program)
Expand Down
159 changes: 137 additions & 22 deletions partner_programs/services.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from collections import OrderedDict

from partner_programs.models import PartnerProgramUserProfile
from project_rates.models import Criteria, ProjectScore


logger = logging.getLogger()


Expand All @@ -23,16 +23,14 @@ class ProjectScoreDataPreparer:
"Класс_курс": "ОШИБКА",
}

EXPERT_ERROR_FIELDS = {
"Фамилия эксперта": "ОШИБКА"
}
EXPERT_ERROR_FIELDS = {"Фамилия эксперта": "ОШИБКА"}

def __init__(
self,
user_profiles: dict[int, PartnerProgramUserProfile],
scores: dict[int, list[ProjectScore]],
project_id: int,
program_id: int
program_id: int,
):
self._project_id = project_id
self._user_profiles = user_profiles
Expand All @@ -41,35 +39,54 @@ def __init__(

def get_project_user_info(self) -> dict[str, str]:
try:
user_program_profile: PartnerProgramUserProfile = self._user_profiles.get(self._project_id)
user_program_profile_json: dict = user_program_profile.partner_program_data if user_program_profile else {}
user_program_profile: PartnerProgramUserProfile = self._user_profiles.get(
self._project_id
)
user_program_profile_json: dict = (
user_program_profile.partner_program_data if user_program_profile else {}
)

user_info: dict[str, str] = {
"Фамилия": user_program_profile.user.last_name if user_program_profile else '',
"Имя": user_program_profile.user.first_name if user_program_profile else '',
"Отчество": user_program_profile.user.patronymic if user_program_profile else '',
"Фамилия": (
user_program_profile.user.last_name if user_program_profile else ""
),
"Имя": (
user_program_profile.user.first_name if user_program_profile else ""
),
"Отчество": (
user_program_profile.user.patronymic if user_program_profile else ""
),
"Email": (
user_program_profile_json.get('email') if user_program_profile_json.get('email')
user_program_profile_json.get("email")
if user_program_profile_json.get("email")
else user_program_profile.user.email
),
"Регион_РФ": user_program_profile_json.get('region', ''),
"Учебное_заведение": user_program_profile_json.get('education_type', ''),
"Название_учебного_заведения": user_program_profile_json.get('institution_name', ''),
"Класс_курс": user_program_profile_json.get('class_course', ''),
"Регион_РФ": user_program_profile_json.get("region", ""),
"Учебное_заведение": user_program_profile_json.get("education_type", ""),
"Название_учебного_заведения": user_program_profile_json.get(
"institution_name", ""
),
"Класс_курс": user_program_profile_json.get("class_course", ""),
}
return user_info
except Exception as e:
logger.error(f"Prepare export rates data about user error: {str(e)}", exc_info=True)
logger.error(
f"Prepare export rates data about user error: {str(e)}", exc_info=True
)
return self.USER_ERROR_FIELDS

def get_project_expert_info(self) -> dict[str, str]:
try:
project_scores: list[ProjectScore] = self._scores.get(self._project_id, [])
first_score = project_scores[0] if project_scores else None
expert_last_name: dict[str, str] = {"Фамилия эксперта": first_score.user.last_name if first_score else ''}
expert_last_name: dict[str, str] = {
"Фамилия эксперта": first_score.user.last_name if first_score else ""
}
return expert_last_name
except Exception as e:
logger.error(f"Prepare export rates data about expert error: {str(e)}", exc_info=True)
logger.error(
f"Prepare export rates data about expert error: {str(e)}", exc_info=True
)
return self.EXPERT_ERROR_FIELDS

def get_project_scores_info(self) -> dict[str, str]:
Expand All @@ -78,16 +95,114 @@ def get_project_scores_info(self) -> dict[str, str]:
project_scores: list[ProjectScore] = self._scores.get(self._project_id, [])
score_info_with_out_comment: dict[str, str] = {
score.criteria.name: score.value
for score in project_scores if score.criteria.name != "Комментарий"
for score in project_scores
if score.criteria.name != "Комментарий"
}
project_scores_dict.update(score_info_with_out_comment)
comment = next((score for score in project_scores if score.criteria.name == "Комментарий"), None)
comment = next(
(
score
for score in project_scores
if score.criteria.name == "Комментарий"
),
None,
)
if comment is not None:
project_scores_dict["Комментарий"] = comment.value
return project_scores_dict
except Exception as e:
logger.error(f"Prepare export rates data about project_scores error: {str(e)}", exc_info=True)
logger.error(
f"Prepare export rates data about project_scores error: {str(e)}",
exc_info=True,
)
return {
criteria.name: "ОШИБКА"
for criteria in Criteria.objects.filter(partner_program__id=self._program_id)
for criteria in Criteria.objects.filter(
partner_program__id=self._program_id
)
}


BASE_COLUMNS = [
("row_number", "№ п/п"),
("project_name", "Название проекта"),
("project_description", "Описание проекта"),
("project_region", "Регион проекта"),
("project_presentation", "Ссылка на презентацию"),
("team_size", "Количество человек в команде"),
("leader_full_name", "Имя фамилия лидера"),
]


def _leader_full_name(user):
if not user:
return ""
if hasattr(user, "get_full_name") and callable(user.get_full_name):
full = user.get_full_name()
if full:
return full
first = getattr(user, "first_name", "") or ""
last = getattr(user, "last_name", "") or ""
return (first + " " + last).strip() or getattr(user, "username", "") or str(user.pk)


def _calc_team_size(project):
try:
if hasattr(project, "get_collaborators_user_list"):
return 1 + len(project.get_collaborators_user_list())
if hasattr(project, "collaborator_set"):
return 1 + project.collaborator_set.count()
except Exception:
pass
return 1


def build_program_field_columns(program) -> list[tuple[str, str]]:
program_fields = program.fields.all().order_by("pk")
return [
(f"name:{program_field.name}", program_field.label)
for program_field in program_fields
]


def row_dict_for_link(
program_project_link,
extra_field_keys_order: list[str],
row_number: int,
) -> OrderedDict:
"""
program_project_link: PartnerProgramProject
extra_field_keys_order: список псевдоключей "name:<field.name>" в нужном порядке
row_number: порядковый номер строки в Excel (начиная с 1)
"""
project = program_project_link.project
row = OrderedDict()

row["row_number"] = row_number

row["project_name"] = project.name or ""
row["project_description"] = project.description or ""
row["project_region"] = project.region or ""
row["project_presentation"] = project.presentation_address or ""
row["team_size"] = _calc_team_size(project)
row["leader_full_name"] = _leader_full_name(getattr(project, "leader", None))

values_map: dict[str, str] = {}
prefetched_values = getattr(program_project_link, "_prefetched_field_values", None)
field_values_iterable = (
prefetched_values
if prefetched_values is not None
else program_project_link.field_values.all()
)

for field_value in field_values_iterable:
if (
field_value.field.partner_program_id
== program_project_link.partner_program_id
):
values_map[f"name:{field_value.field.name}"] = field_value.get_value()

for field_key in extra_field_keys_order:
row[field_key] = values_map.get(field_key, "")

return row
6 changes: 6 additions & 0 deletions partner_programs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
PartnerProgramCreateUserAndRegister,
PartnerProgramDataSchema,
PartnerProgramDetail,
PartnerProgramExportProjectsAPIView,
PartnerProgramList,
PartnerProgramProjectsAPIView,
PartnerProgramProjectSubmitView,
Expand Down Expand Up @@ -50,4 +51,9 @@
PartnerProgramProjectsAPIView.as_view(),
name="partner-program-projects",
),
path(
"<int:pk>/export-projects/",
PartnerProgramExportProjectsAPIView.as_view(),
name="partner-program-export-projects",
),
]
110 changes: 103 additions & 7 deletions partner_programs/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import io
import unicodedata
from datetime import date

from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction
from django.db.models import Prefetch
from django.http import FileResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.timezone import now
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from openpyxl import Workbook
from rest_framework import generics, permissions, status
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.generics import GenericAPIView
Expand Down Expand Up @@ -34,16 +41,18 @@
PartnerProgramUserSerializer,
ProgramProjectFilterRequestSerializer,
)
from partner_programs.services import (
BASE_COLUMNS,
build_program_field_columns,
row_dict_for_link,
)
from partner_programs.utils import filter_program_projects_by_field_name
from projects.models import Project
from projects.serializers import (
PartnerProgramFieldValueUpdateSerializer,
ProjectListSerializer,
)
from vacancy.mapping import (
MessageTypeEnum,
UserProgramRegisterParams,
)
from vacancy.mapping import MessageTypeEnum, UserProgramRegisterParams
from vacancy.tasks import send_email

User = get_user_model()
Expand Down Expand Up @@ -255,9 +264,7 @@ def get_project(self, project_id):
except Project.DoesNotExist:
raise NotFound("Проект не найден")

@swagger_auto_schema(
request_body=PartnerProgramFieldValueUpdateSerializer(many=True)
)
@swagger_auto_schema(request_body=PartnerProgramFieldValueUpdateSerializer(many=True))
def put(self, request, project_id, *args, **kwargs):
project = self.get_project(project_id)

Expand Down Expand Up @@ -439,3 +446,92 @@ class PartnerProgramProjectsAPIView(generics.ListAPIView):
def get_queryset(self):
program = get_object_or_404(PartnerProgram, pk=self.kwargs["pk"])
return Project.objects.filter(program_links__partner_program=program).distinct()


def _slugify_filename(filename: str) -> str:
"""
Преобразует произвольную строку в безопасное имя файла:
- нормализует Unicode;
- оставляет только буквы, цифры, дефисы, подчёркивания и пробелы;
- заменяет группы пробелов на один дефис.
"""
normalized_name = unicodedata.normalize("NFKD", filename)
safe_chars = [
char for char in normalized_name if char.isalnum() or char in ("-", "_", " ")
]
cleaned_name = "".join(safe_chars)
return "-".join(cleaned_name.split())


class PartnerProgramExportProjectsAPIView(APIView):
"""Возвращает Excel-файл со всеми проектами программы."""

permission_classes = [permissions.IsAdminUser]

def get(self, request, pk: int):
try:
program = PartnerProgram.objects.get(pk=pk)
except PartnerProgram.DoesNotExist:
return Response(
{"detail": "Программа не найдена."}, status=status.HTTP_404_NOT_FOUND
)

user = request.user
if not (
getattr(user, "is_staff", False)
or getattr(user, "is_superuser", False)
or program.is_manager(user)
):
return Response(
{"detail": "Недостаточно прав."}, status=status.HTTP_403_FORBIDDEN
)

only_submitted = request.query_params.get("only_submitted") in (
"1",
"true",
"True",
)

extra_cols = build_program_field_columns(program)
header_pairs = BASE_COLUMNS + extra_cols

fv_qs = PartnerProgramFieldValue.objects.select_related("field").filter(
field__partner_program_id=program.id
)
links_qs = program.program_projects.select_related(
"project", "project__leader"
).prefetch_related(
Prefetch("field_values", queryset=fv_qs, to_attr="_prefetched_field_values")
)
if only_submitted:
links_qs = links_qs.filter(submitted=True)

wb = Workbook(write_only=True)
ws = wb.create_sheet(title="Проекты")
ws.append([title for _, title in header_pairs])

extra_keys_order = [key for key, _ in extra_cols]

for row_number, program_project_link in enumerate(links_qs, start=1):
row_dict = row_dict_for_link(
program_project_link=program_project_link,
extra_field_keys_order=extra_keys_order,
row_number=row_number,
)
ws.append([row_dict.get(key, "") for key, _ in header_pairs])

bio = io.BytesIO()
wb.save(bio)
bio.seek(0)

fname_base = _slugify_filename(
f"{program.name or 'program'}-{program.pk}-projects-{date.today():%Y-%m-%d}"
)
filename = f"{fname_base}.xlsx"

return FileResponse(
bio,
as_attachment=True,
filename=filename,
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)