diff --git a/GermanApp/Application/DTOs/QuizDto.cs b/GermanApp/Application/DTOs/QuizDto.cs
new file mode 100644
index 0000000..dcccaae
--- /dev/null
+++ b/GermanApp/Application/DTOs/QuizDto.cs
@@ -0,0 +1,151 @@
+using GermanApp.Domain.Entities;
+
+namespace GermanApp.Application.DTOs;
+
+///
+/// Data Transfer Object for Quiz - used for API responses.
+/// This is a read-only representation of a Quiz entity.
+///
+public record QuizDto(
+ int Id,
+ int LessonId,
+ string LessonTitle,
+ string Title,
+ string? Description,
+ int PassingScore,
+ int TimeLimitMinutes,
+ bool ShuffleQuestions,
+ bool IsActive,
+ int QuestionCount,
+ int TotalPoints,
+ DateTime CreatedAt,
+ DateTime? UpdatedAt);
+
+///
+/// Data Transfer Object for creating a new Quiz.
+///
+public record CreateQuizDto(
+ int LessonId,
+ string Title,
+ string? Description,
+ int PassingScore,
+ int TimeLimitMinutes,
+ bool ShuffleQuestions);
+
+///
+/// Data Transfer Object for updating an existing Quiz.
+///
+public record UpdateQuizDto(
+ int LessonId,
+ string Title,
+ string? Description,
+ int PassingScore,
+ int TimeLimitMinutes,
+ bool ShuffleQuestions,
+ bool IsActive);
+
+///
+/// Data Transfer Object for quiz with its questions.
+///
+public record QuizWithQuestionsDto(
+ int Id,
+ int LessonId,
+ string LessonTitle,
+ string Title,
+ string? Description,
+ int PassingScore,
+ int TimeLimitMinutes,
+ bool ShuffleQuestions,
+ bool IsActive,
+ DateTime CreatedAt,
+ DateTime? UpdatedAt,
+ IReadOnlyList Questions);
+
+///
+/// Data Transfer Object for quiz list item (summary).
+///
+public record QuizListItemDto(
+ int Id,
+ int LessonId,
+ string LessonTitle,
+ string Title,
+ bool IsActive,
+ int QuestionCount,
+ DateTime CreatedAt);
+
+///
+/// Data Transfer Object for submitting quiz answers.
+///
+public record SubmitQuizDto(
+ int QuizId,
+ IReadOnlyList Answers);
+
+///
+/// Extension methods for mapping between Quiz entity and DTOs.
+///
+public static class QuizDtoExtensions
+{
+ public static QuizDto ToDto(this Quiz quiz) => new(
+ quiz.Id,
+ quiz.LessonId,
+ quiz.Lesson?.Title ?? "Unknown",
+ quiz.Title,
+ quiz.Description,
+ quiz.PassingScore,
+ quiz.TimeLimitMinutes,
+ quiz.ShuffleQuestions,
+ quiz.IsActive,
+ quiz.Questions?.Count ?? 0,
+ quiz.Questions?.Sum(q => q.Points) ?? 0,
+ quiz.CreatedAt,
+ quiz.UpdatedAt);
+
+ public static QuizListItemDto ToListItemDto(this Quiz quiz) => new(
+ quiz.Id,
+ quiz.LessonId,
+ quiz.Lesson?.Title ?? "Unknown",
+ quiz.Title,
+ quiz.IsActive,
+ quiz.Questions?.Count ?? 0,
+ quiz.CreatedAt);
+
+ public static QuizWithQuestionsDto ToQuizWithQuestionsDto(this Quiz quiz) => new(
+ quiz.Id,
+ quiz.LessonId,
+ quiz.Lesson?.Title ?? "Unknown",
+ quiz.Title,
+ quiz.Description,
+ quiz.PassingScore,
+ quiz.TimeLimitMinutes,
+ quiz.ShuffleQuestions,
+ quiz.IsActive,
+ quiz.CreatedAt,
+ quiz.UpdatedAt,
+ quiz.Questions?.OrderBy(q => q.Order).Select(q => q.ToDto()).ToList() ?? new List());
+
+ public static Quiz ToEntity(this CreateQuizDto dto) =>
+ Quiz.Create(
+ dto.LessonId,
+ dto.Title,
+ dto.Description,
+ dto.PassingScore,
+ dto.TimeLimitMinutes);
+
+ public static void UpdateFromDto(this Quiz quiz, UpdateQuizDto dto)
+ {
+ quiz.UpdateLesson(dto.LessonId);
+ quiz.UpdateTitle(dto.Title);
+ quiz.UpdateDescription(dto.Description);
+ quiz.UpdatePassingScore(dto.PassingScore);
+ quiz.UpdateTimeLimit(dto.TimeLimitMinutes);
+ quiz.UpdateShuffleQuestions(dto.ShuffleQuestions);
+
+ if (dto.IsActive != quiz.IsActive)
+ {
+ if (dto.IsActive)
+ quiz.Activate();
+ else
+ quiz.Deactivate();
+ }
+ }
+}
diff --git a/GermanApp/Application/DTOs/QuizQuestionDto.cs b/GermanApp/Application/DTOs/QuizQuestionDto.cs
index ccfc6db..e23d12f 100644
--- a/GermanApp/Application/DTOs/QuizQuestionDto.cs
+++ b/GermanApp/Application/DTOs/QuizQuestionDto.cs
@@ -8,11 +8,14 @@ namespace GermanApp.Application.DTOs;
///
public record QuizQuestionDto(
int Id,
+ int QuizId,
int LessonId,
+ string QuizTitle,
string LessonTitle,
string QuestionText,
QuestionType Type,
string? CorrectAnswer,
+ int Points,
int Difficulty,
int Order,
bool IsActive,
@@ -24,10 +27,11 @@ public record QuizQuestionDto(
/// Data Transfer Object for creating a new QuizQuestion.
///
public record CreateQuizQuestionDto(
- int LessonId,
+ int QuizId,
string QuestionText,
QuestionType Type,
string? CorrectAnswer,
+ int Points,
int Difficulty,
int Order,
IReadOnlyList? Options);
@@ -36,10 +40,11 @@ public record CreateQuizQuestionDto(
/// Data Transfer Object for updating an existing QuizQuestion.
///
public record UpdateQuizQuestionDto(
- int LessonId,
+ int QuizId,
string QuestionText,
QuestionType Type,
string? CorrectAnswer,
+ int Points,
int Difficulty,
int Order,
bool IsActive,
@@ -77,9 +82,11 @@ public record UpdateQuizOptionDto(
///
public record QuizQuestionWithAnswerDto(
int QuizQuestionId,
+ int QuizId,
int LessonId,
string QuestionText,
QuestionType Type,
+ int Points,
IReadOnlyList Options,
string? UserAnswer,
bool IsCorrect);
@@ -88,7 +95,7 @@ public record QuizQuestionWithAnswerDto(
/// Data Transfer Object for submitting quiz answers.
///
public record SubmitQuizAnswersDto(
- int LessonId,
+ int QuizId,
IReadOnlyList Answers);
///
@@ -103,9 +110,12 @@ public record QuizAnswerDto(
/// Data Transfer Object for quiz result.
///
public record QuizResultDto(
+ int QuizId,
int LessonId,
+ string QuizTitle,
int TotalQuestions,
- int CorrectAnswers,
+ int TotalPoints,
+ int ScorePoints,
double ScorePercentage,
bool Passed,
TimeSpan TimeTaken,
@@ -118,6 +128,7 @@ public record QuizQuestionResultDto(
int QuizQuestionId,
string QuestionText,
QuestionType Type,
+ int Points,
bool IsCorrect,
string? CorrectAnswer,
string? UserAnswer);
@@ -129,11 +140,14 @@ public static class QuizQuestionDtoExtensions
{
public static QuizQuestionDto ToDto(this QuizQuestion quizQuestion) => new(
quizQuestion.Id,
- quizQuestion.LessonId,
- quizQuestion.Lesson?.Title ?? "Unknown",
+ quizQuestion.QuizId,
+ quizQuestion.Quiz?.LessonId ?? 0,
+ quizQuestion.Quiz?.Title ?? "Unknown",
+ quizQuestion.Quiz?.Lesson?.Title ?? "Unknown",
quizQuestion.QuestionText,
quizQuestion.Type,
quizQuestion.CorrectAnswer,
+ quizQuestion.Points,
quizQuestion.Difficulty,
quizQuestion.Order,
quizQuestion.IsActive,
@@ -143,19 +157,21 @@ public static class QuizQuestionDtoExtensions
public static QuizQuestion ToEntity(this CreateQuizQuestionDto dto) =>
QuizQuestion.Create(
- dto.LessonId,
+ dto.QuizId,
dto.QuestionText,
dto.Type,
dto.CorrectAnswer ?? string.Empty,
+ dto.Points,
dto.Difficulty,
dto.Order);
public static void UpdateFromDto(this QuizQuestion quizQuestion, UpdateQuizQuestionDto dto)
{
- quizQuestion.UpdateLesson(dto.LessonId);
+ quizQuestion.UpdateQuiz(dto.QuizId);
quizQuestion.UpdateQuestionText(dto.QuestionText);
quizQuestion.UpdateType(dto.Type);
quizQuestion.UpdateCorrectAnswer(dto.CorrectAnswer ?? string.Empty);
+ quizQuestion.UpdatePoints(dto.Points);
quizQuestion.UpdateDifficulty(dto.Difficulty);
quizQuestion.UpdateOrder(dto.Order);
diff --git a/GermanApp/Application/Services/QuizQuestionService.cs b/GermanApp/Application/Services/QuizQuestionService.cs
index cefe363..afc42d3 100644
--- a/GermanApp/Application/Services/QuizQuestionService.cs
+++ b/GermanApp/Application/Services/QuizQuestionService.cs
@@ -1,5 +1,6 @@
using GermanApp.Application.DTOs;
using GermanApp.Domain.Entities;
+using GermanApp.Domain.Exceptions;
using GermanApp.Domain.Interfaces;
namespace GermanApp.Application.Services;
@@ -12,15 +13,18 @@ public class QuizQuestionService
{
private readonly IQuizQuestionRepository _quizQuestionRepository;
private readonly IQuizOptionRepository _quizOptionRepository;
+ private readonly IQuizRepository _quizRepository;
private readonly ILessonRepository _lessonRepository;
public QuizQuestionService(
IQuizQuestionRepository quizQuestionRepository,
IQuizOptionRepository quizOptionRepository,
+ IQuizRepository quizRepository,
ILessonRepository lessonRepository)
{
_quizQuestionRepository = quizQuestionRepository;
_quizOptionRepository = quizOptionRepository;
+ _quizRepository = quizRepository;
_lessonRepository = lessonRepository;
}
@@ -92,13 +96,13 @@ public class QuizQuestionService
///
public virtual async Task CreateQuizQuestionAsync(CreateQuizQuestionDto dto, CancellationToken cancellationToken = default)
{
- // Validate that the lesson exists
- var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
- if (lesson == null)
- throw new ArgumentException("Lesson does not exist");
+ // Validate that the quiz exists
+ var quiz = await _quizRepository.GetByIdAsync(dto.QuizId, cancellationToken);
+ if (quiz == null)
+ throw new NotFoundException("Quiz does not exist");
- // Validate question order uniqueness within the lesson
- await ValidateQuestionOrderUniquenessAsync(0, dto.LessonId, dto.Order, cancellationToken);
+ // Validate question order uniqueness within the quiz
+ await ValidateQuestionOrderUniquenessAsync(0, dto.QuizId, dto.Order, cancellationToken);
// Create the question
var question = dto.ToEntity();
@@ -128,13 +132,13 @@ public class QuizQuestionService
if (question == null)
return null;
- // Validate that the lesson exists
- var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
- if (lesson == null)
- throw new ArgumentException("Lesson does not exist");
+ // Validate that the quiz exists
+ var quiz = await _quizRepository.GetByIdAsync(dto.QuizId, cancellationToken);
+ if (quiz == null)
+ throw new NotFoundException("Quiz does not exist");
- // Validate question order uniqueness within the lesson
- await ValidateQuestionOrderUniquenessAsync(id, dto.LessonId, dto.Order, cancellationToken);
+ // Validate question order uniqueness within the quiz
+ await ValidateQuestionOrderUniquenessAsync(id, dto.QuizId, dto.Order, cancellationToken);
// Update the question
question.UpdateFromDto(dto);
@@ -160,13 +164,13 @@ public class QuizQuestionService
}
///
- /// Validates that the question order is unique within its lesson.
+ /// Validates that the question order is unique within its quiz.
///
- private async Task ValidateQuestionOrderUniquenessAsync(int excludeQuestionId, int lessonId, int order, CancellationToken cancellationToken)
+ private async Task ValidateQuestionOrderUniquenessAsync(int excludeQuestionId, int quizId, int order, CancellationToken cancellationToken)
{
- var exists = await _quizQuestionRepository.OrderExistsInLessonAsync(lessonId, order, excludeQuestionId, cancellationToken);
+ var exists = await _quizQuestionRepository.OrderExistsInQuizAsync(quizId, order, excludeQuestionId, cancellationToken);
if (exists)
- throw new ArgumentException("A quiz question with this order already exists in the specified lesson");
+ throw new ValidationException("A quiz question with this order already exists in the specified quiz");
}
///
@@ -277,18 +281,21 @@ public class QuizQuestionService
}
///
- /// Submits quiz answers and returns the result.
+ /// Submits quiz answers for a specific quiz and returns the result.
///
- public virtual async Task SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
+ public virtual async Task SubmitQuizAnswersForQuizAsync(int userId, SubmitQuizDto dto, CancellationToken cancellationToken = default)
{
- var questions = await _quizQuestionRepository.GetByLessonAsync(dto.LessonId, cancellationToken);
- var questionDictionary = questions.ToDictionary(q => q.Id);
+ var quiz = await _quizRepository.GetWithQuestionsAsync(dto.QuizId, cancellationToken);
+ if (quiz == null)
+ throw new NotFoundException("Quiz does not exist");
+ var questions = quiz.Questions?.Where(q => q.IsActive).OrderBy(q => q.Order).ToList() ?? new List();
var totalQuestions = questions.Count;
- var correctAnswers = 0;
+ var totalPoints = questions.Sum(q => q.Points);
+ var scorePoints = 0;
var questionResults = new List();
- // Get user's answers
+ // Get user's answers (using QuizAnswerDto which has the same fields as SubmitAnswerDto)
var userAnswers = dto.Answers.ToDictionary(a => a.QuizQuestionId);
foreach (var question in questions)
@@ -299,7 +306,7 @@ public class QuizQuestionService
if (userAnswers.TryGetValue(question.Id, out var userAnswer))
{
- userAnswerText = userAnswer?.AnswerText ?? string.Empty;
+ userAnswerText = userAnswer.AnswerText ?? string.Empty;
switch (question.Type)
{
@@ -341,7 +348,7 @@ public class QuizQuestionService
// Case-insensitive comparison with trimming
if (!string.IsNullOrEmpty(userAnswerText) && !string.IsNullOrEmpty(correctAnswer))
{
- isCorrect = userAnswerText.Equals(correctAnswer, StringComparison.OrdinalIgnoreCase);
+ isCorrect = userAnswerText.Trim().Equals(correctAnswer.Trim(), StringComparison.OrdinalIgnoreCase);
}
break;
@@ -353,30 +360,47 @@ public class QuizQuestionService
}
if (isCorrect)
- correctAnswers++;
+ scorePoints += question.Points;
questionResults.Add(new QuizQuestionResultDto(
question.Id,
question.QuestionText,
question.Type,
+ question.Points,
isCorrect,
correctAnswer,
userAnswerText));
}
- var scorePercentage = totalQuestions > 0 ? (double)correctAnswers / totalQuestions * 100 : 0;
- var passed = scorePercentage >= 80; // Passing score is 80%
+ var scorePercentage = totalPoints > 0 ? (double)scorePoints / totalPoints * 100 : 0;
+ var passed = quiz.IsPassed(scorePercentage);
return new QuizResultDto(
- dto.LessonId,
+ quiz.Id,
+ quiz.LessonId,
+ quiz.Title,
totalQuestions,
- correctAnswers,
+ totalPoints,
+ scorePoints,
scorePercentage,
passed,
TimeSpan.Zero, // Would be calculated based on actual time taken
questionResults);
}
+ ///
+ /// Submits quiz answers and returns the result (legacy - converts SubmitQuizAnswersDto to SubmitQuizDto).
+ ///
+ public virtual async Task SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
+ {
+ // Convert legacy DTO to new format
+ // SubmitQuizAnswersDto has QuizId and Answers (IReadOnlyList)
+ // SubmitQuizDto has QuizId and Answers (IReadOnlyList)
+ // They're the same, so just create a new SubmitQuizDto
+ var newDto = new SubmitQuizDto(dto.QuizId, dto.Answers);
+ return await SubmitQuizAnswersForQuizAsync(userId, newDto, cancellationToken);
+ }
+
///
/// Gets the number of quiz questions for a lesson.
///
@@ -417,4 +441,43 @@ public class QuizQuestionService
return distribution;
}
+
+ // ============================================
+ // Quiz-based methods (QuizId)
+ // ============================================
+
+ ///
+ /// Gets quiz questions by quiz ID.
+ ///
+ public virtual async Task> GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionRepository.GetByQuizAsync(quizId, cancellationToken);
+ return questions.Select(q => q.ToDto()).ToList();
+ }
+
+ ///
+ /// Gets active quiz questions by quiz ID.
+ ///
+ public virtual async Task> GetActiveQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionRepository.GetActiveByQuizAsync(quizId, cancellationToken);
+ return questions.Select(q => q.ToDto()).ToList();
+ }
+
+ ///
+ /// Gets a random set of quiz questions for a quiz.
+ ///
+ public virtual async Task> GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionRepository.GetRandomQuestionsForQuizAsync(quizId, count, cancellationToken);
+ return questions.Select(q => q.ToDto()).ToList();
+ }
+
+ ///
+ /// Gets the total points for a quiz.
+ ///
+ public virtual async Task GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ return await _quizQuestionRepository.GetTotalPointsForQuizAsync(quizId, cancellationToken);
+ }
}
diff --git a/GermanApp/Application/Services/QuizService.cs b/GermanApp/Application/Services/QuizService.cs
new file mode 100644
index 0000000..b319b4c
--- /dev/null
+++ b/GermanApp/Application/Services/QuizService.cs
@@ -0,0 +1,273 @@
+using GermanApp.Application.DTOs;
+using GermanApp.Domain.Entities;
+using GermanApp.Domain.Exceptions;
+using GermanApp.Domain.Interfaces;
+
+namespace GermanApp.Application.Services;
+
+///
+/// Application service for managing quizzes.
+/// This is part of the Application layer.
+///
+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;
+ }
+
+ ///
+ /// Gets all quizzes.
+ ///
+ public virtual async Task> GetAllQuizzesAsync(CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizRepository.GetAllOrderedAsync(cancellationToken);
+ return quizzes.Select(q => q.ToDto()).ToList();
+ }
+
+ ///
+ /// Gets all quizzes as list items (summary).
+ ///
+ public virtual async Task> GetAllQuizListItemsAsync(CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizRepository.GetAllOrderedAsync(cancellationToken);
+ return quizzes.Select(q => q.ToListItemDto()).ToList();
+ }
+
+ ///
+ /// Gets a quiz by its ID.
+ ///
+ public virtual async Task GetQuizByIdAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
+ return quiz?.ToDto();
+ }
+
+ ///
+ /// Gets a quiz with its questions by ID.
+ ///
+ public virtual async Task GetQuizWithQuestionsAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizRepository.GetWithQuestionsAsync(id, cancellationToken);
+ return quiz?.ToQuizWithQuestionsDto();
+ }
+
+ ///
+ /// Gets quizzes by lesson ID.
+ ///
+ public virtual async Task> GetQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizRepository.GetByLessonAsync(lessonId, cancellationToken);
+ return quizzes.Select(q => q.ToDto()).ToList();
+ }
+
+ ///
+ /// Gets active quizzes by lesson ID.
+ ///
+ public virtual async Task> GetActiveQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizRepository.GetActiveByLessonAsync(lessonId, cancellationToken);
+ return quizzes.Select(q => q.ToDto()).ToList();
+ }
+
+ ///
+ /// Gets the first quiz for a lesson.
+ ///
+ public virtual async Task GetFirstQuizByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizRepository.GetFirstByLessonAsync(lessonId, cancellationToken);
+ return quiz?.ToDto();
+ }
+
+ ///
+ /// Creates a new quiz.
+ ///
+ public virtual async Task 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();
+ }
+
+ ///
+ /// Updates an existing quiz.
+ ///
+ public virtual async Task 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();
+ }
+
+ ///
+ /// Deletes a quiz by its ID.
+ ///
+ public virtual async Task 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;
+ }
+
+ ///
+ /// Checks if a quiz exists for a lesson.
+ ///
+ public virtual async Task ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ return await _quizRepository.ExistsByLessonAsync(lessonId, cancellationToken);
+ }
+
+ ///
+ /// Checks if a quiz exists by ID.
+ ///
+ public virtual async Task ExistsAsync(int id, CancellationToken cancellationToken = default)
+ {
+ return await _quizRepository.ExistsAsync(id, cancellationToken);
+ }
+
+ ///
+ /// Gets the total number of quizzes.
+ ///
+ public virtual async Task GetTotalQuizCountAsync(CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
+ return quizzes.Count;
+ }
+
+ ///
+ /// Gets the total number of quizzes for a lesson.
+ ///
+ public virtual async Task GetQuizCountByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizRepository.GetByLessonAsync(lessonId, cancellationToken);
+ return quizzes.Count;
+ }
+
+ ///
+ /// Gets quizzes by passing score range.
+ ///
+ public virtual async Task> 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();
+ }
+
+ ///
+ /// Gets active quizzes.
+ ///
+ public virtual async Task> GetActiveQuizzesAsync(CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
+ return quizzes.Where(q => q.IsActive)
+ .Select(q => q.ToDto())
+ .ToList();
+ }
+
+ ///
+ /// Activates a quiz.
+ ///
+ public virtual async Task 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();
+ }
+
+ ///
+ /// Deactivates a quiz.
+ ///
+ public virtual async Task 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();
+ }
+}
diff --git a/GermanApp/Application/UseCases/Commands/CreateQuizCommand.cs b/GermanApp/Application/UseCases/Commands/CreateQuizCommand.cs
new file mode 100644
index 0000000..036432a
--- /dev/null
+++ b/GermanApp/Application/UseCases/Commands/CreateQuizCommand.cs
@@ -0,0 +1,162 @@
+using GermanApp.Application.DTOs;
+using GermanApp.Domain.Entities;
+using GermanApp.Domain.Exceptions;
+using GermanApp.Domain.Interfaces;
+
+namespace GermanApp.Application.UseCases.Commands;
+
+///
+/// Command for creating a new quiz.
+/// This follows the CQRS pattern - commands represent write operations.
+///
+public record CreateQuizCommand(CreateQuizDto QuizData) : ICommand;
+
+///
+/// Handler for the CreateQuizCommand.
+///
+public class CreateQuizCommandHandler : ICommandHandler
+{
+ private readonly IQuizRepository _quizRepository;
+ private readonly ILessonRepository _lessonRepository;
+
+ public CreateQuizCommandHandler(
+ IQuizRepository quizRepository,
+ ILessonRepository lessonRepository)
+ {
+ _quizRepository = quizRepository;
+ _lessonRepository = lessonRepository;
+ }
+
+ public async Task Handle(CreateQuizCommand command, CancellationToken cancellationToken)
+ {
+ // Validate that the lesson exists
+ var lesson = await _lessonRepository.GetByIdAsync(command.QuizData.LessonId, cancellationToken);
+ if (lesson == null)
+ throw new NotFoundException("Lesson does not exist");
+
+ // Validate business rules
+ if (command.QuizData.PassingScore < 0 || command.QuizData.PassingScore > 100)
+ {
+ throw new ValidationException("Passing score must be between 0 and 100");
+ }
+
+ if (command.QuizData.TimeLimitMinutes < 0)
+ {
+ throw new ValidationException("Time limit cannot be negative");
+ }
+
+ // Convert DTO to domain entity
+ var quiz = command.QuizData.ToEntity();
+
+ // Add to repository
+ var createdQuiz = await _quizRepository.AddAsync(quiz, cancellationToken);
+
+ // Return DTO representation
+ return createdQuiz.ToDto();
+ }
+}
+
+///
+/// Command for updating an existing quiz.
+///
+public record UpdateQuizCommand(int Id, UpdateQuizDto QuizData) : ICommand;
+
+///
+/// Handler for the UpdateQuizCommand.
+///
+public class UpdateQuizCommandHandler : ICommandHandler
+{
+ private readonly IQuizRepository _quizRepository;
+ private readonly ILessonRepository _lessonRepository;
+
+ public UpdateQuizCommandHandler(
+ IQuizRepository quizRepository,
+ ILessonRepository lessonRepository)
+ {
+ _quizRepository = quizRepository;
+ _lessonRepository = lessonRepository;
+ }
+
+ public async Task Handle(UpdateQuizCommand command, CancellationToken cancellationToken)
+ {
+ var quiz = await _quizRepository.GetByIdAsync(command.Id, cancellationToken);
+ if (quiz == null)
+ return null;
+
+ // Validate that the lesson exists
+ var lesson = await _lessonRepository.GetByIdAsync(command.QuizData.LessonId, cancellationToken);
+ if (lesson == null)
+ throw new NotFoundException("Lesson does not exist");
+
+ // Validate business rules
+ if (command.QuizData.PassingScore < 0 || command.QuizData.PassingScore > 100)
+ {
+ throw new ValidationException("Passing score must be between 0 and 100");
+ }
+
+ if (command.QuizData.TimeLimitMinutes < 0)
+ {
+ throw new ValidationException("Time limit cannot be negative");
+ }
+
+ // Update the quiz
+ quiz.UpdateFromDto(command.QuizData);
+ await _quizRepository.UpdateAsync(quiz, cancellationToken);
+
+ // Reload and return
+ var updatedQuiz = await _quizRepository.GetByIdAsync(command.Id, cancellationToken);
+ return updatedQuiz?.ToDto();
+ }
+}
+
+///
+/// Command for deleting a quiz.
+///
+public record DeleteQuizCommand(int Id) : ICommand;
+
+///
+/// Handler for the DeleteQuizCommand.
+///
+public class DeleteQuizCommandHandler : ICommandHandler
+{
+ private readonly IQuizRepository _quizRepository;
+
+ public DeleteQuizCommandHandler(IQuizRepository quizRepository)
+ {
+ _quizRepository = quizRepository;
+ }
+
+ public async Task Handle(DeleteQuizCommand command, CancellationToken cancellationToken)
+ {
+ var quiz = await _quizRepository.GetByIdAsync(command.Id, cancellationToken);
+ if (quiz == null)
+ return false;
+
+ await _quizRepository.DeleteAsync(quiz, cancellationToken);
+ return true;
+ }
+}
+
+///
+/// Command for getting a quiz with its questions.
+///
+public record GetQuizWithQuestionsCommand(int Id) : ICommand;
+
+///
+/// Handler for the GetQuizWithQuestionsCommand.
+///
+public class GetQuizWithQuestionsCommandHandler : ICommandHandler
+{
+ private readonly IQuizRepository _quizRepository;
+
+ public GetQuizWithQuestionsCommandHandler(IQuizRepository quizRepository)
+ {
+ _quizRepository = quizRepository;
+ }
+
+ public async Task Handle(GetQuizWithQuestionsCommand command, CancellationToken cancellationToken)
+ {
+ var quiz = await _quizRepository.GetWithQuestionsAsync(command.Id, cancellationToken);
+ return quiz?.ToQuizWithQuestionsDto();
+ }
+}
diff --git a/GermanApp/Domain/Entities/Quiz.cs b/GermanApp/Domain/Entities/Quiz.cs
new file mode 100644
index 0000000..6d52f01
--- /dev/null
+++ b/GermanApp/Domain/Entities/Quiz.cs
@@ -0,0 +1,182 @@
+namespace GermanApp.Domain.Entities;
+
+///
+/// Represents a quiz that can be associated with a lesson.
+/// A quiz contains multiple questions and has a passing score threshold.
+///
+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 Questions { get; private set; } = new List();
+
+ ///
+ /// Constructor for EF Core deserialization.
+ ///
+ private Quiz() { }
+
+ ///
+ /// Factory method to create a new quiz.
+ ///
+ /// ID of the associated lesson
+ /// Title of the quiz
+ /// Optional description
+ /// Passing score percentage (default 80)
+ /// Time limit in minutes (0 for no limit)
+ 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
+ };
+ }
+
+ ///
+ /// Updates the quiz title.
+ ///
+ public void UpdateTitle(string newTitle)
+ {
+ if (string.IsNullOrWhiteSpace(newTitle))
+ throw new ArgumentException("Quiz title cannot be empty", nameof(newTitle));
+
+ Title = newTitle;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Updates the quiz description.
+ ///
+ public void UpdateDescription(string? newDescription)
+ {
+ Description = newDescription;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Updates the passing score.
+ ///
+ 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;
+ }
+
+ ///
+ /// Updates the time limit.
+ ///
+ public void UpdateTimeLimit(int newTimeLimitMinutes)
+ {
+ if (newTimeLimitMinutes < 0)
+ throw new ArgumentOutOfRangeException(nameof(newTimeLimitMinutes), "Time limit cannot be negative");
+
+ TimeLimitMinutes = newTimeLimitMinutes;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Updates the associated lesson.
+ ///
+ public void UpdateLesson(int newLessonId)
+ {
+ LessonId = newLessonId;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Updates the shuffle questions setting.
+ ///
+ public void UpdateShuffleQuestions(bool newShuffleQuestions)
+ {
+ ShuffleQuestions = newShuffleQuestions;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Activates the quiz.
+ ///
+ public void Activate()
+ {
+ IsActive = true;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Deactivates the quiz.
+ ///
+ public void Deactivate()
+ {
+ IsActive = false;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Adds a question to this quiz.
+ ///
+ public void AddQuestion(QuizQuestion question)
+ {
+ if (question == null)
+ throw new ArgumentNullException(nameof(question));
+
+ Questions.Add(question);
+ }
+
+ ///
+ /// Removes a question from this quiz.
+ ///
+ public void RemoveQuestion(QuizQuestion question)
+ {
+ if (question == null)
+ throw new ArgumentNullException(nameof(question));
+
+ Questions.Remove(question);
+ }
+
+ ///
+ /// Checks if the quiz is passed based on a score percentage.
+ ///
+ public bool IsPassed(double scorePercentage) => scorePercentage >= PassingScore;
+
+ ///
+ /// Calculates the score percentage based on correct answers.
+ ///
+ public double CalculateScore(int correctAnswers, int totalQuestions)
+ {
+ if (totalQuestions == 0)
+ return 0;
+
+ return (double)correctAnswers / totalQuestions * 100;
+ }
+}
diff --git a/GermanApp/Domain/Entities/QuizQuestion.cs b/GermanApp/Domain/Entities/QuizQuestion.cs
index 8758dd4..7e2af86 100644
--- a/GermanApp/Domain/Entities/QuizQuestion.cs
+++ b/GermanApp/Domain/Entities/QuizQuestion.cs
@@ -1,17 +1,18 @@
namespace GermanApp.Domain.Entities;
///
-/// Represents a quiz question for a lesson.
+/// Represents a quiz question for a quiz.
/// Each question has a type (multiple choice, true/false, fill-in-the-blank, etc.)
-/// and is associated with a specific lesson.
+/// and is associated with a specific quiz.
///
public class QuizQuestion
{
public int Id { get; private set; }
- public int LessonId { get; private set; }
+ public int QuizId { get; private set; }
public string QuestionText { get; private set; } = string.Empty;
public QuestionType Type { get; private set; }
public string? CorrectAnswer { get; private set; }
+ public int Points { get; private set; } = 1; // Points for this question
public int Difficulty { get; private set; } // 1-5 scale
public int Order { get; private set; }
public bool IsActive { get; private set; } = true;
@@ -19,7 +20,7 @@ public class QuizQuestion
public DateTime? UpdatedAt { get; private set; }
// Navigation property
- public virtual Lesson? Lesson { get; private set; }
+ public virtual Quiz? Quiz { get; private set; }
public virtual ICollection Options { get; private set; } = new List();
///
@@ -30,32 +31,38 @@ public class QuizQuestion
///
/// Factory method to create a new quiz question.
///
- /// ID of the parent lesson
+ /// ID of the parent quiz
/// The question text
/// The question type
/// The correct answer
- /// Difficulty level (1-5)
- /// Sort order within the lesson
+ /// Points for this question (default 1)
+ /// Difficulty level (1-5, default 3)
+ /// Sort order within the quiz
public static QuizQuestion Create(
- int lessonId,
+ int quizId,
string questionText,
QuestionType type,
string correctAnswer,
+ int points = 1,
int difficulty = 3,
int order = 1)
{
if (difficulty < 1 || difficulty > 5)
throw new ArgumentOutOfRangeException(nameof(difficulty), "Difficulty must be between 1 and 5");
+ if (points < 0)
+ throw new ArgumentOutOfRangeException(nameof(points), "Points must be positive");
+
if (string.IsNullOrWhiteSpace(questionText))
throw new ArgumentException("Question text cannot be empty", nameof(questionText));
return new QuizQuestion
{
- LessonId = lessonId,
+ QuizId = quizId,
QuestionText = questionText,
Type = type,
CorrectAnswer = correctAnswer,
+ Points = points,
Difficulty = difficulty,
Order = order,
CreatedAt = DateTime.UtcNow
@@ -83,6 +90,18 @@ public class QuizQuestion
UpdatedAt = DateTime.UtcNow;
}
+ ///
+ /// Updates the points for this question.
+ ///
+ public void UpdatePoints(int newPoints)
+ {
+ if (newPoints < 0)
+ throw new ArgumentOutOfRangeException(nameof(newPoints), "Points must be positive");
+
+ Points = newPoints;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
///
/// Updates the difficulty level.
///
@@ -114,11 +133,11 @@ public class QuizQuestion
}
///
- /// Updates the associated lesson.
+ /// Updates the associated quiz.
///
- public void UpdateLesson(int newLessonId)
+ public void UpdateQuiz(int newQuizId)
{
- LessonId = newLessonId;
+ QuizId = newQuizId;
UpdatedAt = DateTime.UtcNow;
}
diff --git a/GermanApp/Domain/Interfaces/IRepository.cs b/GermanApp/Domain/Interfaces/IRepository.cs
index 5fe04fc..d2e0503 100644
--- a/GermanApp/Domain/Interfaces/IRepository.cs
+++ b/GermanApp/Domain/Interfaces/IRepository.cs
@@ -173,6 +173,76 @@ public interface IQuizQuestionRepository : IRepository
/// Checks if a quiz question with the given order exists in a lesson.
///
Task OrderExistsInLessonAsync(int lessonId, int order, int? excludeId = null, CancellationToken cancellationToken = default);
+
+ // ============================================
+ // Quiz-based methods (QuizId)
+ // ============================================
+
+ ///
+ /// Gets quiz questions by quiz ID.
+ ///
+ Task> GetByQuizAsync(int quizId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets active quiz questions by quiz ID.
+ ///
+ Task> GetActiveByQuizAsync(int quizId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets quiz questions by quiz with options loaded.
+ ///
+ Task> GetByQuizWithOptionsAsync(int quizId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets a random set of quiz questions for a quiz.
+ ///
+ Task> GetRandomQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the total points for a quiz.
+ ///
+ Task GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Checks if a quiz question with the given order exists in a quiz.
+ ///
+ Task OrderExistsInQuizAsync(int quizId, int order, int? excludeId = null, CancellationToken cancellationToken = default);
+}
+
+///
+/// Repository interface for Quiz entities.
+///
+public interface IQuizRepository : IRepository
+{
+ ///
+ /// Gets quizzes by lesson ID.
+ ///
+ Task> GetByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets all quizzes ordered by lesson and quiz order.
+ ///
+ Task> GetAllOrderedAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets active quizzes by lesson ID.
+ ///
+ Task> GetActiveByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets a quiz with its questions and options.
+ ///
+ Task GetWithQuestionsAsync(int id, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the first quiz for a lesson.
+ ///
+ Task GetFirstByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Checks if a quiz exists for a lesson.
+ ///
+ Task ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
}
///
diff --git a/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs b/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
index f4023c1..6add88e 100644
--- a/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
+++ b/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
@@ -19,6 +19,7 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
public DbSet Users { get; set; } = null!;
public DbSet UserProgress { get; set; } = null!;
public DbSet RefreshTokens { get; set; } = null!;
+ public DbSet Quizzes { get; set; } = null!;
public DbSet QuizQuestions { get; set; } = null!;
public DbSet QuizOptions { get; set; } = null!;
@@ -134,17 +135,17 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
.OnDelete(DeleteBehavior.Cascade);
});
- // Configure QuizQuestion entity
- modelBuilder.Entity(builder =>
+ // Configure Quiz entity
+ modelBuilder.Entity(builder =>
{
builder.HasKey(q => q.Id);
builder.Property(q => q.LessonId).IsRequired();
- builder.Property(q => q.QuestionText).IsRequired().HasMaxLength(2000);
- builder.Property(q => q.Type).IsRequired();
- builder.Property(q => q.CorrectAnswer).HasMaxLength(1000);
- builder.Property(q => q.Difficulty).IsRequired().HasDefaultValue(3);
- builder.Property(q => q.Order).IsRequired().HasDefaultValue(1);
+ builder.Property(q => q.Title).IsRequired().HasMaxLength(200);
+ builder.Property(q => q.Description).HasMaxLength(2000);
+ builder.Property(q => q.PassingScore).IsRequired().HasDefaultValue(80);
+ builder.Property(q => q.TimeLimitMinutes).HasDefaultValue(0);
builder.Property(q => q.IsActive).HasDefaultValue(true);
+ builder.Property(q => q.ShuffleQuestions).HasDefaultValue(true);
builder.Property(q => q.CreatedAt).IsRequired();
builder.Property(q => q.UpdatedAt).IsRequired(false);
@@ -154,8 +155,36 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
.HasForeignKey(q => q.LessonId)
.OnDelete(DeleteBehavior.Cascade);
- // Unique constraint: one question per lesson per order
- builder.HasIndex(q => new { q.LessonId, q.Order }).IsUnique();
+ // Navigation to questions
+ builder.HasMany(q => q.Questions)
+ .WithOne(qq => qq.Quiz)
+ .HasForeignKey(qq => qq.QuizId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Configure QuizQuestion entity
+ modelBuilder.Entity(builder =>
+ {
+ builder.HasKey(q => q.Id);
+ builder.Property(q => q.QuizId).IsRequired();
+ builder.Property(q => q.QuestionText).IsRequired().HasMaxLength(2000);
+ builder.Property(q => q.Type).IsRequired();
+ builder.Property(q => q.CorrectAnswer).HasMaxLength(1000);
+ builder.Property(q => q.Points).IsRequired().HasDefaultValue(1);
+ builder.Property(q => q.Difficulty).IsRequired().HasDefaultValue(3);
+ builder.Property(q => q.Order).IsRequired().HasDefaultValue(1);
+ builder.Property(q => q.IsActive).HasDefaultValue(true);
+ builder.Property(q => q.CreatedAt).IsRequired();
+ builder.Property(q => q.UpdatedAt).IsRequired(false);
+
+ // Foreign key to Quiz
+ builder.HasOne(q => q.Quiz)
+ .WithMany(qq => qq.Questions)
+ .HasForeignKey(q => q.QuizId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ // Unique constraint: one question per quiz per order
+ builder.HasIndex(q => new { q.QuizId, q.Order }).IsUnique();
// Navigation to options
builder.HasMany(q => q.Options)
diff --git a/GermanApp/Infrastructure/Data/Repositories/QuizQuestionRepository.cs b/GermanApp/Infrastructure/Data/Repositories/QuizQuestionRepository.cs
index 96c8928..f3890de 100644
--- a/GermanApp/Infrastructure/Data/Repositories/QuizQuestionRepository.cs
+++ b/GermanApp/Infrastructure/Data/Repositories/QuizQuestionRepository.cs
@@ -21,7 +21,8 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
+ .ThenInclude(q => q.Lesson)
.Include(q => q.Options.OrderBy(o => o.Order))
.FirstOrDefaultAsync(q => q.Id == id, cancellationToken);
}
@@ -29,7 +30,8 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
+ .ThenInclude(q => q.Lesson)
.Include(q => q.Options)
.AsNoTracking()
.ToListAsync(cancellationToken);
@@ -63,9 +65,10 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task> GetByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
+ .ThenInclude(q => q.Lesson)
.Include(q => q.Options.OrderBy(o => o.Order))
- .Where(q => q.LessonId == lessonId)
+ .Where(q => q.Quiz != null && q.Quiz.LessonId == lessonId)
.OrderBy(q => q.Order)
.AsNoTracking()
.ToListAsync(cancellationToken);
@@ -74,9 +77,11 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task> GetAllOrderedAsync(CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
+ .ThenInclude(q => q.Lesson)
.Include(q => q.Options.OrderBy(o => o.Order))
- .OrderBy(q => q.Lesson.Order)
+ .OrderBy(q => q.Quiz.Lesson.Order)
+ .ThenBy(q => q.Quiz.Id)
.ThenBy(q => q.Order)
.AsNoTracking()
.ToListAsync(cancellationToken);
@@ -85,9 +90,10 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task> GetActiveByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
+ .ThenInclude(q => q.Lesson)
.Include(q => q.Options.OrderBy(o => o.Order))
- .Where(q => q.LessonId == lessonId && q.IsActive)
+ .Where(q => q.Quiz != null && q.Quiz.LessonId == lessonId && q.IsActive)
.OrderBy(q => q.Order)
.AsNoTracking()
.ToListAsync(cancellationToken);
@@ -96,9 +102,10 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task> GetRandomQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
+ .ThenInclude(q => q.Lesson)
.Include(q => q.Options.OrderBy(o => o.Order))
- .Where(q => q.LessonId == lessonId && q.IsActive)
+ .Where(q => q.Quiz != null && q.Quiz.LessonId == lessonId && q.IsActive)
.OrderBy(q => Guid.NewGuid()) // Random ordering
.Take(count)
.AsNoTracking()
@@ -108,7 +115,7 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task> GetByDifficultyAsync(int difficulty, CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
.Include(q => q.Options)
.Where(q => q.Difficulty == difficulty)
.OrderBy(q => q.Order)
@@ -119,7 +126,7 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task> GetByTypeAsync(QuestionType type, CancellationToken cancellationToken = default)
{
return await _context.QuizQuestions
- .Include(q => q.Lesson)
+ .Include(q => q.Quiz)
.Include(q => q.Options)
.Where(q => q.Type == type)
.OrderBy(q => q.Order)
@@ -130,7 +137,79 @@ public class QuizQuestionRepository : IQuizQuestionRepository
public async Task OrderExistsInLessonAsync(int lessonId, int order, int? excludeId = null, CancellationToken cancellationToken = default)
{
var query = _context.QuizQuestions
- .Where(q => q.LessonId == lessonId && q.Order == order);
+ .Where(q => q.Quiz != null && q.Quiz.LessonId == lessonId && q.Order == order);
+
+ if (excludeId.HasValue)
+ {
+ query = query.Where(q => q.Id != excludeId.Value);
+ }
+
+ return await query.AnyAsync(cancellationToken);
+ }
+
+ // ============================================
+ // Quiz-based methods (QuizId)
+ // ============================================
+
+ public async Task> GetByQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ return await _context.QuizQuestions
+ .Include(q => q.Quiz)
+ .Include(q => q.Options.OrderBy(o => o.Order))
+ .Where(q => q.QuizId == quizId)
+ .OrderBy(q => q.Order)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task> GetActiveByQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ return await _context.QuizQuestions
+ .Include(q => q.Quiz)
+ .Include(q => q.Options.OrderBy(o => o.Order))
+ .Where(q => q.QuizId == quizId && q.IsActive)
+ .OrderBy(q => q.Order)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task> GetByQuizWithOptionsAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ return await _context.QuizQuestions
+ .Include(q => q.Quiz)
+ .Include(q => q.Options.OrderBy(o => o.Order))
+ .Where(q => q.QuizId == quizId)
+ .OrderBy(q => q.Order)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task> GetRandomQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
+ {
+ return await _context.QuizQuestions
+ .Include(q => q.Quiz)
+ .Include(q => q.Options.OrderBy(o => o.Order))
+ .Where(q => q.QuizId == quizId && q.IsActive)
+ .OrderBy(q => Guid.NewGuid()) // Random ordering
+ .Take(count)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ var questions = await _context.QuizQuestions
+ .Where(q => q.QuizId == quizId && q.IsActive)
+ .Select(q => q.Points)
+ .ToListAsync(cancellationToken);
+
+ return questions.Sum();
+ }
+
+ public async Task OrderExistsInQuizAsync(int quizId, int order, int? excludeId = null, CancellationToken cancellationToken = default)
+ {
+ var query = _context.QuizQuestions
+ .Where(q => q.QuizId == quizId && q.Order == order);
if (excludeId.HasValue)
{
diff --git a/GermanApp/Infrastructure/Data/Repositories/QuizRepository.cs b/GermanApp/Infrastructure/Data/Repositories/QuizRepository.cs
new file mode 100644
index 0000000..4c08a7e
--- /dev/null
+++ b/GermanApp/Infrastructure/Data/Repositories/QuizRepository.cs
@@ -0,0 +1,114 @@
+using GermanApp.Domain.Entities;
+using GermanApp.Domain.Interfaces;
+using GermanApp.Infrastructure.Data.DbContext;
+using Microsoft.EntityFrameworkCore;
+
+namespace GermanApp.Infrastructure.Data.Repositories;
+
+///
+/// Entity Framework Core implementation of IQuizRepository.
+/// This is part of the Infrastructure layer.
+///
+public class QuizRepository : IQuizRepository
+{
+ private readonly AppDbContext _context;
+
+ public QuizRepository(AppDbContext context)
+ {
+ _context = context;
+ }
+
+ public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .Include(q => q.Lesson)
+ .FirstOrDefaultAsync(q => q.Id == id, cancellationToken);
+ }
+
+ public async Task> GetAllAsync(CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .Include(q => q.Lesson)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task AddAsync(Quiz entity, CancellationToken cancellationToken = default)
+ {
+ await _context.Quizzes.AddAsync(entity, cancellationToken);
+ await _context.SaveChangesAsync(cancellationToken);
+ return entity;
+ }
+
+ public async Task UpdateAsync(Quiz entity, CancellationToken cancellationToken = default)
+ {
+ _context.Quizzes.Update(entity);
+ await _context.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task DeleteAsync(Quiz entity, CancellationToken cancellationToken = default)
+ {
+ _context.Quizzes.Remove(entity);
+ await _context.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task ExistsAsync(int id, CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .AnyAsync(q => q.Id == id, cancellationToken);
+ }
+
+ public async Task> GetByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .Include(q => q.Lesson)
+ .Where(q => q.LessonId == lessonId)
+ .OrderBy(q => q.Id)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task> GetAllOrderedAsync(CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .Include(q => q.Lesson)
+ .OrderBy(q => q.Lesson.Order)
+ .ThenBy(q => q.Id)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task> GetActiveByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .Include(q => q.Lesson)
+ .Where(q => q.LessonId == lessonId && q.IsActive)
+ .OrderBy(q => q.Id)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task GetWithQuestionsAsync(int id, CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .Include(q => q.Lesson)
+ .Include(q => q.Questions.OrderBy(qq => qq.Order))
+ .ThenInclude(qq => qq.Options.OrderBy(o => o.Order))
+ .FirstOrDefaultAsync(q => q.Id == id, cancellationToken);
+ }
+
+ public async Task GetFirstByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .Include(q => q.Lesson)
+ .Where(q => q.LessonId == lessonId && q.IsActive)
+ .OrderBy(q => q.Id)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
+ public async Task ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ return await _context.Quizzes
+ .AnyAsync(q => q.LessonId == lessonId, cancellationToken);
+ }
+}
diff --git a/GermanApp/Presentation/Controllers/QuizQuestionsController.cs b/GermanApp/Presentation/Controllers/QuizQuestionsController.cs
index 6c56470..4eeb360 100644
--- a/GermanApp/Presentation/Controllers/QuizQuestionsController.cs
+++ b/GermanApp/Presentation/Controllers/QuizQuestionsController.cs
@@ -35,7 +35,20 @@ public class QuizQuestionsController : ControllerBase
}
///
- /// Gets all active quiz questions for a specific lesson.
+ /// Gets all active quiz questions for a specific quiz.
+ ///
+ /// The quiz ID
+ /// List of quiz questions for the specified quiz
+ [HttpGet("by-quiz/{quizId}")]
+ [AllowAnonymous]
+ public async Task GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionService.GetActiveQuizQuestionsByQuizAsync(quizId, cancellationToken);
+ return Ok(questions);
+ }
+
+ ///
+ /// Gets all active quiz questions for a specific lesson (legacy - uses first quiz).
///
/// The lesson ID
/// List of quiz questions for the specified lesson
@@ -63,12 +76,26 @@ public class QuizQuestionsController : ControllerBase
}
///
- /// Gets a random set of quiz questions for a lesson.
+ /// Gets a random set of quiz questions for a quiz.
+ ///
+ /// The quiz ID
+ /// Number of questions to return
+ /// List of random quiz questions for the quiz
+ [HttpGet("random/{quizId}/{count}")]
+ [AllowAnonymous]
+ public async Task GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionService.GetRandomQuizQuestionsForQuizAsync(quizId, count, cancellationToken);
+ return Ok(questions);
+ }
+
+ ///
+ /// Gets a random set of quiz questions for a lesson (legacy - uses first quiz).
///
/// The lesson ID
/// Number of questions to return
/// List of random quiz questions for the lesson
- [HttpGet("random/{lessonId}/{count}")]
+ [HttpGet("random/by-lesson/{lessonId}/{count}")]
[AllowAnonymous]
public async Task GetRandomQuizQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
{
@@ -105,7 +132,7 @@ public class QuizQuestionsController : ControllerBase
///
/// Creates a new quiz question.
///
- /// The quiz question data
+ /// The quiz question data (requires QuizId)
/// The created quiz question
[HttpPost]
[Authorize(Roles = "Admin")]
@@ -236,13 +263,15 @@ public class QuizQuestionsController : ControllerBase
/// Submits quiz answers and returns the result.
///
/// The user ID
- /// The quiz answers submission
+ /// The quiz answers submission (requires QuizId)
/// The quiz result
[HttpPost("submit/{userId}")]
[Authorize]
public async Task SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
{
- var result = await _quizQuestionService.SubmitQuizAnswersAsync(userId, dto, cancellationToken);
+ // Convert legacy DTO to new format
+ var newDto = new SubmitQuizDto(dto.QuizId, dto.Answers);
+ var result = await _quizQuestionService.SubmitQuizAnswersForQuizAsync(userId, newDto, cancellationToken);
return Ok(result);
}
diff --git a/GermanApp/Presentation/Controllers/QuizzesController.cs b/GermanApp/Presentation/Controllers/QuizzesController.cs
new file mode 100644
index 0000000..097c464
--- /dev/null
+++ b/GermanApp/Presentation/Controllers/QuizzesController.cs
@@ -0,0 +1,344 @@
+using GermanApp.Application.DTOs;
+using GermanApp.Application.Services;
+using GermanApp.Domain.Entities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace GermanApp.Presentation.Controllers;
+
+///
+/// API controller for managing quizzes.
+/// This is part of the Presentation layer.
+///
+[ApiController]
+[Route("api/[controller]")]
+[Authorize]
+public class QuizzesController : ControllerBase
+{
+ private readonly QuizService _quizService;
+ private readonly QuizQuestionService _quizQuestionService;
+
+ public QuizzesController(
+ QuizService quizService,
+ QuizQuestionService quizQuestionService)
+ {
+ _quizService = quizService;
+ _quizQuestionService = quizQuestionService;
+ }
+
+ ///
+ /// Gets all quizzes.
+ ///
+ /// List of all quizzes
+ [HttpGet]
+ [AllowAnonymous]
+ public async Task GetAllQuizzesAsync(CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizService.GetAllQuizzesAsync(cancellationToken);
+ return Ok(quizzes);
+ }
+
+ ///
+ /// Gets all quizzes as list items (summary).
+ ///
+ /// List of quiz summary items
+ [HttpGet("list")]
+ [AllowAnonymous]
+ public async Task GetAllQuizListItemsAsync(CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizService.GetAllQuizListItemsAsync(cancellationToken);
+ return Ok(quizzes);
+ }
+
+ ///
+ /// Gets a specific quiz by its ID.
+ ///
+ /// The quiz ID
+ /// The quiz with the specified ID
+ [HttpGet("{id}")]
+ [AllowAnonymous]
+ public async Task GetQuizByIdAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizService.GetQuizByIdAsync(id, cancellationToken);
+ if (quiz == null)
+ return NotFound();
+ return Ok(quiz);
+ }
+
+ ///
+ /// Gets a quiz with its questions by ID.
+ ///
+ /// The quiz ID
+ /// The quiz with questions
+ [HttpGet("{id}/with-questions")]
+ [AllowAnonymous]
+ public async Task GetQuizWithQuestionsAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizService.GetQuizWithQuestionsAsync(id, cancellationToken);
+ if (quiz == null)
+ return NotFound();
+ return Ok(quiz);
+ }
+
+ ///
+ /// Gets all quizzes for a specific lesson.
+ ///
+ /// The lesson ID
+ /// List of quizzes for the specified lesson
+ [HttpGet("by-lesson/{lessonId}")]
+ [AllowAnonymous]
+ public async Task GetQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizService.GetQuizzesByLessonAsync(lessonId, cancellationToken);
+ return Ok(quizzes);
+ }
+
+ ///
+ /// Gets active quizzes for a specific lesson.
+ ///
+ /// The lesson ID
+ /// List of active quizzes for the specified lesson
+ [HttpGet("active/by-lesson/{lessonId}")]
+ [AllowAnonymous]
+ public async Task GetActiveQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizService.GetActiveQuizzesByLessonAsync(lessonId, cancellationToken);
+ return Ok(quizzes);
+ }
+
+ ///
+ /// Gets the first quiz for a lesson.
+ ///
+ /// The lesson ID
+ /// The first quiz for the lesson
+ [HttpGet("first/by-lesson/{lessonId}")]
+ [AllowAnonymous]
+ public async Task GetFirstQuizByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizService.GetFirstQuizByLessonAsync(lessonId, cancellationToken);
+ if (quiz == null)
+ return NotFound();
+ return Ok(quiz);
+ }
+
+ ///
+ /// Gets all active quizzes.
+ ///
+ /// List of active quizzes
+ [HttpGet("active")]
+ [AllowAnonymous]
+ public async Task GetActiveQuizzesAsync(CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizService.GetActiveQuizzesAsync(cancellationToken);
+ return Ok(quizzes);
+ }
+
+ ///
+ /// Creates a new quiz.
+ ///
+ /// The quiz data
+ /// The created quiz
+ [HttpPost]
+ [Authorize(Roles = "Admin")]
+ public async Task CreateQuizAsync(CreateQuizDto dto, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizService.CreateQuizAsync(dto, cancellationToken);
+ return CreatedAtAction(nameof(GetQuizByIdAsync), new { id = quiz.Id }, quiz);
+ }
+
+ ///
+ /// Updates an existing quiz.
+ ///
+ /// The quiz ID
+ /// The updated quiz data
+ /// The updated quiz
+ [HttpPut("{id}")]
+ [Authorize(Roles = "Admin")]
+ public async Task UpdateQuizAsync(int id, UpdateQuizDto dto, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizService.UpdateQuizAsync(id, dto, cancellationToken);
+ if (quiz == null)
+ return NotFound();
+ return Ok(quiz);
+ }
+
+ ///
+ /// Deletes a quiz by its ID.
+ ///
+ /// The quiz ID
+ /// No content on success
+ [HttpDelete("{id}")]
+ [Authorize(Roles = "Admin")]
+ public async Task DeleteQuizAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var result = await _quizService.DeleteQuizAsync(id, cancellationToken);
+ if (!result)
+ return NotFound();
+ return NoContent();
+ }
+
+ ///
+ /// Activates a quiz.
+ ///
+ /// The quiz ID
+ /// The activated quiz
+ [HttpPost("{id}/activate")]
+ [Authorize(Roles = "Admin")]
+ public async Task ActivateQuizAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizService.ActivateQuizAsync(id, cancellationToken);
+ if (quiz == null)
+ return NotFound();
+ return Ok(quiz);
+ }
+
+ ///
+ /// Deactivates a quiz.
+ ///
+ /// The quiz ID
+ /// The deactivated quiz
+ [HttpPost("{id}/deactivate")]
+ [Authorize(Roles = "Admin")]
+ public async Task DeactivateQuizAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var quiz = await _quizService.DeactivateQuizAsync(id, cancellationToken);
+ if (quiz == null)
+ return NotFound();
+ return Ok(quiz);
+ }
+
+ ///
+ /// Checks if a quiz exists by ID.
+ ///
+ /// The quiz ID
+ /// True if the quiz exists
+ [HttpGet("exists/{id}")]
+ [AllowAnonymous]
+ public async Task ExistsAsync(int id, CancellationToken cancellationToken = default)
+ {
+ var exists = await _quizService.ExistsAsync(id, cancellationToken);
+ return Ok(new { exists });
+ }
+
+ ///
+ /// Checks if a quiz exists for a lesson.
+ ///
+ /// The lesson ID
+ /// True if a quiz exists for the lesson
+ [HttpGet("exists/by-lesson/{lessonId}")]
+ [AllowAnonymous]
+ public async Task ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var exists = await _quizService.ExistsByLessonAsync(lessonId, cancellationToken);
+ return Ok(new { lessonId, exists });
+ }
+
+ ///
+ /// Gets the total number of quizzes.
+ ///
+ /// The total count of quizzes
+ [HttpGet("count")]
+ [AllowAnonymous]
+ public async Task GetTotalQuizCountAsync(CancellationToken cancellationToken = default)
+ {
+ var count = await _quizService.GetTotalQuizCountAsync(cancellationToken);
+ return Ok(new { count });
+ }
+
+ ///
+ /// Gets the number of quizzes for a lesson.
+ ///
+ /// The lesson ID
+ /// The count of quizzes for the lesson
+ [HttpGet("count/by-lesson/{lessonId}")]
+ [AllowAnonymous]
+ public async Task GetQuizCountByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
+ {
+ var count = await _quizService.GetQuizCountByLessonAsync(lessonId, cancellationToken);
+ return Ok(new { lessonId, count });
+ }
+
+ ///
+ /// Gets quizzes by passing score range.
+ ///
+ /// Minimum passing score
+ /// Maximum passing score
+ /// List of quizzes in the score range
+ [HttpGet("by-passing-score/{minScore}/{maxScore}")]
+ [AllowAnonymous]
+ public async Task GetQuizzesByPassingScoreRangeAsync(int minScore, int maxScore, CancellationToken cancellationToken = default)
+ {
+ var quizzes = await _quizService.GetQuizzesByPassingScoreRangeAsync(minScore, maxScore, cancellationToken);
+ return Ok(quizzes);
+ }
+
+ ///
+ /// Gets quiz questions for a specific quiz.
+ ///
+ /// The quiz ID
+ /// List of quiz questions for the quiz
+ [HttpGet("{quizId}/questions")]
+ [AllowAnonymous]
+ public async Task GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionService.GetQuizQuestionsByQuizAsync(quizId, cancellationToken);
+ return Ok(questions);
+ }
+
+ ///
+ /// Gets active quiz questions for a specific quiz.
+ ///
+ /// The quiz ID
+ /// List of active quiz questions for the quiz
+ [HttpGet("{quizId}/questions/active")]
+ [AllowAnonymous]
+ public async Task GetActiveQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionService.GetActiveQuizQuestionsByQuizAsync(quizId, cancellationToken);
+ return Ok(questions);
+ }
+
+ ///
+ /// Gets random quiz questions for a quiz.
+ ///
+ /// The quiz ID
+ /// Number of questions to return
+ /// List of random quiz questions
+ [HttpGet("{quizId}/questions/random/{count}")]
+ [AllowAnonymous]
+ public async Task GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
+ {
+ var questions = await _quizQuestionService.GetRandomQuizQuestionsForQuizAsync(quizId, count, cancellationToken);
+ return Ok(questions);
+ }
+
+ ///
+ /// Submits quiz answers and returns the result.
+ ///
+ /// The user ID
+ /// The quiz ID
+ /// The quiz answers submission
+ /// The quiz result
+ [HttpPost("submit/{userId}/{quizId}")]
+ [Authorize]
+ public async Task SubmitQuizAnswersAsync(int userId, int quizId, SubmitQuizDto dto, CancellationToken cancellationToken = default)
+ {
+ // Update the submission to use the correct quizId
+ var submission = new SubmitQuizDto(quizId, dto.Answers);
+ var result = await _quizQuestionService.SubmitQuizAnswersForQuizAsync(userId, submission, cancellationToken);
+ return Ok(result);
+ }
+
+ ///
+ /// Gets the total points for a quiz.
+ ///
+ /// The quiz ID
+ /// The total points for the quiz
+ [HttpGet("{quizId}/total-points")]
+ [AllowAnonymous]
+ public async Task GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default)
+ {
+ var points = await _quizQuestionService.GetTotalPointsForQuizAsync(quizId, cancellationToken);
+ return Ok(new { quizId, points });
+ }
+}
diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs
index 7912aba..13647b4 100644
--- a/GermanApp/Program.cs
+++ b/GermanApp/Program.cs
@@ -134,6 +134,7 @@ try
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
@@ -171,8 +172,17 @@ try
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped, CreateLessonCommandHandler>();
+
+ // Note: QuizQuestionService requires IQuizRepository which is registered above
+
+ // Register Quiz command handlers
+ builder.Services.AddScoped, CreateQuizCommandHandler>();
+ builder.Services.AddScoped, UpdateQuizCommandHandler>();
+ builder.Services.AddScoped, DeleteQuizCommandHandler>();
+ builder.Services.AddScoped, GetQuizWithQuestionsCommandHandler>();
var app = builder.Build();