Figures must not give the answer away: blank titles on new figured questions, 7 trivial questions removed, 7 reworded; apply_fixups tool
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -330,6 +330,8 @@ has been done before.
|
|||||||
time; this rule is the mechanism, so new figures need no other switch. Write them with
|
time; this rule is the mechanism, so new figures need no other switch. Write them with
|
||||||
`tools/figures/batch.py` (`docs/content-authoring.md` «سوالِ شکلدار»). The lesson-level list
|
`tools/figures/batch.py` (`docs/content-authoring.md` «سوالِ شکلدار»). The lesson-level list
|
||||||
(`/api/lessons/{id}/questions`) does not reorder; only the island quiz does.
|
(`/api/lessons/{id}/questions`) does not reorder; only the island quiz does.
|
||||||
|
**A figure must not give the answer away** (title, labels, or drawing the thing the question asks for); the
|
||||||
|
rules and the fix-up tool (`tools/figures/apply_fixups.py`) are in `docs/content-authoring.md`.
|
||||||
- `POST /api/nodes/{id}/quiz/complete` completes the island at any score (score sets stars/XP) and returns
|
- `POST /api/nodes/{id}/quiz/complete` completes the island at any score (score sets stars/XP) and returns
|
||||||
`nextNodeId`. It refuses a locked island (403 `node_locked`, same rule as `/api/home`) and a user who has
|
`nextNodeId`. It refuses a locked island (403 `node_locked`, same rule as `/api/home`) and a user who has
|
||||||
not answered every question (409 `answers_missing`; required = `max(client totalQuestions, min(4, questions
|
not answered every question (409 `answers_missing`; required = `max(client totalQuestions, min(4, questions
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -161,3 +161,22 @@ python tools/figures/batch.py merge batch.json # افزود
|
|||||||
چند شکل در یک صفحه، به `renderToString` با `identifierPrefix` یکتا برای هر شکل نیاز است، وگرنه `useId` در همهی شکلها
|
چند شکل در یک صفحه، به `renderToString` با `identifierPrefix` یکتا برای هر شکل نیاز است، وگرنه `useId` در همهی شکلها
|
||||||
یکی میشود و clipPathها روی هم میافتند و منحنیها نصفه دیده میشوند (خطای ابزارِ آزمایش، نه اپ).
|
یکی میشود و clipPathها روی هم میافتند و منحنیها نصفه دیده میشوند (خطای ابزارِ آزمایش، نه اپ).
|
||||||
فضای سهبعدی: `{"kind":"space",…}` با نقطه، بردار، پارهخط، چندضلعی (صفحه)، جعبه و خطچینهای تصویر؛ تصویرِ مایلِ ثابت.
|
فضای سهبعدی: `{"kind":"space",…}` با نقطه، بردار، پارهخط، چندضلعی (صفحه)، جعبه و خطچینهای تصویر؛ تصویرِ مایلِ ثابت.
|
||||||
|
|
||||||
|
### شکل نباید جواب را لو بدهد
|
||||||
|
|
||||||
|
گزارشِ مالک (۲۰۲۶-۱۰-۱۵): سوالِ «نقطهی A(۰، ۵، ۰) روی کدام محور است؟» شکلی داشت که نقطه را با همان مختصات روی محور
|
||||||
|
نشان میداد؛ شکل جواب را میداد. قاعدهها برای ساختنِ هر سوالِ شکلدار:
|
||||||
|
|
||||||
|
1. **متنِ شکل** (`title`، برچسبها، `label`) جواب یا چیزی مساوی با آن نباشد. عنوان را خالی بگذارید یا کلی («شکل»)؛
|
||||||
|
عنوانِ «هرم با قاعدهی مربع» زیرِ سوالِ «مقطع چیست؟» و «دو دایرهی مماس» زیرِ «چند نقطهی مشترک؟» جواب بود.
|
||||||
|
2. **چیزی را که سوال میپرسد نکشید.** اگر متن همهی داده را داده (معادله، مختصات) و شکل نتیجه را هم نشان میدهد،
|
||||||
|
شکل کارِ دانشآموز را میکند. یا داده را فقط در شکل بگذارید (و متن آن را نگوید)، یا شکل را برنداشته سوال را بیشکل بگذارید.
|
||||||
|
مثالهای بد: نقطهای که «روی دایره است یا نه» و روی دایره کشیده شده؛ «لوزی با یک زاویهی قائمه چه نام دارد» با مربع کشیدهشده؛
|
||||||
|
خطی که «وضعیتش نسبت به دایره» پرسیده میشود و مماس کشیده شده؛ «نمودار x = ۱ چیست» با همان خط.
|
||||||
|
3. **مقدار را روی مماس، `drop` و ابزارهایی که خودشان عدد مینویسند نگذارید** (ابزارِ `tangent` «شیب = …» را چاپ میکند؛
|
||||||
|
مماس را با معادلهی خط بکشید).
|
||||||
|
4. هر سوالِ شکلدار را قبل از ادغام یک بار «بهجای دانشآموز» بخوانید: اگر فقط با نگاه به شکل، بیهیچ فکر، جواب میشود،
|
||||||
|
دوباره بنویسید یا بیشکل بگذارید.
|
||||||
|
|
||||||
|
اصلاحِ سوالِ منتشرشده: `tools/figures/apply_fixups.py` (حذف و خالیکردنِ عنوان، هم روی seed هم با `--db` روی سرور،
|
||||||
|
پس از `pg_dump`)؛ seed خودش چیزی را بازنویسی نمیکند.
|
||||||
|
|||||||
79
tools/figures/apply_fixups.py
Normal file
79
tools/figures/apply_fixups.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
اصلاحِ سوالهای شکلدارِ از قبل منتشرشده (seed هیچوقت حذف یا بازنویسی نمیکند، پس اصلاح یک ابزارِ جدا میخواهد).
|
||||||
|
|
||||||
|
python tools/figures/apply_fixups.py fixups.json # فقط فایلهای seed محلی
|
||||||
|
venv/bin/python tools/figures/apply_fixups.py --db fixups.json # دیتابیسِ همین ماشین (روی سرور، پس از pg_dump)
|
||||||
|
|
||||||
|
fixups.json: {"delete": [[path, prompt], ...], "blank_title": [[path, prompt], ...]}
|
||||||
|
delete سوال را کامل پاک میکند (پاسخهای ثبتشدهاش هم میرود؛ فقط برای سوالِ تازهمنتشرشده)
|
||||||
|
blank_title عنوانِ زیرِ شکل را خالی میکند (عنوان گاهی جوابِ سوال را لو میداد)
|
||||||
|
سوال با (path, prompt) شناخته میشود؛ هر کدام پیدا نشود فهرست میشود، بیصدا رد نمیشود.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def key(path, prompt):
|
||||||
|
return (tuple(path), prompt.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def apply_seed(fx):
|
||||||
|
p = os.path.join(ROOT, 'content', 'seed', 'questions.json')
|
||||||
|
data = json.load(open(p))
|
||||||
|
dele = {key(*x) for x in fx.get('delete', [])}
|
||||||
|
blank = {key(*x) for x in fx.get('blank_title', [])}
|
||||||
|
seen_d, seen_b, out = set(), set(), []
|
||||||
|
for q in data['questions']:
|
||||||
|
k = key(q['path'], q['prompt'])
|
||||||
|
if k in dele:
|
||||||
|
seen_d.add(k)
|
||||||
|
continue
|
||||||
|
if k in blank and q.get('figure'):
|
||||||
|
q['figure']['title'] = ''
|
||||||
|
seen_b.add(k)
|
||||||
|
out.append(q)
|
||||||
|
data['questions'] = out
|
||||||
|
json.dump(data, open(p, 'w'), ensure_ascii=False, indent=1)
|
||||||
|
return dele - seen_d, blank - seen_b
|
||||||
|
|
||||||
|
|
||||||
|
def apply_db(fx):
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
from content.models import Question
|
||||||
|
lessons = {}
|
||||||
|
from content.models import Lesson
|
||||||
|
for l in Lesson.objects.select_related('node__unit__grade'):
|
||||||
|
lessons[(l.node.unit.grade.title, l.node.unit.title, l.node.title, l.title)] = l.pk
|
||||||
|
miss_d, miss_b = set(), set()
|
||||||
|
for path, prompt in fx.get('delete', []):
|
||||||
|
lid = lessons.get(tuple(path))
|
||||||
|
qs = Question.objects.filter(lesson_id=lid, prompt=prompt.strip()) if lid else Question.objects.none()
|
||||||
|
if not qs.exists():
|
||||||
|
miss_d.add(key(path, prompt))
|
||||||
|
else:
|
||||||
|
qs.delete()
|
||||||
|
for path, prompt in fx.get('blank_title', []):
|
||||||
|
lid = lessons.get(tuple(path))
|
||||||
|
qs = list(Question.objects.filter(lesson_id=lid, prompt=prompt.strip())) if lid else []
|
||||||
|
if not qs:
|
||||||
|
miss_b.add(key(path, prompt))
|
||||||
|
for q in qs:
|
||||||
|
if q.figure:
|
||||||
|
q.figure = {**q.figure, 'title': ''}
|
||||||
|
q.save(update_fields=['figure'])
|
||||||
|
return miss_d, miss_b
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = [a for a in sys.argv[1:] if a != '--db']
|
||||||
|
fx = json.load(open(args[0]))
|
||||||
|
miss_d, miss_b = (apply_db if '--db' in sys.argv else apply_seed)(fx)
|
||||||
|
print(f'deleted/blanked ok; not found: delete={len(miss_d)} blank_title={len(miss_b)}')
|
||||||
|
for k in list(miss_d)[:5] + list(miss_b)[:5]:
|
||||||
|
print(' missing:', k[1][:60])
|
||||||
Reference in New Issue
Block a user