DeutschLernen/GermanApp/Domain/Entities/Quiz.cs
Lasse Rune Hansen 242d5ea60b feat(backend): Complete Quiz System implementation
- 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>
2026-06-13 07:41:46 +02:00

182 lines
5.5 KiB
C#

namespace GermanApp.Domain.Entities;
/// <summary>
/// Represents a quiz that can be associated with a lesson.
/// A quiz contains multiple questions and has a passing score threshold.
/// </summary>
public class Quiz
{
public int Id { get; private set; }
public int LessonId { get; private set; }
public string Title { get; private set; } = string.Empty;
public string? Description { get; private set; }
public int PassingScore { get; private set; } = 80; // Default 80%
public int TimeLimitMinutes { get; private set; } = 0; // 0 = no limit
public bool IsActive { get; private set; } = true;
public bool ShuffleQuestions { get; private set; } = true;
public DateTime CreatedAt { get; private set; }
public DateTime? UpdatedAt { get; private set; }
// Navigation property
public virtual Lesson? Lesson { get; private set; }
public virtual ICollection<QuizQuestion> Questions { get; private set; } = new List<QuizQuestion>();
/// <summary>
/// Constructor for EF Core deserialization.
/// </summary>
private Quiz() { }
/// <summary>
/// Factory method to create a new quiz.
/// </summary>
/// <param name="lessonId">ID of the associated lesson</param>
/// <param name="title">Title of the quiz</param>
/// <param name="description">Optional description</param>
/// <param name="passingScore">Passing score percentage (default 80)</param>
/// <param name="timeLimitMinutes">Time limit in minutes (0 for no limit)</param>
public static Quiz Create(
int lessonId,
string title,
string? description = null,
int passingScore = 80,
int timeLimitMinutes = 0)
{
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("Quiz title cannot be empty", nameof(title));
if (passingScore < 0 || passingScore > 100)
throw new ArgumentOutOfRangeException(nameof(passingScore), "Passing score must be between 0 and 100");
if (timeLimitMinutes < 0)
throw new ArgumentOutOfRangeException(nameof(timeLimitMinutes), "Time limit cannot be negative");
return new Quiz
{
LessonId = lessonId,
Title = title,
Description = description,
PassingScore = passingScore,
TimeLimitMinutes = timeLimitMinutes,
CreatedAt = DateTime.UtcNow
};
}
/// <summary>
/// Updates the quiz title.
/// </summary>
public void UpdateTitle(string newTitle)
{
if (string.IsNullOrWhiteSpace(newTitle))
throw new ArgumentException("Quiz title cannot be empty", nameof(newTitle));
Title = newTitle;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Updates the quiz description.
/// </summary>
public void UpdateDescription(string? newDescription)
{
Description = newDescription;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Updates the passing score.
/// </summary>
public void UpdatePassingScore(int newPassingScore)
{
if (newPassingScore < 0 || newPassingScore > 100)
throw new ArgumentOutOfRangeException(nameof(newPassingScore), "Passing score must be between 0 and 100");
PassingScore = newPassingScore;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Updates the time limit.
/// </summary>
public void UpdateTimeLimit(int newTimeLimitMinutes)
{
if (newTimeLimitMinutes < 0)
throw new ArgumentOutOfRangeException(nameof(newTimeLimitMinutes), "Time limit cannot be negative");
TimeLimitMinutes = newTimeLimitMinutes;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Updates the associated lesson.
/// </summary>
public void UpdateLesson(int newLessonId)
{
LessonId = newLessonId;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Updates the shuffle questions setting.
/// </summary>
public void UpdateShuffleQuestions(bool newShuffleQuestions)
{
ShuffleQuestions = newShuffleQuestions;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Activates the quiz.
/// </summary>
public void Activate()
{
IsActive = true;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Deactivates the quiz.
/// </summary>
public void Deactivate()
{
IsActive = false;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Adds a question to this quiz.
/// </summary>
public void AddQuestion(QuizQuestion question)
{
if (question == null)
throw new ArgumentNullException(nameof(question));
Questions.Add(question);
}
/// <summary>
/// Removes a question from this quiz.
/// </summary>
public void RemoveQuestion(QuizQuestion question)
{
if (question == null)
throw new ArgumentNullException(nameof(question));
Questions.Remove(question);
}
/// <summary>
/// Checks if the quiz is passed based on a score percentage.
/// </summary>
public bool IsPassed(double scorePercentage) => scorePercentage >= PassingScore;
/// <summary>
/// Calculates the score percentage based on correct answers.
/// </summary>
public double CalculateScore(int correctAnswers, int totalQuestions)
{
if (totalQuestions == 0)
return 0;
return (double)correctAnswers / totalQuestions * 100;
}
}