From 8fc12935f6b29a1540a3e3cfd739f9e43146754b Mon Sep 17 00:00:00 2001 From: Toksi Date: Wed, 15 Oct 2025 14:51:50 +0500 Subject: [PATCH 1/2] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81=D0=B8=D0=BC=D0=B2?= =?UTF-8?q?=D0=BE=D0=BB=D1=8B=20=D0=B2=20openpyxl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- partner_programs/services.py | 30 ++++++++++++++++++++++++++++++ partner_programs/views.py | 5 ++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/partner_programs/services.py b/partner_programs/services.py index b10e23c4..ec1c9680 100644 --- a/partner_programs/services.py +++ b/partner_programs/services.py @@ -1,6 +1,8 @@ import logging from collections import OrderedDict +from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE + from partner_programs.models import PartnerProgramUserProfile from project_rates.models import Criteria, ProjectScore @@ -132,6 +134,34 @@ def get_project_scores_info(self) -> dict[str, str]: ("team_size", "Количество человек в команде"), ("leader_full_name", "Имя фамилия лидера"), ] +EXCEL_CELL_MAX = 32767 # лимит символов в ячейке Excel + + +def sanitize_excel_value(value): + """ + Приводит значение к безопасному для openpyxl виду: + - None -> "" + - для строк: вычищает запрещённые символы, нормализует переносы строк, + и обрезает до лимита Excel (32767). + - для чисел/булевых оставляет как есть. + """ + if value is None: + return "" + + if isinstance(value, (int, float, bool)): + return value + + text = str(value) + # нормализуем переносы (на всякий случай) + text = text.replace("\r\n", "\n").replace("\r", "\n") + # выкидываем запрещённые символы (в т.ч. \x0B) + text = ILLEGAL_CHARACTERS_RE.sub(" ", text) + + # Excel не примет строки длиннее 32767 + if len(text) > EXCEL_CELL_MAX: + text = text[: EXCEL_CELL_MAX - 3] + "..." + + return text def _leader_full_name(user): diff --git a/partner_programs/views.py b/partner_programs/views.py index e985ff1c..2783bb14 100644 --- a/partner_programs/views.py +++ b/partner_programs/views.py @@ -45,6 +45,7 @@ BASE_COLUMNS, build_program_field_columns, row_dict_for_link, + sanitize_excel_value, ) from partner_programs.utils import filter_program_projects_by_field_name from projects.models import Project @@ -518,7 +519,9 @@ def get(self, request, pk: int): extra_field_keys_order=extra_keys_order, row_number=row_number, ) - ws.append([row_dict.get(key, "") for key, _ in header_pairs]) + raw_values = [row_dict.get(key, "") for key, _ in header_pairs] + safe_values = [sanitize_excel_value(v) for v in raw_values] + ws.append(safe_values) bio = io.BytesIO() wb.save(bio) From cb8c1e7f95ef27d5852aee2c3ffd2280f9ce3ee4 Mon Sep 17 00:00:00 2001 From: Toksi Date: Wed, 15 Oct 2025 14:55:24 +0500 Subject: [PATCH 2/2] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81=D0=B8=D0=BC=D0=B2?= =?UTF-8?q?=D0=BE=D0=BB=D1=8B=20=D0=B2=20openpyxl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- partner_programs/serializers.py | 4 +++- partner_programs/services.py | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/partner_programs/serializers.py b/partner_programs/serializers.py index 3bd6a09a..43e2806f 100644 --- a/partner_programs/serializers.py +++ b/partner_programs/serializers.py @@ -80,7 +80,9 @@ 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) diff --git a/partner_programs/services.py b/partner_programs/services.py index ec1c9680..1aac24fc 100644 --- a/partner_programs/services.py +++ b/partner_programs/services.py @@ -152,12 +152,9 @@ def sanitize_excel_value(value): return value text = str(value) - # нормализуем переносы (на всякий случай) text = text.replace("\r\n", "\n").replace("\r", "\n") - # выкидываем запрещённые символы (в т.ч. \x0B) text = ILLEGAL_CHARACTERS_RE.sub(" ", text) - # Excel не примет строки длиннее 32767 if len(text) > EXCEL_CELL_MAX: text = text[: EXCEL_CELL_MAX - 3] + "..."