Like/dislike + report under every question and lesson, lesson reports and vote columns in admin; practice islands refresh from file; audit fixes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 12:56:47 +03:30
parent b075b93f2a
commit 2e0753587c
12 changed files with 1331 additions and 951 deletions

View File

@@ -204,6 +204,8 @@ def _sync_questions(rows, ledger, counts):
to_create.append(({**{k: v for k, v in row.items() if k != 'path'}, 'lessonId': lesson_id}, qkey))
existing.add((lesson_id, prompt))
_refresh_practice(rows, lessons, ledger, counts)
if missing:
raise question_io.ImportError_(
'این درس‌ها در برنامه‌ی درسی پیدا نشدند (اول کوریکولوم را وارد کنید): '
@@ -216,3 +218,61 @@ def _sync_questions(rows, ledger, counts):
counts['created'] += result['created']
for _row, qkey in to_create:
ledger.record(qkey, 'question')
PRACTICE_PREFIX = 'تمرین: '
def _refresh_practice(rows, lessons, ledger, counts):
"""
جزیره‌های «تمرین: …» را فایل می‌سازد و ادمین سوال‌هایش را کم‌وبیش دست نمی‌زند؛ پس اصلاحِ فایل
(گزینه‌ی هم‌ارزِ جواب، جوابِ غلط) باید به سوالِ موجود برسد، وگرنه سید فقط «سوالِ تازه» می‌سازد و نسخه‌ی
معیوب می‌ماند:
- همان صورت، گزینه‌ها/پاسخِ درست فرق دارد → گزینه‌ها جا به جا (همان idها) به‌روز می‌شود تا پاسخ‌های
ثبت‌شده نشکنند؛
- سوالی که خودِ سید قبلاً تحویل داده (دفتر) و حالا در فایل نیست و کسی جوابش نداده → پاک می‌شود.
سوالِ دستیِ ادمین (در دفتر نیست) هرگز دست نمی‌خورد. آزموده: content/tests_seed_content.py.
"""
practice_lessons = {
lid for path, lid in lessons.items() if path[2].startswith(PRACTICE_PREFIX)
}
if not practice_lessons:
return
want = {}
file_keys = set()
for row in rows:
path = tuple(str(p).strip() for p in row.get('path', []))
if len(path) != 4 or not path[2].startswith(PRACTICE_PREFIX):
continue
lid = lessons.get(path)
prompt = (row.get('prompt') or '').strip()
if lid is None:
continue
want[(lid, prompt)] = row
file_keys.add(_key('question', *path, prompt))
by_id = {}
for lid, path in ((v, k) for k, v in lessons.items() if v in practice_lessons):
by_id[lid] = path
qs = Question.objects.filter(lesson_id__in=practice_lessons).prefetch_related('choices')
stale = []
for q in qs.iterator(chunk_size=500):
row = want.get((q.lesson_id, q.prompt))
if row is None:
if _key('question', *by_id[q.lesson_id], q.prompt) in ledger.known:
stale.append(q.pk)
continue
new_choices = [str(c).strip() for c in row['choices']]
old = sorted(q.choices.all(), key=lambda c: c.pk)
if len(old) != len(new_choices):
continue
for index, choice in enumerate(old):
value, correct = new_choices[index], index == row['correct']
if choice.value != value or choice.is_correct != correct:
choice.value, choice.is_correct = value, correct
choice.save(update_fields=['value', 'is_correct'])
counts['updated'] += 1
if stale:
removable = list(Question.objects.filter(pk__in=stale, answers__isnull=True).values_list('pk', flat=True))
if removable:
Question.objects.filter(pk__in=removable).delete()
counts['retired'] += len(removable)