Администрирование
Чтобы настроить админ-панель, перейдём в файл 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 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