Skip to content
Merged

Dev #537

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e2f34d6
Реализована возможность добавлять партнёрские материалы: файлы, ссылк…
Toksi86 Jul 22, 2025
6b7a2f3
Расширена логика создания новостей: добавлена поддержка публикаций дл…
Toksi86 Jul 22, 2025
cc304fe
Merge pull request #530 from PROCOLLAB-github/feature/project-mospoly…
Toksi86 Jul 22, 2025
c12b3cf
Добавлена связка проектов и программ через промежуточную модель; Доба…
Toksi86 Jul 30, 2025
42830e6
Убран неиспользуемый импорт
Toksi86 Jul 30, 2025
77d1883
Merge pull request #531 from PROCOLLAB-github/feature/project-mospoly…
Toksi86 Jul 30, 2025
44154f5
Расширена фильтрация проектов по участию в программах в связи с новой…
Toksi86 Jul 31, 2025
c701dd1
Merge pull request #532 from PROCOLLAB-github/feature/project-mospoly…
Toksi86 Jul 31, 2025
d12cd44
Доработана валидация данных дополнительных полей программ
Toksi86 Aug 4, 2025
9fd489d
Merge pull request #533 from PROCOLLAB-github/feature/project-mospoly…
Toksi86 Aug 4, 2025
a8cc117
Реализована возможность помечать программы как конкурсные, и сдавать …
Toksi86 Aug 11, 2025
105dcd9
Merge pull request #534 from PROCOLLAB-github/feature/project-mospoly…
Toksi86 Aug 11, 2025
66f532d
Изменена схема выдачи детальной информации о проекте, произведена опт…
Toksi86 Aug 11, 2025
e9f0778
Merge pull request #535 from PROCOLLAB-github/feature/project-mospoly…
Toksi86 Aug 11, 2025
a42e9a0
Добавлена фильтрация по дополнительным полям программ
Toksi86 Aug 12, 2025
1897442
Merge pull request #536 from PROCOLLAB-github/feature/project-mospoly…
Toksi86 Aug 12, 2025
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
60 changes: 25 additions & 35 deletions news/permissions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404
from rest_framework import permissions
from rest_framework.exceptions import NotFound
from rest_framework.permissions import SAFE_METHODS

from partner_programs.models import PartnerProgram
Expand All @@ -10,47 +10,37 @@


class IsNewsCreatorOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
"""
read/update/delete permission
currently can only be updated/deleted in admin panel
"""
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
if (
isinstance(obj.content_object, Project)
and obj.content_object.leader == request.user
):
return True
if isinstance(obj.content_object, User) and obj.content_object == request.user:
return True
if isinstance(obj.content_object, PartnerProgram):
# TODO: implement
pass

if view.kwargs.get("project_pk"):
project = get_object_or_404(Project, pk=view.kwargs["project_pk"])
return request.user == project.leader

if view.kwargs.get("user_pk"):
user = get_object_or_404(User, pk=view.kwargs["user_pk"])
return request.user == user

if view.kwargs.get("partnerprogram_pk"):
program = get_object_or_404(
PartnerProgram, pk=view.kwargs["partnerprogram_pk"]
)
return program.is_manager(request.user)

return False

def has_permission(self, request, view):
"""
Creation permission
Currently can only be created via admin panel
"""
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True

if view.kwargs.get("project_pk"):
try:
project = Project.objects.get(pk=view.kwargs["project_pk"])
if request.method in SAFE_METHODS or (request.user == project.leader):
return True
except Project.DoesNotExist:
raise NotFound
if isinstance(obj.content_object, Project):
return obj.content_object.leader == request.user

if view.kwargs.get("user_pk"):
try:
user = User.objects.get(pk=view.kwargs["user_pk"])
if request.method in SAFE_METHODS or (request.user == user):
return True
except User.DoesNotExist:
raise NotFound
if isinstance(obj.content_object, User):
return obj.content_object == request.user

