Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using PIED_LMS.Contract.Abstractions.Storage;
using PIED_LMS.Contract.Services.Course;
using PIED_LMS.Contract.Services.Identity;
using PIED_LMS.Application.UserCases;

using PIED_LMS.Domain.Abstractions;

namespace PIED_LMS.Application.UserCases.Commands.Course;
Expand Down Expand Up @@ -35,20 +35,7 @@
return new ServiceResponse<Guid>(false, errorMessage);
}

var (curriculumJson, curriculumError) = CourseContentHelper.ValidateAndSerializeCurriculum(request.Curriculum);
if (curriculumError is not null)
{
logger.LogWarning("Course creation validation failed: {ValidationError}", curriculumError);
return new ServiceResponse<Guid>(false, curriculumError);
}

var insight = CourseContentHelper.ValidateAndNormalizeInsight(request.Insight);
if (insight is null)
{
const string errorMessage = "Insight is required and cannot be empty.";
logger.LogWarning("Course creation validation failed: {ValidationError}", errorMessage);
return new ServiceResponse<Guid>(false, errorMessage);
}

// Subtask 5.4: Implement file upload and course creation
string? thumbnailPath = null;
Expand Down Expand Up @@ -90,11 +77,39 @@
Seats = request.Seats,
Price = request.Price,
Value = request.Value,
Curriculum = curriculumJson,
Insight = insight,
CreatedAt = DateTime.UtcNow
};

