Skip to content

Commit e2f34d6

Browse files
committed
Реализована возможность добавлять партнёрские материалы: файлы, ссылки, название гиперссылки
1 parent 10191ea commit e2f34d6

4 files changed

Lines changed: 222 additions & 23 deletions

File tree

partner_programs/admin.py

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
1-
import tablib
21
import re
32
import urllib.parse
3+
4+
import tablib
45
from django.contrib import admin
56
from django.db.models import QuerySet
6-
from django.http import HttpResponse, HttpRequest
7+
from django.http import HttpRequest, HttpResponse
78
from django.urls import path
89
from django.utils import timezone
910

10-
from mailing.views import MailingTemplateRender
1111
from core.utils import XlsxFileToExport
12-
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
12+
from mailing.views import MailingTemplateRender
13+
from partner_programs.models import (
14+
PartnerProgram,
15+
PartnerProgramMaterial,
16+
PartnerProgramUserProfile,
17+
)
18+
from partner_programs.services import ProjectScoreDataPreparer
1319
from project_rates.models import Criteria, ProjectScore
1420
from projects.models import Project
15-
from partner_programs.services import ProjectScoreDataPreparer
21+
22+
23+
class PartnerProgramMaterialInline(admin.StackedInline):
24+
model = PartnerProgramMaterial
25+
extra = 1
26+
fields = ("title", "url", "file")
27+
readonly_fields = ("datetime_created", "datetime_updated")
1628

1729

1830
@admin.register(PartnerProgram)
1931
class PartnerProgramAdmin(admin.ModelAdmin):
32+
inlines = [PartnerProgramMaterialInline]
2033
list_display = ("id", "name", "tag", "city", "datetime_created")
2134
list_display_links = (
2235
"id",
@@ -54,7 +67,9 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
5467
"partner_programs/admin/program_manager_change_form.html"
5568
)
5669
else:
57-
self.change_form_template = "partner_programs/admin/programs_change_form.html"
70+
self.change_form_template = (
71+
"partner_programs/admin/programs_change_form.html"
72+
)
5873

5974
return super().change_view(request, object_id, form_url, extra_context)
6075

@@ -145,7 +160,7 @@ def get_export_file(self, partner_program: PartnerProgram):
145160

146161
binary_data = response_data.export("xlsx")
147162
file_name = (
148-
f'{partner_program.name} {timezone.now().strftime("%d-%m-%Y %H:%M:%S")}'
163+
f"{partner_program.name} {timezone.now().strftime('%d-%m-%Y %H:%M:%S')}"
149164
)
150165
response = HttpResponse(
151166
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@@ -155,22 +170,26 @@ def get_export_file(self, partner_program: PartnerProgram):
155170
return response
156171

157172
def get_export_rates_view(self, request, object_id):
158-
rates_data_to_write: list[dict] = self._get_prepared_rates_data_for_export(object_id)
173+
rates_data_to_write: list[dict] = self._get_prepared_rates_data_for_export(
174+
object_id
175+
)
159176

160177
xlsx_file_writer = XlsxFileToExport()
161178
xlsx_file_writer.write_data_to_xlsx(rates_data_to_write)
162179
binary_data_to_export: bytes = xlsx_file_writer.get_binary_data_from_self_file()
163180
xlsx_file_writer.delete_self_xlsx_file_from_local_machine()
164181

165182
encoded_file_name: str = urllib.parse.quote(
166-
f'{PartnerProgram.objects.get(pk=object_id).name}_оценки {timezone.now().strftime("%d-%m-%Y %H:%M:%S")}'
183+
f"{PartnerProgram.objects.get(pk=object_id).name}_оценки {timezone.now().strftime('%d-%m-%Y %H:%M:%S')}"
167184
f".xlsx"
168185
)
169186
response = HttpResponse(
170187
binary_data_to_export,
171188
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
172189
)
173-
response["Content-Disposition"] = f'attachment; filename*=UTF-8\'\'{encoded_file_name}'
190+
response["Content-Disposition"] = (
191+
f"attachment; filename*=UTF-8''{encoded_file_name}"
192+
)
174193
return response
175194

176195
def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]:
@@ -179,19 +198,22 @@ def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]:
179198
Columns example:
180199
ФИО|Email|Регион_РФ|Учебное_заведение|Название_учебного_заведения|Класс_курс|Фамилия эксперта|**criteria
181200
"""
182-
criterias = Criteria.objects.filter(partner_program__id=program_id).select_related("partner_program")
201+
criterias = Criteria.objects.filter(
202+
partner_program__id=program_id
203+
).select_related("partner_program")
183204
scores = (
184-
ProjectScore.objects
185-
.filter(criteria__in=criterias)
205+
ProjectScore.objects.filter(criteria__in=criterias)
186206
.select_related("user", "criteria", "project")
187207
.order_by("project", "criteria")
188208
)
189-
user_programm_profiles = (
190-
PartnerProgramUserProfile.objects
191-
.filter(partner_program__id=program_id)
192-
.select_related("user")
209+
user_programm_profiles = PartnerProgramUserProfile.objects.filter(
210+
partner_program__id=program_id
211+
).select_related("user")
212+
projects = (
213+
Project.objects.filter(scores__in=scores)
214+
.select_related("leader")
215+
.distinct()
193216
)
194-
projects = Project.objects.filter(scores__in=scores).select_related("leader").distinct()
195217

196218
# To reduce the number of DB requests.
197219
user_profiles_dict: dict[int, PartnerProgramUserProfile] = {
@@ -203,7 +225,9 @@ def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]:
203225

204226
prepared_projects_rates_data: list[dict] = []
205227
for project in projects:
206-
project_data_preparer = ProjectScoreDataPreparer(user_profiles_dict, scores_dict, project.id, program_id)
228+
project_data_preparer = ProjectScoreDataPreparer(
229+
user_profiles_dict, scores_dict, project.id, program_id
230+
)
207231
full_project_rates_data: dict = {
208232
**project_data_preparer.get_project_user_info(),
209233
**project_data_preparer.get_project_expert_info(),
@@ -242,3 +266,23 @@ def get_form(self, request, obj=None, **kwargs):
242266
form = super().get_form(request, obj, **kwargs)
243267
form.base_fields["project"].required = False
244268
return form
269+
270+
271+
@admin.register(PartnerProgramMaterial)
272+
class PartnerProgramMaterialAdmin(admin.ModelAdmin):
273+
list_display = ("title", "program", "short_url", "has_file", "datetime_created")
274+
list_filter = ("program",)
275+
search_fields = ("title", "program__name")
276+
277+
readonly_fields = ("datetime_created", "datetime_updated")
278+
279+
def short_url(self, obj):
280+
return obj.url[:60] if obj.url else "—"
281+
282+
short_url.short_description = "Ссылка"
283+
284+
def has_file(self, obj):
285+
return bool(obj.file)
286+
287+
has_file.boolean = True
288+
has_file.short_description = "Файл"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Generated by Django 4.2.11 on 2025-07-21 09:55
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("files", "0007_auto_20230929_1727"),
11+
("partner_programs", "0006_partnerprogram_projects_availability"),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="PartnerProgramMaterial",
17+
fields=[
18+
(
19+
"id",
20+
models.BigAutoField(
21+
auto_created=True,
22+
primary_key=True,
23+
serialize=False,
24+
verbose_name="ID",
25+
),
26+
),
27+
(
28+
"title",
29+
models.CharField(
30+
help_text="Например, 'Кейс Сбера'",
31+
max_length=255,
32+
verbose_name="Название материала",
33+
),
34+
),
35+
(
36+
"url",
37+
models.URLField(
38+
blank=True,
39+
help_text="Укажите ссылку вручную или прикрепите файл",
40+
null=True,
41+
verbose_name="Ссылка на материал",
42+
),
43+
),
44+
("datetime_created", models.DateTimeField(auto_now_add=True)),
45+
("datetime_updated", models.DateTimeField(auto_now=True)),
46+
(
47+
"file",
48+
models.ForeignKey(
49+
blank=True,
50+
help_text="Если указан файл, ссылка берётся из него",
51+
null=True,
52+
on_delete=django.db.models.deletion.SET_NULL,
53+
to="files.userfile",
54+
verbose_name="Файл",
55+
),
56+
),
57+
(
58+
"program",
59+
models.ForeignKey(
60+
on_delete=django.db.models.deletion.CASCADE,
61+
related_name="materials",
62+
to="partner_programs.partnerprogram",
63+
verbose_name="Программа",
64+
),
65+
),
66+
],
67+
options={
68+
"verbose_name": "Материал программы",
69+
"verbose_name_plural": "Материалы программ",
70+
},
71+
),
72+
]

partner_programs/models.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.contrib.auth import get_user_model
2+
from django.core.exceptions import ValidationError
23
from django.db import models
34

5+
from files.models import UserFile
46
from partner_programs.constants import get_default_data_schema
57
from projects.models import Project
68

@@ -26,6 +28,7 @@ class PartnerProgram(models.Model):
2628
datetime_created: A DateTimeField indicating date of creation.
2729
datetime_updated: A DateTimeField indicating date of update.
2830
"""
31+
2932
PROJECTS_AVAILABILITY_CHOISES = [
3033
("all_users", "Всем пользователям"),
3134
("experts_only", "Только экспертам"),
@@ -96,7 +99,9 @@ class PartnerProgram(models.Model):
9699
datetime_created = models.DateTimeField(
97100
verbose_name="Дата создания", auto_now_add=True
98101
)
99-
datetime_updated = models.DateTimeField(verbose_name="Дата изменения", auto_now=True)
102+
datetime_updated = models.DateTimeField(
103+
verbose_name="Дата изменения", auto_now=True
104+
)
100105

101106
class Meta:
102107
verbose_name = "Программа"
@@ -148,3 +153,62 @@ class Meta:
148153

149154
def __str__(self):
150155
return f"PartnerProgramUserProfile<{self.pk}> - {self.user} {self.project} {self.partner_program}"
156+
157+
158+
class PartnerProgramMaterial(models.Model):
159+
"""
160+
Материал для программы: прямая ссылка или ссылка на прикреплённый файл.
161+
"""
162+
163+
program = models.ForeignKey(
164+
PartnerProgram,
165+
on_delete=models.CASCADE,
166+
related_name="materials",
167+
verbose_name="Программа",
168+
)
169+
170+
title = models.CharField(
171+
max_length=255,
172+
verbose_name="Название материала",
173+
help_text="Укажите текст для гиперссылки",
174+
)
175+
176+
url = models.URLField(
177+
blank=True,
178+
null=True,
179+
verbose_name="Ссылка на материал",
180+
help_text="Укажите ссылку вручную или прикрепите файл",
181+
)
182+
183+
file = models.ForeignKey(
184+
UserFile,
185+
on_delete=models.SET_NULL,
186+
blank=True,
187+
null=True,
188+
verbose_name="Файл",
189+
help_text="Если указан файл, ссылка берётся из него",
190+
)
191+
192+
datetime_created = models.DateTimeField(auto_now_add=True)
193+
datetime_updated = models.DateTimeField(auto_now=True)
194+
195+
class Meta:
196+
verbose_name = "Материал программы"
197+
verbose_name_plural = "Материалы программ"
198+
199+
def clean(self):
200+
if self.file and not self.url:
201+
self.url = self.file.link
202+
203+
if not self.file and not self.url:
204+
raise ValidationError("Необходимо указать либо файл, либо ссылку.")
205+
206+
if self.file and self.url and self.url != self.file.link:
207+
raise ValidationError("Укажите либо файл, либо ссылку, но не оба сразу.")
208+
209+
def save(self, *args, **kwargs):
210+
self.full_clean()
211+
super().save(*args, **kwargs)
212+
213+
def __str__(self):
214+
return f"{self.title} для программы {self.program.name}"

partner_programs/serializers.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rest_framework import serializers
33

44
from core.services import get_likes_count, get_links, get_views_count, is_fan
5-
from partner_programs.models import PartnerProgram
5+
from partner_programs.models import PartnerProgram, PartnerProgramMaterial
66

77
User = get_user_model()
88

@@ -49,7 +49,18 @@ class Meta:
4949
)
5050

5151

52-
class PartnerProgramForMemberSerializer(serializers.ModelSerializer):
52+
class PartnerProgramWithMaterialsMixin(serializers.ModelSerializer):
53+
materials = serializers.SerializerMethodField()
54+
55+
def get_materials(self, program):
56+
materials = program.materials.all()
57+
return PartnerProgramMaterialSerializer(materials, many=True).data
58+
59+
class Meta:
60+
abstract = True
61+
62+
63+
class PartnerProgramForMemberSerializer(PartnerProgramWithMaterialsMixin):
5364
"""Serializer for PartnerProgram model for member of this program"""
5465

5566
views_count = serializers.SerializerMethodField(method_name="count_views")
@@ -79,6 +90,7 @@ class Meta:
7990
"description",
8091
"city",
8192
"links",
93+
"materials",
8294
"image_address",
8395
"cover_image_address",
8496
"presentation_address",
@@ -87,7 +99,7 @@ class Meta:
8799
)
88100

89101

90-
class PartnerProgramForUnregisteredUserSerializer(serializers.ModelSerializer):
102+
class PartnerProgramForUnregisteredUserSerializer(PartnerProgramWithMaterialsMixin):
91103
"""Serializer for PartnerProgram model for unregistered users in the program"""
92104

93105
class Meta:
@@ -97,6 +109,7 @@ class Meta:
97109
"name",
98110
"tag",
99111
"city",
112+
"materials",
100113
"image_address",
101114
"cover_image_address",
102115
"advertisement_image_address",
@@ -138,3 +151,9 @@ class Meta:
138151
"name",
139152
"tag",
140153
]
154+
155+
156+
class PartnerProgramMaterialSerializer(serializers.ModelSerializer):
157+
class Meta:
158+
model = PartnerProgramMaterial
159+
fields = ("title", "url")

0 commit comments

Comments
 (0)