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,
|
||||
string Title,
|
||||
string Description,
|
||||
int LevelId,
|
||||
string LevelName,
|
||||
string LevelCode,
|
||||
int Order,
|
||||
string Topic,
|
||||
int Level,
|
||||
string LevelDescription,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt);
|
||||
|
||||
|
|
@ -24,9 +21,7 @@ public record LessonDto(
|
|||
public record CreateLessonDto(
|
||||
string Title,
|
||||
string Description,
|
||||
int LevelId,
|
||||
int Order,
|
||||
string Topic);
|
||||
int Level);
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for updating an existing Lesson.
|
||||
|
|
@ -34,9 +29,7 @@ public record CreateLessonDto(
|
|||
public record UpdateLessonDto(
|
||||
string Title,
|
||||
string Description,
|
||||
int LevelId,
|
||||
int Order,
|
||||
string Topic);
|
||||
int Level);
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for mapping between Lesson entity and DTOs.
|
||||
|
|
@ -47,23 +40,21 @@ public static class LessonDtoExtensions
|
|||
lesson.Id,
|
||||
lesson.Title,
|
||||
lesson.Description,
|
||||
lesson.LevelId,
|
||||
lesson.Level?.Name ?? "Unknown",
|
||||
lesson.Level?.Code ?? "??",
|
||||
lesson.Order,
|
||||
lesson.Topic,
|
||||
lesson.Level,
|
||||
GetLevelDescription(lesson.Level),
|
||||
lesson.CreatedAt,
|
||||
lesson.UpdatedAt);
|
||||
|
||||
public static Lesson ToEntity(this CreateLessonDto dto) =>
|
||||
Lesson.Create(dto.LevelId, dto.Title, dto.Order, dto.Topic, dto.Description);
|
||||
|
||||
public static void UpdateFromDto(this Lesson lesson, UpdateLessonDto dto)
|
||||
private static string GetLevelDescription(int level) => level switch
|
||||
{
|
||||
lesson.UpdateTitle(dto.Title);
|
||||
lesson.UpdateDescription(dto.Description);
|
||||
lesson.UpdateLevel(dto.LevelId);
|
||||
lesson.UpdateOrder(dto.Order);
|
||||
lesson.UpdateTopic(dto.Topic);
|
||||
}
|
||||
1 => "A1 (Beginner)",
|
||||
2 => "A2 (Elementary)",
|
||||
3 => "B1 (Intermediate)",
|
||||
4 => "B2 (Upper Intermediate)",
|
||||
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();
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
namespace GermanApp.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a lesson in the DeutschLernen system.
|
||||
/// Lessons are organized within CEFR levels and must be completed in order.
|
||||
/// Represents a German language lesson in the system.
|
||||
/// </summary>
|
||||
public class Lesson
|
||||
{
|
||||
// Private setter for domain behavior, but internal for EF Core
|
||||
public int Id { get; private set; }
|
||||
public int LevelId { get; private set; }
|
||||
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 bool IsActive { get; private set; } = true;
|
||||
public int Level { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// Navigation property (EF Core will handle this)
|
||||
public virtual Level? Level { get; private set; }
|
||||
// Navigation properties would go here in EF Core
|
||||
// public ICollection<VocabularyWord> Words { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for EF Core deserialization.
|
||||
|
|
@ -27,84 +24,35 @@ public class Lesson
|
|||
/// <summary>
|
||||
/// Factory method to create a new lesson.
|
||||
/// </summary>
|
||||
/// <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 = "")
|
||||
public static Lesson Create(string title, string description, int level)
|
||||
{
|
||||
return new Lesson
|
||||
{
|
||||
LevelId = levelId,
|
||||
Title = title,
|
||||
Order = order,
|
||||
Topic = topic,
|
||||
Description = description,
|
||||
Level = level,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the lesson's title.
|
||||
/// Updates the lesson details.
|
||||
/// </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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the lesson's topic.
|
||||
/// Domain behavior: Check if lesson is at beginner level.
|
||||
/// </summary>
|
||||
public void UpdateTopic(string newTopic)
|
||||
{
|
||||
Topic = newTopic;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
public bool IsBeginnerLevel() => Level <= 2;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the lesson's description.
|
||||
/// Domain behavior: Check if lesson is at advanced level.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
public bool IsAdvancedLevel() => Level >= 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Repository interface for Lesson entities.
|
||||
/// </summary>
|
||||
|
|
@ -75,61 +49,15 @@ public interface ILessonRepository : IRepository<Lesson, int>
|
|||
/// <summary>
|
||||
/// Gets lessons by level.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Lesson>> GetByLevelAsync(int levelId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Lesson>> GetByLevelAsync(int level, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all lessons ordered by level and lesson order.
|
||||
/// Gets beginner-level lessons.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Lesson>> GetAllOrderedAsync(CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first lesson in a level.
|
||||
/// Gets advanced-level lessons.
|
||||
/// </summary>
|
||||
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);
|
||||
Task<IReadOnlyList<Lesson>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
|||
}
|
||||
|
||||
// DbSets for domain entities
|
||||
public DbSet<Level> Levels { get; set; } = null!;
|
||||
public DbSet<Lesson> Lessons { get; set; } = null!;
|
||||
public DbSet<User> Users { get; set; } = null!;
|
||||
public DbSet<UserProgress> UserProgress { get; set; } = null!;
|
||||
public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;
|
||||
|
||||
// Note: Value objects are not stored directly as entities.
|
||||
|
|
@ -27,41 +25,16 @@ public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
|||
{
|
||||
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
|
||||
modelBuilder.Entity<Lesson>(builder =>
|
||||
{
|
||||
builder.HasKey(l => l.Id);
|
||||
builder.Property(l => l.Title).IsRequired().HasMaxLength(200);
|
||||
builder.Property(l => l.Description).HasMaxLength(2000);
|
||||
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.Description).IsRequired().HasMaxLength(2000);
|
||||
builder.Property(l => l.Level).IsRequired();
|
||||
builder.Property(l => l.CreatedAt).IsRequired();
|
||||
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
|
||||
// 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
|
||||
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)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<int>("LevelId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Order")
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
|
|
@ -54,54 +46,14 @@ namespace GermanApp.Migrations
|
|||
.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")
|
||||
|
|
@ -193,54 +145,6 @@ namespace GermanApp.Migrations
|
|||
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)
|
||||
|
|
@ -249,25 +153,6 @@ namespace GermanApp.Migrations
|
|||
.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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,12 @@ public class LessonRepository : ILessonRepository
|
|||
public async Task<Lesson?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.Include(l => l.Level)
|
||||
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Lesson>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.Include(l => l.Level)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
|
@ -58,104 +56,27 @@ public class LessonRepository : ILessonRepository
|
|||
.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
|
||||
.Include(l => l.Level)
|
||||
.Where(l => l.LevelId == levelId)
|
||||
.OrderBy(l => l.Order)
|
||||
.Where(l => l.Level == level)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Lesson>> GetAllOrderedAsync(CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.Include(l => l.Level)
|
||||
.OrderBy(l => l.Level.Order)
|
||||
.ThenBy(l => l.Order)
|
||||
.Where(l => l.Level <= 2)
|
||||
.AsNoTracking()
|
||||
.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
|
||||
.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)
|
||||
.Where(l => l.Level >= 4)
|
||||
.AsNoTracking()
|
||||
.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.
|
||||
/// </summary>
|
||||
/// <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();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
|
@ -24,18 +24,6 @@ public static class SeedDataExtension
|
|||
// Only seed if there are no users
|
||||
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
|
||||
var adminUser = User.Create(
|
||||
"admin",
|
||||
|
|
@ -56,17 +44,17 @@ public static class SeedDataExtension
|
|||
// Seed some lessons
|
||||
var lessons = new[]
|
||||
{
|
||||
Lesson.Create(1, "Greetings", 1, "Greetings", "Basic German greetings and introductions"),
|
||||
Lesson.Create(1, "Numbers", 2, "Numbers", "German numbers 1-100"),
|
||||
Lesson.Create(2, "Grammar Basics", 1, "Grammar", "Basic German grammar rules"),
|
||||
Lesson.Create(2, "Everyday Phrases", 2, "Phrases", "Common phrases for daily conversations"),
|
||||
Lesson.Create(5, "Advanced Grammar", 1, "Grammar", "Complex German grammar")
|
||||
Lesson.Create("Greetings", "Basic German greetings and introductions", 1),
|
||||
Lesson.Create("Numbers", "German numbers 1-100", 1),
|
||||
Lesson.Create("Grammar Basics", "Basic German grammar rules", 2),
|
||||
Lesson.Create("Everyday Phrases", "Common phrases for daily conversations", 2),
|
||||
Lesson.Create("Advanced Grammar", "Complex German grammar", 4)
|
||||
};
|
||||
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.UseCases.Commands;
|
||||
using GermanApp.Domain.Entities;
|
||||
using GermanApp.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
|
@ -46,13 +45,8 @@ public static class LessonsEndpoints
|
|||
// GET /api/lessons/beginner
|
||||
group.MapGet("/beginner", async ([FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
// Beginner lessons are A1 (level 1) and A2 (level 2)
|
||||
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()));
|
||||
var lessons = await repository.GetBeginnerLessonsAsync();
|
||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||
})
|
||||
.WithName("GetBeginnerLessons")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
|
|
@ -64,13 +58,8 @@ public static class LessonsEndpoints
|
|||
// GET /api/lessons/advanced
|
||||
group.MapGet("/advanced", async ([FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
// Advanced lessons are B2 (level 4) and C1 (level 5)
|
||||
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()));
|
||||
var lessons = await repository.GetAdvancedLessonsAsync();
|
||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||
})
|
||||
.WithName("GetAdvancedLessons")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
|
|
@ -121,7 +110,7 @@ public static class LessonsEndpoints
|
|||
return Results.NotFound();
|
||||
|
||||
// Map DTO to entity
|
||||
existingLesson.UpdateFromDto(dto);
|
||||
existingLesson.Update(dto.Title, dto.Description, dto.Level);
|
||||
await repository.UpdateAsync(existingLesson);
|
||||
|
||||
return Results.Ok(existingLesson.ToDto());
|
||||
|
|
|
|||
|
|
@ -153,15 +153,12 @@ try
|
|||
name: "api",
|
||||
pattern: "api/{controller}/{action}/{id?}" );
|
||||
|
||||
// Seed database with initial data
|
||||
app.SeedDatabase();
|
||||
|
||||
// Map Clean Architecture endpoints
|
||||
app.MapLessonsEndpoints();
|
||||
|
||||
// Seed database with initial data
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
await app.SeedDatabaseAsync();
|
||||
}
|
||||
|
||||
// Keep original WeatherForecast endpoint for reference
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ Implement the core backend functionality, including lesson management, AI servic
|
|||
### Features
|
||||
| # | 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.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 |
|
||||
|
|
@ -265,8 +265,8 @@ Implement the complete React + TypeScript frontend application with all UI compo
|
|||
|
||||
| Phase | Duration | Hours | Features | Status |
|
||||
|-------|----------|-------|----------|--------|
|
||||
| Phase 1: Foundation | 2 weeks | 30-42h | 2 | ✅ Complete (1.1 ✅, 1.2 ✅) |
|
||||
| Phase 2: Core Backend | 2 weeks | 42-58h | 4 | 🚀 In Progress (2.1 🚀) |
|
||||
| Phase 1: Foundation | 2 weeks | 30-42h | 2 | 🚀 In Progress (1.1 ✅, 1.2 🚀) |
|
||||
| Phase 2: Core Backend | 2 weeks | 42-58h | 4 | ⏳ Planned |
|
||||
| Phase 3: Content & Features | 2 weeks | 30-42h | 2 | ⏳ Planned |
|
||||
| Phase 4: Frontend | 2 weeks | 10-16h | 1 | ⏳ Planned |
|
||||
| **Total** | **8 weeks** | **112-158h** | **9** | 🚀 In Progress |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Feature: Lesson & Content Management
|
||||
|
||||
> **Status**: 🚀 In Progress
|
||||
> **Status**: ⏳ Planned
|
||||
> **Priority**: High
|
||||
> **Complexity**: High
|
||||
> **Estimate**: 10-16 hours
|
||||
|
|
@ -10,8 +10,6 @@
|
|||
> **PR**: -
|
||||
> **Related Features**: Infrastructure Setup, User Authentication, Vocabulary System, Quiz System, Story Integration
|
||||
|
||||
**Phase 1: Database & Models - COMPLETED ✅**
|
||||
|
||||
---
|
||||
|
||||
## 📌 Overview
|
||||
|
|
@ -134,12 +132,12 @@ Each lesson contains:
|
|||
## 🚀 Implementation Plan
|
||||
|
||||
### Phase 1: Database & Models (2-3 hours)
|
||||
- [x] Create Level entity and repository
|
||||
- [x] Create Lesson entity and repository
|
||||
- [x] Create UserProgress entity and repository
|
||||
- [x] Set up relationships between entities
|
||||
- [x] Create migrations for Levels, Lessons, UserProgress
|
||||
- [x] Seed initial A1 level and lessons
|
||||
- [ ] Create Level entity and repository
|
||||
- [ ] Create Lesson entity and repository
|
||||
- [ ] Create UserProgress entity and repository
|
||||
- [ ] Set up relationships between entities
|
||||
- [ ] Create migrations for Levels, Lessons, UserProgress
|
||||
- [ ] Seed initial A1 level and lessons
|
||||
|
||||
### Phase 2: Backend Services (3-5 hours)
|
||||
- [ ] Create LevelService with CRUD operations
|
||||
|
|
@ -182,16 +180,16 @@ Each lesson contains:
|
|||
## ✅ Tasks
|
||||
|
||||
### Backend
|
||||
- [x] Create Domain/Entities/Level.cs
|
||||
- [x] Create Domain/Entities/Lesson.cs
|
||||
- [x] Create Domain/Entities/UserProgress.cs
|
||||
- [x] Create Domain/Interfaces/ILevelRepository.cs
|
||||
- [x] Create Domain/Interfaces/ILessonRepository.cs
|
||||
- [x] Create Infrastructure/Data/Repositories/LevelRepository.cs
|
||||
- [x] Create Infrastructure/Data/Repositories/LessonRepository.cs
|
||||
- [x] Create Infrastructure/Data/Repositories/UserProgressRepository.cs
|
||||
- [x] Create Application/DTOs/LevelDto.cs
|
||||
- [x] Create Application/DTOs/LessonDto.cs
|
||||
- [ ] Create Domain/Entities/Level.cs
|
||||
- [ ] Create Domain/Entities/Lesson.cs
|
||||
- [ ] Create Domain/Entities/UserProgress.cs
|
||||
- [ ] Create Domain/Interfaces/ILevelRepository.cs
|
||||
- [ ] Create Domain/Interfaces/ILessonRepository.cs
|
||||
- [ ] Create Infrastructure/Data/Repositories/LevelRepository.cs
|
||||
- [ ] Create Infrastructure/Data/Repositories/LessonRepository.cs
|
||||
- [ ] Create Infrastructure/Data/Repositories/UserProgressRepository.cs
|
||||
- [ ] Create Application/DTOs/LevelDto.cs
|
||||
- [ ] Create Application/DTOs/LessonDto.cs
|
||||
- [ ] Create Application/DTOs/UserProgressDto.cs
|
||||
- [ ] Create Application/Services/LevelService.cs
|
||||
- [ ] Create Application/Services/LessonService.cs
|
||||
|
|
@ -203,11 +201,11 @@ Each lesson contains:
|
|||
- [ ] Write integration tests for controllers
|
||||
|
||||
### Database
|
||||
- [x] Create migration for Levels table
|
||||
- [x] Create migration for Lessons table
|
||||
- [x] Create migration for UserProgress table
|
||||
- [x] Seed A1 level with initial lessons
|
||||
- [x] Add indexes for performance
|
||||
- [ ] Create migration for Levels table
|
||||
- [ ] Create migration for Lessons table
|
||||
- [ ] Create migration for UserProgress table
|
||||
- [ ] Seed A1 level with initial lessons
|
||||
- [ ] Add indexes for performance
|
||||
|
||||
### Business Logic
|
||||
- [ ] Implement LessonUnlockService
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue