|
| 1 | +import io |
| 2 | +import unicodedata |
| 3 | +from datetime import date |
| 4 | + |
1 | 5 | from django.contrib.auth import get_user_model |
2 | 6 | from django.db import IntegrityError, transaction |
| 7 | +from django.db.models import Prefetch |
| 8 | +from django.http import FileResponse |
3 | 9 | from django.shortcuts import get_object_or_404 |
4 | 10 | from django.utils import timezone |
5 | 11 | from django.utils.timezone import now |
6 | 12 | from drf_yasg import openapi |
7 | 13 | from drf_yasg.utils import swagger_auto_schema |
| 14 | +from openpyxl import Workbook |
8 | 15 | from rest_framework import generics, permissions, status |
9 | 16 | from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError |
10 | 17 | from rest_framework.generics import GenericAPIView |
|
34 | 41 | PartnerProgramUserSerializer, |
35 | 42 | ProgramProjectFilterRequestSerializer, |
36 | 43 | ) |
| 44 | +from partner_programs.services import ( |
| 45 | + BASE_COLUMNS, |
| 46 | + build_program_field_columns, |
| 47 | + row_dict_for_link, |
| 48 | +) |
37 | 49 | from partner_programs.utils import filter_program_projects_by_field_name |
38 | 50 | from projects.models import Project |
39 | 51 | from projects.serializers import ( |
40 | 52 | PartnerProgramFieldValueUpdateSerializer, |
41 | 53 | ProjectListSerializer, |
42 | 54 | ) |
43 | | -from vacancy.mapping import ( |
44 | | - MessageTypeEnum, |
45 | | - UserProgramRegisterParams, |
46 | | -) |
| 55 | +from vacancy.mapping import MessageTypeEnum, UserProgramRegisterParams |
47 | 56 | from vacancy.tasks import send_email |
48 | 57 |
|
49 | 58 | User = get_user_model() |
@@ -255,9 +264,7 @@ def get_project(self, project_id): |
255 | 264 | except Project.DoesNotExist: |
256 | 265 | raise NotFound("Проект не найден") |
257 | 266 |
|
258 | | - @swagger_auto_schema( |
259 | | - request_body=PartnerProgramFieldValueUpdateSerializer(many=True) |
260 | | - ) |
| 267 | + @swagger_auto_schema(request_body=PartnerProgramFieldValueUpdateSerializer(many=True)) |
261 | 268 | def put(self, request, project_id, *args, **kwargs): |
262 | 269 | project = self.get_project(project_id) |
263 | 270 |
|
@@ -439,3 +446,92 @@ class PartnerProgramProjectsAPIView(generics.ListAPIView): |
439 | 446 | def get_queryset(self): |
440 | 447 | program = get_object_or_404(PartnerProgram, pk=self.kwargs["pk"]) |
441 | 448 | return Project.objects.filter(program_links__partner_program=program).distinct() |
| 449 | + |
| 450 | + |
| 451 | +def _slugify_filename(filename: str) -> str: |
| 452 | + """ |
| 453 | + Преобразует произвольную строку в безопасное имя файла: |
| 454 | + - нормализует Unicode; |
| 455 | + - оставляет только буквы, цифры, дефисы, подчёркивания и пробелы; |
| 456 | + - заменяет группы пробелов на один дефис. |
| 457 | + """ |
| 458 | + normalized_name = unicodedata.normalize("NFKD", filename) |
| 459 | + safe_chars = [ |
| 460 | + char for char in normalized_name if char.isalnum() or char in ("-", "_", " ") |
| 461 | + ] |
| 462 | + cleaned_name = "".join(safe_chars) |
| 463 | + return "-".join(cleaned_name.split()) |
| 464 | + |
| 465 | + |
| 466 | +class PartnerProgramExportProjectsAPIView(APIView): |
| 467 | + """Возвращает Excel-файл со всеми проектами программы.""" |
| 468 | + |
| 469 | + permission_classes = [permissions.IsAdminUser] |
| 470 | + |
| 471 | + def get(self, request, pk: int): |
| 472 | + try: |
| 473 | + program = PartnerProgram.objects.get(pk=pk) |
| 474 | + except PartnerProgram.DoesNotExist: |
| 475 | + return Response( |
| 476 | + {"detail": "Программа не найдена."}, status=status.HTTP_404_NOT_FOUND |
| 477 | + ) |
| 478 | + |
| 479 | + user = request.user |
| 480 | + if not ( |
| 481 | + getattr(user, "is_staff", False) |
| 482 | + or getattr(user, "is_superuser", False) |
| 483 | + or program.is_manager(user) |
| 484 | + ): |
| 485 | + return Response( |
| 486 | + {"detail": "Недостаточно прав."}, status=status.HTTP_403_FORBIDDEN |
| 487 | + ) |
| 488 | + |
| 489 | + only_submitted = request.query_params.get("only_submitted") in ( |
| 490 | + "1", |
| 491 | + "true", |
| 492 | + "True", |
| 493 | + ) |
| 494 | + |
| 495 | + extra_cols = build_program_field_columns(program) |
| 496 | + header_pairs = BASE_COLUMNS + extra_cols |
| 497 | + |
| 498 | + fv_qs = PartnerProgramFieldValue.objects.select_related("field").filter( |
| 499 | + field__partner_program_id=program.id |
| 500 | + ) |
| 501 | + links_qs = program.program_projects.select_related( |
| 502 | + "project", "project__leader" |
| 503 | + ).prefetch_related( |
| 504 | + Prefetch("field_values", queryset=fv_qs, to_attr="_prefetched_field_values") |
| 505 | + ) |
| 506 | + if only_submitted: |
| 507 | + links_qs = links_qs.filter(submitted=True) |
| 508 | + |
| 509 | + wb = Workbook(write_only=True) |
| 510 | + ws = wb.create_sheet(title="Проекты") |
| 511 | + ws.append([title for _, title in header_pairs]) |
| 512 | + |
| 513 | + extra_keys_order = [key for key, _ in extra_cols] |
| 514 | + |
| 515 | + for row_number, program_project_link in enumerate(links_qs, start=1): |
| 516 | + row_dict = row_dict_for_link( |
| 517 | + program_project_link=program_project_link, |
| 518 | + extra_field_keys_order=extra_keys_order, |
| 519 | + row_number=row_number, |
| 520 | + ) |
| 521 | + ws.append([row_dict.get(key, "") for key, _ in header_pairs]) |
| 522 | + |
| 523 | + bio = io.BytesIO() |
| 524 | + wb.save(bio) |
| 525 | + bio.seek(0) |
| 526 | + |
| 527 | + fname_base = _slugify_filename( |
| 528 | + f"{program.name or 'program'}-{program.pk}-projects-{date.today():%Y-%m-%d}" |
| 529 | + ) |
| 530 | + filename = f"{fname_base}.xlsx" |
| 531 | + |
| 532 | + return FileResponse( |
| 533 | + bio, |
| 534 | + as_attachment=True, |
| 535 | + filename=filename, |
| 536 | + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| 537 | + ) |
0 commit comments