diff --git a/backend/UniVerse.Api.Tests/Helpers/ApiWebApplicationFactory.cs b/backend/UniVerse.Api.Tests/Helpers/ApiWebApplicationFactory.cs index c9868a3..90b0fef 100644 --- a/backend/UniVerse.Api.Tests/Helpers/ApiWebApplicationFactory.cs +++ b/backend/UniVerse.Api.Tests/Helpers/ApiWebApplicationFactory.cs @@ -177,7 +177,7 @@ public class ApiWebApplicationFactory : WebApplicationFactory var pagedLectures = PagedResult.Create([lectureDto], 1, 1, 20); var pagedEnrollments = PagedResult.Create([], 0, 1, 20); - stub.GetAllAsync(Arg.Any()).Returns(pagedLectures); + stub.GetAllAsync(Arg.Any(), Arg.Any()).Returns(pagedLectures); stub.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(detailDto); stub.CreateAsync(Arg.Any()).Returns(lectureDto); stub.UpdateAsync(Arg.Any(), Arg.Any()).Returns(lectureDto); diff --git a/backend/UniVerse.Api.Tests/Lectures/LectureServiceTests.cs b/backend/UniVerse.Api.Tests/Lectures/LectureServiceTests.cs new file mode 100644 index 0000000..9f93fc8 --- /dev/null +++ b/backend/UniVerse.Api.Tests/Lectures/LectureServiceTests.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore; +using NSubstitute; +using UniVerse.Application.DTOs.Lectures; +using UniVerse.Application.Interfaces; +using UniVerse.Domain.Entities; +using UniVerse.Infrastructure.Data; +using UniVerse.Infrastructure.Services; +using Xunit; + +namespace UniVerse.Api.Tests.Lectures; + +public class LectureServiceTests +{ + [Fact] + public async Task GetAllAsync_MarksLecturesEnrolledByCurrentUser() + { + await using var db = CreateDbContext(); + var service = new LectureService(db, Substitute.For()); + var startsAt = DateTime.UtcNow.AddDays(1); + + db.Users.Add(new User { Id = 1, Email = "student@test.local" }); + db.Courses.Add(new Course { Id = 1, Name = "Course" }); + db.Lectures.AddRange( + Lecture(1, startsAt), + Lecture(2, startsAt.AddDays(1))); + db.LectureEnrollments.Add(new LectureEnrollment { LectureId = 1, UserId = 1 }); + await db.SaveChangesAsync(); + + var result = await service.GetAllAsync(new LectureFilterRequest(null, null, null, null, null, null, null, null), 1); + + Assert.True(result.Items.Single(item => item.Id == 1).IsEnrolled); + Assert.False(result.Items.Single(item => item.Id == 2).IsEnrolled); + } + + private static AppDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"LectureServiceTests_{Guid.NewGuid()}") + .Options; + return new AppDbContext(options); + } + + private static Lecture Lecture(int id, DateTime startsAt) => new() + { + Id = id, + CourseId = 1, + Title = $"Lecture {id}", + StartsAt = startsAt, + EndsAt = startsAt.AddHours(2), + IsOpen = true, + MaxEnrollments = 30 + }; +} diff --git a/backend/UniVerse.Api/Controllers/LecturesController.cs b/backend/UniVerse.Api/Controllers/LecturesController.cs index 9eb1d4a..e8237b8 100644 --- a/backend/UniVerse.Api/Controllers/LecturesController.cs +++ b/backend/UniVerse.Api/Controllers/LecturesController.cs @@ -31,13 +31,14 @@ public class LecturesController : ControllerBase /// Фильтры: dateFrom, dateTo, courseId, teacherId, format (Online/Offline), /// isOpen, tagId, search; параметры пагинации. /// + /// Включает флаг `isEnrolled` — записан ли текущий пользователь на лекцию. /// Список лекций (пагинированный). /// Требуется аутентификация. [HttpGet] [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task GetAll([FromQuery] LectureFilterRequest filter) => - Ok(await _lectures.GetAllAsync(filter)); + Ok(await _lectures.GetAllAsync(filter, CurrentUserId)); /// Получить детальную карточку лекции по ID. /// diff --git a/backend/UniVerse.Api/openapi.json b/backend/UniVerse.Api/openapi.json index 223c948..62c98b1 100644 --- a/backend/UniVerse.Api/openapi.json +++ b/backend/UniVerse.Api/openapi.json @@ -1140,7 +1140,7 @@ "Lectures" ], "summary": "Получить каталог лекций с фильтрацией и пагинацией.", - "description": "**Required:** any authenticated user", + "description": "Включает флаг `isEnrolled` — записан ли текущий пользователь на лекцию.\n\n**Required:** any authenticated user", "parameters": [ { "name": "DateFrom", @@ -4745,6 +4745,9 @@ "createdAt": { "type": "string", "format": "date-time" + }, + "isEnrolled": { + "type": "boolean" } }, "additionalProperties": false diff --git a/backend/UniVerse.Application/DTOs/Lectures/LectureDtos.cs b/backend/UniVerse.Application/DTOs/Lectures/LectureDtos.cs index 37f3340..24a34ee 100644 --- a/backend/UniVerse.Application/DTOs/Lectures/LectureDtos.cs +++ b/backend/UniVerse.Application/DTOs/Lectures/LectureDtos.cs @@ -19,7 +19,8 @@ public record LectureDto( int MaxEnrollments, int EnrollmentsCount, string? OnlineUrl, - DateTime CreatedAt + DateTime CreatedAt, + bool IsEnrolled = false ); public record LectureDetailDto( diff --git a/backend/UniVerse.Application/Interfaces/ILectureService.cs b/backend/UniVerse.Application/Interfaces/ILectureService.cs index 2dd11ad..a7e8228 100644 --- a/backend/UniVerse.Application/Interfaces/ILectureService.cs +++ b/backend/UniVerse.Application/Interfaces/ILectureService.cs @@ -5,7 +5,7 @@ namespace UniVerse.Application.Interfaces; public interface ILectureService { - Task> GetAllAsync(LectureFilterRequest filter); + Task> GetAllAsync(LectureFilterRequest filter, int? currentUserId = null); Task GetByIdAsync(int id, int? currentUserId = null); Task CreateAsync(CreateLectureRequest request); Task UpdateAsync(int id, UpdateLectureRequest request); diff --git a/backend/UniVerse.Application/Mappings/MappingExtensions.cs b/backend/UniVerse.Application/Mappings/MappingExtensions.cs index a8b581b..6b5942e 100644 --- a/backend/UniVerse.Application/Mappings/MappingExtensions.cs +++ b/backend/UniVerse.Application/Mappings/MappingExtensions.cs @@ -46,14 +46,14 @@ public static class MappingExtensions ); // --- Lecture --- - public static LectureDto ToDto(this Lecture lecture) => new( + public static LectureDto ToDto(this Lecture lecture, bool isEnrolled = false) => new( lecture.Id, lecture.CourseId, lecture.Course?.Name ?? "", lecture.TeacherId, lecture.Teacher?.DisplayName, lecture.LocationId, lecture.Location?.Name, lecture.Title, lecture.Description, lecture.Format, lecture.StartsAt, lecture.EndsAt, lecture.IsOpen, lecture.MaxEnrollments, lecture.Enrollments.Count, - lecture.OnlineUrl, lecture.CreatedAt + lecture.OnlineUrl, lecture.CreatedAt, isEnrolled ); public static LectureDetailDto ToDetailDto(this Lecture lecture, bool isEnrolled) => new( diff --git a/backend/UniVerse.Infrastructure/Services/LectureService.cs b/backend/UniVerse.Infrastructure/Services/LectureService.cs index 5d7a279..8674d10 100644 --- a/backend/UniVerse.Infrastructure/Services/LectureService.cs +++ b/backend/UniVerse.Infrastructure/Services/LectureService.cs @@ -24,7 +24,7 @@ public class LectureService : ILectureService .Include(l => l.Course).Include(l => l.Teacher) .Include(l => l.Location).Include(l => l.Enrollments); - public async Task> GetAllAsync(LectureFilterRequest filter) + public async Task> GetAllAsync(LectureFilterRequest filter, int? currentUserId = null) { var query = BaseQuery(); if (filter.CourseId.HasValue) query = query.Where(l => l.CourseId == filter.CourseId); @@ -43,7 +43,11 @@ public class LectureService : ILectureService var total = await query.CountAsync(); var items = await query.OrderBy(l => l.StartsAt) .Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync(); - return PagedResult.Create(items.Select(l => l.ToDto()).ToList(), total, filter.Page, filter.PageSize); + return PagedResult.Create( + items.Select(l => l.ToDto(currentUserId.HasValue && l.Enrollments.Any(e => e.UserId == currentUserId.Value))).ToList(), + total, + filter.Page, + filter.PageSize); } public async Task GetByIdAsync(int id, int? currentUserId = null) diff --git a/frontend/src/stores/lectures.ts b/frontend/src/stores/lectures.ts index 84f6a28..820bf91 100644 --- a/frontend/src/stores/lectures.ts +++ b/frontend/src/stores/lectures.ts @@ -94,7 +94,7 @@ export const useLecturesStore = defineStore('lectures', () => { } function isRegistered(lectureId: string) { - return registered.value.includes(lectureId) + return registered.value.includes(lectureId) || Boolean(lectures.value.find(item => item.id === lectureId)?.registered) } return { diff --git a/frontend/src/views/student/CatalogView.vue b/frontend/src/views/student/CatalogView.vue index e5d0550..f4ff151 100644 --- a/frontend/src/views/student/CatalogView.vue +++ b/frontend/src/views/student/CatalogView.vue @@ -109,6 +109,10 @@ async function registerLecture(id: string) { addToast?.(err instanceof Error ? err.message : 'Не удалось записаться на лекцию.', 'error') } } + +function isRegistered(id: string) { + return lecturesStore.isRegistered(id) +}