Skip to content

Commit fd239d5

Browse files
committed
Merge remote-tracking branch 'origin/dev' into devops-structure-rework
2 parents 75d0cfd + c33df3b commit fd239d5

75 files changed

Lines changed: 6686 additions & 110 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import logging
22
import io
3+
import urllib.parse
34
import unicodedata
45
import pandas as pd
56

67
from django.core.mail import EmailMultiAlternatives
8+
from django.http import HttpResponse
9+
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
710

811

912
logger = logging.getLogger()
13+
EXCEL_CELL_MAX = 32767
1014

1115

1216
class Email:
@@ -88,3 +92,33 @@ def ascii_filename(filename: str) -> str:
8892
ascii_name = "".join(char if char.isascii() else "_" for char in safe_name)
8993
ascii_name = " ".join(ascii_name.split())
9094
return ascii_name or "export"
95+
96+
97+
def sanitize_excel_value(value):
98+
if value is None:
99+
return ""
100+
if isinstance(value, (int, float, bool)):
101+
return value
102+
103+
text = str(value).replace("\r\n", "\n").replace("\r", "\n")
104+
text = ILLEGAL_CHARACTERS_RE.sub(" ", text)
105+
if len(text) > EXCEL_CELL_MAX:
106+
text = text[: EXCEL_CELL_MAX - 3] + "..."
107+
return text
108+
109+
110+
def build_xlsx_download_response(binary_data: bytes, *, base_name: str) -> HttpResponse:
111+
safe_name = sanitize_filename(base_name)
112+
encoded_file_name = urllib.parse.quote(f"{safe_name}.xlsx")
113+
fallback_filename = f"{ascii_filename(base_name)}.xlsx"
114+
115+
response = HttpResponse(
116+
binary_data,
117+
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
118+
)
119+
response["Content-Disposition"] = (
120+
"attachment; "
121+
f"filename=\"{fallback_filename}\"; "
122+
f"filename*=UTF-8''{encoded_file_name}"
123+
)
124+
return response

courses/__init__.py

Whitespace-only changes.

courses/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from courses.admin_config import * # noqa: F401,F403

courses/admin_config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import answers, content, progress, site # noqa: F401

courses/admin_config/answers.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from django.contrib import admin
2+
3+
from courses.models import UserTaskAnswer, UserTaskAnswerFile, UserTaskAnswerOption
4+
5+
from .inlines import UserTaskAnswerFileInline, UserTaskAnswerOptionInline
6+
7+
8+
@admin.register(UserTaskAnswer)
9+
class UserTaskAnswerAdmin(admin.ModelAdmin):
10+
list_display = (
11+
"id",
12+
"user",
13+
"task",
14+
"status",
15+
"is_correct",
16+
"submitted_at",
17+
"reviewed_by",
18+
"reviewed_at",
19+
)
20+
list_display_links = ("id",)
21+
list_filter = (
22+
"status",
23+
"is_correct",
24+
"task__check_type",
25+
"task__answer_type",
26+
"task__lesson__module__course",
27+
)
28+
search_fields = (
29+
"id",
30+
"user__email",
31+
"user__first_name",
32+
"user__last_name",
33+
"task__title",
34+
)
35+
raw_id_fields = ("user", "task", "reviewed_by")
36+
readonly_fields = ("submitted_at", "datetime_created", "datetime_updated")
37+
list_select_related = (
38+
"user",
39+
"task",
40+
"reviewed_by",
41+
"task__lesson",
42+
"task__lesson__module",
43+
"task__lesson__module__course",
44+
)
45+
inlines = [UserTaskAnswerOptionInline, UserTaskAnswerFileInline]
46+
47+
48+
@admin.register(UserTaskAnswerOption)
49+
class UserTaskAnswerOptionAdmin(admin.ModelAdmin):
50+
list_display = ("id", "answer", "option", "get_user", "get_task")
51+
list_display_links = ("id",)
52+
search_fields = (
53+
"id",
54+
"answer__user__email",
55+
"answer__task__title",
56+
"option__text",
57+
)
58+
raw_id_fields = ("answer", "option")
59+
list_select_related = ("answer", "option", "answer__user", "answer__task")
60+
61+
@admin.display(description="Пользователь", ordering="answer__user")
62+
def get_user(self, obj):
63+
return obj.answer.user
64+
65+
@admin.display(description="Задание", ordering="answer__task")
66+
def get_task(self, obj):
67+
return obj.answer.task
68+
69+
70+
@admin.register(UserTaskAnswerFile)
71+
class UserTaskAnswerFileAdmin(admin.ModelAdmin):
72+
list_display = ("id", "answer", "file", "file_name", "file_size", "datetime_uploaded")
73+
list_display_links = ("id",)
74+
search_fields = ("id", "file_name", "answer__task__title", "answer__user__email")
75+
raw_id_fields = ("answer", "file")
76+
readonly_fields = ("file_name", "file_size", "datetime_uploaded")
77+
list_select_related = ("answer", "file", "answer__user", "answer__task")

0 commit comments

Comments
 (0)