The frontend plays node quizzes, but a node only completed once every one of its
8-13 lessons was completed separately, so students were stuck after the first
island of each unit. POST /api/nodes/{id}/quiz/complete now completes the node
(any score; score only sets stars/xp) and returns nextNodeId.
Also closes the other backend gaps found while auditing the frontend:
- /api/ai/ask without lessonId now picks the book from the user's grade instead
of skipping retrieval, so the "ask Pi" tab is grounded in the textbook.
- guest sessions return a refreshToken; they used to go silently 401 after 1h.
- lesson_complete throttle 10/hour -> 120/hour (a lesson here is 1-2 questions).
- progressPercent is per grade, not over all 2,617 lessons of every grade.
- a one-year plan (299,000 toman) is created by migration when no plan exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 lines
1.6 KiB
Python
34 lines
1.6 KiB
Python
# Generated by Django 5.2.17 on 2026-09-23 23:10
|
|
|
|
import django.db.models.deletion
|
|
from django.db import migrations, models
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
|
dependencies = [
|
|
('content', '0008_grade_id_equals_number'),
|
|
]
|
|
|
|
operations = [
|
|
migrations.CreateModel(
|
|
name='UserNodeCompletion',
|
|
fields=[
|
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
('user_id', models.IntegerField(db_index=True)),
|
|
('correct_count', models.PositiveIntegerField()),
|
|
('total_questions', models.PositiveIntegerField()),
|
|
('stars', models.PositiveSmallIntegerField()),
|
|
('xp_earned', models.PositiveIntegerField(default=0)),
|
|
('coins_earned', models.PositiveIntegerField(default=0)),
|
|
('idempotency_key', models.CharField(blank=True, max_length=100, null=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='completions', to='content.node')),
|
|
],
|
|
options={
|
|
'indexes': [models.Index(fields=['user_id', 'node'], name='content_use_user_id_f372a4_idx')],
|
|
'constraints': [models.UniqueConstraint(condition=models.Q(('idempotency_key__isnull', False), models.Q(('idempotency_key', ''), _negated=True)), fields=('user_id', 'node', 'idempotency_key'), name='uniq_node_completion_idempotency_key')],
|
|
},
|
|
),
|
|
]
|