Перейти к содержанию

Администрирование

Чтобы настроить админ-панель, перейдём в файл admin.py в наших приложениях.

Далее необходимо зарегистрировать все модели. Чтобы дополнительно их настроить, создадим новые классы, наследуемые от admin.ModelAdmin и повесим на них декоратор @admin.register(НазваниеМодели). Внутри классов переопределим следующие атрибуты:

  • list_display - то, что будет отображаться в табличном виде;
  • list_filter - фильтры в таблице;
  • search_fields - по каким полям искать нужные строчки;
  • readonly_fields - "самописная" информация, не зависящая от конкретного поля. Будет отображаться в "карточке" объекта.

С помощью декоратора @admin.display можно создать новые "поля" в табличном представлении модели или в "карточках" объектов.

Доступ к полям "соединённых" таблиц можно получить, используя двойное подчёркивание (__).

Адаптеры
from django.contrib import admin

from apps.adapter.models import Adapter


@admin.register(Adapter)
class AdapterAdmin(admin.ModelAdmin):
    list_display = ["get_full_name"]

    @admin.display(description="Фамилия и имя")
    def get_full_name(self, obj: Adapter) -> str:
        return f"{obj.last_name} {obj.first_name}"

Для этой модели в табличном виде будем выводить только фамилию и имя, остальные параметры оставим по умолчанию.

Список адаптеров
Список адаптеров

Пользователи
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User

from apps.oidc.models import ItmoIdProfile

class ItmoIdProfileInline(admin.StackedInline):
    model = ItmoIdProfile
    can_delete = True
    verbose_name = "профиль ITMO.ID"
    verbose_name_plural = "профили ITMO.ID"


class UserAdmin(BaseUserAdmin):
    list_display = ["username", "get_full_name", "get_isu", "get_course", "get_faculty", "get_group"]
    list_filter = ["profile__course", "profile__faculty"]
    search_fields = ["username", "last_name", "first_name", "profile__isu"]
    inlines = [ItmoIdProfileInline]

    @admin.display(description="Фамилия и имя", ordering="last_name")
    def get_full_name(self, obj: User) -> str:
        return f"{obj.last_name} {obj.first_name}"

    @admin.display(description="ИСУ")
    def get_isu(self, obj: User) -> int:
        return obj.profile.isu

    @admin.display(description="Курс")
    def get_course(self, obj: User) -> int:
        return obj.profile.course

    @admin.display(description="Факультет")
    def get_faculty(self, obj: User) -> str:
        return obj.profile.faculty

    @admin.display(description="Группа")
    def get_group(self, obj: User) -> str:
        return obj.profile.group


admin.site.unregister(User)
admin.site.register(User, UserAdmin)

Для модели ItmoIdProfile создадим класс, наследуемый от admin.StackedInline, чтобы не создавать для неё отдельную таблицу, а выводить данные в модели пользователей. Для это укажем новый класс в атрибуте inlines класса UserAdmin.

Также дополним базовый класс UserAdmin новыми методами для отображения данных профиля в табличном виде, добавим фильтры и поля для поиска. Затем перерегистрируем класс в админ-панели.

Список пользователей
Список пользователей

Информация о пользователе
Информация о пользователе

import csv
from collections import defaultdict

from django.contrib import admin
from django.db.models import Q, Avg, QuerySet
from django.http import HttpResponse
from django.utils.safestring import mark_safe
from requests import Request

from apps.survey.models import SurveyGroup


@admin.register(SurveyGroup)
class SurveyGroupAdmin(admin.ModelAdmin):
    list_display = ["name", "students_count", "get_pass_count"]
    search_fields = ["name"]
    filter_horizontal = ["adapters"]
    readonly_fields = ["get_adapter_scores"]
    actions = ["export_adapter_scores_as_csv"]

    @admin.display(description="Количество прошедших")
    def get_pass_count(self, obj: SurveyGroup) -> int:
        return SurveyAnswer.objects.filter(user__profile__group=obj.name).distinct("user").count()

    @staticmethod
    def convert_score(score: int | float) -> int | float:
        if 0 <= score < 2:
            return 0
        elif 2 <= score < 4:
            return 0.5
        elif 4 <= score <= 5:
            return 1

    @admin.display(description="Баллы адаптеров")
    def get_adapter_scores(self, obj: SurveyGroup) -> str:
        if not obj.students_count:
            return "Не указано количество студентов!"

        pass_rate = (
            SurveyAnswer.objects.filter(user__profile__group=obj.name).distinct("user").count() / obj.students_count
        )
        general_score = 0
        general_scores_by_questions = []

        general_questions = SurveyQuestion.objects.filter(
            ~Q(component__in=[SurveyQuestion.Type.ADAPTER_SLIDER, SurveyQuestion.Type.TEXT_AREA])
        ).all()
        for question in general_questions:
            score = SurveyQuestion.objects.filter(id=question.id, answers__user__profile__group=obj.name).aggregate(
                average_score=Avg("answers__score")
            )["average_score"]
            converted_score = self.convert_score(score)
            general_score += converted_score
            general_scores_by_questions.append(f"{question.get_component_display()}: {converted_score}")

        adapters = obj.adapters.all()
        adapter_scores = defaultdict(float)
        adapter_scores_by_questions = defaultdict(list)

        adapter_questions = SurveyQuestion.objects.filter(component=SurveyQuestion.Type.ADAPTER_SLIDER).all()
        for question in adapter_questions:
            for adapter in adapters:
                score = question.answers.filter(adapter=adapter, user__profile__group=obj.name).aggregate(
                    average_score=Avg("score")
                )["average_score"]
                converted_score = self.convert_score(score)
                adapter_scores[f"{adapter.last_name} {adapter.first_name}"] += converted_score
                adapter_scores_by_questions[f"{adapter.last_name} {adapter.first_name}"].append(
                    f"{question.get_component_display()}: {converted_score}"
                )

        formatted_scores = []
        for adapter, scores in adapter_scores_by_questions.items():
            main_score = (adapter_scores[adapter] + general_score + 1) * pass_rate + pass_rate
            main_criteria = f"Оценка: {main_score}"
            sum_score = f"Сумма: {main_score + adapter_scores[adapter] + general_score}"
            formatted_scores.append(
                f"<b>{adapter}</b>:<br>"
                f"{'<br>'.join([main_criteria] + scores + general_scores_by_questions + [sum_score])}"
            )
        return mark_safe("<br>⎯<br>".join(formatted_scores))

    @admin.action(description="Экспортировать результаты опроса")
    def export_adapter_scores_as_csv(self, request: Request, queryset: QuerySet[SurveyGroup]) -> HttpResponse:
        meta = self.model._meta

        fields = ["Группа", "Количество студентов", "Количество прошедших", "Результат"]
        response = HttpResponse(content_type="text/csv")
        response["Content-Disposition"] = "attachment; filename={}.csv".format(meta)
        writer = csv.DictWriter(response, fieldnames=fields, delimiter=";")
        writer.writeheader()

        for group in queryset:
            pass_count = SurveyAnswer.objects.filter(user__profile__group=group.name).distinct("user").count()
            row = {
                "Группа": group.name,
                "Количество студентов": group.students_count,
                "Количество прошедших": pass_count,
                "Результат": self.get_adapter_scores(group).replace("<br>", "\n"),
            }
            writer.writerow(row)

        return response

Для этой модели была реализован метод для readonly_fields, который подсчитывает и выводит баллы за каждый вопрос, переведённые по соответствующим критериям, для каждого адаптера группы.

Этот метод также вызывается в другом методе, который отвечает за экспорт данных в CSV. Далее этот метод используется как action в админ-панели.

Список групп
Список групп

Информация о группе
Информация о группе

from django.contrib import admin

from apps.survey.models import SurveyFaculty


@admin.register(SurveyFaculty)
class SurveyFacultyAdmin(admin.ModelAdmin):
    list_display = ["name", "is_active"]
    list_filter = ["is_active"]
    search_fields = ["name"]
    list_editable = ["is_active"]

Список факультетов
Список факультетов

from django.contrib import admin

from apps.survey.models import SurveyQuestion


@admin.register(SurveyQuestion)
class SurveyQuestionAdmin(admin.ModelAdmin):
    list_display = ["component", "text", "order"]
    ordering = ["order"]
    list_filter = ["component"]
    search_fields = ["text", "help_text"]

Список вопросов
Список вопросов

Информация о вопросе
Информация о вопросе

from django.contrib import admin

from apps.survey.models import SurveyAnswer


@admin.register(SurveyAnswer)
class SurveyAnswerAdmin(admin.ModelAdmin):
    list_display = ["get_type", "get_text", "value", "score"]
    list_filter = ["question__component", "score"]
    search_fields = ["question__text", "question__help_text", "value"]

    @admin.display(ordering="question__component", description="Тип виджета")
    def get_type(self, obj: SurveyAnswer) -> str:
        return obj.question.get_component_display()

    @admin.display(ordering="question__text", description="Вопрос")
    def get_text(self, obj: SurveyAnswer) -> str:
        return obj.question.text

Список ответов
Список ответов

Информация об ответе
Информация об ответе