try
{
if (request.Curriculum != null)
{
var domainCurriculum = request.Curriculum.Select(c =>
new Domain.Entities.CurriculumSection(c.Title, c.Summary, c.Content)).ToList();
course.SetCurriculum(domainCurriculum);
}
course.SetInsight(request.Insight);
}
catch (ArgumentException ex)
{
logger.LogWarning("Course creation validation failed: {ValidationError}", ex.Message);

if (!string.IsNullOrEmpty(thumbnailPath))
{
try
{
await fileStorageService.DeleteFileAsync(thumbnailPath, cancellationToken);
logger.LogInformation("Deleted orphaned thumbnail {ThumbnailPath} after validation failure", thumbnailPath);
}
catch (Exception deleteEx)
{
logger.LogError(deleteEx, "Failed to delete orphaned thumbnail {ThumbnailPath}", thumbnailPath);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
Comment on lines +104 to +107
}

return new ServiceResponse<Guid>(false, ex.Message);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Add course to repository
var repository = unitOfWork.Repository<Domain.Entities.Course>();
await repository.AddAsync(course, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using PIED_LMS.Contract.Abstractions.Storage;
using PIED_LMS.Contract.Services.Course;
using PIED_LMS.Contract.Services.Identity;
using PIED_LMS.Application.UserCases;

using PIED_LMS.Domain.Abstractions;

namespace PIED_LMS.Application.UserCases.Commands.Course;
Expand Down Expand Up @@ -37,21 +37,43 @@ public async Task<ServiceResponse<string>> Handle(UpdateCourseCommand request, C
return new ServiceResponse<string>(false, validationError);
}

var (curriculumJson, curriculumError) = CourseContentHelper.ValidateAndSerializeCurriculum(request.Curriculum);
if (curriculumError is not null)


// Validate new slug uniqueness if slug changed
var newSlug = course.Slug;
if (!string.IsNullOrWhiteSpace(request.Slug) && request.Slug != course.Slug)
{
logger.LogWarning("Course update validation failed for course {CourseId}: {ValidationError}",
request.Id, curriculumError);
return new ServiceResponse<string>(false, curriculumError);
var slugToValidate = request.Slug.ToLowerInvariant().Trim();
var slugExists = await repository.AnyAsync(
c => c.Slug == slugToValidate && c.Id != request.Id,
cancellationToken);

if (slugExists)
{
logger.LogWarning("Course update failed for course {CourseId}: Slug '{Slug}' already exists",
request.Id, slugToValidate);
return new ServiceResponse<string>(false, "Slug already exists. Please provide a unique slug.");
}

newSlug = slugToValidate;
}

var insight = CourseContentHelper.ValidateAndNormalizeInsight(request.Insight);
if (insight is null)
// Validate Domain rules (Curriculum and Insight) before any side-effects
try
{
const string errorMessage = "Insight is required and cannot be empty.";
logger.LogWarning("Course update validation failed for course {CourseId}: {ValidationError}",
request.Id, errorMessage);
return new ServiceResponse<string>(false, errorMessage);
if (request.Curriculum != null)
{
var domainCurriculum = request.Curriculum.Select(c =>
new Domain.Entities.CurriculumSection(c.Title, c.Summary, c.Content)).ToList();
course.SetCurriculum(domainCurriculum);
}

course.SetInsight(request.Insight);
}
catch (ArgumentException ex)
{
logger.LogWarning("Course update validation failed for course {CourseId}: {ValidationError}", request.Id, ex.Message);
return new ServiceResponse<string>(false, ex.Message);
}

// Subtask 6.3: Implement thumbnail update logic
Expand Down Expand Up @@ -85,25 +107,6 @@ public async Task<ServiceResponse<string>> Handle(UpdateCourseCommand request, C
}

// Subtask 6.4: Implement course update and commit
// Validate new slug uniqueness if slug changed
var newSlug = course.Slug;
if (!string.IsNullOrWhiteSpace(request.Slug) && request.Slug != course.Slug)
{
var slugToValidate = request.Slug.ToLowerInvariant().Trim();
var slugExists = await repository.AnyAsync(
c => c.Slug == slugToValidate && c.Id != request.Id,
cancellationToken);

if (slugExists)
{
logger.LogWarning("Course update failed for course {CourseId}: Slug '{Slug}' already exists",
request.Id, slugToValidate);
return new ServiceResponse<string>(false, "Slug already exists. Please provide a unique slug.");
}

newSlug = slugToValidate;
}

// Track changed properties for logging
var changedProperties = new List<string>();
if (course.Title != request.Title) changedProperties.Add($"Title: '{course.Title}' -> '{request.Title}'");
Expand All @@ -113,8 +116,6 @@ public async Task<ServiceResponse<string>> Handle(UpdateCourseCommand request, C
if (course.Status != request.Status) changedProperties.Add($"Status: {course.Status} -> {request.Status}");
if (course.Slug != newSlug) changedProperties.Add($"Slug: '{course.Slug}' -> '{newSlug}'");
if (course.ThumbnailPath != newThumbnailPath) changedProperties.Add("ThumbnailPath");
if (course.Curriculum != curriculumJson) changedProperties.Add("Curriculum");
if (course.Insight != insight) changedProperties.Add("Insight");

// Update course properties
course.Title = request.Title;
Expand All @@ -131,8 +132,6 @@ public async Task<ServiceResponse<string>> Handle(UpdateCourseCommand request, C
course.Seats = request.Seats;
course.Price = request.Price;
course.Value = request.Value;
course.Curriculum = curriculumJson;
course.Insight = insight;
course.UpdatedAt = DateTime.UtcNow;

// Update course in repository
Expand Down
73 changes: 0 additions & 73 deletions backend/Src/PIED_LMS.Application/UserCases/CourseContentHelper.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using PIED_LMS.Contract.Abstractions.Storage;
using PIED_LMS.Contract.Services.Course;
using PIED_LMS.Contract.Services.Identity;
using PIED_LMS.Application.UserCases;

using PIED_LMS.Domain.Abstractions;

namespace PIED_LMS.Application.UserCases.Queries.Course;
Expand Down Expand Up @@ -108,7 +108,8 @@ private async Task<CourseDto> MapToCourseDto(Domain.Entities.Course course, Canc
t.ProfilePictureUrl
)).ToList();

var curriculum = CourseContentHelper.DeserializeCurriculum(course.Curriculum, logger, course.Id);
var curriculumDto = course.Curriculum?.Select(c =>
new CurriculumSectionDto(c.Title, c.Summary, c.Content.ToList())).ToList() ?? new List<CurriculumSectionDto>();

return new CourseDto(
course.Id,
Expand All @@ -127,7 +128,7 @@ private async Task<CourseDto> MapToCourseDto(Domain.Entities.Course course, Canc
course.CreatedAt,
course.UpdatedAt,
course.Value,
curriculum,
curriculumDto,
course.Insight ?? string.Empty
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using PIED_LMS.Contract.Services.Course;
using PIED_LMS.Contract.Services.Identity;
using PIED_LMS.Application.UserCases;

using PIED_LMS.Domain.Abstractions;

namespace PIED_LMS.Application.UserCases.Queries.Course;
Expand Down Expand Up @@ -28,12 +28,13 @@ public async Task<ServiceResponse<List<CurriculumSectionDto>>> Handle(
);
}

var curriculum = CourseContentHelper.DeserializeCurriculum(course.Curriculum, logger, course.Id);
var curriculumDto = course.Curriculum?.Select(c =>
new CurriculumSectionDto(c.Title, c.Summary, c.Content.ToList())).ToList() ?? new List<CurriculumSectionDto>();

return new ServiceResponse<List<CurriculumSectionDto>>(
true,
"Curriculum retrieved successfully",
curriculum
curriculumDto
);
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using PIED_LMS.Contract.Abstractions.Storage;
using PIED_LMS.Contract.Services.Course;
using PIED_LMS.Contract.Services.Identity;
using PIED_LMS.Application.UserCases;

using PIED_LMS.Domain.Abstractions;

namespace PIED_LMS.Application.UserCases.Queries.Course;
Expand Down Expand Up @@ -119,7 +119,8 @@ private async Task<CourseDto> MapToCourseDto(Domain.Entities.Course course, Canc
t.ProfilePictureUrl
)).ToList();

var curriculum = CourseContentHelper.DeserializeCurriculum(course.Curriculum, logger, course.Id);
var curriculumDto = course.Curriculum?.Select(c =>
new CurriculumSectionDto(c.Title, c.Summary, c.Content.ToList())).ToList() ?? new List<CurriculumSectionDto>();

return new CourseDto(
course.Id,
Expand All @@ -138,7 +139,7 @@ private async Task<CourseDto> MapToCourseDto(Domain.Entities.Course course, Canc
course.CreatedAt,
course.UpdatedAt,
course.Value,
curriculum,
curriculumDto,
course.Insight ?? string.Empty
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using PIED_LMS.Contract.Abstractions.Storage;
using PIED_LMS.Contract.Abstractions.Storage;
using PIED_LMS.Contract.Services.Identity;
using PIED_LMS.Domain.Abstractions;
using PIED_LMS.Domain.Entities;

namespace PIED_LMS.Application.UserCases.Queries;
Expand Down
8 changes: 4 additions & 4 deletions backend/Src/PIED_LMS.Contract/Services/Course/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public record CreateCourseCommand(
string? Seats,
string? Price,
int Value,
string? Curriculum,
string? Insight
List<CurriculumSectionDto>? Curriculum,
string Insight
) : IRequest<ServiceResponse<Guid>>;

// Update Course Command
Expand All @@ -38,8 +38,8 @@ public record UpdateCourseCommand(
string? Seats,
string? Price,
int Value,
string? Curriculum,
string? Insight
List<CurriculumSectionDto>? Curriculum,
string Insight
) : IRequest<ServiceResponse<string>>;

// Delete Course Command
Expand Down
4 changes: 2 additions & 2 deletions backend/Src/PIED_LMS.Contract/Services/Course/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed record CreateCourseRequest(
[FromForm(Name = "seats")] string? Seats,
[FromForm(Name = "price")] string? Price,
[FromForm(Name = "value")] int Value,
[FromForm(Name = "curriculum")] string? Curriculum,
[FromForm(Name = "curriculum")] List<CurriculumSectionDto>? Curriculum,
[FromForm(Name = "insight")]
[Required(ErrorMessage = "Insight is required and cannot be empty.")]
string Insight
Expand All @@ -37,7 +37,7 @@ public sealed record UpdateCourseRequest(
[FromForm(Name = "seats")] string? Seats,
[FromForm(Name = "price")] string? Price,
[FromForm(Name = "value")] int Value,
[FromForm(Name = "curriculum")] string? Curriculum,
[FromForm(Name = "curriculum")] List<CurriculumSectionDto>? Curriculum,
[FromForm(Name = "insight")]
[Required(ErrorMessage = "Insight is required and cannot be empty.")]
string Insight
Expand Down
Loading
Loading