Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
323 lines
No EOL
10 KiB
Dart
323 lines
No EOL
10 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:injectable/injectable.dart';
|
|
import 'package:mnemo_cards_backend/database/database.dart';
|
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
|
import 'package:mnemo_cards_backend/tests/generators/models/creation_test_data.dart';
|
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
import 'package:drift/drift.dart' as drift;
|
|
|
|
import 'generators/pack_test_generator.dart';
|
|
|
|
@lazySingleton
|
|
class TestManager {
|
|
final AppDatabase _db;
|
|
|
|
TestManager(this._db);
|
|
|
|
Future<TestStatisticsDto?> _testStatisticsDto(
|
|
String userId, String testId) async {
|
|
final statistics = await _db.testDao.getTestStatistics(userId, testId);
|
|
if (statistics == null) return null;
|
|
|
|
// Convert TestStatistic to TestStatisticsDto
|
|
// metadata is stored as JSON string with 'attempts' key
|
|
final metadataMap = statistics.metadata.isNotEmpty
|
|
? (json.decode(statistics.metadata) as Map<String, dynamic>? ?? {})
|
|
: <String, dynamic>{};
|
|
final attempts = (metadataMap['attempts'] as List<dynamic>?) ?? [];
|
|
|
|
// Get words from the last attempt, or empty list if no attempts
|
|
final lastAttempt = attempts.isNotEmpty
|
|
? attempts.last as Map<String, dynamic>?
|
|
: null;
|
|
final wordsJson = (lastAttempt?['words'] as List<dynamic>?) ?? [];
|
|
|
|
// Convert words JSON to WordStatisticsDto
|
|
final words = wordsJson
|
|
.map((w) => WordStatisticsDto.fromJson(w as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
return TestStatisticsDto(
|
|
testId: statistics.testId,
|
|
words: AllWordsStatisticsDto(words: words),
|
|
attempts: attempts.length,
|
|
sessionToken: lastAttempt?['sessionToken'] as String?,
|
|
);
|
|
}
|
|
|
|
Future<TestDto?> fetchTest(String id, UserModel user) async {
|
|
final testId = id;
|
|
|
|
final test = await _db.testDao.getTestById(testId);
|
|
if (test == null) return null;
|
|
|
|
final questions = await _db.testDao.getTestQuestions(testId);
|
|
final statistics = await _testStatisticsDto(user.id!, testId);
|
|
|
|
// Convert Test to TestDto
|
|
final questionsList = questions.map((q) {
|
|
// Build question JSON from separate fields
|
|
final questionJson = <String, dynamic>{
|
|
'questionType': q.questionType,
|
|
'id': q.id,
|
|
'word': q.word,
|
|
};
|
|
|
|
// Parse options (JSON array of buttons)
|
|
try {
|
|
final options = json.decode(q.options) as List<dynamic>;
|
|
questionJson['buttons'] = options;
|
|
} catch (e) {
|
|
questionJson['buttons'] = [];
|
|
}
|
|
|
|
// Add answer
|
|
questionJson['answer'] = q.answer;
|
|
|
|
// Parse uiData (image, text, audio, template)
|
|
try {
|
|
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
|
|
questionJson.addAll(uiData);
|
|
} catch (e) {
|
|
// If uiData is empty or invalid, ignore
|
|
}
|
|
|
|
return AbstractTestQuestion.fromJson(questionJson);
|
|
}).toList();
|
|
|
|
return TestDto(
|
|
id: testId.toString(),
|
|
name: test.name,
|
|
color: test.color,
|
|
cover: test.cover,
|
|
version: test.version ?? '1.0',
|
|
time: test.time,
|
|
timeSubtitle: test.timeSubtitle,
|
|
questions: questionsList,
|
|
statistics: statistics,
|
|
);
|
|
}
|
|
|
|
Future<List<TestDto>> availableTests(UserModel userModel) async {
|
|
final tests = await _db.testDao.getAllTests();
|
|
|
|
final testDtos = <TestDto>[];
|
|
for (final test in tests) {
|
|
final questions = await _db.testDao.getTestQuestions(test.id);
|
|
final statistics = await _testStatisticsDto(userModel.id!, test.id);
|
|
|
|
final questionsList = questions.map((q) {
|
|
// Build question JSON from separate fields
|
|
final questionJson = <String, dynamic>{
|
|
'questionType': q.questionType,
|
|
'id': q.id,
|
|
'word': q.word,
|
|
};
|
|
|
|
// Parse options (JSON array of buttons)
|
|
try {
|
|
final options = json.decode(q.options) as List<dynamic>;
|
|
questionJson['buttons'] = options;
|
|
} catch (e) {
|
|
questionJson['buttons'] = [];
|
|
}
|
|
|
|
// Add answer
|
|
questionJson['answer'] = q.answer;
|
|
|
|
// Parse uiData (image, text, audio, template)
|
|
try {
|
|
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
|
|
questionJson.addAll(uiData);
|
|
} catch (e) {
|
|
// If uiData is empty or invalid, ignore
|
|
}
|
|
|
|
return AbstractTestQuestion.fromJson(questionJson);
|
|
}).toList();
|
|
|
|
testDtos.add(TestDto(
|
|
id: test.id.toString(),
|
|
name: test.name,
|
|
color: test.color,
|
|
cover: test.cover,
|
|
version: test.version ?? '1.0',
|
|
time: test.time,
|
|
timeSubtitle: test.timeSubtitle,
|
|
questions: questionsList,
|
|
statistics: statistics,
|
|
));
|
|
}
|
|
|
|
return testDtos;
|
|
}
|
|
|
|
Future<List<TestDto>> fetchPackTests(
|
|
UserModel user,
|
|
CardPackModel model,
|
|
) async {
|
|
final packId = model.id;
|
|
if (packId == null) return [];
|
|
|
|
final tests = await _db.testDao.getTestsByPackId(packId);
|
|
|
|
final testDtos = <TestDto>[];
|
|
for (final test in tests) {
|
|
final questions = await _db.testDao.getTestQuestions(test.id);
|
|
final statistics = await _testStatisticsDto(user.id!, test.id);
|
|
|
|
final questionsList = questions.map((q) {
|
|
// Build question JSON from separate fields
|
|
final questionJson = <String, dynamic>{
|
|
'questionType': q.questionType,
|
|
'id': q.id,
|
|
'word': q.word,
|
|
};
|
|
|
|
// Parse options (JSON array of buttons)
|
|
try {
|
|
final options = json.decode(q.options) as List<dynamic>;
|
|
questionJson['buttons'] = options;
|
|
} catch (e) {
|
|
questionJson['buttons'] = [];
|
|
}
|
|
|
|
// Add answer
|
|
questionJson['answer'] = q.answer;
|
|
|
|
// Parse uiData (image, text, audio, template)
|
|
try {
|
|
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
|
|
questionJson.addAll(uiData);
|
|
} catch (e) {
|
|
// If uiData is empty or invalid, ignore
|
|
}
|
|
|
|
return AbstractTestQuestion.fromJson(questionJson);
|
|
}).toList();
|
|
|
|
testDtos.add(TestDto(
|
|
id: test.id.toString(),
|
|
name: test.name,
|
|
color: test.color,
|
|
cover: test.cover,
|
|
version: test.version ?? '1.0',
|
|
time: test.time,
|
|
timeSubtitle: test.timeSubtitle,
|
|
questions: questionsList,
|
|
statistics: statistics,
|
|
));
|
|
}
|
|
|
|
return testDtos;
|
|
}
|
|
|
|
Future<void> refreshCustomCreationTestsData() async {
|
|
// Load custom creation test data from database
|
|
// For now, keep it simple
|
|
}
|
|
|
|
Future<void> updateGeneratedTests(CardPackModel model) async {
|
|
final packId = model.id;
|
|
if (packId == null) return;
|
|
|
|
// Get pack data from database
|
|
final pack = await _db.packDao.getPackById(packId);
|
|
if (pack == null) return;
|
|
|
|
// Get all cards for the pack
|
|
final cards = await _db.packDao.getPackCards(packId);
|
|
if (cards.isEmpty) return;
|
|
|
|
// Delete old generated tests for this pack
|
|
final existingTests = await _db.testDao.getTestsByPackId(packId);
|
|
final oldGeneratedTests = existingTests
|
|
.where((test) => test.version == 'generated')
|
|
.toList();
|
|
for (final oldTest in oldGeneratedTests) {
|
|
await _db.testDao.softDeleteTest(oldTest.id);
|
|
}
|
|
|
|
// Create creation test data
|
|
final testDataItems = cards.map((card) {
|
|
return TestDataItem(
|
|
id: card.id.toString(),
|
|
original: card.original,
|
|
translation: card.translation,
|
|
image: card.image,
|
|
audio: card.original, // Use original as audio
|
|
);
|
|
}).toList();
|
|
|
|
final creationTestData = CreationTestData(
|
|
items: testDataItems,
|
|
title: pack.title,
|
|
color: pack.color,
|
|
packId: packId.toString(),
|
|
);
|
|
|
|
// Generate test using PackTestGenerator
|
|
final generator = PackTestGenerator(creationTestData);
|
|
final testDto = await generator.generate(
|
|
name: '${pack.title} Test',
|
|
ratios: {
|
|
TestQuestionType.simple: 0.7,
|
|
TestQuestionType.input_buttons: 0.3,
|
|
},
|
|
multiply: 1.0,
|
|
);
|
|
|
|
// Save test to database
|
|
await addTest(testDto);
|
|
|
|
// Link test to pack
|
|
await _db.testDao.linkTestToPack(testDto.id!, packId);
|
|
}
|
|
|
|
Future<void> addTest(TestDto testDto) async {
|
|
await _db.transaction(() async {
|
|
// Create test
|
|
final testCompanion = TestsCompanion.insert(
|
|
name: testDto.name,
|
|
color: drift.Value(testDto.color),
|
|
cover: drift.Value(testDto.cover),
|
|
version: drift.Value(testDto.version),
|
|
time: drift.Value(testDto.time),
|
|
timeSubtitle: drift.Value(testDto.timeSubtitle),
|
|
);
|
|
|
|
final testId = await _db.testDao.createTest(testCompanion);
|
|
|
|
// Create questions
|
|
int orderIndex = 0;
|
|
for (final question in testDto.questions) {
|
|
final questionJson = question.toJson();
|
|
|
|
// Extract key fields
|
|
final word = questionJson['word'] as String? ?? '';
|
|
final answer = questionJson['answer'] as String? ?? '';
|
|
final buttons = questionJson['buttons'] as List<dynamic>? ?? [];
|
|
|
|
// UI data (image, text, audio, template)
|
|
final uiData = <String, dynamic>{};
|
|
if (questionJson['image'] != null) uiData['image'] = questionJson['image'];
|
|
if (questionJson['text'] != null) uiData['text'] = questionJson['text'];
|
|
if (questionJson['audio'] != null) uiData['audio'] = questionJson['audio'];
|
|
if (questionJson['template'] != null) uiData['template'] = questionJson['template'];
|
|
|
|
final questionCompanion = TestQuestionsCompanion.insert(
|
|
testId: testId,
|
|
orderIndex: drift.Value(orderIndex++),
|
|
questionType: question.questionType.name,
|
|
word: word,
|
|
answer: answer,
|
|
options: drift.Value(json.encode(buttons)),
|
|
uiData: drift.Value(json.encode(uiData)),
|
|
);
|
|
|
|
await _db.testDao.createTestQuestion(questionCompanion);
|
|
}
|
|
});
|
|
}
|
|
} |