Skip to content

Commit cbd89ef

Browse files
authored
Merge pull request #554 from PROCOLLAB-github/hotfix/export_program_info
Hotfix/export program info
2 parents b9ff1fc + cb8c1e7 commit cbd89ef

3 files changed

Lines changed: 276 additions & 29 deletions

File tree

partner_programs/services.py

Lines changed: 164 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
2+
from collections import OrderedDict
3+
4+
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
25

36
from partner_programs.models import PartnerProgramUserProfile
47
from project_rates.models import Criteria, ProjectScore
58

6-
79
logger = logging.getLogger()
810

911

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

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

3030
def __init__(
3131
self,
3232
user_profiles: dict[int, PartnerProgramUserProfile],
3333
scores: dict[int, list[ProjectScore]],
3434
project_id: int,
35-
program_id: int
35+
program_id: int,
3636
):
3737
self._project_id = project_id
3838
self._user_profiles = user_profiles
@@ -41,35 +41,54 @@ def __init__(
4141

4242
def get_project_user_info(self) -> dict[str, str]:
4343
try:
44-
user_program_profile: PartnerProgramUserProfile = self._user_profiles.get(self._project_id)
45-
user_program_profile_json: dict = user_program_profile.partner_program_data if user_program_profile else {}
44+
user_program_profile: PartnerProgramUserProfile = self._user_profiles.get(
45+
self._project_id
46+
)
47+
user_program_profile_json: dict = (
48+
user_program_profile.partner_program_data if user_program_profile else {}
49+
)
4650

4751
user_info: dict[str, str] = {
48-
"Фамилия": user_program_profile.user.last_name if user_program_profile else '',
49-
"Имя": user_program_profile.user.first_name if user_program_profile else '',
50-
"Отчество": user_program_profile.user.patronymic if user_program_profile else '',
52+
"Фамилия": (
53+
user_program_profile.user.last_name if user_program_profile else ""
54+
),
55+
"Имя": (
56+
user_program_profile.user.first_name if user_program_profile else ""
57+
),
58+
"Отчество": (
59+
user_program_profile.user.patronymic if user_program_profile else ""
60+
),
5161
"Email": (
52-
user_program_profile_json.get('email') if user_program_profile_json.get('email')
62+
user_program_profile_json.get("email")
63+
if user_program_profile_json.get("email")
5364
else user_program_profile.user.email
5465
),
55-
"Регион_РФ": user_program_profile_json.get('region', ''),
56-
"Учебное_заведение": user_program_profile_json.get('education_type', ''),
57-
"Название_учебного_заведения": user_program_profile_json.get('institution_name', ''),
58-
"Класс_курс": user_program_profile_json.get('class_course', ''),
66+
"Регион_РФ": user_program_profile_json.get("region", ""),
67+
"Учебное_заведение": user_program_profile_json.get("education_type", ""),
68+
"Название_учебного_заведения": user_program_profile_json.get(
69+
"institution_name", ""
70+
),
71+
"Класс_курс": user_program_profile_json.get("class_course", ""),
5972
}
6073
return user_info
6174
except Exception as e:
62-
logger.error(f"Prepare export rates data about user error: {str(e)}", exc_info=True)
75+
logger.error(
76+
f"Prepare export rates data about user error: {str(e)}", exc_info=True
77+
)
6378
return self.USER_ERROR_FIELDS
6479

6580
def get_project_expert_info(self) -> dict[str, str]:
6681
try:
6782
project_scores: list[ProjectScore] = self._scores.get(self._project_id, [])
6883
first_score = project_scores[0] if project_scores else None
69-
expert_last_name: dict[str, str] = {"Фамилия эксперта": first_score.user.last_name if first_score else ''}
84+
expert_last_name: dict[str, str] = {
85+
"Фамилия эксперта": first_score.user.last_name if first_score else ""
86+
}
7087
return expert_last_name
7188
except Exception as e:
72-
logger.error(f"Prepare export rates data about expert error: {str(e)}", exc_info=True)
89+
logger.error(
90+
f"Prepare export rates data about expert error: {str(e)}", exc_info=True
91+
)
7392
return self.EXPERT_ERROR_FIELDS
7493

7594
def get_project_scores_info(self) -> dict[str, str]:
@@ -78,16 +97,139 @@ def get_project_scores_info(self) -> dict[str, str]:
7897
project_scores: list[ProjectScore] = self._scores.get(self._project_id, [])
7998
score_info_with_out_comment: dict[str, str] = {
8099
score.criteria.name: score.value
81-
for score in project_scores if score.criteria.name != "Комментарий"
100+
for score in project_scores
101+
if score.criteria.name != "Комментарий"
82102
}
83103
project_scores_dict.update(score_info_with_out_comment)
84-
comment = next((score for score in project_scores if score.criteria.name == "Комментарий"), None)
104+
comment = next(
105+
(
106+
score
107+
for score in project_scores
108+
if score.criteria.name == "Комментарий"
109+
),
110+
None,
111+
)
85112
if comment is not None:
86113
project_scores_dict["Комментарий"] = comment.value
87114
return project_scores_dict
88115
except Exception as e:
89-
logger.error(f"Prepare export rates data about project_scores error: {str(e)}", exc_info=True)
116+
logger.error(
117+
f"Prepare export rates data about project_scores error: {str(e)}",
118+
exc_info=True,
119+
)
90120
return {
91121
criteria.name: "ОШИБКА"
92-
for criteria in Criteria.objects.filter(partner_program__id=self._program_id)
122+
for criteria in Criteria.objects.filter(
123+
partner_program__id=self._program_id
124+
)
93125
}
126+
127+
128+
BASE_COLUMNS = [
129+
("row_number", "№ п/п"),
130+
("project_name", "Название проекта"),
131+
("project_description", "Описание проекта"),
132+
("project_region", "Регион проекта"),
133+
("project_presentation", "Ссылка на презентацию"),
134+
("team_size", "Количество человек в команде"),
135+
("leader_full_name", "Имя фамилия лидера"),
136+
]
137+
EXCEL_CELL_MAX = 32767 # лимит символов в ячейке Excel
138+
139+
140+
def sanitize_excel_value(value):
141+
"""
142+
Приводит значение к безопасному для openpyxl виду:
143+
- None -> ""
144+
- для строк: вычищает запрещённые символы, нормализует переносы строк,
145+
и обрезает до лимита Excel (32767).
146+
- для чисел/булевых оставляет как есть.
147+
"""
148+
if value is None:
149+
return ""
150+
151+
if isinstance(value, (int, float, bool)):
152+
return value
153+
154+
text = str(value)
155+
text = text.replace("\r\n", "\n").replace("\r", "\n")
156+
text = ILLEGAL_CHARACTERS_RE.sub(" ", text)
157+
158+
if len(text) > EXCEL_CELL_MAX:
159+
text = text[: EXCEL_CELL_MAX - 3] + "..."
160+
161+
return text
162+
163+
164+
def _leader_full_name(user):
165+
if not user:
166+
return ""
167+
if hasattr(user, "get_full_name") and callable(user.get_full_name):
168+
full = user.get_full_name()
169+
if full:
170+
return full
171+
first = getattr(user, "first_name", "") or ""
172+
last = getattr(user, "last_name", "") or ""
173+
return (first + " " + last).strip() or getattr(user, "username", "") or str(user.pk)
174+
175+
176+
def _calc_team_size(project):
177+
try:
178+
if hasattr(project, "get_collaborators_user_list"):
179+
return 1 + len(project.get_collaborators_user_list())
180+
if hasattr(project, "collaborator_set"):
181+
return 1 + project.collaborator_set.count()
182+
except Exception:
183+
pass
184+
return 1
185+
186+
187+
def build_program_field_columns(program) -> list[tuple[str, str]]:
188+
program_fields = program.fields.all().order_by("pk")
189+
return [
190+
(f"name:{program_field.name}", program_field.label)
191+
for program_field in program_fields
192+
]
193+
194+
195+
def row_dict_for_link(
196+
program_project_link,
197+
extra_field_keys_order: list[str],
198+
row_number: int,
199+
) -> OrderedDict:
200+
"""
201+
program_project_link: PartnerProgramProject
202+
extra_field_keys_order: список псевдоключей "name:<field.name>" в нужном порядке
203+
row_number: порядковый номер строки в Excel (начиная с 1)
204+
"""
205+
project = program_project_link.project
206+
row = OrderedDict()
207+
208+
row["row_number"] = row_number
209+
210+
row["project_name"] = project.name or ""
211+
row["project_description"] = project.description or ""
212+
row["project_region"] = project.region or ""
213+
row["project_presentation"] = project.presentation_address or ""
214+
row["team_size"] = _calc_team_size(project)
215+
row["leader_full_name"] = _leader_full_name(getattr(project, "leader", None))
216+
217+
values_map: dict[str, str] = {}
218+
prefetched_values = getattr(program_project_link, "_prefetched_field_values", None)
219+
field_values_iterable = (
220+
prefetched_values
221+
if prefetched_values is not None
222+
else program_project_link.field_values.all()
223+
)
224+
225+
for field_value in field_values_iterable:
226+
if (
227+
field_value.field.partner_program_id
228+
== program_project_link.partner_program_id
229+
):
230+
values_map[f"name:{field_value.field.name}"] = field_value.get_value()
231+
232+
for field_key in extra_field_keys_order:
233+
row[field_key] = values_map.get(field_key, "")
234+
235+
return row

partner_programs/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
PartnerProgramCreateUserAndRegister,
66
PartnerProgramDataSchema,
77
PartnerProgramDetail,
8+
PartnerProgramExportProjectsAPIView,
89
PartnerProgramList,
910
PartnerProgramProjectsAPIView,
1011
PartnerProgramProjectSubmitView,
@@ -50,4 +51,9 @@
5051
PartnerProgramProjectsAPIView.as_view(),
5152
name="partner-program-projects",
5253
),
54+
path(
55+
"<int:pk>/export-projects/",
56+
PartnerProgramExportProjectsAPIView.as_view(),
57+
name="partner-program-export-projects",
58+
),
5359
]

0 commit comments

Comments
 (0)