if isinstance(obj.content_object, PartnerProgram):
return obj.content_object.is_manager(request.user)

return False
14 changes: 10 additions & 4 deletions news/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.response import Response

from core.serializers import SetViewedSerializer, SetLikedSerializer
from core.serializers import SetLikedSerializer, SetViewedSerializer
from core.services import add_view, set_like
from news.mixins import NewsQuerysetMixin
from news.models import News
from news.pagination import NewsPagination
from news.permissions import IsNewsCreatorOrReadOnly
from news.serializers import (
NewsListSerializer,
NewsDetailSerializer,
NewsListCreateSerializer,
NewsListSerializer,
)
from partner_programs.models import PartnerProgram
from projects.models import Project

User = get_user_model()
Expand Down Expand Up @@ -44,7 +45,12 @@ def post(self, request: Request, *args, **kwargs) -> Response:
NewsDetailSerializer(news).data, status=status.HTTP_201_CREATED
)

# creating partner program news, not implemented yet, return 400
if kwargs.get("partnerprogram_pk"):
program = get_object_or_404(PartnerProgram, pk=kwargs["partnerprogram_pk"])
news = News.objects.add_news(program, **data)
return Response(
NewsDetailSerializer(news).data, status=status.HTTP_201_CREATED
)
return Response(status=status.HTTP_400_BAD_REQUEST)

def get(self, request: Request, *args, **kwargs) -> Response:
Expand Down
135 changes: 115 additions & 20 deletions partner_programs/admin.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
import tablib
import re
import urllib.parse

import tablib
from django.contrib import admin
from django.db.models import QuerySet
from django.http import HttpResponse, HttpRequest
from django.http import HttpRequest, HttpResponse
from django.urls import path
from django.utils import timezone

from mailing.views import MailingTemplateRender
from core.utils import XlsxFileToExport
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
from mailing.views import MailingTemplateRender
from partner_programs.models import (
PartnerProgram,
PartnerProgramField,
PartnerProgramFieldValue,
PartnerProgramMaterial,
PartnerProgramProject,
PartnerProgramUserProfile,
)
from partner_programs.services import ProjectScoreDataPreparer
from project_rates.models import Criteria, ProjectScore
from projects.models import Project
from partner_programs.services import ProjectScoreDataPreparer


class PartnerProgramMaterialInline(admin.StackedInline):
model = PartnerProgramMaterial
extra = 1
fields = ("title", "url", "file")
readonly_fields = ("datetime_created", "datetime_updated")


class PartnerProgramFieldInline(admin.TabularInline):
model = PartnerProgramField
extra = 0


@admin.register(PartnerProgram)
class PartnerProgramAdmin(admin.ModelAdmin):
inlines = [PartnerProgramMaterialInline, PartnerProgramFieldInline]
list_display = ("id", "name", "tag", "city", "datetime_created")
list_display_links = (
"id",
Expand All @@ -32,7 +53,7 @@ class PartnerProgramAdmin(admin.ModelAdmin):
)
list_filter = ("city",)

filter_horizontal = ("users",)
filter_horizontal = ("users", "managers")
date_hierarchy = "datetime_started"

def get_queryset(self, request: HttpRequest) -> QuerySet[PartnerProgram]:
Expand All @@ -54,7 +75,9 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
"partner_programs/admin/program_manager_change_form.html"
)
else:
self.change_form_template = "partner_programs/admin/programs_change_form.html"
self.change_form_template = (
"partner_programs/admin/programs_change_form.html"
)

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

Expand Down Expand Up @@ -145,7 +168,7 @@ def get_export_file(self, partner_program: PartnerProgram):

