- Add Quiz entity with full domain logic (Create, Update, Activate, Deactivate) - Add QuizQuestion entity updated to use QuizId instead of LessonId - Add QuizRepository with all CRUD and query methods - Add QuizQuestionRepository with QuizId-based and LessonId-based (legacy) methods - Add QuizDto, CreateQuizDto, UpdateQuizDto, QuizWithQuestionsDto, QuizListItemDto - Add QuizQuestionDto updated with QuizId, LessonId, QuizTitle, Points fields - Add CreateQuizCommand, UpdateQuizCommand, DeleteQuizCommand, GetQuizWithQuestionsCommand - Add QuizService with full quiz management (CRUD, activate, deactivate, counts, queries) - Add QuizQuestionService updated with QuizId-based methods - Add QuizzesController with comprehensive endpoints (GET, POST, PUT, DELETE) - Add QuizzesController endpoints for quiz questions (by-quiz, random, active) - Update QuizQuestionsController with new QuizId-based endpoints (backward compatible) - Register QuizRepository and QuizService in DI container - Update IRepository interfaces with QuizId-based methods - Add SubmitQuizDto for quiz answer submission Clean Architecture layers maintained: - Domain: Quiz, QuizQuestion entities with business logic - Application: DTOs, Commands, Services - Infrastructure: Repositories, DbContext - Presentation: Controllers Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
273 lines
9.7 KiB
C#
273 lines
9.7 KiB
C#
using GermanApp.Application.DTOs;
|
|
using GermanApp.Domain.Entities;
|
|
using GermanApp.Domain.Exceptions;
|
|
using GermanApp.Domain.Interfaces;
|
|
|
|
namespace GermanApp.Application.Services;
|
|
|
|
/// <summary>
|
|
/// Application service for managing quizzes.
|
|
/// This is part of the Application layer.
|
|
/// </summary>
|
|
public class QuizService
|
|
{
|
|
private readonly IQuizRepository _quizRepository;
|
|
private readonly IQuizQuestionRepository _quizQuestionRepository;
|
|
private readonly IQuizOptionRepository _quizOptionRepository;
|
|
private readonly ILessonRepository _lessonRepository;
|
|
|
|
public QuizService(
|
|
IQuizRepository quizRepository,
|
|
IQuizQuestionRepository quizQuestionRepository,
|
|
IQuizOptionRepository quizOptionRepository,
|
|
ILessonRepository lessonRepository)
|
|
{
|
|
_quizRepository = quizRepository;
|
|
_quizQuestionRepository = quizQuestionRepository;
|
|
_quizOptionRepository = quizOptionRepository;
|
|
_lessonRepository = lessonRepository;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all quizzes.
|
|
/// </summary>
|
|
public virtual async Task<IReadOnlyList<QuizDto>> GetAllQuizzesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetAllOrderedAsync(cancellationToken);
|
|
return quizzes.Select(q => q.ToDto()).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all quizzes as list items (summary).
|
|
/// </summary>
|
|
public virtual async Task<IReadOnlyList<QuizListItemDto>> GetAllQuizListItemsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetAllOrderedAsync(cancellationToken);
|
|
return quizzes.Select(q => q.ToListItemDto()).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a quiz by its ID.
|
|
/// </summary>
|
|
public virtual async Task<QuizDto?> GetQuizByIdAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
return quiz?.ToDto();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a quiz with its questions by ID.
|
|
/// </summary>
|
|
public virtual async Task<QuizWithQuestionsDto?> GetQuizWithQuestionsAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
var quiz = await _quizRepository.GetWithQuestionsAsync(id, cancellationToken);
|
|
return quiz?.ToQuizWithQuestionsDto();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets quizzes by lesson ID.
|
|
/// </summary>
|
|
public virtual async Task<IReadOnlyList<QuizDto>> GetQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetByLessonAsync(lessonId, cancellationToken);
|
|
return quizzes.Select(q => q.ToDto()).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets active quizzes by lesson ID.
|
|
/// </summary>
|
|
public virtual async Task<IReadOnlyList<QuizDto>> GetActiveQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetActiveByLessonAsync(lessonId, cancellationToken);
|
|
return quizzes.Select(q => q.ToDto()).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the first quiz for a lesson.
|
|
/// </summary>
|
|
public virtual async Task<QuizDto?> GetFirstQuizByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
|
{
|
|
var quiz = await _quizRepository.GetFirstByLessonAsync(lessonId, cancellationToken);
|
|
return quiz?.ToDto();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new quiz.
|
|
/// </summary>
|
|
public virtual async Task<QuizDto> CreateQuizAsync(CreateQuizDto dto, CancellationToken cancellationToken = default)
|
|
{
|
|
// Validate that the lesson exists
|
|
var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
|
|
if (lesson == null)
|
|
throw new NotFoundException("Lesson does not exist");
|
|
|
|
// Validate business rules
|
|
if (dto.PassingScore < 0 || dto.PassingScore > 100)
|
|
{
|
|
throw new ValidationException("Passing score must be between 0 and 100");
|
|
}
|
|
|
|
if (dto.TimeLimitMinutes < 0)
|
|
{
|
|
throw new ValidationException("Time limit cannot be negative");
|
|
}
|
|
|
|
// Convert DTO to domain entity
|
|
var quiz = dto.ToEntity();
|
|
|
|
// Add to repository
|
|
var createdQuiz = await _quizRepository.AddAsync(quiz, cancellationToken);
|
|
|
|
// Return DTO representation
|
|
return createdQuiz.ToDto();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates an existing quiz.
|
|
/// </summary>
|
|
public virtual async Task<QuizDto?> UpdateQuizAsync(int id, UpdateQuizDto dto, CancellationToken cancellationToken = default)
|
|
{
|
|
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
if (quiz == null)
|
|
return null;
|
|
|
|
// Validate that the lesson exists
|
|
var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
|
|
if (lesson == null)
|
|
throw new NotFoundException("Lesson does not exist");
|
|
|
|
// Validate business rules
|
|
if (dto.PassingScore < 0 || dto.PassingScore > 100)
|
|
{
|
|
throw new ValidationException("Passing score must be between 0 and 100");
|
|
}
|
|
|
|
if (dto.TimeLimitMinutes < 0)
|
|
{
|
|
throw new ValidationException("Time limit cannot be negative");
|
|
}
|
|
|
|
// Update the quiz
|
|
quiz.UpdateFromDto(dto);
|
|
await _quizRepository.UpdateAsync(quiz, cancellationToken);
|
|
|
|
// Reload and return
|
|
var updatedQuiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
return updatedQuiz?.ToDto();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a quiz by its ID.
|
|
/// </summary>
|
|
public virtual async Task<bool> DeleteQuizAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
if (quiz == null)
|
|
return false;
|
|
|
|
// First, delete all options for all questions in this quiz
|
|
var questions = await _quizQuestionRepository.GetByQuizAsync(id, cancellationToken);
|
|
foreach (var question in questions)
|
|
{
|
|
await _quizOptionRepository.DeleteByQuestionAsync(question.Id, cancellationToken);
|
|
}
|
|
|
|
// Then delete all questions for this quiz
|
|
foreach (var question in questions)
|
|
{
|
|
await _quizQuestionRepository.DeleteAsync(question, cancellationToken);
|
|
}
|
|
|
|
// Finally, delete the quiz itself
|
|
await _quizRepository.DeleteAsync(quiz, cancellationToken);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a quiz exists for a lesson.
|
|
/// </summary>
|
|
public virtual async Task<bool> ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _quizRepository.ExistsByLessonAsync(lessonId, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a quiz exists by ID.
|
|
/// </summary>
|
|
public virtual async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _quizRepository.ExistsAsync(id, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total number of quizzes.
|
|
/// </summary>
|
|
public virtual async Task<int> GetTotalQuizCountAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
|
|
return quizzes.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total number of quizzes for a lesson.
|
|
/// </summary>
|
|
public virtual async Task<int> GetQuizCountByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetByLessonAsync(lessonId, cancellationToken);
|
|
return quizzes.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets quizzes by passing score range.
|
|
/// </summary>
|
|
public virtual async Task<IReadOnlyList<QuizDto>> GetQuizzesByPassingScoreRangeAsync(int minScore, int maxScore, CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
|
|
return quizzes.Where(q => q.PassingScore >= minScore && q.PassingScore <= maxScore)
|
|
.Select(q => q.ToDto())
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets active quizzes.
|
|
/// </summary>
|
|
public virtual async Task<IReadOnlyList<QuizDto>> GetActiveQuizzesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
|
|
return quizzes.Where(q => q.IsActive)
|
|
.Select(q => q.ToDto())
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Activates a quiz.
|
|
/// </summary>
|
|
public virtual async Task<QuizDto?> ActivateQuizAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
if (quiz == null)
|
|
return null;
|
|
|
|
quiz.Activate();
|
|
await _quizRepository.UpdateAsync(quiz, cancellationToken);
|
|
|
|
var updatedQuiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
return updatedQuiz?.ToDto();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deactivates a quiz.
|
|
/// </summary>
|
|
public virtual async Task<QuizDto?> DeactivateQuizAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
if (quiz == null)
|
|
return null;
|
|
|
|
quiz.Deactivate();
|
|
await _quizRepository.UpdateAsync(quiz, cancellationToken);
|
|
|
|
var updatedQuiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
|
|
return updatedQuiz?.ToDto();
|
|
}
|
|
}
|