Compare commits
No commits in common. "8e05c2b6e2cc5c517ccd85f02d9125c665f38c1a" and "a492cdd7906c075d50635bc7d96404570a120038" have entirely different histories.
8e05c2b6e2
...
a492cdd790
18 changed files with 89 additions and 1293 deletions
|
|
@ -10,11 +10,8 @@ public record LessonDto(
|
||||||
int Id,
|
int Id,
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
int LevelId,
|
int Level,
|
||||||
string LevelName,
|
string LevelDescription,
|
||||||
string LevelCode,
|
|
||||||
int Order,
|
|
||||||
string Topic,
|
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
DateTime? UpdatedAt);
|
DateTime? UpdatedAt);
|
||||||
|
|
||||||
|
|
@ -24,9 +21,7 @@ public record LessonDto(
|
||||||
public record CreateLessonDto(
|
public record CreateLessonDto(
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
int LevelId,
|
int Level);
|
||||||
int Order,
|
|
||||||
string Topic);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating an existing Lesson.
|
/// Data Transfer Object for updating an existing Lesson.
|
||||||
|
|
@ -34,9 +29,7 @@ public record CreateLessonDto(
|
||||||
public record UpdateLessonDto(
|
public record UpdateLessonDto(
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
int LevelId,
|
int Level);
|
||||||
int Order,
|
|
||||||
string Topic);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods for mapping between Lesson entity and DTOs.
|
/// Extension methods for mapping between Lesson entity and DTOs.
|
||||||
|
|
@ -47,23 +40,21 @@ public static class LessonDtoExtensions
|
||||||
lesson.Id,
|
lesson.Id,
|
||||||
lesson.Title,
|
lesson.Title,
|
||||||
lesson.Description,
|
lesson.Description,
|
||||||
lesson.LevelId,
|
lesson.Level,
|
||||||
lesson.Level?.Name ?? "Unknown",
|
GetLevelDescription(lesson.Level),
|
||||||
lesson.Level?.Code ?? "??",
|
|
||||||
lesson.Order,
|
|
||||||
lesson.Topic,
|
|
||||||
lesson.CreatedAt,
|
lesson.CreatedAt,
|
||||||
lesson.UpdatedAt);
|
lesson.UpdatedAt);
|
||||||
|
|
||||||
public static Lesson ToEntity(this CreateLessonDto dto) =>
|
private static string GetLevelDescription(int level) => level switch
|
||||||
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);
|
1 => "A1 (Beginner)",
|
||||||
lesson.UpdateDescription(dto.Description);
|
2 => "A2 (Elementary)",
|
||||||
lesson.UpdateLevel(dto.LevelId);
|
3 => "B1 (Intermediate)",
|
||||||
lesson.UpdateOrder(dto.Order);
|
4 => "B2 (Upper Intermediate)",
|
||||||
lesson.UpdateTopic(dto.Topic);
|
5 => "C1 (Advanced)",
|
||||||
}
|
_ => "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Lesson ToEntity(this CreateLessonDto dto) =>
|
||||||
|
Lesson.Create(dto.Title, dto.Description, dto.Level);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.LevelId < 1 || lesson.LevelId > 5)
|
if (lesson.Level < 1 || lesson.Level > 5)
|
||||||
{
|
{
|
||||||
throw new ValidationException("Level must be between 1 and 5");
|
throw new ValidationException("Level must be between 1 and 5");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,20 @@
|
||||||
namespace GermanApp.Domain.Entities;
|
namespace GermanApp.Domain.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a lesson in the DeutschLernen system.
|
/// Represents a German language lesson in the 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 bool IsActive { get; private set; } = true;
|
public int Level { get; private set; }
|
||||||
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 property (EF Core will handle this)
|
// Navigation properties would go here in EF Core
|
||||||
public virtual Level? Level { get; private set; }
|
// public ICollection<VocabularyWord> Words { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor for EF Core deserialization.
|
/// Constructor for EF Core deserialization.
|
||||||
|
|
@ -27,84 +24,35 @@ public class Lesson
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory method to create a new lesson.
|
/// Factory method to create a new lesson.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="levelId">ID of the parent level</param>
|
public static Lesson Create(string title, string description, int level)
|
||||||
/// <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's title.
|
/// Updates the lesson details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateTitle(string newTitle)
|
public void Update(string title, string description, int level)
|
||||||
{
|
{
|
||||||
Title = newTitle;
|
Title = title;
|
||||||
|
Description = description;
|
||||||
|
Level = level;
|
||||||
UpdatedAt = DateTime.UtcNow;
|
UpdatedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the lesson's topic.
|
/// Domain behavior: Check if lesson is at beginner level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateTopic(string newTopic)
|
public bool IsBeginnerLevel() => Level <= 2;
|
||||||
{
|
|
||||||
Topic = newTopic;
|
|
||||||
UpdatedAt = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the lesson's description.
|
/// Domain behavior: Check if lesson is at advanced level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateDescription(string newDescription)
|
public bool IsAdvancedLevel() => Level >= 4;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
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,32 +41,6 @@ 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>
|
||||||
|
|
@ -75,61 +49,15 @@ public interface ILessonRepository : IRepository<Lesson, int>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets lessons by level.
|
/// Gets lessons by level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Lesson>> GetByLevelAsync(int levelId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Lesson>> GetByLevelAsync(int level, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all lessons ordered by level and lesson order.
|
/// Gets beginner-level lessons.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Lesson>> GetAllOrderedAsync(CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the first lesson in a level.
|
/// Gets advanced-level lessons.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Lesson?> GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Lesson>> GetAdvancedLessonsAsync(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,10 +14,8 @@ 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.
|
||||||
|
|
@ -27,41 +25,16 @@ 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).HasMaxLength(2000);
|
builder.Property(l => l.Description).IsRequired().HasMaxLength(2000);
|
||||||
builder.Property(l => l.Topic).IsRequired().HasMaxLength(100);
|
builder.Property(l => l.Level).IsRequired();
|
||||||
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 =>
|
||||||
// {
|
// {
|
||||||
|
|
@ -72,31 +45,6 @@ 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 =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using GermanApp.Infrastructure.Data.DbContext;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace GermanApp.Infrastructure.Data.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(AppDbContext))]
|
|
||||||
[Migration("20260607192743_AddLessonManagementTables")]
|
|
||||||
partial class AddLessonManagementTables
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "9.0.0")
|
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2000)
|
|
||||||
.HasColumnType("character varying(2000)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<int>("LevelId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Order")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<string>("Topic")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("LevelId", "Order")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Lessons");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.Level", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Code")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<int>("Order")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Code")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("Order")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Levels");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ExpiresAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<DateTime?>("RevokedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("Token")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("character varying(255)");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("RefreshTokens");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.User", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<string>("CurrentLevel")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)")
|
|
||||||
.HasDefaultValue("A1");
|
|
||||||
|
|
||||||
b.Property<string>("Email")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("character varying(255)");
|
|
||||||
|
|
||||||
b.Property<int>("Streak")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int>("TotalPoints")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<string>("Username")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("character varying(50)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Email")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("Username")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastAttemptDate")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<int>("LessonId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("QuizScore")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("LessonId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId", "LessonId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("UserProgress");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("GermanApp.Domain.Entities.Level", "Level")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LevelId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Level");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("GermanApp.Domain.Entities.User", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LessonId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("GermanApp.Domain.Entities.User", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Lesson");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace GermanApp.Infrastructure.Data.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddLessonManagementTables : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Level",
|
|
||||||
table: "Lessons",
|
|
||||||
newName: "Order");
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "IsActive",
|
|
||||||
table: "Lessons",
|
|
||||||
type: "boolean",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<int>(
|
|
||||||
name: "LevelId",
|
|
||||||
table: "Lessons",
|
|
||||||
type: "integer",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: 0);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "Topic",
|
|
||||||
table: "Lessons",
|
|
||||||
type: "character varying(100)",
|
|
||||||
maxLength: 100,
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Levels",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
|
||||||
Code = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
|
||||||
Order = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Levels", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserProgress",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
UserId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
LessonId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
IsCompleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
|
||||||
QuizScore = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
|
||||||
LastAttemptDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserProgress", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserProgress_Lessons_LessonId",
|
|
||||||
column: x => x.LessonId,
|
|
||||||
principalTable: "Lessons",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserProgress_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Lessons_LevelId_Order",
|
|
||||||
table: "Lessons",
|
|
||||||
columns: new[] { "LevelId", "Order" },
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Levels_Code",
|
|
||||||
table: "Levels",
|
|
||||||
column: "Code",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Levels_Order",
|
|
||||||
table: "Levels",
|
|
||||||
column: "Order",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserProgress_LessonId",
|
|
||||||
table: "UserProgress",
|
|
||||||
column: "LessonId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserProgress_UserId_LessonId",
|
|
||||||
table: "UserProgress",
|
|
||||||
columns: new[] { "UserId", "LessonId" },
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Lessons_Levels_LevelId",
|
|
||||||
table: "Lessons",
|
|
||||||
column: "LevelId",
|
|
||||||
principalTable: "Levels",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Lessons_Levels_LevelId",
|
|
||||||
table: "Lessons");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Levels");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserProgress");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_Lessons_LevelId_Order",
|
|
||||||
table: "Lessons");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "IsActive",
|
|
||||||
table: "Lessons");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "LevelId",
|
|
||||||
table: "Lessons");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "Topic",
|
|
||||||
table: "Lessons");
|
|
||||||
|
|
||||||
migrationBuilder.RenameColumn(
|
|
||||||
name: "Order",
|
|
||||||
table: "Lessons",
|
|
||||||
newName: "Level");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -38,15 +38,7 @@ namespace GermanApp.Migrations
|
||||||
.HasMaxLength(2000)
|
.HasMaxLength(2000)
|
||||||
.HasColumnType("character varying(2000)");
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
b.Property<int>("Level")
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<int>("LevelId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Order")
|
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
|
|
@ -54,54 +46,14 @@ namespace GermanApp.Migrations
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("character varying(200)");
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
b.Property<string>("Topic")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("LevelId", "Order")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Lessons");
|
b.ToTable("Lessons");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.Level", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Code")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(10)
|
|
||||||
.HasColumnType("character varying(10)");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(100)
|
|
||||||
.HasColumnType("character varying(100)");
|
|
||||||
|
|
||||||
b.Property<int>("Order")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Code")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("Order")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Levels");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
|
modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
@ -193,54 +145,6 @@ namespace GermanApp.Migrations
|
||||||
b.ToTable("Users");
|
b.ToTable("Users");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<DateTime>("LastAttemptDate")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<int>("LessonId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("QuizScore")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("LessonId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId", "LessonId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("UserProgress");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("GermanApp.Domain.Entities.Level", "Level")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LevelId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Level");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
|
modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("GermanApp.Domain.Entities.User", null)
|
b.HasOne("GermanApp.Domain.Entities.User", null)
|
||||||
|
|
@ -249,25 +153,6 @@ namespace GermanApp.Migrations
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("LessonId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("GermanApp.Domain.Entities.User", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Lesson");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -58,104 +56,27 @@ public class LessonRepository : ILessonRepository
|
||||||
.AnyAsync(l => l.Id == id, cancellationToken);
|
.AnyAsync(l => l.Id == id, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Lesson>> GetByLevelAsync(int levelId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Lesson>> GetByLevelAsync(int level, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
.Include(l => l.Level)
|
.Where(l => l.Level == level)
|
||||||
.Where(l => l.LevelId == levelId)
|
|
||||||
.OrderBy(l => l.Order)
|
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Lesson>> GetAllOrderedAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
.Include(l => l.Level)
|
.Where(l => l.Level <= 2)
|
||||||
.OrderBy(l => l.Level.Order)
|
|
||||||
.ThenBy(l => l.Order)
|
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Lesson?> GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Lesson>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _context.Lessons
|
return await _context.Lessons
|
||||||
.Include(l => l.Level)
|
.Where(l => l.Level >= 4)
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
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 async Task SeedDatabaseAsync(this WebApplication app)
|
public static void SeedDatabase(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,18 +24,6 @@ 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",
|
||||||
|
|
@ -56,17 +44,17 @@ public static class SeedDataExtension
|
||||||
// Seed some lessons
|
// Seed some lessons
|
||||||
var lessons = new[]
|
var lessons = new[]
|
||||||
{
|
{
|
||||||
Lesson.Create(1, "Greetings", 1, "Greetings", "Basic German greetings and introductions"),
|
Lesson.Create("Greetings", "Basic German greetings and introductions", 1),
|
||||||
Lesson.Create(1, "Numbers", 2, "Numbers", "German numbers 1-100"),
|
Lesson.Create("Numbers", "German numbers 1-100", 1),
|
||||||
Lesson.Create(2, "Grammar Basics", 1, "Grammar", "Basic German grammar rules"),
|
Lesson.Create("Grammar Basics", "Basic German grammar rules", 2),
|
||||||
Lesson.Create(2, "Everyday Phrases", 2, "Phrases", "Common phrases for daily conversations"),
|
Lesson.Create("Everyday Phrases", "Common phrases for daily conversations", 2),
|
||||||
Lesson.Create(5, "Advanced Grammar", 1, "Grammar", "Complex German grammar")
|
Lesson.Create("Advanced Grammar", "Complex German grammar", 4)
|
||||||
};
|
};
|
||||||
dbContext.Lessons.AddRange(lessons);
|
dbContext.Lessons.AddRange(lessons);
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
dbContext.SaveChanges();
|
||||||
|
|
||||||
Console.WriteLine("Database seeded with levels, admin user, test user, and sample lessons.");
|
Console.WriteLine("Database seeded with admin user, test user, and sample lessons.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -46,13 +45,8 @@ 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) =>
|
||||||
{
|
{
|
||||||
// Beginner lessons are A1 (level 1) and A2 (level 2)
|
var lessons = await repository.GetBeginnerLessonsAsync();
|
||||||
var beginnerLessons = new List<Lesson>();
|
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||||
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)
|
||||||
|
|
@ -64,13 +58,8 @@ 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) =>
|
||||||
{
|
{
|
||||||
// Advanced lessons are B2 (level 4) and C1 (level 5)
|
var lessons = await repository.GetAdvancedLessonsAsync();
|
||||||
var advancedLessons = new List<Lesson>();
|
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||||
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)
|
||||||
|
|
@ -121,7 +110,7 @@ public static class LessonsEndpoints
|
||||||
return Results.NotFound();
|
return Results.NotFound();
|
||||||
|
|
||||||
// Map DTO to entity
|
// Map DTO to entity
|
||||||
existingLesson.UpdateFromDto(dto);
|
existingLesson.Update(dto.Title, dto.Description, dto.Level);
|
||||||
await repository.UpdateAsync(existingLesson);
|
await repository.UpdateAsync(existingLesson);
|
||||||
|
|
||||||
return Results.Ok(existingLesson.ToDto());
|
return Results.Ok(existingLesson.ToDto());
|
||||||
|
|
|
||||||
|
|
@ -153,15 +153,12 @@ try
|
||||||
name: "api",
|
name: "api",
|
||||||
pattern: "api/{controller}/{action}/{id?}" );
|
pattern: "api/{controller}/{action}/{id?}" );
|
||||||
|
|
||||||
|
// Seed database with initial data
|
||||||
|
app.SeedDatabase();
|
||||||
|
|
||||||
// Map Clean Architecture endpoints
|
// Map Clean Architecture endpoints
|
||||||
app.MapLessonsEndpoints();
|
app.MapLessonsEndpoints();
|
||||||
|
|
||||||
// Seed database with initial data
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
await app.SeedDatabaseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep original WeatherForecast endpoint for reference
|
// Keep original WeatherForecast endpoint for reference
|
||||||
app.MapGet("/weatherforecast", () =>
|
app.MapGet("/weatherforecast", () =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ Implement the core backend functionality, including lesson management, AI servic
|
||||||
### Features
|
### Features
|
||||||
| # | Feature | Description | Hours | Status | Dependencies |
|
| # | Feature | Description | Hours | Status | Dependencies |
|
||||||
|---|---------|-------------|-------|--------|--------------|
|
|---|---------|-------------|-------|--------|--------------|
|
||||||
| 2.1 | [Lesson Management](features/lesson-management.md) | Lessons, levels, progress tracking | 10-16h | 🚀 In Progress (Phase 1: DB & Models ✅) | Phase 1 |
|
| 2.1 | [Lesson Management](features/lesson-management.md) | Lessons, levels, progress tracking | 10-16h | ⏳ Planned | Phase 1 |
|
||||||
| 2.2 | [AI Services](features/ai-services.md) | Mistral, Vosk, Coqui TTS integration | 10-16h | ⏳ Planned | Phase 1 |
|
| 2.2 | [AI Services](features/ai-services.md) | Mistral, Vosk, Coqui TTS integration | 10-16h | ⏳ Planned | Phase 1 |
|
||||||
| 2.3 | [Vocabulary System](features/vocabulary-system.md) | Word storage, audio, import | 8-12h | ⏳ Planned | Phase 1, 2.1 |
|
| 2.3 | [Vocabulary System](features/vocabulary-system.md) | Word storage, audio, import | 8-12h | ⏳ Planned | Phase 1, 2.1 |
|
||||||
| 2.4 | [Quiz System](features/quiz-system.md) | Multiple question types, scoring | 6-10h | ⏳ Planned | Phase 1, 2.1 |
|
| 2.4 | [Quiz System](features/quiz-system.md) | Multiple question types, scoring | 6-10h | ⏳ Planned | Phase 1, 2.1 |
|
||||||
|
|
@ -265,8 +265,8 @@ Implement the complete React + TypeScript frontend application with all UI compo
|
||||||
|
|
||||||
| Phase | Duration | Hours | Features | Status |
|
| Phase | Duration | Hours | Features | Status |
|
||||||
|-------|----------|-------|----------|--------|
|
|-------|----------|-------|----------|--------|
|
||||||
| Phase 1: Foundation | 2 weeks | 30-42h | 2 | ✅ Complete (1.1 ✅, 1.2 ✅) |
|
| Phase 1: Foundation | 2 weeks | 30-42h | 2 | 🚀 In Progress (1.1 ✅, 1.2 🚀) |
|
||||||
| Phase 2: Core Backend | 2 weeks | 42-58h | 4 | 🚀 In Progress (2.1 🚀) |
|
| Phase 2: Core Backend | 2 weeks | 42-58h | 4 | ⏳ Planned |
|
||||||
| Phase 3: Content & Features | 2 weeks | 30-42h | 2 | ⏳ Planned |
|
| Phase 3: Content & Features | 2 weeks | 30-42h | 2 | ⏳ Planned |
|
||||||
| Phase 4: Frontend | 2 weeks | 10-16h | 1 | ⏳ Planned |
|
| Phase 4: Frontend | 2 weeks | 10-16h | 1 | ⏳ Planned |
|
||||||
| **Total** | **8 weeks** | **112-158h** | **9** | 🚀 In Progress |
|
| **Total** | **8 weeks** | **112-158h** | **9** | 🚀 In Progress |
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Feature: Lesson & Content Management
|
# Feature: Lesson & Content Management
|
||||||
|
|
||||||
> **Status**: 🚀 In Progress
|
> **Status**: ⏳ Planned
|
||||||
> **Priority**: High
|
> **Priority**: High
|
||||||
> **Complexity**: High
|
> **Complexity**: High
|
||||||
> **Estimate**: 10-16 hours
|
> **Estimate**: 10-16 hours
|
||||||
|
|
@ -10,8 +10,6 @@
|
||||||
> **PR**: -
|
> **PR**: -
|
||||||
> **Related Features**: Infrastructure Setup, User Authentication, Vocabulary System, Quiz System, Story Integration
|
> **Related Features**: Infrastructure Setup, User Authentication, Vocabulary System, Quiz System, Story Integration
|
||||||
|
|
||||||
**Phase 1: Database & Models - COMPLETED ✅**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 Overview
|
## 📌 Overview
|
||||||
|
|
@ -134,12 +132,12 @@ Each lesson contains:
|
||||||
## 🚀 Implementation Plan
|
## 🚀 Implementation Plan
|
||||||
|
|
||||||
### Phase 1: Database & Models (2-3 hours)
|
### Phase 1: Database & Models (2-3 hours)
|
||||||
- [x] Create Level entity and repository
|
- [ ] Create Level entity and repository
|
||||||
- [x] Create Lesson entity and repository
|
- [ ] Create Lesson entity and repository
|
||||||
- [x] Create UserProgress entity and repository
|
- [ ] Create UserProgress entity and repository
|
||||||
- [x] Set up relationships between entities
|
- [ ] Set up relationships between entities
|
||||||
- [x] Create migrations for Levels, Lessons, UserProgress
|
- [ ] Create migrations for Levels, Lessons, UserProgress
|
||||||
- [x] Seed initial A1 level and lessons
|
- [ ] Seed initial A1 level and lessons
|
||||||
|
|
||||||
### Phase 2: Backend Services (3-5 hours)
|
### Phase 2: Backend Services (3-5 hours)
|
||||||
- [ ] Create LevelService with CRUD operations
|
- [ ] Create LevelService with CRUD operations
|
||||||
|
|
@ -182,16 +180,16 @@ Each lesson contains:
|
||||||
## ✅ Tasks
|
## ✅ Tasks
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- [x] Create Domain/Entities/Level.cs
|
- [ ] Create Domain/Entities/Level.cs
|
||||||
- [x] Create Domain/Entities/Lesson.cs
|
- [ ] Create Domain/Entities/Lesson.cs
|
||||||
- [x] Create Domain/Entities/UserProgress.cs
|
- [ ] Create Domain/Entities/UserProgress.cs
|
||||||
- [x] Create Domain/Interfaces/ILevelRepository.cs
|
- [ ] Create Domain/Interfaces/ILevelRepository.cs
|
||||||
- [x] Create Domain/Interfaces/ILessonRepository.cs
|
- [ ] Create Domain/Interfaces/ILessonRepository.cs
|
||||||
- [x] Create Infrastructure/Data/Repositories/LevelRepository.cs
|
- [ ] Create Infrastructure/Data/Repositories/LevelRepository.cs
|
||||||
- [x] Create Infrastructure/Data/Repositories/LessonRepository.cs
|
- [ ] Create Infrastructure/Data/Repositories/LessonRepository.cs
|
||||||
- [x] Create Infrastructure/Data/Repositories/UserProgressRepository.cs
|
- [ ] Create Infrastructure/Data/Repositories/UserProgressRepository.cs
|
||||||
- [x] Create Application/DTOs/LevelDto.cs
|
- [ ] Create Application/DTOs/LevelDto.cs
|
||||||
- [x] Create Application/DTOs/LessonDto.cs
|
- [ ] Create Application/DTOs/LessonDto.cs
|
||||||
- [ ] Create Application/DTOs/UserProgressDto.cs
|
- [ ] Create Application/DTOs/UserProgressDto.cs
|
||||||
- [ ] Create Application/Services/LevelService.cs
|
- [ ] Create Application/Services/LevelService.cs
|
||||||
- [ ] Create Application/Services/LessonService.cs
|
- [ ] Create Application/Services/LessonService.cs
|
||||||
|
|
@ -203,11 +201,11 @@ Each lesson contains:
|
||||||
- [ ] Write integration tests for controllers
|
- [ ] Write integration tests for controllers
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
- [x] Create migration for Levels table
|
- [ ] Create migration for Levels table
|
||||||
- [x] Create migration for Lessons table
|
- [ ] Create migration for Lessons table
|
||||||
- [x] Create migration for UserProgress table
|
- [ ] Create migration for UserProgress table
|
||||||
- [x] Seed A1 level with initial lessons
|
- [ ] Seed A1 level with initial lessons
|
||||||
- [x] Add indexes for performance
|
- [ ] Add indexes for performance
|
||||||
|
|
||||||
### Business Logic
|
### Business Logic
|
||||||
- [ ] Implement LessonUnlockService
|
- [ ] Implement LessonUnlockService
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue