feat(trainer): «сцена-герой» редизайн + конструктор только для админов
- мощный визуал: уравнение на яркой градиентной «сцене» (бело на индиго→фиолет, текстура-клетка), рабочая зона снизу на белом; верный ответ заливает сцену изумрудом, неверный — красным (с pop/shake) - конструктор генераторов — ТОЛЬКО админ: страница /trainer-builder гейт ip.isAdmin; роуты POST/PUT/DELETE /generators → requireRole(admin); сайдбар-пункт hidden:!isAdm - выделен отдельным цветом: янтарная кнопка «Конструктор» в баре режима (только админ) → /trainer-builder - тема пользовательских генераторов: «Мои генераторы» для админа / «Авторские» для остальных (видят published) - тесты custom-generators 13/13 (админ создаёт; учитель/ученик 403); страница-смоук 33/33; эмодзи/eval 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,12 +22,13 @@ router.post('/assign', requireRole('teacher', 'admin'), c.assignToClass);
|
||||
// Аналитика класса — только учитель/админ (владение проверяется в хендлере).
|
||||
router.get('/class-stats', requireRole('teacher', 'admin'), c.classStats);
|
||||
|
||||
// Конструктор генераторов (P13): чтение — own+published; мутации — учитель/админ + ownership.
|
||||
// Конструктор генераторов (P13): чтение — own+published (ученики видят published);
|
||||
// СОЗДАНИЕ/правка — ТОЛЬКО админ (конструктор — админский инструмент).
|
||||
const cg = require('../controllers/customGeneratorController');
|
||||
router.get('/generators', cg.genList);
|
||||
router.post('/generators', requireRole('teacher', 'admin'), cg.genCreate);
|
||||
router.post('/generators', requireRole('admin'), cg.genCreate);
|
||||
router.get('/generators/:id', cg.genGet); // @public-by-design: own/published в хендлере
|
||||
router.put('/generators/:id', requireRole('teacher', 'admin'), cg.genUpdate);
|
||||
router.delete('/generators/:id', requireRole('teacher', 'admin'), cg.genDelete);
|
||||
router.put('/generators/:id', requireRole('admin'), cg.genUpdate);
|
||||
router.delete('/generators/:id', requireRole('admin'), cg.genDelete);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -41,60 +41,66 @@ describe('validateGenSpec', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/practice/generators CRUD', () => {
|
||||
let teacher, other, student, gid;
|
||||
describe('/api/practice/generators CRUD (конструктор — только админ)', () => {
|
||||
let admin, other, teacher, student, gid;
|
||||
before(async () => {
|
||||
admin = (await getToken('admin')).token;
|
||||
other = (await getToken('admin')).token;
|
||||
teacher = (await getToken('teacher')).token;
|
||||
other = (await getToken('teacher')).token;
|
||||
student = (await getToken('student')).token;
|
||||
});
|
||||
|
||||
it('учитель создаёт генератор', async () => {
|
||||
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, teacher);
|
||||
it('админ создаёт генератор', async () => {
|
||||
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, admin);
|
||||
assert.equal(res.status, 200, `got ${res.status}`);
|
||||
assert.equal(res.body.ok, true);
|
||||
assert.ok(/^cg\d+$/.test(res.body.generator.id), 'id вида cg<dbid>');
|
||||
gid = res.body.generator.dbid;
|
||||
});
|
||||
|
||||
it('учителю создавать запрещено (403)', async () => {
|
||||
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, teacher);
|
||||
assert.equal(res.status, 403, `got ${res.status}`);
|
||||
});
|
||||
|
||||
it('ученику создавать запрещено (403)', async () => {
|
||||
const res = await inject('POST', '/api/practice/generators', { spec: SPEC }, student);
|
||||
assert.equal(res.status, 403, `got ${res.status}`);
|
||||
});
|
||||
|
||||
it('невалидный спек → 400', async () => {
|
||||
const res = await inject('POST', '/api/practice/generators', { spec: { title: '' } }, teacher);
|
||||
const res = await inject('POST', '/api/practice/generators', { spec: { title: '' } }, admin);
|
||||
assert.equal(res.status, 400, `got ${res.status}`);
|
||||
});
|
||||
|
||||
it('автор видит свой генератор в списке', async () => {
|
||||
const res = await inject('GET', '/api/practice/generators', null, teacher);
|
||||
const res = await inject('GET', '/api/practice/generators', null, admin);
|
||||
assert.equal(res.status, 200);
|
||||
assert.ok(res.body.generators.some(g => g.dbid === gid), 'свой генератор в списке');
|
||||
});
|
||||
|
||||
it('чужой draft не виден другому учителю', async () => {
|
||||
it('чужой draft не виден другому админу', async () => {
|
||||
const res = await inject('GET', '/api/practice/generators', null, other);
|
||||
assert.ok(!res.body.generators.some(g => g.dbid === gid), 'чужой draft скрыт');
|
||||
});
|
||||
|
||||
it('чужой не может изменить (403)', async () => {
|
||||
const res = await inject('PUT', '/api/practice/generators/' + gid, { spec: SPEC }, other);
|
||||
it('учителю изменять запрещено (403, роль)', async () => {
|
||||
const res = await inject('PUT', '/api/practice/generators/' + gid, { spec: SPEC }, teacher);
|
||||
assert.equal(res.status, 403, `got ${res.status}`);
|
||||
});
|
||||
|
||||
it('публикация делает генератор видимым другим', async () => {
|
||||
const pub = await inject('PUT', '/api/practice/generators/' + gid, { status: 'published' }, teacher);
|
||||
it('публикация делает генератор видимым другим (и ученику)', async () => {
|
||||
const pub = await inject('PUT', '/api/practice/generators/' + gid, { status: 'published' }, admin);
|
||||
assert.equal(pub.status, 200);
|
||||
assert.equal(pub.body.generator.status, 'published');
|
||||
const res = await inject('GET', '/api/practice/generators', null, other);
|
||||
assert.ok(res.body.generators.some(g => g.dbid === gid), 'published виден другому');
|
||||
const res = await inject('GET', '/api/practice/generators', null, student);
|
||||
assert.ok(res.body.generators.some(g => g.dbid === gid), 'published виден ученику');
|
||||
});
|
||||
|
||||
it('автор удаляет свой генератор', async () => {
|
||||
const res = await inject('DELETE', '/api/practice/generators/' + gid, null, teacher);
|
||||
const res = await inject('DELETE', '/api/practice/generators/' + gid, null, admin);
|
||||
assert.equal(res.status, 200);
|
||||
const after = await inject('GET', '/api/practice/generators/' + gid, null, teacher);
|
||||
const after = await inject('GET', '/api/practice/generators/' + gid, null, admin);
|
||||
assert.equal(after.status, 404, 'после удаления 404');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user