binary_data = response_data.export("xlsx")
file_name = (
f'{partner_program.name} {timezone.now().strftime("%d-%m-%Y %H:%M:%S")}'
f"{partner_program.name} {timezone.now().strftime('%d-%m-%Y %H:%M:%S')}"
)
response = HttpResponse(
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Expand All @@ -155,22 +178,26 @@ def get_export_file(self, partner_program: PartnerProgram):
return response

def get_export_rates_view(self, request, object_id):
rates_data_to_write: list[dict] = self._get_prepared_rates_data_for_export(object_id)
rates_data_to_write: list[dict] = self._get_prepared_rates_data_for_export(
object_id
)

xlsx_file_writer = XlsxFileToExport()
xlsx_file_writer.write_data_to_xlsx(rates_data_to_write)
binary_data_to_export: bytes = xlsx_file_writer.get_binary_data_from_self_file()
xlsx_file_writer.delete_self_xlsx_file_from_local_machine()

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

def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]:
Expand All @@ -179,19 +206,22 @@ def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]:
Columns example:
ФИО|Email|Регион_РФ|Учебное_заведение|Название_учебного_заведения|Класс_курс|Фамилия эксперта|**criteria
"""
criterias = Criteria.objects.filter(partner_program__id=program_id).select_related("partner_program")
criterias = Criteria.objects.filter(
partner_program__id=program_id
).select_related("partner_program")
scores = (
ProjectScore.objects
.filter(criteria__in=criterias)
ProjectScore.objects.filter(criteria__in=criterias)
.select_related("user", "criteria", "project")
.order_by("project", "criteria")
)
user_programm_profiles = (
PartnerProgramUserProfile.objects
.filter(partner_program__id=program_id)
.select_related("user")
user_programm_profiles = PartnerProgramUserProfile.objects.filter(
partner_program__id=program_id
).select_related("user")
projects = (
Project.objects.filter(scores__in=scores)
.select_related("leader")
.distinct()
)
projects = Project.objects.filter(scores__in=scores).select_related("leader").distinct()

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

prepared_projects_rates_data: list[dict] = []
for project in projects:
project_data_preparer = ProjectScoreDataPreparer(user_profiles_dict, scores_dict, project.id, program_id)
project_data_preparer = ProjectScoreDataPreparer(
user_profiles_dict, scores_dict, project.id, program_id
)
full_project_rates_data: dict = {
**project_data_preparer.get_project_user_info(),
**project_data_preparer.get_project_expert_info(),
Expand Down Expand Up @@ -242,3 +274,66 @@ def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
form.base_fields["project"].required = False
return form


@admin.register(PartnerProgramMaterial)
class PartnerProgramMaterialAdmin(admin.ModelAdmin):
list_display = ("title", "program", "short_url", "has_file", "datetime_created")
list_filter = ("program",)
search_fields = ("title", "program__name")

readonly_fields = ("datetime_created", "datetime_updated")

def short_url(self, obj):
return obj.url[:60] if obj.url else "—"

short_url.short_description = "Ссылка"

def has_file(self, obj):
return bool(obj.file)

has_file.boolean = True
has_file.short_description = "Файл"


class PartnerProgramFieldValueInline(admin.TabularInline):
model = PartnerProgramFieldValue
extra = 0
autocomplete_fields = ("field",)
readonly_fields = ("get_display_value",)

def get_display_value(self, obj):
return obj.value_text or "-"

get_display_value.short_description = "Значение"


@admin.register(PartnerProgramProject)
class PartnerProgramProjectAdmin(admin.ModelAdmin):
list_display = (
"id",
"project",
"partner_program",
"datetime_created",
"submitted",
"datetime_submitted",
)
list_filter = ("partner_program",)
search_fields = ("project__name", "partner_program__name")
inlines = [PartnerProgramFieldValueInline]
autocomplete_fields = ("project", "partner_program")


@admin.register(PartnerProgramField)
class PartnerProgramFieldAdmin(admin.ModelAdmin):
list_display = (
"id",
"partner_program",
"name",
"label",
"field_type",
"is_required",
"show_filter",
)
list_filter = ("partner_program",)
search_fields = ("name", "label", "help_text")
Loading