Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 64 additions & 20 deletions partner_programs/admin.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
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,
PartnerProgramMaterial,
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")


@admin.register(PartnerProgram)
class PartnerProgramAdmin(admin.ModelAdmin):
inlines = [PartnerProgramMaterialInline]
list_display = ("id", "name", "tag", "city", "datetime_created")
list_display_links = (
"id",
Expand All @@ -32,7 +45,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 +67,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 +160,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 +170,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 +198,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 +225,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 +266,23 @@ 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 = "Файл"
72 changes: 72 additions & 0 deletions partner_programs/migrations/0007_partnerprogrammaterial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Generated by Django 4.2.11 on 2025-07-21 09:55

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("files", "0007_auto_20230929_1727"),
("partner_programs", "0006_partnerprogram_projects_availability"),
]

operations = [
migrations.CreateModel(
name="PartnerProgramMaterial",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"title",
models.CharField(
help_text="Например, 'Кейс Сбера'",
max_length=255,
verbose_name="Название материала",
),
),
(
"url",
models.URLField(
blank=True,
help_text="Укажите ссылку вручную или прикрепите файл",
null=True,
verbose_name="Ссылка на материал",
),
),
("datetime_created", models.DateTimeField(auto_now_add=True)),
("datetime_updated", models.DateTimeField(auto_now=True)),
(
"file",
models.ForeignKey(
blank=True,
help_text="Если указан файл, ссылка берётся из него",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="files.userfile",
verbose_name="Файл",
),
),
(
"program",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="materials",
to="partner_programs.partnerprogram",
verbose_name="Программа",
),
),
],
options={
"verbose_name": "Материал программы",
"verbose_name_plural": "Материалы программ",
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.2.11 on 2025-07-22 08:44

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("partner_programs", "0007_partnerprogrammaterial"),
]

operations = [
migrations.AddField(
model_name="partnerprogram",
name="managers",
field=models.ManyToManyField(
blank=True,
help_text="Пользователи, имеющие право создавать и редактировать новости",
related_name="managed_partner_programs",
to=settings.AUTH_USER_MODEL,
verbose_name="Менеджеры программы",
),
),
migrations.AlterField(
model_name="partnerprogrammaterial",
name="title",
field=models.CharField(
help_text="Укажите текст для гиперссылки",
max_length=255,
verbose_name="Название материала",
),
),
]
Loading