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>
This commit is contained in:
parent
04ad7ef008
commit
242d5ea60b
14 changed files with 1618 additions and 77 deletions
151
GermanApp/Application/DTOs/QuizDto.cs
Normal file
151
GermanApp/Application/DTOs/QuizDto.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
using GermanApp.Domain.Entities;
|
||||||
|
|
||||||
|
namespace GermanApp.Application.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for Quiz - used for API responses.
|
||||||
|
/// This is a read-only representation of a Quiz entity.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for creating a new Quiz.
|
||||||
|
/// </summary>
|
||||||
|
public record CreateQuizDto(
|
||||||
|
int LessonId,
|
||||||
|
string Title,
|
||||||
|
string? Description,
|
||||||
|
int PassingScore,
|
||||||
|
int TimeLimitMinutes,
|
||||||
|
bool ShuffleQuestions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for updating an existing Quiz.
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateQuizDto(
|
||||||
|
int LessonId,
|
||||||
|
string Title,
|
||||||
|
string? Description,
|
||||||
|
int PassingScore,
|
||||||
|
int TimeLimitMinutes,
|
||||||
|
bool ShuffleQuestions,
|
||||||
|
bool IsActive);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for quiz with its questions.
|
||||||
|
/// </summary>
|
||||||
|
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<QuizQuestionDto> Questions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for quiz list item (summary).
|
||||||
|
/// </summary>
|
||||||
|
public record QuizListItemDto(
|
||||||
|
int Id,
|
||||||
|
int LessonId,
|
||||||
|
string LessonTitle,
|
||||||
|
string Title,
|
||||||
|
bool IsActive,
|
||||||
|
int QuestionCount,
|
||||||
|
DateTime CreatedAt);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object for submitting quiz answers.
|
||||||
|
/// </summary>
|
||||||
|
public record SubmitQuizDto(
|
||||||
|
int QuizId,
|
||||||
|
IReadOnlyList<QuizAnswerDto> Answers);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for mapping between Quiz entity and DTOs.
|
||||||
|
/// </summary>
|
||||||
|
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<QuizQuestionDto>());
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,11 +8,14 @@ namespace GermanApp.Application.DTOs;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record QuizQuestionDto(
|
public record QuizQuestionDto(
|
||||||
int Id,
|
int Id,
|
||||||
|
int QuizId,
|
||||||
int LessonId,
|
int LessonId,
|
||||||
|
string QuizTitle,
|
||||||
string LessonTitle,
|
string LessonTitle,
|
||||||
string QuestionText,
|
string QuestionText,
|
||||||
QuestionType Type,
|
QuestionType Type,
|
||||||
string? CorrectAnswer,
|
string? CorrectAnswer,
|
||||||
|
int Points,
|
||||||
int Difficulty,
|
int Difficulty,
|
||||||
int Order,
|
int Order,
|
||||||
bool IsActive,
|
bool IsActive,
|
||||||
|
|
@ -24,10 +27,11 @@ public record QuizQuestionDto(
|
||||||
/// Data Transfer Object for creating a new QuizQuestion.
|
/// Data Transfer Object for creating a new QuizQuestion.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateQuizQuestionDto(
|
public record CreateQuizQuestionDto(
|
||||||
int LessonId,
|
int QuizId,
|
||||||
string QuestionText,
|
string QuestionText,
|
||||||
QuestionType Type,
|
QuestionType Type,
|
||||||
string? CorrectAnswer,
|
string? CorrectAnswer,
|
||||||
|
int Points,
|
||||||
int Difficulty,
|
int Difficulty,
|
||||||
int Order,
|
int Order,
|
||||||
IReadOnlyList<CreateQuizOptionDto>? Options);
|
IReadOnlyList<CreateQuizOptionDto>? Options);
|
||||||
|
|
@ -36,10 +40,11 @@ public record CreateQuizQuestionDto(
|
||||||
/// Data Transfer Object for updating an existing QuizQuestion.
|
/// Data Transfer Object for updating an existing QuizQuestion.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record UpdateQuizQuestionDto(
|
public record UpdateQuizQuestionDto(
|
||||||
int LessonId,
|
int QuizId,
|
||||||
string QuestionText,
|
string QuestionText,
|
||||||
QuestionType Type,
|
QuestionType Type,
|
||||||
string? CorrectAnswer,
|
string? CorrectAnswer,
|
||||||
|
int Points,
|
||||||
int Difficulty,
|
int Difficulty,
|
||||||
int Order,
|
int Order,
|
||||||
bool IsActive,
|
bool IsActive,
|
||||||
|
|
@ -77,9 +82,11 @@ public record UpdateQuizOptionDto(
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record QuizQuestionWithAnswerDto(
|
public record QuizQuestionWithAnswerDto(
|
||||||
int QuizQuestionId,
|
int QuizQuestionId,
|
||||||
|
int QuizId,
|
||||||
int LessonId,
|
int LessonId,
|
||||||
string QuestionText,
|
string QuestionText,
|
||||||
QuestionType Type,
|
QuestionType Type,
|
||||||
|
int Points,
|
||||||
IReadOnlyList<QuizOptionDto> Options,
|
IReadOnlyList<QuizOptionDto> Options,
|
||||||
string? UserAnswer,
|
string? UserAnswer,
|
||||||
bool IsCorrect);
|
bool IsCorrect);
|
||||||
|
|
@ -88,7 +95,7 @@ public record QuizQuestionWithAnswerDto(
|
||||||
/// Data Transfer Object for submitting quiz answers.
|
/// Data Transfer Object for submitting quiz answers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record SubmitQuizAnswersDto(
|
public record SubmitQuizAnswersDto(
|
||||||
int LessonId,
|
int QuizId,
|
||||||
IReadOnlyList<QuizAnswerDto> Answers);
|
IReadOnlyList<QuizAnswerDto> Answers);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -103,9 +110,12 @@ public record QuizAnswerDto(
|
||||||
/// Data Transfer Object for quiz result.
|
/// Data Transfer Object for quiz result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record QuizResultDto(
|
public record QuizResultDto(
|
||||||
|
int QuizId,
|
||||||
int LessonId,
|
int LessonId,
|
||||||
|
string QuizTitle,
|
||||||
int TotalQuestions,
|
int TotalQuestions,
|
||||||
int CorrectAnswers,
|
int TotalPoints,
|
||||||
|
int ScorePoints,
|
||||||
double ScorePercentage,
|
double ScorePercentage,
|
||||||
bool Passed,
|
bool Passed,
|
||||||
TimeSpan TimeTaken,
|
TimeSpan TimeTaken,
|
||||||
|
|
@ -118,6 +128,7 @@ public record QuizQuestionResultDto(
|
||||||
int QuizQuestionId,
|
int QuizQuestionId,
|
||||||
string QuestionText,
|
string QuestionText,
|
||||||
QuestionType Type,
|
QuestionType Type,
|
||||||
|
int Points,
|
||||||
bool IsCorrect,
|
bool IsCorrect,
|
||||||
string? CorrectAnswer,
|
string? CorrectAnswer,
|
||||||
string? UserAnswer);
|
string? UserAnswer);
|
||||||
|
|
@ -129,11 +140,14 @@ public static class QuizQuestionDtoExtensions
|
||||||
{
|
{
|
||||||
public static QuizQuestionDto ToDto(this QuizQuestion quizQuestion) => new(
|
public static QuizQuestionDto ToDto(this QuizQuestion quizQuestion) => new(
|
||||||
quizQuestion.Id,
|
quizQuestion.Id,
|
||||||
quizQuestion.LessonId,
|
quizQuestion.QuizId,
|
||||||
quizQuestion.Lesson?.Title ?? "Unknown",
|
quizQuestion.Quiz?.LessonId ?? 0,
|
||||||
|
quizQuestion.Quiz?.Title ?? "Unknown",
|
||||||
|
quizQuestion.Quiz?.Lesson?.Title ?? "Unknown",
|
||||||
quizQuestion.QuestionText,
|
quizQuestion.QuestionText,
|
||||||
quizQuestion.Type,
|
quizQuestion.Type,
|
||||||
quizQuestion.CorrectAnswer,
|
quizQuestion.CorrectAnswer,
|
||||||
|
quizQuestion.Points,
|
||||||
quizQuestion.Difficulty,
|
quizQuestion.Difficulty,
|
||||||
quizQuestion.Order,
|
quizQuestion.Order,
|
||||||
quizQuestion.IsActive,
|
quizQuestion.IsActive,
|
||||||
|
|
@ -143,19 +157,21 @@ public static class QuizQuestionDtoExtensions
|
||||||
|
|
||||||
public static QuizQuestion ToEntity(this CreateQuizQuestionDto dto) =>
|
public static QuizQuestion ToEntity(this CreateQuizQuestionDto dto) =>
|
||||||
QuizQuestion.Create(
|
QuizQuestion.Create(
|
||||||
dto.LessonId,
|
dto.QuizId,
|
||||||
dto.QuestionText,
|
dto.QuestionText,
|
||||||
dto.Type,
|
dto.Type,
|
||||||
dto.CorrectAnswer ?? string.Empty,
|
dto.CorrectAnswer ?? string.Empty,
|
||||||
|
dto.Points,
|
||||||
dto.Difficulty,
|
dto.Difficulty,
|
||||||
dto.Order);
|
dto.Order);
|
||||||
|
|
||||||
public static void UpdateFromDto(this QuizQuestion quizQuestion, UpdateQuizQuestionDto dto)
|
public static void UpdateFromDto(this QuizQuestion quizQuestion, UpdateQuizQuestionDto dto)
|
||||||
{
|
{
|
||||||
quizQuestion.UpdateLesson(dto.LessonId);
|
quizQuestion.UpdateQuiz(dto.QuizId);
|
||||||
quizQuestion.UpdateQuestionText(dto.QuestionText);
|
quizQuestion.UpdateQuestionText(dto.QuestionText);
|
||||||
quizQuestion.UpdateType(dto.Type);
|
quizQuestion.UpdateType(dto.Type);
|
||||||
quizQuestion.UpdateCorrectAnswer(dto.CorrectAnswer ?? string.Empty);
|
quizQuestion.UpdateCorrectAnswer(dto.CorrectAnswer ?? string.Empty);
|
||||||
|
quizQuestion.UpdatePoints(dto.Points);
|
||||||
quizQuestion.UpdateDifficulty(dto.Difficulty);
|
quizQuestion.UpdateDifficulty(dto.Difficulty);
|
||||||
quizQuestion.UpdateOrder(dto.Order);
|
quizQuestion.UpdateOrder(dto.Order);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using GermanApp.Application.DTOs;
|
using GermanApp.Application.DTOs;
|
||||||
using GermanApp.Domain.Entities;
|
using GermanApp.Domain.Entities;
|
||||||
|
using GermanApp.Domain.Exceptions;
|
||||||
using GermanApp.Domain.Interfaces;
|
using GermanApp.Domain.Interfaces;
|
||||||
|
|
||||||
namespace GermanApp.Application.Services;
|
namespace GermanApp.Application.Services;
|
||||||
|
|
@ -12,15 +13,18 @@ public class QuizQuestionService
|
||||||
{
|
{
|
||||||
private readonly IQuizQuestionRepository _quizQuestionRepository;
|
private readonly IQuizQuestionRepository _quizQuestionRepository;
|
||||||
private readonly IQuizOptionRepository _quizOptionRepository;
|
private readonly IQuizOptionRepository _quizOptionRepository;
|
||||||
|
private readonly IQuizRepository _quizRepository;
|
||||||
private readonly ILessonRepository _lessonRepository;
|
private readonly ILessonRepository _lessonRepository;
|
||||||
|
|
||||||
public QuizQuestionService(
|
public QuizQuestionService(
|
||||||
IQuizQuestionRepository quizQuestionRepository,
|
IQuizQuestionRepository quizQuestionRepository,
|
||||||
IQuizOptionRepository quizOptionRepository,
|
IQuizOptionRepository quizOptionRepository,
|
||||||
|
IQuizRepository quizRepository,
|
||||||
ILessonRepository lessonRepository)
|
ILessonRepository lessonRepository)
|
||||||
{
|
{
|
||||||
_quizQuestionRepository = quizQuestionRepository;
|
_quizQuestionRepository = quizQuestionRepository;
|
||||||
_quizOptionRepository = quizOptionRepository;
|
_quizOptionRepository = quizOptionRepository;
|
||||||
|
_quizRepository = quizRepository;
|
||||||
_lessonRepository = lessonRepository;
|
_lessonRepository = lessonRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,13 +96,13 @@ public class QuizQuestionService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<QuizQuestionDto> CreateQuizQuestionAsync(CreateQuizQuestionDto dto, CancellationToken cancellationToken = default)
|
public virtual async Task<QuizQuestionDto> CreateQuizQuestionAsync(CreateQuizQuestionDto dto, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// Validate that the lesson exists
|
// Validate that the quiz exists
|
||||||
var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
|
var quiz = await _quizRepository.GetByIdAsync(dto.QuizId, cancellationToken);
|
||||||
if (lesson == null)
|
if (quiz == null)
|
||||||
throw new ArgumentException("Lesson does not exist");
|
throw new NotFoundException("Quiz does not exist");
|
||||||
|
|
||||||
// Validate question order uniqueness within the lesson
|
// Validate question order uniqueness within the quiz
|
||||||
await ValidateQuestionOrderUniquenessAsync(0, dto.LessonId, dto.Order, cancellationToken);
|
await ValidateQuestionOrderUniquenessAsync(0, dto.QuizId, dto.Order, cancellationToken);
|
||||||
|
|
||||||
// Create the question
|
// Create the question
|
||||||
var question = dto.ToEntity();
|
var question = dto.ToEntity();
|
||||||
|
|
@ -128,13 +132,13 @@ public class QuizQuestionService
|
||||||
if (question == null)
|
if (question == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Validate that the lesson exists
|
// Validate that the quiz exists
|
||||||
var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
|
var quiz = await _quizRepository.GetByIdAsync(dto.QuizId, cancellationToken);
|
||||||
if (lesson == null)
|
if (quiz == null)
|
||||||
throw new ArgumentException("Lesson does not exist");
|
throw new NotFoundException("Quiz does not exist");
|
||||||
|
|
||||||
// Validate question order uniqueness within the lesson
|
// Validate question order uniqueness within the quiz
|
||||||
await ValidateQuestionOrderUniquenessAsync(id, dto.LessonId, dto.Order, cancellationToken);
|
await ValidateQuestionOrderUniquenessAsync(id, dto.QuizId, dto.Order, cancellationToken);
|
||||||
|
|
||||||
// Update the question
|
// Update the question
|
||||||
question.UpdateFromDto(dto);
|
question.UpdateFromDto(dto);
|
||||||
|
|
@ -160,13 +164,13 @@ public class QuizQuestionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates that the question order is unique within its lesson.
|
/// Validates that the question order is unique within its quiz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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)
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -277,18 +281,21 @@ public class QuizQuestionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Submits quiz answers and returns the result.
|
/// Submits quiz answers for a specific quiz and returns the result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<QuizResultDto> SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
|
public virtual async Task<QuizResultDto> SubmitQuizAnswersForQuizAsync(int userId, SubmitQuizDto dto, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var questions = await _quizQuestionRepository.GetByLessonAsync(dto.LessonId, cancellationToken);
|
var quiz = await _quizRepository.GetWithQuestionsAsync(dto.QuizId, cancellationToken);
|
||||||
var questionDictionary = questions.ToDictionary(q => q.Id);
|
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<QuizQuestion>();
|
||||||
var totalQuestions = questions.Count;
|
var totalQuestions = questions.Count;
|
||||||
var correctAnswers = 0;
|
var totalPoints = questions.Sum(q => q.Points);
|
||||||
|
var scorePoints = 0;
|
||||||
var questionResults = new List<QuizQuestionResultDto>();
|
var questionResults = new List<QuizQuestionResultDto>();
|
||||||
|
|
||||||
// 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);
|
var userAnswers = dto.Answers.ToDictionary(a => a.QuizQuestionId);
|
||||||
|
|
||||||
foreach (var question in questions)
|
foreach (var question in questions)
|
||||||
|
|
@ -299,7 +306,7 @@ public class QuizQuestionService
|
||||||
|
|
||||||
if (userAnswers.TryGetValue(question.Id, out var userAnswer))
|
if (userAnswers.TryGetValue(question.Id, out var userAnswer))
|
||||||
{
|
{
|
||||||
userAnswerText = userAnswer?.AnswerText ?? string.Empty;
|
userAnswerText = userAnswer.AnswerText ?? string.Empty;
|
||||||
|
|
||||||
switch (question.Type)
|
switch (question.Type)
|
||||||
{
|
{
|
||||||
|
|
@ -341,7 +348,7 @@ public class QuizQuestionService
|
||||||
// Case-insensitive comparison with trimming
|
// Case-insensitive comparison with trimming
|
||||||
if (!string.IsNullOrEmpty(userAnswerText) && !string.IsNullOrEmpty(correctAnswer))
|
if (!string.IsNullOrEmpty(userAnswerText) && !string.IsNullOrEmpty(correctAnswer))
|
||||||
{
|
{
|
||||||
isCorrect = userAnswerText.Equals(correctAnswer, StringComparison.OrdinalIgnoreCase);
|
isCorrect = userAnswerText.Trim().Equals(correctAnswer.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -353,30 +360,47 @@ public class QuizQuestionService
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCorrect)
|
if (isCorrect)
|
||||||
correctAnswers++;
|
scorePoints += question.Points;
|
||||||
|
|
||||||
questionResults.Add(new QuizQuestionResultDto(
|
questionResults.Add(new QuizQuestionResultDto(
|
||||||
question.Id,
|
question.Id,
|
||||||
question.QuestionText,
|
question.QuestionText,
|
||||||
question.Type,
|
question.Type,
|
||||||
|
question.Points,
|
||||||
isCorrect,
|
isCorrect,
|
||||||
correctAnswer,
|
correctAnswer,
|
||||||
userAnswerText));
|
userAnswerText));
|
||||||
}
|
}
|
||||||
|
|
||||||
var scorePercentage = totalQuestions > 0 ? (double)correctAnswers / totalQuestions * 100 : 0;
|
var scorePercentage = totalPoints > 0 ? (double)scorePoints / totalPoints * 100 : 0;
|
||||||
var passed = scorePercentage >= 80; // Passing score is 80%
|
var passed = quiz.IsPassed(scorePercentage);
|
||||||
|
|
||||||
return new QuizResultDto(
|
return new QuizResultDto(
|
||||||
dto.LessonId,
|
quiz.Id,
|
||||||
|
quiz.LessonId,
|
||||||
|
quiz.Title,
|
||||||
totalQuestions,
|
totalQuestions,
|
||||||
correctAnswers,
|
totalPoints,
|
||||||
|
scorePoints,
|
||||||
scorePercentage,
|
scorePercentage,
|
||||||
passed,
|
passed,
|
||||||
TimeSpan.Zero, // Would be calculated based on actual time taken
|
TimeSpan.Zero, // Would be calculated based on actual time taken
|
||||||
questionResults);
|
questionResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Submits quiz answers and returns the result (legacy - converts SubmitQuizAnswersDto to SubmitQuizDto).
|
||||||
|
/// </summary>
|
||||||
|
public virtual async Task<QuizResultDto> SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Convert legacy DTO to new format
|
||||||
|
// SubmitQuizAnswersDto has QuizId and Answers (IReadOnlyList<QuizAnswerDto>)
|
||||||
|
// SubmitQuizDto has QuizId and Answers (IReadOnlyList<QuizAnswerDto>)
|
||||||
|
// They're the same, so just create a new SubmitQuizDto
|
||||||
|
var newDto = new SubmitQuizDto(dto.QuizId, dto.Answers);
|
||||||
|
return await SubmitQuizAnswersForQuizAsync(userId, newDto, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of quiz questions for a lesson.
|
/// Gets the number of quiz questions for a lesson.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -417,4 +441,43 @@ public class QuizQuestionService
|
||||||
|
|
||||||
return distribution;
|
return distribution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Quiz-based methods (QuizId)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets quiz questions by quiz ID.
|
||||||
|
/// </summary>
|
||||||
|
public virtual async Task<IReadOnlyList<QuizQuestionDto>> GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionRepository.GetByQuizAsync(quizId, cancellationToken);
|
||||||
|
return questions.Select(q => q.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets active quiz questions by quiz ID.
|
||||||
|
/// </summary>
|
||||||
|
public virtual async Task<IReadOnlyList<QuizQuestionDto>> GetActiveQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionRepository.GetActiveByQuizAsync(quizId, cancellationToken);
|
||||||
|
return questions.Select(q => q.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a random set of quiz questions for a quiz.
|
||||||
|
/// </summary>
|
||||||
|
public virtual async Task<IReadOnlyList<QuizQuestionDto>> GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionRepository.GetRandomQuestionsForQuizAsync(quizId, count, cancellationToken);
|
||||||
|
return questions.Select(q => q.ToDto()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total points for a quiz.
|
||||||
|
/// </summary>
|
||||||
|
public virtual async Task<int> GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _quizQuestionRepository.GetTotalPointsForQuizAsync(quizId, cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
273
GermanApp/Application/Services/QuizService.cs
Normal file
273
GermanApp/Application/Services/QuizService.cs
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
162
GermanApp/Application/UseCases/Commands/CreateQuizCommand.cs
Normal file
162
GermanApp/Application/UseCases/Commands/CreateQuizCommand.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for creating a new quiz.
|
||||||
|
/// This follows the CQRS pattern - commands represent write operations.
|
||||||
|
/// </summary>
|
||||||
|
public record CreateQuizCommand(CreateQuizDto QuizData) : ICommand<QuizDto>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for the CreateQuizCommand.
|
||||||
|
/// </summary>
|
||||||
|
public class CreateQuizCommandHandler : ICommandHandler<CreateQuizCommand, QuizDto>
|
||||||
|
{
|
||||||
|
private readonly IQuizRepository _quizRepository;
|
||||||
|
private readonly ILessonRepository _lessonRepository;
|
||||||
|
|
||||||
|
public CreateQuizCommandHandler(
|
||||||
|
IQuizRepository quizRepository,
|
||||||
|
ILessonRepository lessonRepository)
|
||||||
|
{
|
||||||
|
_quizRepository = quizRepository;
|
||||||
|
_lessonRepository = lessonRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QuizDto> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for updating an existing quiz.
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateQuizCommand(int Id, UpdateQuizDto QuizData) : ICommand<QuizDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for the UpdateQuizCommand.
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateQuizCommandHandler : ICommandHandler<UpdateQuizCommand, QuizDto?>
|
||||||
|
{
|
||||||
|
private readonly IQuizRepository _quizRepository;
|
||||||
|
private readonly ILessonRepository _lessonRepository;
|
||||||
|
|
||||||
|
public UpdateQuizCommandHandler(
|
||||||
|
IQuizRepository quizRepository,
|
||||||
|
ILessonRepository lessonRepository)
|
||||||
|
{
|
||||||
|
_quizRepository = quizRepository;
|
||||||
|
_lessonRepository = lessonRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QuizDto?> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for deleting a quiz.
|
||||||
|
/// </summary>
|
||||||
|
public record DeleteQuizCommand(int Id) : ICommand<bool>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for the DeleteQuizCommand.
|
||||||
|
/// </summary>
|
||||||
|
public class DeleteQuizCommandHandler : ICommandHandler<DeleteQuizCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IQuizRepository _quizRepository;
|
||||||
|
|
||||||
|
public DeleteQuizCommandHandler(IQuizRepository quizRepository)
|
||||||
|
{
|
||||||
|
_quizRepository = quizRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for getting a quiz with its questions.
|
||||||
|
/// </summary>
|
||||||
|
public record GetQuizWithQuestionsCommand(int Id) : ICommand<QuizWithQuestionsDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for the GetQuizWithQuestionsCommand.
|
||||||
|
/// </summary>
|
||||||
|
public class GetQuizWithQuestionsCommandHandler : ICommandHandler<GetQuizWithQuestionsCommand, QuizWithQuestionsDto?>
|
||||||
|
{
|
||||||
|
private readonly IQuizRepository _quizRepository;
|
||||||
|
|
||||||
|
public GetQuizWithQuestionsCommandHandler(IQuizRepository quizRepository)
|
||||||
|
{
|
||||||
|
_quizRepository = quizRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QuizWithQuestionsDto?> Handle(GetQuizWithQuestionsCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var quiz = await _quizRepository.GetWithQuestionsAsync(command.Id, cancellationToken);
|
||||||
|
return quiz?.ToQuizWithQuestionsDto();
|
||||||
|
}
|
||||||
|
}
|
||||||
182
GermanApp/Domain/Entities/Quiz.cs
Normal file
182
GermanApp/Domain/Entities/Quiz.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
namespace GermanApp.Domain.Entities;
|
namespace GermanApp.Domain.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.)
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class QuizQuestion
|
public class QuizQuestion
|
||||||
{
|
{
|
||||||
public int Id { get; private set; }
|
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 string QuestionText { get; private set; } = string.Empty;
|
||||||
public QuestionType Type { get; private set; }
|
public QuestionType Type { get; private set; }
|
||||||
public string? CorrectAnswer { 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 Difficulty { get; private set; } // 1-5 scale
|
||||||
public int Order { get; private set; }
|
public int Order { get; private set; }
|
||||||
public bool IsActive { get; private set; } = true;
|
public bool IsActive { get; private set; } = true;
|
||||||
|
|
@ -19,7 +20,7 @@ public class QuizQuestion
|
||||||
public DateTime? UpdatedAt { get; private set; }
|
public DateTime? UpdatedAt { get; private set; }
|
||||||
|
|
||||||
// Navigation property
|
// Navigation property
|
||||||
public virtual Lesson? Lesson { get; private set; }
|
public virtual Quiz? Quiz { get; private set; }
|
||||||
public virtual ICollection<QuizOption> Options { get; private set; } = new List<QuizOption>();
|
public virtual ICollection<QuizOption> Options { get; private set; } = new List<QuizOption>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -30,32 +31,38 @@ public class QuizQuestion
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory method to create a new quiz question.
|
/// Factory method to create a new quiz question.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lessonId">ID of the parent lesson</param>
|
/// <param name="quizId">ID of the parent quiz</param>
|
||||||
/// <param name="questionText">The question text</param>
|
/// <param name="questionText">The question text</param>
|
||||||
/// <param name="type">The question type</param>
|
/// <param name="type">The question type</param>
|
||||||
/// <param name="correctAnswer">The correct answer</param>
|
/// <param name="correctAnswer">The correct answer</param>
|
||||||
/// <param name="difficulty">Difficulty level (1-5)</param>
|
/// <param name="points">Points for this question (default 1)</param>
|
||||||
/// <param name="order">Sort order within the lesson</param>
|
/// <param name="difficulty">Difficulty level (1-5, default 3)</param>
|
||||||
|
/// <param name="order">Sort order within the quiz</param>
|
||||||
public static QuizQuestion Create(
|
public static QuizQuestion Create(
|
||||||
int lessonId,
|
int quizId,
|
||||||
string questionText,
|
string questionText,
|
||||||
QuestionType type,
|
QuestionType type,
|
||||||
string correctAnswer,
|
string correctAnswer,
|
||||||
|
int points = 1,
|
||||||
int difficulty = 3,
|
int difficulty = 3,
|
||||||
int order = 1)
|
int order = 1)
|
||||||
{
|
{
|
||||||
if (difficulty < 1 || difficulty > 5)
|
if (difficulty < 1 || difficulty > 5)
|
||||||
throw new ArgumentOutOfRangeException(nameof(difficulty), "Difficulty must be between 1 and 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))
|
if (string.IsNullOrWhiteSpace(questionText))
|
||||||
throw new ArgumentException("Question text cannot be empty", nameof(questionText));
|
throw new ArgumentException("Question text cannot be empty", nameof(questionText));
|
||||||
|
|
||||||
return new QuizQuestion
|
return new QuizQuestion
|
||||||
{
|
{
|
||||||
LessonId = lessonId,
|
QuizId = quizId,
|
||||||
QuestionText = questionText,
|
QuestionText = questionText,
|
||||||
Type = type,
|
Type = type,
|
||||||
CorrectAnswer = correctAnswer,
|
CorrectAnswer = correctAnswer,
|
||||||
|
Points = points,
|
||||||
Difficulty = difficulty,
|
Difficulty = difficulty,
|
||||||
Order = order,
|
Order = order,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
|
|
@ -83,6 +90,18 @@ public class QuizQuestion
|
||||||
UpdatedAt = DateTime.UtcNow;
|
UpdatedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the points for this question.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdatePoints(int newPoints)
|
||||||
|
{
|
||||||
|
if (newPoints < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(newPoints), "Points must be positive");
|
||||||
|
|
||||||
|
Points = newPoints;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the difficulty level.
|
/// Updates the difficulty level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -114,11 +133,11 @@ public class QuizQuestion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the associated lesson.
|
/// Updates the associated quiz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateLesson(int newLessonId)
|
public void UpdateQuiz(int newQuizId)
|
||||||
{
|
{
|
||||||
LessonId = newLessonId;
|
QuizId = newQuizId;
|
||||||
UpdatedAt = DateTime.UtcNow;
|
UpdatedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,76 @@ public interface IQuizQuestionRepository : IRepository<QuizQuestion, int>
|
||||||
/// Checks if a quiz question with the given order exists in a lesson.
|
/// Checks if a quiz question with the given order exists in a lesson.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<bool> OrderExistsInLessonAsync(int lessonId, int order, int? excludeId = null, CancellationToken cancellationToken = default);
|
Task<bool> OrderExistsInLessonAsync(int lessonId, int order, int? excludeId = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Quiz-based methods (QuizId)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets quiz questions by quiz ID.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<QuizQuestion>> GetByQuizAsync(int quizId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets active quiz questions by quiz ID.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<QuizQuestion>> GetActiveByQuizAsync(int quizId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets quiz questions by quiz with options loaded.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<QuizQuestion>> GetByQuizWithOptionsAsync(int quizId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a random set of quiz questions for a quiz.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<QuizQuestion>> GetRandomQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total points for a quiz.
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a quiz question with the given order exists in a quiz.
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> OrderExistsInQuizAsync(int quizId, int order, int? excludeId = null, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repository interface for Quiz entities.
|
||||||
|
/// </summary>
|
||||||
|
public interface IQuizRepository : IRepository<Quiz, int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets quizzes by lesson ID.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<Quiz>> GetByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all quizzes ordered by lesson and quiz order.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<Quiz>> GetAllOrderedAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets active quizzes by lesson ID.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<Quiz>> GetActiveByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a quiz with its questions and options.
|
||||||
|
/// </summary>
|
||||||
|
Task<Quiz?> GetWithQuestionsAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the first quiz for a lesson.
|
||||||
|
/// </summary>
|
||||||
|
Task<Quiz?> GetFirstByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a quiz exists for a lesson.
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
public DbSet<User> Users { get; set; } = null!;
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
public DbSet<UserProgress> UserProgress { get; set; } = null!;
|
public DbSet<UserProgress> UserProgress { get; set; } = null!;
|
||||||
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
||||||
|
public DbSet<Quiz> Quizzes { get; set; } = null!;
|
||||||
public DbSet<QuizQuestion> QuizQuestions { get; set; } = null!;
|
public DbSet<QuizQuestion> QuizQuestions { get; set; } = null!;
|
||||||
public DbSet<QuizOption> QuizOptions { get; set; } = null!;
|
public DbSet<QuizOption> QuizOptions { get; set; } = null!;
|
||||||
|
|
||||||
|
|
@ -134,17 +135,17 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure QuizQuestion entity
|
// Configure Quiz entity
|
||||||
modelBuilder.Entity<QuizQuestion>(builder =>
|
modelBuilder.Entity<Quiz>(builder =>
|
||||||
{
|
{
|
||||||
builder.HasKey(q => q.Id);
|
builder.HasKey(q => q.Id);
|
||||||
builder.Property(q => q.LessonId).IsRequired();
|
builder.Property(q => q.LessonId).IsRequired();
|
||||||
builder.Property(q => q.QuestionText).IsRequired().HasMaxLength(2000);
|
builder.Property(q => q.Title).IsRequired().HasMaxLength(200);
|
||||||
builder.Property(q => q.Type).IsRequired();
|
builder.Property(q => q.Description).HasMaxLength(2000);
|
||||||
builder.Property(q => q.CorrectAnswer).HasMaxLength(1000);
|
builder.Property(q => q.PassingScore).IsRequired().HasDefaultValue(80);
|
||||||
builder.Property(q => q.Difficulty).IsRequired().HasDefaultValue(3);
|
builder.Property(q => q.TimeLimitMinutes).HasDefaultValue(0);
|
||||||
builder.Property(q => q.Order).IsRequired().HasDefaultValue(1);
|
|
||||||
builder.Property(q => q.IsActive).HasDefaultValue(true);
|
builder.Property(q => q.IsActive).HasDefaultValue(true);
|
||||||
|
builder.Property(q => q.ShuffleQuestions).HasDefaultValue(true);
|
||||||
builder.Property(q => q.CreatedAt).IsRequired();
|
builder.Property(q => q.CreatedAt).IsRequired();
|
||||||
builder.Property(q => q.UpdatedAt).IsRequired(false);
|
builder.Property(q => q.UpdatedAt).IsRequired(false);
|
||||||
|
|
||||||
|
|
@ -154,8 +155,36 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
.HasForeignKey(q => q.LessonId)
|
.HasForeignKey(q => q.LessonId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
// Unique constraint: one question per lesson per order
|
// Navigation to questions
|
||||||
builder.HasIndex(q => new { q.LessonId, q.Order }).IsUnique();
|
builder.HasMany(q => q.Questions)
|
||||||
|
.WithOne(qq => qq.Quiz)
|
||||||
|
.HasForeignKey(qq => qq.QuizId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure QuizQuestion entity
|
||||||
|
modelBuilder.Entity<QuizQuestion>(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
|
// Navigation to options
|
||||||
builder.HasMany(q => q.Options)
|
builder.HasMany(q => q.Options)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<QuizQuestion?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
public async Task<QuizQuestion?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
|
.ThenInclude(q => q.Lesson)
|
||||||
.Include(q => q.Options.OrderBy(o => o.Order))
|
.Include(q => q.Options.OrderBy(o => o.Order))
|
||||||
.FirstOrDefaultAsync(q => q.Id == id, cancellationToken);
|
.FirstOrDefaultAsync(q => q.Id == id, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +30,8 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<IReadOnlyList<QuizQuestion>> GetAllAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<QuizQuestion>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
|
.ThenInclude(q => q.Lesson)
|
||||||
.Include(q => q.Options)
|
.Include(q => q.Options)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
@ -63,9 +65,10 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<IReadOnlyList<QuizQuestion>> GetByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<QuizQuestion>> GetByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
|
.ThenInclude(q => q.Lesson)
|
||||||
.Include(q => q.Options.OrderBy(o => o.Order))
|
.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)
|
.OrderBy(q => q.Order)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
@ -74,9 +77,11 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<IReadOnlyList<QuizQuestion>> GetAllOrderedAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<QuizQuestion>> GetAllOrderedAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
|
.ThenInclude(q => q.Lesson)
|
||||||
.Include(q => q.Options.OrderBy(o => o.Order))
|
.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)
|
.ThenBy(q => q.Order)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
@ -85,9 +90,10 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<IReadOnlyList<QuizQuestion>> GetActiveByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<QuizQuestion>> GetActiveByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
|
.ThenInclude(q => q.Lesson)
|
||||||
.Include(q => q.Options.OrderBy(o => o.Order))
|
.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)
|
.OrderBy(q => q.Order)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
@ -96,9 +102,10 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<IReadOnlyList<QuizQuestion>> GetRandomQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<QuizQuestion>> GetRandomQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
|
.ThenInclude(q => q.Lesson)
|
||||||
.Include(q => q.Options.OrderBy(o => o.Order))
|
.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
|
.OrderBy(q => Guid.NewGuid()) // Random ordering
|
||||||
.Take(count)
|
.Take(count)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|
@ -108,7 +115,7 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<IReadOnlyList<QuizQuestion>> GetByDifficultyAsync(int difficulty, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<QuizQuestion>> GetByDifficultyAsync(int difficulty, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
.Include(q => q.Options)
|
.Include(q => q.Options)
|
||||||
.Where(q => q.Difficulty == difficulty)
|
.Where(q => q.Difficulty == difficulty)
|
||||||
.OrderBy(q => q.Order)
|
.OrderBy(q => q.Order)
|
||||||
|
|
@ -119,7 +126,7 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<IReadOnlyList<QuizQuestion>> GetByTypeAsync(QuestionType type, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<QuizQuestion>> GetByTypeAsync(QuestionType type, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.QuizQuestions
|
return await _context.QuizQuestions
|
||||||
.Include(q => q.Lesson)
|
.Include(q => q.Quiz)
|
||||||
.Include(q => q.Options)
|
.Include(q => q.Options)
|
||||||
.Where(q => q.Type == type)
|
.Where(q => q.Type == type)
|
||||||
.OrderBy(q => q.Order)
|
.OrderBy(q => q.Order)
|
||||||
|
|
@ -130,7 +137,79 @@ public class QuizQuestionRepository : IQuizQuestionRepository
|
||||||
public async Task<bool> OrderExistsInLessonAsync(int lessonId, int order, int? excludeId = null, CancellationToken cancellationToken = default)
|
public async Task<bool> OrderExistsInLessonAsync(int lessonId, int order, int? excludeId = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var query = _context.QuizQuestions
|
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<IReadOnlyList<QuizQuestion>> 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<IReadOnlyList<QuizQuestion>> 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<IReadOnlyList<QuizQuestion>> 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<IReadOnlyList<QuizQuestion>> 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<int> 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<bool> 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)
|
if (excludeId.HasValue)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
114
GermanApp/Infrastructure/Data/Repositories/QuizRepository.cs
Normal file
114
GermanApp/Infrastructure/Data/Repositories/QuizRepository.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entity Framework Core implementation of IQuizRepository.
|
||||||
|
/// This is part of the Infrastructure layer.
|
||||||
|
/// </summary>
|
||||||
|
public class QuizRepository : IQuizRepository
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public QuizRepository(AppDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Quiz?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Quizzes
|
||||||
|
.Include(q => q.Lesson)
|
||||||
|
.FirstOrDefaultAsync(q => q.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Quiz>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Quizzes
|
||||||
|
.Include(q => q.Lesson)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Quiz> 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<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Quizzes
|
||||||
|
.AnyAsync(q => q.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Quiz>> 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<IReadOnlyList<Quiz>> 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<IReadOnlyList<Quiz>> 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<Quiz?> 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<Quiz?> 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<bool> ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Quizzes
|
||||||
|
.AnyAsync(q => q.LessonId == lessonId, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,20 @@ public class QuizQuestionsController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all active quiz questions for a specific lesson.
|
/// Gets all active quiz questions for a specific quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quizId">The quiz ID</param>
|
||||||
|
/// <returns>List of quiz questions for the specified quiz</returns>
|
||||||
|
[HttpGet("by-quiz/{quizId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionService.GetActiveQuizQuestionsByQuizAsync(quizId, cancellationToken);
|
||||||
|
return Ok(questions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all active quiz questions for a specific lesson (legacy - uses first quiz).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lessonId">The lesson ID</param>
|
/// <param name="lessonId">The lesson ID</param>
|
||||||
/// <returns>List of quiz questions for the specified lesson</returns>
|
/// <returns>List of quiz questions for the specified lesson</returns>
|
||||||
|
|
@ -63,12 +76,26 @@ public class QuizQuestionsController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a random set of quiz questions for a lesson.
|
/// Gets a random set of quiz questions for a quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quizId">The quiz ID</param>
|
||||||
|
/// <param name="count">Number of questions to return</param>
|
||||||
|
/// <returns>List of random quiz questions for the quiz</returns>
|
||||||
|
[HttpGet("random/{quizId}/{count}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionService.GetRandomQuizQuestionsForQuizAsync(quizId, count, cancellationToken);
|
||||||
|
return Ok(questions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a random set of quiz questions for a lesson (legacy - uses first quiz).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lessonId">The lesson ID</param>
|
/// <param name="lessonId">The lesson ID</param>
|
||||||
/// <param name="count">Number of questions to return</param>
|
/// <param name="count">Number of questions to return</param>
|
||||||
/// <returns>List of random quiz questions for the lesson</returns>
|
/// <returns>List of random quiz questions for the lesson</returns>
|
||||||
[HttpGet("random/{lessonId}/{count}")]
|
[HttpGet("random/by-lesson/{lessonId}/{count}")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> GetRandomQuizQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
|
public async Task<IActionResult> GetRandomQuizQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|
@ -105,7 +132,7 @@ public class QuizQuestionsController : ControllerBase
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new quiz question.
|
/// Creates a new quiz question.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The quiz question data</param>
|
/// <param name="dto">The quiz question data (requires QuizId)</param>
|
||||||
/// <returns>The created quiz question</returns>
|
/// <returns>The created quiz question</returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
|
|
@ -236,13 +263,15 @@ public class QuizQuestionsController : ControllerBase
|
||||||
/// Submits quiz answers and returns the result.
|
/// Submits quiz answers and returns the result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The user ID</param>
|
/// <param name="userId">The user ID</param>
|
||||||
/// <param name="dto">The quiz answers submission</param>
|
/// <param name="dto">The quiz answers submission (requires QuizId)</param>
|
||||||
/// <returns>The quiz result</returns>
|
/// <returns>The quiz result</returns>
|
||||||
[HttpPost("submit/{userId}")]
|
[HttpPost("submit/{userId}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
|
public async Task<IActionResult> 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);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
344
GermanApp/Presentation/Controllers/QuizzesController.cs
Normal file
344
GermanApp/Presentation/Controllers/QuizzesController.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API controller for managing quizzes.
|
||||||
|
/// This is part of the Presentation layer.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all quizzes.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List of all quizzes</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetAllQuizzesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quizzes = await _quizService.GetAllQuizzesAsync(cancellationToken);
|
||||||
|
return Ok(quizzes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all quizzes as list items (summary).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List of quiz summary items</returns>
|
||||||
|
[HttpGet("list")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetAllQuizListItemsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quizzes = await _quizService.GetAllQuizListItemsAsync(cancellationToken);
|
||||||
|
return Ok(quizzes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a specific quiz by its ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The quiz ID</param>
|
||||||
|
/// <returns>The quiz with the specified ID</returns>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetQuizByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quiz = await _quizService.GetQuizByIdAsync(id, cancellationToken);
|
||||||
|
if (quiz == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(quiz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a quiz with its questions by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The quiz ID</param>
|
||||||
|
/// <returns>The quiz with questions</returns>
|
||||||
|
[HttpGet("{id}/with-questions")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetQuizWithQuestionsAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quiz = await _quizService.GetQuizWithQuestionsAsync(id, cancellationToken);
|
||||||
|
if (quiz == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(quiz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all quizzes for a specific lesson.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lessonId">The lesson ID</param>
|
||||||
|
/// <returns>List of quizzes for the specified lesson</returns>
|
||||||
|
[HttpGet("by-lesson/{lessonId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quizzes = await _quizService.GetQuizzesByLessonAsync(lessonId, cancellationToken);
|
||||||
|
return Ok(quizzes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets active quizzes for a specific lesson.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lessonId">The lesson ID</param>
|
||||||
|
/// <returns>List of active quizzes for the specified lesson</returns>
|
||||||
|
[HttpGet("active/by-lesson/{lessonId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetActiveQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quizzes = await _quizService.GetActiveQuizzesByLessonAsync(lessonId, cancellationToken);
|
||||||
|
return Ok(quizzes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the first quiz for a lesson.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lessonId">The lesson ID</param>
|
||||||
|
/// <returns>The first quiz for the lesson</returns>
|
||||||
|
[HttpGet("first/by-lesson/{lessonId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetFirstQuizByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quiz = await _quizService.GetFirstQuizByLessonAsync(lessonId, cancellationToken);
|
||||||
|
if (quiz == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(quiz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all active quizzes.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List of active quizzes</returns>
|
||||||
|
[HttpGet("active")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetActiveQuizzesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quizzes = await _quizService.GetActiveQuizzesAsync(cancellationToken);
|
||||||
|
return Ok(quizzes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The quiz data</param>
|
||||||
|
/// <returns>The created quiz</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<IActionResult> CreateQuizAsync(CreateQuizDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quiz = await _quizService.CreateQuizAsync(dto, cancellationToken);
|
||||||
|
return CreatedAtAction(nameof(GetQuizByIdAsync), new { id = quiz.Id }, quiz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an existing quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The quiz ID</param>
|
||||||
|
/// <param name="dto">The updated quiz data</param>
|
||||||
|
/// <returns>The updated quiz</returns>
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<IActionResult> UpdateQuizAsync(int id, UpdateQuizDto dto, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quiz = await _quizService.UpdateQuizAsync(id, dto, cancellationToken);
|
||||||
|
if (quiz == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(quiz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a quiz by its ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The quiz ID</param>
|
||||||
|
/// <returns>No content on success</returns>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<IActionResult> DeleteQuizAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = await _quizService.DeleteQuizAsync(id, cancellationToken);
|
||||||
|
if (!result)
|
||||||
|
return NotFound();
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activates a quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The quiz ID</param>
|
||||||
|
/// <returns>The activated quiz</returns>
|
||||||
|
[HttpPost("{id}/activate")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<IActionResult> ActivateQuizAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quiz = await _quizService.ActivateQuizAsync(id, cancellationToken);
|
||||||
|
if (quiz == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(quiz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deactivates a quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The quiz ID</param>
|
||||||
|
/// <returns>The deactivated quiz</returns>
|
||||||
|
[HttpPost("{id}/deactivate")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<IActionResult> DeactivateQuizAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quiz = await _quizService.DeactivateQuizAsync(id, cancellationToken);
|
||||||
|
if (quiz == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(quiz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a quiz exists by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The quiz ID</param>
|
||||||
|
/// <returns>True if the quiz exists</returns>
|
||||||
|
[HttpGet("exists/{id}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> ExistsAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var exists = await _quizService.ExistsAsync(id, cancellationToken);
|
||||||
|
return Ok(new { exists });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a quiz exists for a lesson.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lessonId">The lesson ID</param>
|
||||||
|
/// <returns>True if a quiz exists for the lesson</returns>
|
||||||
|
[HttpGet("exists/by-lesson/{lessonId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var exists = await _quizService.ExistsByLessonAsync(lessonId, cancellationToken);
|
||||||
|
return Ok(new { lessonId, exists });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total number of quizzes.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The total count of quizzes</returns>
|
||||||
|
[HttpGet("count")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetTotalQuizCountAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var count = await _quizService.GetTotalQuizCountAsync(cancellationToken);
|
||||||
|
return Ok(new { count });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of quizzes for a lesson.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lessonId">The lesson ID</param>
|
||||||
|
/// <returns>The count of quizzes for the lesson</returns>
|
||||||
|
[HttpGet("count/by-lesson/{lessonId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetQuizCountByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var count = await _quizService.GetQuizCountByLessonAsync(lessonId, cancellationToken);
|
||||||
|
return Ok(new { lessonId, count });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets quizzes by passing score range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minScore">Minimum passing score</param>
|
||||||
|
/// <param name="maxScore">Maximum passing score</param>
|
||||||
|
/// <returns>List of quizzes in the score range</returns>
|
||||||
|
[HttpGet("by-passing-score/{minScore}/{maxScore}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetQuizzesByPassingScoreRangeAsync(int minScore, int maxScore, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var quizzes = await _quizService.GetQuizzesByPassingScoreRangeAsync(minScore, maxScore, cancellationToken);
|
||||||
|
return Ok(quizzes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets quiz questions for a specific quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quizId">The quiz ID</param>
|
||||||
|
/// <returns>List of quiz questions for the quiz</returns>
|
||||||
|
[HttpGet("{quizId}/questions")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionService.GetQuizQuestionsByQuizAsync(quizId, cancellationToken);
|
||||||
|
return Ok(questions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets active quiz questions for a specific quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quizId">The quiz ID</param>
|
||||||
|
/// <returns>List of active quiz questions for the quiz</returns>
|
||||||
|
[HttpGet("{quizId}/questions/active")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetActiveQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionService.GetActiveQuizQuestionsByQuizAsync(quizId, cancellationToken);
|
||||||
|
return Ok(questions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets random quiz questions for a quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quizId">The quiz ID</param>
|
||||||
|
/// <param name="count">Number of questions to return</param>
|
||||||
|
/// <returns>List of random quiz questions</returns>
|
||||||
|
[HttpGet("{quizId}/questions/random/{count}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var questions = await _quizQuestionService.GetRandomQuizQuestionsForQuizAsync(quizId, count, cancellationToken);
|
||||||
|
return Ok(questions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Submits quiz answers and returns the result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user ID</param>
|
||||||
|
/// <param name="quizId">The quiz ID</param>
|
||||||
|
/// <param name="dto">The quiz answers submission</param>
|
||||||
|
/// <returns>The quiz result</returns>
|
||||||
|
[HttpPost("submit/{userId}/{quizId}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total points for a quiz.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quizId">The quiz ID</param>
|
||||||
|
/// <returns>The total points for the quiz</returns>
|
||||||
|
[HttpGet("{quizId}/total-points")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetTotalPointsForQuizAsync(int quizId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var points = await _quizQuestionService.GetTotalPointsForQuizAsync(quizId, cancellationToken);
|
||||||
|
return Ok(new { quizId, points });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -134,6 +134,7 @@ try
|
||||||
builder.Services.AddScoped<ILevelRepository, LevelRepository>();
|
builder.Services.AddScoped<ILevelRepository, LevelRepository>();
|
||||||
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
||||||
builder.Services.AddScoped<IUserProgressRepository, UserProgressRepository>();
|
builder.Services.AddScoped<IUserProgressRepository, UserProgressRepository>();
|
||||||
|
builder.Services.AddScoped<IQuizRepository, QuizRepository>();
|
||||||
builder.Services.AddScoped<IQuizQuestionRepository, QuizQuestionRepository>();
|
builder.Services.AddScoped<IQuizQuestionRepository, QuizQuestionRepository>();
|
||||||
builder.Services.AddScoped<IQuizOptionRepository, QuizOptionRepository>();
|
builder.Services.AddScoped<IQuizOptionRepository, QuizOptionRepository>();
|
||||||
|
|
||||||
|
|
@ -171,9 +172,18 @@ try
|
||||||
builder.Services.AddScoped<LevelService>();
|
builder.Services.AddScoped<LevelService>();
|
||||||
builder.Services.AddScoped<LessonService>();
|
builder.Services.AddScoped<LessonService>();
|
||||||
builder.Services.AddScoped<ProgressService>();
|
builder.Services.AddScoped<ProgressService>();
|
||||||
|
builder.Services.AddScoped<QuizService>();
|
||||||
builder.Services.AddScoped<QuizQuestionService>();
|
builder.Services.AddScoped<QuizQuestionService>();
|
||||||
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
||||||
|
|
||||||
|
// Note: QuizQuestionService requires IQuizRepository which is registered above
|
||||||
|
|
||||||
|
// Register Quiz command handlers
|
||||||
|
builder.Services.AddScoped<ICommandHandler<CreateQuizCommand, QuizDto>, CreateQuizCommandHandler>();
|
||||||
|
builder.Services.AddScoped<ICommandHandler<UpdateQuizCommand, QuizDto?>, UpdateQuizCommandHandler>();
|
||||||
|
builder.Services.AddScoped<ICommandHandler<DeleteQuizCommand, bool>, DeleteQuizCommandHandler>();
|
||||||
|
builder.Services.AddScoped<ICommandHandler<GetQuizWithQuestionsCommand, QuizWithQuestionsDto?>, GetQuizWithQuestionsCommandHandler>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue