fix(backend): update Lesson entity and related code to use LevelId foreign key
- Updated Lesson entity to use LevelId (int) instead of Level (int) - Added navigation property Level (Level entity) - Added Order, Topic, and IsActive properties to Lesson - Updated Lesson.Create() factory method signature to accept levelId, title, order, topic, description - Split Update method into individual property update methods - Updated LessonDto to use LevelId, LevelName, LevelCode instead of Level int - Added CreateLessonDto and UpdateLessonDto with all required fields - Added UpdateFromDto extension method for Lesson - Updated CreateLessonCommand to validate LevelId instead of Level - Updated SeedDataExtension to seed Level entities first, then use new Lesson.Create() signature - Made SeedDatabaseAsync async and updated Program.cs to await it - Updated LessonsEndpoints to use GetByLevelAsync for beginner/advanced queries - Fixed missing using directive for Domain.Entities Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
81350042a0
commit
ba81359afa
13 changed files with 709 additions and 61 deletions
|
|
@ -10,8 +10,11 @@ public record LessonDto(
|
||||||
int Id,
|
int Id,
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
int Level,
|
int LevelId,
|
||||||
string LevelDescription,
|
string LevelName,
|
||||||
|
string LevelCode,
|
||||||
|
int Order,
|
||||||
|
string Topic,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
DateTime? UpdatedAt);
|
DateTime? UpdatedAt);
|
||||||
|
|
||||||
|
|
@ -21,7 +24,9 @@ public record LessonDto(
|
||||||
public record CreateLessonDto(
|
public record CreateLessonDto(
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
int Level);
|
int LevelId,
|
||||||
|
int Order,
|
||||||
|
string Topic);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating an existing Lesson.
|
/// Data Transfer Object for updating an existing Lesson.
|
||||||
|
|
@ -29,7 +34,9 @@ public record CreateLessonDto(
|
||||||
public record UpdateLessonDto(
|
public record UpdateLessonDto(
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
int Level);
|
int LevelId,
|
||||||
|
int Order,
|
||||||
|
string Topic);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods for mapping between Lesson entity and DTOs.
|
/// Extension methods for mapping between Lesson entity and DTOs.
|
||||||
|
|
@ -40,21 +47,23 @@ public static class LessonDtoExtensions
|
||||||
lesson.Id,
|
lesson.Id,
|
||||||
lesson.Title,
|
lesson.Title,
|
||||||
lesson.Description,
|
lesson.Description,
|
||||||
lesson.Level,
|
lesson.LevelId,
|
||||||
GetLevelDescription(lesson.Level),
|
lesson.Level?.Name ?? "Unknown",
|
||||||
|
lesson.Level?.Code ?? "??",
|
||||||
|
lesson.Order,
|
||||||
|
lesson.Topic,
|
||||||
lesson.CreatedAt,
|
lesson.CreatedAt,
|
||||||
lesson.UpdatedAt);
|
lesson.UpdatedAt);
|
||||||
|
|
||||||
private static string GetLevelDescription(int level) => level switch
|
|
||||||
{
|
|
||||||
1 => "A1 (Beginner)",
|
|
||||||
2 => "A2 (Elementary)",
|
|
||||||
3 => "B1 (Intermediate)",
|
|
||||||
4 => "B2 (Upper Intermediate)",
|
|
||||||
5 => "C1 (Advanced)",
|
|
||||||
_ => "Unknown"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static Lesson ToEntity(this CreateLessonDto dto) =>
|
public static Lesson ToEntity(this CreateLessonDto dto) =>
|
||||||
Lesson.Create(dto.Title, dto.Description, dto.Level);
|
Lesson.Create(dto.LevelId, dto.Title, dto.Order, dto.Topic, dto.Description);
|
||||||
|
|
||||||
|
public static void UpdateFromDto(this Lesson lesson, UpdateLessonDto dto)
|
||||||
|
{
|
||||||
|
lesson.UpdateTitle(dto.Title);
|
||||||
|
lesson.UpdateDescription(dto.Description);
|
||||||
|
lesson.UpdateLevel(dto.LevelId);
|
||||||
|
lesson.UpdateOrder(dto.Order);
|
||||||
|
lesson.UpdateTopic(dto.Topic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public class CreateLessonCommandHandler : ICommandHandler<CreateLessonCommand, L
|
||||||
var lesson = command.LessonData.ToEntity();
|
var lesson = command.LessonData.ToEntity();
|
||||||
|
|
||||||
// Validate business rules (could be extracted to a validator)
|
// Validate business rules (could be extracted to a validator)
|
||||||
if (lesson.Level < 1 || lesson.Level > 5)
|
if (lesson.LevelId < 1 || lesson.LevelId > 5)
|
||||||
{
|
{
|
||||||
throw new ValidationException("Level must be between 1 and 5");
|
throw new ValidationException("Level must be between 1 and 5");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
namespace GermanApp.Domain.Entities;
|
namespace GermanApp.Domain.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a German language lesson in the system.
|
/// Represents a lesson in the DeutschLernen system.
|
||||||
|
/// Lessons are organized within CEFR levels and must be completed in order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Lesson
|
public class Lesson
|
||||||
{
|
{
|
||||||
// Private setter for domain behavior, but internal for EF Core
|
|
||||||
public int Id { get; private set; }
|
public int Id { get; private set; }
|
||||||
|
public int LevelId { get; private set; }
|
||||||
public string Title { get; private set; } = string.Empty;
|
public string Title { get; private set; } = string.Empty;
|
||||||
|
public int Order { get; private set; }
|
||||||
|
public string Topic { get; private set; } = string.Empty;
|
||||||
public string Description { get; private set; } = string.Empty;
|
public string Description { get; private set; } = string.Empty;
|
||||||
public int Level { get; private set; }
|
public bool IsActive { get; private set; } = true;
|
||||||
public DateTime CreatedAt { get; private set; }
|
public DateTime CreatedAt { get; private set; }
|
||||||
public DateTime? UpdatedAt { get; private set; }
|
public DateTime? UpdatedAt { get; private set; }
|
||||||
|
|
||||||
// Navigation properties would go here in EF Core
|
// Navigation property (EF Core will handle this)
|
||||||
// public ICollection<VocabularyWord> Words { get; private set; }
|
public virtual Level? Level { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor for EF Core deserialization.
|
/// Constructor for EF Core deserialization.
|
||||||
|
|
@ -24,35 +27,84 @@ public class Lesson
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory method to create a new lesson.
|
/// Factory method to create a new lesson.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Lesson Create(string title, string description, int level)
|
/// <param name="levelId">ID of the parent level</param>
|
||||||
|
/// <param name="title">Title of the lesson</param>
|
||||||
|
/// <param name="order">Sort order within the level</param>
|
||||||
|
/// <param name="topic">Main topic covered in the lesson</param>
|
||||||
|
/// <param name="description">Description of what the lesson covers</param>
|
||||||
|
public static Lesson Create(int levelId, string title, int order, string topic, string description = "")
|
||||||
{
|
{
|
||||||
return new Lesson
|
return new Lesson
|
||||||
{
|
{
|
||||||
|
LevelId = levelId,
|
||||||
Title = title,
|
Title = title,
|
||||||
|
Order = order,
|
||||||
|
Topic = topic,
|
||||||
Description = description,
|
Description = description,
|
||||||
Level = level,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the lesson details.
|
/// Updates the lesson's title.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Update(string title, string description, int level)
|
public void UpdateTitle(string newTitle)
|
||||||
{
|
{
|
||||||
Title = title;
|
Title = newTitle;
|
||||||
Description = description;
|
|
||||||
Level = level;
|
|
||||||
UpdatedAt = DateTime.UtcNow;
|
UpdatedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Domain behavior: Check if lesson is at beginner level.
|
/// Updates the lesson's topic.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsBeginnerLevel() => Level <= 2;
|
public void UpdateTopic(string newTopic)
|
||||||
|
{
|
||||||
|
Topic = newTopic;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Domain behavior: Check if lesson is at advanced level.
|
/// Updates the lesson's description.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAdvancedLevel() => Level >= 4;
|
public void UpdateDescription(string newDescription)
|
||||||
|
{
|
||||||
|
Description = newDescription;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the lesson's sort order.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateOrder(int newOrder)
|
||||||
|
{
|
||||||
|
Order = newOrder;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the lesson's level.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateLevel(int newLevelId)
|
||||||
|
{
|
||||||
|
LevelId = newLevelId;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activates the lesson.
|
||||||
|
/// </summary>
|
||||||
|
public void Activate()
|
||||||
|
{
|
||||||
|
IsActive = true;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deactivates the lesson.
|
||||||
|
/// </summary>
|
||||||
|
public void Deactivate()
|
||||||
|
{
|
||||||
|
IsActive = false;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
GermanApp/Domain/Entities/Level.cs
Normal file
57
GermanApp/Domain/Entities/Level.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
namespace GermanApp.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a CEFR level in the DeutschLernen system (A1, A2, B1, B2, C1).
|
||||||
|
/// </summary>
|
||||||
|
public class Level
|
||||||
|
{
|
||||||
|
public int Id { get; private set; }
|
||||||
|
public string Name { get; private set; } = string.Empty;
|
||||||
|
public string Code { get; private set; } = string.Empty;
|
||||||
|
public int Order { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for EF Core deserialization.
|
||||||
|
/// </summary>
|
||||||
|
private Level() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory method to create a new level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">Display name of the level (e.g., "Beginner A1")</param>
|
||||||
|
/// <param name="code">Short code for the level (e.g., "A1")</param>
|
||||||
|
/// <param name="order">Sort order of the level</param>
|
||||||
|
public static Level Create(string name, string code, int order)
|
||||||
|
{
|
||||||
|
return new Level
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Code = code.ToUpperInvariant(),
|
||||||
|
Order = order
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the level's display name.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateName(string newName)
|
||||||
|
{
|
||||||
|
Name = newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the level's code.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateCode(string newCode)
|
||||||
|
{
|
||||||
|
Code = newCode.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the level's sort order.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateOrder(int newOrder)
|
||||||
|
{
|
||||||
|
Order = newOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
GermanApp/Domain/Entities/UserProgress.cs
Normal file
82
GermanApp/Domain/Entities/UserProgress.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
namespace GermanApp.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a user's progress through lessons.
|
||||||
|
/// Tracks completion status and quiz scores.
|
||||||
|
/// </summary>
|
||||||
|
public class UserProgress
|
||||||
|
{
|
||||||
|
public int Id { get; private set; }
|
||||||
|
public int UserId { get; private set; }
|
||||||
|
public int LessonId { get; private set; }
|
||||||
|
public bool IsCompleted { get; private set; }
|
||||||
|
public int QuizScore { get; private set; }
|
||||||
|
public DateTime LastAttemptDate { get; private set; }
|
||||||
|
|
||||||
|
// Navigation properties
|
||||||
|
public virtual User? User { get; private set; }
|
||||||
|
public virtual Lesson? Lesson { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for EF Core deserialization.
|
||||||
|
/// </summary>
|
||||||
|
private UserProgress() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory method to create a new user progress record.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">ID of the user</param>
|
||||||
|
/// <param name="lessonId">ID of the lesson</param>
|
||||||
|
public static UserProgress Create(int userId, int lessonId)
|
||||||
|
{
|
||||||
|
return new UserProgress
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
LessonId = lessonId,
|
||||||
|
IsCompleted = false,
|
||||||
|
QuizScore = 0,
|
||||||
|
LastAttemptDate = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the lesson as completed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quizScore">The score achieved on the quiz (0-100)</param>
|
||||||
|
public void MarkAsCompleted(int quizScore)
|
||||||
|
{
|
||||||
|
if (quizScore < 0 || quizScore > 100)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(quizScore), "Score must be between 0 and 100");
|
||||||
|
|
||||||
|
IsCompleted = true;
|
||||||
|
QuizScore = quizScore;
|
||||||
|
LastAttemptDate = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the quiz score without marking as completed.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateQuizScore(int quizScore)
|
||||||
|
{
|
||||||
|
if (quizScore < 0 || quizScore > 100)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(quizScore), "Score must be between 0 and 100");
|
||||||
|
|
||||||
|
QuizScore = quizScore;
|
||||||
|
LastAttemptDate = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the progress (e.g., when user wants to redo a lesson).
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
IsCompleted = false;
|
||||||
|
QuizScore = 0;
|
||||||
|
LastAttemptDate = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the user passed the lesson (80% or higher).
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPassed() => QuizScore >= 80;
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,32 @@ public interface IRepository<TEntity, TId> where TEntity : class
|
||||||
Task<bool> ExistsAsync(TId id, CancellationToken cancellationToken = default);
|
Task<bool> ExistsAsync(TId id, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repository interface for Level entities.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILevelRepository : IRepository<Level, int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a level by its code (e.g., "A1", "B2").
|
||||||
|
/// </summary>
|
||||||
|
Task<Level?> GetByCodeAsync(string code, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all levels ordered by their sort order.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<Level>> GetAllOrderedAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the first level (lowest order number).
|
||||||
|
/// </summary>
|
||||||
|
Task<Level?> GetFirstLevelAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the next level after the specified one.
|
||||||
|
/// </summary>
|
||||||
|
Task<Level?> GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repository interface for Lesson entities.
|
/// Repository interface for Lesson entities.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -49,15 +75,61 @@ public interface ILessonRepository : IRepository<Lesson, int>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets lessons by level.
|
/// Gets lessons by level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Lesson>> GetByLevelAsync(int level, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Lesson>> GetByLevelAsync(int levelId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets beginner-level lessons.
|
/// Gets all lessons ordered by level and lesson order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Lesson>> GetAllOrderedAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets advanced-level lessons.
|
/// Gets the first lesson in a level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Lesson>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default);
|
Task<Lesson?> GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the next lesson after the specified one (in the same level).
|
||||||
|
/// </summary>
|
||||||
|
Task<Lesson?> GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets lessons that the user can access (completed previous lessons or first in level).
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<Lesson>> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repository interface for UserProgress entities.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserProgressRepository : IRepository<UserProgress, int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets user progress for a specific user and lesson.
|
||||||
|
/// </summary>
|
||||||
|
Task<UserProgress?> GetByUserAndLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all progress records for a user.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<UserProgress>> GetByUserAsync(int userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets progress for all lessons in a specific level for a user.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<UserProgress>> GetByUserAndLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a user has completed a lesson (with 80% or higher score).
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> HasUserCompletedLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user's average score for a level.
|
||||||
|
/// </summary>
|
||||||
|
Task<double> GetAverageScoreForLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the percentage of lessons completed in a level.
|
||||||
|
/// </summary>
|
||||||
|
Task<double> GetLevelCompletionPercentageAsync(int userId, int levelId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// DbSets for domain entities
|
// DbSets for domain entities
|
||||||
|
public DbSet<Level> Levels { get; set; } = null!;
|
||||||
public DbSet<Lesson> Lessons { get; set; } = null!;
|
public DbSet<Lesson> Lessons { get; set; } = null!;
|
||||||
public DbSet<User> Users { get; set; } = null!;
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
|
public DbSet<UserProgress> UserProgress { get; set; } = null!;
|
||||||
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
||||||
|
|
||||||
// Note: Value objects are not stored directly as entities.
|
// Note: Value objects are not stored directly as entities.
|
||||||
|
|
@ -25,16 +27,41 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// Configure Level entity
|
||||||
|
modelBuilder.Entity<Level>(builder =>
|
||||||
|
{
|
||||||
|
builder.HasKey(l => l.Id);
|
||||||
|
builder.Property(l => l.Name).IsRequired().HasMaxLength(100);
|
||||||
|
builder.Property(l => l.Code).IsRequired().HasMaxLength(10);
|
||||||
|
builder.Property(l => l.Order).IsRequired();
|
||||||
|
|
||||||
|
// Ensure unique constraints
|
||||||
|
builder.HasIndex(l => l.Code).IsUnique();
|
||||||
|
builder.HasIndex(l => l.Order).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
// Configure Lesson entity
|
// Configure Lesson entity
|
||||||
modelBuilder.Entity<Lesson>(builder =>
|
modelBuilder.Entity<Lesson>(builder =>
|
||||||
{
|
{
|
||||||
builder.HasKey(l => l.Id);
|
builder.HasKey(l => l.Id);
|
||||||
builder.Property(l => l.Title).IsRequired().HasMaxLength(200);
|
builder.Property(l => l.Title).IsRequired().HasMaxLength(200);
|
||||||
builder.Property(l => l.Description).IsRequired().HasMaxLength(2000);
|
builder.Property(l => l.Description).HasMaxLength(2000);
|
||||||
builder.Property(l => l.Level).IsRequired();
|
builder.Property(l => l.Topic).IsRequired().HasMaxLength(100);
|
||||||
|
builder.Property(l => l.LevelId).IsRequired();
|
||||||
|
builder.Property(l => l.Order).IsRequired();
|
||||||
|
builder.Property(l => l.IsActive).HasDefaultValue(true);
|
||||||
builder.Property(l => l.CreatedAt).IsRequired();
|
builder.Property(l => l.CreatedAt).IsRequired();
|
||||||
builder.Property(l => l.UpdatedAt).IsRequired(false);
|
builder.Property(l => l.UpdatedAt).IsRequired(false);
|
||||||
|
|
||||||
|
// Foreign key to Level with unique constraint per level
|
||||||
|
builder.HasOne(l => l.Level)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(l => l.LevelId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// Unique constraint: one lesson per level per order
|
||||||
|
builder.HasIndex(l => new { l.LevelId, l.Order }).IsUnique();
|
||||||
|
|
||||||
// Value object: GermanWord would be configured as an owned entity
|
// Value object: GermanWord would be configured as an owned entity
|
||||||
// builder.OwnsMany(l => l.Words, wordBuilder =>
|
// builder.OwnsMany(l => l.Words, wordBuilder =>
|
||||||
// {
|
// {
|
||||||
|
|
@ -45,6 +72,31 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Configure UserProgress entity
|
||||||
|
modelBuilder.Entity<UserProgress>(builder =>
|
||||||
|
{
|
||||||
|
builder.HasKey(up => up.Id);
|
||||||
|
builder.Property(up => up.UserId).IsRequired();
|
||||||
|
builder.Property(up => up.LessonId).IsRequired();
|
||||||
|
builder.Property(up => up.IsCompleted).HasDefaultValue(false);
|
||||||
|
builder.Property(up => up.QuizScore).HasDefaultValue(0);
|
||||||
|
builder.Property(up => up.LastAttemptDate).IsRequired();
|
||||||
|
|
||||||
|
// Foreign keys
|
||||||
|
builder.HasOne(up => up.User)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(up => up.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(up => up.Lesson)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(up => up.LessonId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// Unique constraint: one progress record per user per lesson
|
||||||
|
builder.HasIndex(up => new { up.UserId, up.LessonId }).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
// Configure User entity
|
// Configure User entity
|
||||||
modelBuilder.Entity<User>(builder =>
|
modelBuilder.Entity<User>(builder =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,14 @@ public class LessonRepository : ILessonRepository
|
||||||
public async Task<Lesson?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
public async Task<Lesson?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
|
.Include(l => l.Level)
|
||||||
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
|
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Lesson>> GetAllAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Lesson>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
|
.Include(l => l.Level)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
@ -56,27 +58,104 @@ public class LessonRepository : ILessonRepository
|
||||||
.AnyAsync(l => l.Id == id, cancellationToken);
|
.AnyAsync(l => l.Id == id, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Lesson>> GetByLevelAsync(int level, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Lesson>> GetByLevelAsync(int levelId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
.Where(l => l.Level == level)
|
.Include(l => l.Level)
|
||||||
|
.Where(l => l.LevelId == levelId)
|
||||||
|
.OrderBy(l => l.Order)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Lesson>> GetAllOrderedAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
.Where(l => l.Level <= 2)
|
.Include(l => l.Level)
|
||||||
|
.OrderBy(l => l.Level.Order)
|
||||||
|
.ThenBy(l => l.Order)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Lesson>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
|
public async Task<Lesson?> GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
.Where(l => l.Level >= 4)
|
.Include(l => l.Level)
|
||||||
|
.Where(l => l.LevelId == levelId)
|
||||||
|
.OrderBy(l => l.Order)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Lesson?> GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var currentLesson = await _context.Lessons
|
||||||
|
.Where(l => l.Id == currentLessonId)
|
||||||
|
.Select(l => new { l.LevelId, l.Order })
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (currentLesson == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await _context.Lessons
|
||||||
|
.Include(l => l.Level)
|
||||||
|
.Where(l => l.LevelId == currentLesson.LevelId && l.Order > currentLesson.Order)
|
||||||
|
.OrderBy(l => l.Order)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Lesson>> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Get all lessons ordered by level and lesson order
|
||||||
|
var allLessons = await _context.Lessons
|
||||||
|
.Include(l => l.Level)
|
||||||
|
.OrderBy(l => l.Level.Order)
|
||||||
|
.ThenBy(l => l.Order)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (allLessons.Count == 0)
|
||||||
|
return new List<Lesson>();
|
||||||
|
|
||||||
|
// Get completed lessons for this user
|
||||||
|
var completedLessonIds = await _context.UserProgress
|
||||||
|
.Where(up => up.UserId == userId && up.IsCompleted)
|
||||||
|
.Select(up => up.LessonId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Find the first lesson in the first level
|
||||||
|
var firstLesson = allLessons.First();
|
||||||
|
|
||||||
|
// If user hasn't completed any lessons, they can only access the first one
|
||||||
|
if (completedLessonIds.Count == 0)
|
||||||
|
return new List<Lesson> { firstLesson };
|
||||||
|
|
||||||
|
// Get the highest ordered lesson that the user has completed
|
||||||
|
var completedLessons = allLessons
|
||||||
|
.Where(l => completedLessonIds.Contains(l.Id))
|
||||||
|
.OrderBy(l => l.Level.Order)
|
||||||
|
.ThenBy(l => l.Order)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (completedLessons.Count == 0)
|
||||||
|
return new List<Lesson> { firstLesson };
|
||||||
|
|
||||||
|
var lastCompleted = completedLessons.Last();
|
||||||
|
|
||||||
|
// User can access all lessons up to and including the next one after the last completed
|
||||||
|
var accessibleLessons = allLessons
|
||||||
|
.TakeWhile(l => l.Id != lastCompleted.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Add the last completed and the next one
|
||||||
|
accessibleLessons.Add(lastCompleted);
|
||||||
|
|
||||||
|
var nextLessonIndex = allLessons.FindIndex(l => l.Id == lastCompleted.Id) + 1;
|
||||||
|
if (nextLessonIndex < allLessons.Count)
|
||||||
|
{
|
||||||
|
accessibleLessons.Add(allLessons[nextLessonIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessibleLessons;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
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 ILevelRepository.
|
||||||
|
/// This is part of the Infrastructure layer.
|
||||||
|
/// </summary>
|
||||||
|
public class LevelRepository : ILevelRepository
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public LevelRepository(AppDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Level?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Levels
|
||||||
|
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Level>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Levels
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Level> AddAsync(Level entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _context.Levels.AddAsync(entity, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(Level entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_context.Levels.Update(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(Level entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_context.Levels.Remove(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Levels
|
||||||
|
.AnyAsync(l => l.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Level?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Levels
|
||||||
|
.FirstOrDefaultAsync(l => l.Code == code.ToUpperInvariant(), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Level>> GetAllOrderedAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Levels
|
||||||
|
.OrderBy(l => l.Order)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Level?> GetFirstLevelAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Levels
|
||||||
|
.OrderBy(l => l.Order)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Level?> GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var currentLevel = await _context.Levels
|
||||||
|
.Where(l => l.Id == currentLevelId)
|
||||||
|
.Select(l => l.Order)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (currentLevel == default)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await _context.Levels
|
||||||
|
.Where(l => l.Order > currentLevel)
|
||||||
|
.OrderBy(l => l.Order)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
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 IUserProgressRepository.
|
||||||
|
/// This is part of the Infrastructure layer.
|
||||||
|
/// </summary>
|
||||||
|
public class UserProgressRepository : IUserProgressRepository
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public UserProgressRepository(AppDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserProgress?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.UserProgress
|
||||||
|
.Include(up => up.User)
|
||||||
|
.Include(up => up.Lesson)
|
||||||
|
.FirstOrDefaultAsync(up => up.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<UserProgress>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.UserProgress
|
||||||
|
.Include(up => up.User)
|
||||||
|
.Include(up => up.Lesson)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserProgress> AddAsync(UserProgress entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _context.UserProgress.AddAsync(entity, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(UserProgress entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_context.UserProgress.Update(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(UserProgress entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_context.UserProgress.Remove(entity);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.UserProgress
|
||||||
|
.AnyAsync(up => up.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserProgress?> GetByUserAndLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.UserProgress
|
||||||
|
.Include(up => up.User)
|
||||||
|
.Include(up => up.Lesson)
|
||||||
|
.FirstOrDefaultAsync(up => up.UserId == userId && up.LessonId == lessonId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<UserProgress>> GetByUserAsync(int userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.UserProgress
|
||||||
|
.Include(up => up.Lesson)
|
||||||
|
.ThenInclude(l => l.Level)
|
||||||
|
.Where(up => up.UserId == userId)
|
||||||
|
.OrderBy(up => up.Lesson.Level!.Order)
|
||||||
|
.ThenBy(up => up.Lesson.Order)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<UserProgress>> GetByUserAndLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.UserProgress
|
||||||
|
.Include(up => up.Lesson)
|
||||||
|
.Where(up => up.UserId == userId && up.Lesson.LevelId == levelId)
|
||||||
|
.OrderBy(up => up.Lesson.Order)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasUserCompletedLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.UserProgress
|
||||||
|
.AnyAsync(up => up.UserId == userId && up.LessonId == lessonId && up.IsCompleted, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<double> GetAverageScoreForLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var progresses = await _context.UserProgress
|
||||||
|
.Where(up => up.UserId == userId && up.Lesson.LevelId == levelId && up.QuizScore > 0)
|
||||||
|
.Select(up => up.QuizScore)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (progresses.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return progresses.Average();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<double> GetLevelCompletionPercentageAsync(int userId, int levelId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Get total lessons in the level
|
||||||
|
var totalLessons = await _context.Lessons
|
||||||
|
.CountAsync(l => l.LevelId == levelId, cancellationToken);
|
||||||
|
|
||||||
|
if (totalLessons == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Get completed lessons count for user in this level
|
||||||
|
var completedCount = await _context.UserProgress
|
||||||
|
.CountAsync(up => up.UserId == userId && up.Lesson.LevelId == levelId && up.IsCompleted, cancellationToken);
|
||||||
|
|
||||||
|
return (double)completedCount / totalLessons * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ public static class SeedDataExtension
|
||||||
/// Seeds the database with initial data.
|
/// Seeds the database with initial data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="app">The web application</param>
|
/// <param name="app">The web application</param>
|
||||||
public static void SeedDatabase(this WebApplication app)
|
public static async Task SeedDatabaseAsync(this WebApplication app)
|
||||||
{
|
{
|
||||||
using var scope = app.Services.CreateScope();
|
using var scope = app.Services.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
@ -24,6 +24,18 @@ public static class SeedDataExtension
|
||||||
// Only seed if there are no users
|
// Only seed if there are no users
|
||||||
if (!dbContext.Users.Any())
|
if (!dbContext.Users.Any())
|
||||||
{
|
{
|
||||||
|
// Seed CEFR levels first
|
||||||
|
var levels = new[]
|
||||||
|
{
|
||||||
|
Level.Create("Beginner A1", "A1", 1),
|
||||||
|
Level.Create("Elementary A2", "A2", 2),
|
||||||
|
Level.Create("Intermediate B1", "B1", 3),
|
||||||
|
Level.Create("Upper Intermediate B2", "B2", 4),
|
||||||
|
Level.Create("Advanced C1", "C1", 5)
|
||||||
|
};
|
||||||
|
dbContext.Levels.AddRange(levels);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
// Seed an admin user
|
// Seed an admin user
|
||||||
var adminUser = User.Create(
|
var adminUser = User.Create(
|
||||||
"admin",
|
"admin",
|
||||||
|
|
@ -44,17 +56,17 @@ public static class SeedDataExtension
|
||||||
// Seed some lessons
|
// Seed some lessons
|
||||||
var lessons = new[]
|
var lessons = new[]
|
||||||
{
|
{
|
||||||
Lesson.Create("Greetings", "Basic German greetings and introductions", 1),
|
Lesson.Create(1, "Greetings", 1, "Greetings", "Basic German greetings and introductions"),
|
||||||
Lesson.Create("Numbers", "German numbers 1-100", 1),
|
Lesson.Create(1, "Numbers", 2, "Numbers", "German numbers 1-100"),
|
||||||
Lesson.Create("Grammar Basics", "Basic German grammar rules", 2),
|
Lesson.Create(2, "Grammar Basics", 1, "Grammar", "Basic German grammar rules"),
|
||||||
Lesson.Create("Everyday Phrases", "Common phrases for daily conversations", 2),
|
Lesson.Create(2, "Everyday Phrases", 2, "Phrases", "Common phrases for daily conversations"),
|
||||||
Lesson.Create("Advanced Grammar", "Complex German grammar", 4)
|
Lesson.Create(5, "Advanced Grammar", 1, "Grammar", "Complex German grammar")
|
||||||
};
|
};
|
||||||
dbContext.Lessons.AddRange(lessons);
|
dbContext.Lessons.AddRange(lessons);
|
||||||
|
|
||||||
dbContext.SaveChanges();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
Console.WriteLine("Database seeded with admin user, test user, and sample lessons.");
|
Console.WriteLine("Database seeded with levels, admin user, test user, and sample lessons.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using GermanApp.Application.DTOs;
|
using GermanApp.Application.DTOs;
|
||||||
using GermanApp.Application.UseCases.Commands;
|
using GermanApp.Application.UseCases.Commands;
|
||||||
|
using GermanApp.Domain.Entities;
|
||||||
using GermanApp.Domain.Interfaces;
|
using GermanApp.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
@ -45,8 +46,13 @@ public static class LessonsEndpoints
|
||||||
// GET /api/lessons/beginner
|
// GET /api/lessons/beginner
|
||||||
group.MapGet("/beginner", async ([FromServices] ILessonRepository repository) =>
|
group.MapGet("/beginner", async ([FromServices] ILessonRepository repository) =>
|
||||||
{
|
{
|
||||||
var lessons = await repository.GetBeginnerLessonsAsync();
|
// Beginner lessons are A1 (level 1) and A2 (level 2)
|
||||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
var beginnerLessons = new List<Lesson>();
|
||||||
|
var level1Lessons = await repository.GetByLevelAsync(1);
|
||||||
|
var level2Lessons = await repository.GetByLevelAsync(2);
|
||||||
|
beginnerLessons.AddRange(level1Lessons);
|
||||||
|
beginnerLessons.AddRange(level2Lessons);
|
||||||
|
return Results.Ok(beginnerLessons.OrderBy(l => l.LevelId).ThenBy(l => l.Order).Select(l => l.ToDto()));
|
||||||
})
|
})
|
||||||
.WithName("GetBeginnerLessons")
|
.WithName("GetBeginnerLessons")
|
||||||
.WithOpenApi(operation => new(operation)
|
.WithOpenApi(operation => new(operation)
|
||||||
|
|
@ -58,8 +64,13 @@ public static class LessonsEndpoints
|
||||||
// GET /api/lessons/advanced
|
// GET /api/lessons/advanced
|
||||||
group.MapGet("/advanced", async ([FromServices] ILessonRepository repository) =>
|
group.MapGet("/advanced", async ([FromServices] ILessonRepository repository) =>
|
||||||
{
|
{
|
||||||
var lessons = await repository.GetAdvancedLessonsAsync();
|
// Advanced lessons are B2 (level 4) and C1 (level 5)
|
||||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
var advancedLessons = new List<Lesson>();
|
||||||
|
var level4Lessons = await repository.GetByLevelAsync(4);
|
||||||
|
var level5Lessons = await repository.GetByLevelAsync(5);
|
||||||
|
advancedLessons.AddRange(level4Lessons);
|
||||||
|
advancedLessons.AddRange(level5Lessons);
|
||||||
|
return Results.Ok(advancedLessons.OrderBy(l => l.LevelId).ThenBy(l => l.Order).Select(l => l.ToDto()));
|
||||||
})
|
})
|
||||||
.WithName("GetAdvancedLessons")
|
.WithName("GetAdvancedLessons")
|
||||||
.WithOpenApi(operation => new(operation)
|
.WithOpenApi(operation => new(operation)
|
||||||
|
|
@ -110,7 +121,7 @@ public static class LessonsEndpoints
|
||||||
return Results.NotFound();
|
return Results.NotFound();
|
||||||
|
|
||||||
// Map DTO to entity
|
// Map DTO to entity
|
||||||
existingLesson.Update(dto.Title, dto.Description, dto.Level);
|
existingLesson.UpdateFromDto(dto);
|
||||||
await repository.UpdateAsync(existingLesson);
|
await repository.UpdateAsync(existingLesson);
|
||||||
|
|
||||||
return Results.Ok(existingLesson.ToDto());
|
return Results.Ok(existingLesson.ToDto());
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ try
|
||||||
pattern: "api/{controller}/{action}/{id?}" );
|
pattern: "api/{controller}/{action}/{id?}" );
|
||||||
|
|
||||||
// Seed database with initial data
|
// Seed database with initial data
|
||||||
app.SeedDatabase();
|
await app.SeedDatabaseAsync();
|
||||||
|
|
||||||
// Map Clean Architecture endpoints
|
// Map Clean Architecture endpoints
|
||||||
app.MapLessonsEndpoints();
|
app.MapLessonsEndpoints();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue