Dev #11

Merged
serega404 merged 87 commits from dev into main 2026-05-25 03:22:55 +03:00
10 changed files with 79 additions and 13 deletions
Showing only changes of commit d29b52f824 - Show all commits
@@ -177,7 +177,7 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
var pagedLectures = PagedResult<LectureDto>.Create([lectureDto], 1, 1, 20); var pagedLectures = PagedResult<LectureDto>.Create([lectureDto], 1, 1, 20);
var pagedEnrollments = PagedResult<EnrollmentDto>.Create([], 0, 1, 20); var pagedEnrollments = PagedResult<EnrollmentDto>.Create([], 0, 1, 20);
stub.GetAllAsync(Arg.Any<LectureFilterRequest>()).Returns(pagedLectures); stub.GetAllAsync(Arg.Any<LectureFilterRequest>(), Arg.Any<int?>()).Returns(pagedLectures);
stub.GetByIdAsync(Arg.Any<int>(), Arg.Any<int?>()).Returns(detailDto); stub.GetByIdAsync(Arg.Any<int>(), Arg.Any<int?>()).Returns(detailDto);
stub.CreateAsync(Arg.Any<CreateLectureRequest>()).Returns(lectureDto); stub.CreateAsync(Arg.Any<CreateLectureRequest>()).Returns(lectureDto);
stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateLectureRequest>()).Returns(lectureDto); stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateLectureRequest>()).Returns(lectureDto);
@@ -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<IGamificationService>());
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<AppDbContext>()
.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
};
}
@@ -31,13 +31,14 @@ public class LecturesController : ControllerBase
/// Фильтры: dateFrom, dateTo, courseId, teacherId, format (Online/Offline), /// Фильтры: dateFrom, dateTo, courseId, teacherId, format (Online/Offline),
/// isOpen, tagId, search; параметры пагинации. /// isOpen, tagId, search; параметры пагинации.
/// </param> /// </param>
/// <remarks>Включает флаг `isEnrolled` — записан ли текущий пользователь на лекцию.</remarks>
/// <response code="200">Список лекций (пагинированный).</response> /// <response code="200">Список лекций (пагинированный).</response>
/// <response code="401">Требуется аутентификация.</response> /// <response code="401">Требуется аутентификация.</response>
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(PagedResult<LectureDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResult<LectureDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> GetAll([FromQuery] LectureFilterRequest filter) => public async Task<ActionResult> GetAll([FromQuery] LectureFilterRequest filter) =>
Ok(await _lectures.GetAllAsync(filter)); Ok(await _lectures.GetAllAsync(filter, CurrentUserId));
/// <summary>Получить детальную карточку лекции по ID.</summary> /// <summary>Получить детальную карточку лекции по ID.</summary>
/// <remarks> /// <remarks>
+4 -1
View File
@@ -1140,7 +1140,7 @@
"Lectures" "Lectures"
], ],
"summary": "Получить каталог лекций с фильтрацией и пагинацией.", "summary": "Получить каталог лекций с фильтрацией и пагинацией.",
"description": "**Required:** any authenticated user", "description": "Включает флаг `isEnrolled` — записан ли текущий пользователь на лекцию.\n\n**Required:** any authenticated user",
"parameters": [ "parameters": [
{ {
"name": "DateFrom", "name": "DateFrom",
@@ -4745,6 +4745,9 @@
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
},
"isEnrolled": {
"type": "boolean"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -19,7 +19,8 @@ public record LectureDto(
int MaxEnrollments, int MaxEnrollments,
int EnrollmentsCount, int EnrollmentsCount,
string? OnlineUrl, string? OnlineUrl,
DateTime CreatedAt DateTime CreatedAt,
bool IsEnrolled = false
); );
public record LectureDetailDto( public record LectureDetailDto(
@@ -5,7 +5,7 @@ namespace UniVerse.Application.Interfaces;
public interface ILectureService public interface ILectureService
{ {
Task<PagedResult<LectureDto>> GetAllAsync(LectureFilterRequest filter); Task<PagedResult<LectureDto>> GetAllAsync(LectureFilterRequest filter, int? currentUserId = null);
Task<LectureDetailDto> GetByIdAsync(int id, int? currentUserId = null); Task<LectureDetailDto> GetByIdAsync(int id, int? currentUserId = null);
Task<LectureDto> CreateAsync(CreateLectureRequest request); Task<LectureDto> CreateAsync(CreateLectureRequest request);
Task<LectureDto> UpdateAsync(int id, UpdateLectureRequest request); Task<LectureDto> UpdateAsync(int id, UpdateLectureRequest request);
@@ -46,14 +46,14 @@ public static class MappingExtensions
); );
// --- Lecture --- // --- 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.Id, lecture.CourseId, lecture.Course?.Name ?? "",
lecture.TeacherId, lecture.Teacher?.DisplayName, lecture.TeacherId, lecture.Teacher?.DisplayName,
lecture.LocationId, lecture.Location?.Name, lecture.LocationId, lecture.Location?.Name,
lecture.Title, lecture.Description, lecture.Format, lecture.Title, lecture.Description, lecture.Format,
lecture.StartsAt, lecture.EndsAt, lecture.IsOpen, lecture.StartsAt, lecture.EndsAt, lecture.IsOpen,
lecture.MaxEnrollments, lecture.Enrollments.Count, lecture.MaxEnrollments, lecture.Enrollments.Count,
lecture.OnlineUrl, lecture.CreatedAt lecture.OnlineUrl, lecture.CreatedAt, isEnrolled
); );
public static LectureDetailDto ToDetailDto(this Lecture lecture, bool isEnrolled) => new( public static LectureDetailDto ToDetailDto(this Lecture lecture, bool isEnrolled) => new(
@@ -24,7 +24,7 @@ public class LectureService : ILectureService
.Include(l => l.Course).Include(l => l.Teacher) .Include(l => l.Course).Include(l => l.Teacher)
.Include(l => l.Location).Include(l => l.Enrollments); .Include(l => l.Location).Include(l => l.Enrollments);
public async Task<PagedResult<LectureDto>> GetAllAsync(LectureFilterRequest filter) public async Task<PagedResult<LectureDto>> GetAllAsync(LectureFilterRequest filter, int? currentUserId = null)
{ {
var query = BaseQuery(); var query = BaseQuery();
if (filter.CourseId.HasValue) query = query.Where(l => l.CourseId == filter.CourseId); 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 total = await query.CountAsync();
var items = await query.OrderBy(l => l.StartsAt) var items = await query.OrderBy(l => l.StartsAt)
.Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync(); .Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync();
return PagedResult<LectureDto>.Create(items.Select(l => l.ToDto()).ToList(), total, filter.Page, filter.PageSize); return PagedResult<LectureDto>.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<LectureDetailDto> GetByIdAsync(int id, int? currentUserId = null) public async Task<LectureDetailDto> GetByIdAsync(int id, int? currentUserId = null)
+1 -1
View File
@@ -94,7 +94,7 @@ export const useLecturesStore = defineStore('lectures', () => {
} }
function isRegistered(lectureId: string) { function isRegistered(lectureId: string) {
return registered.value.includes(lectureId) return registered.value.includes(lectureId) || Boolean(lectures.value.find(item => item.id === lectureId)?.registered)
} }
return { return {
+7 -3
View File
@@ -109,6 +109,10 @@ async function registerLecture(id: string) {
addToast?.(err instanceof Error ? err.message : 'Не удалось записаться на лекцию.', 'error') addToast?.(err instanceof Error ? err.message : 'Не удалось записаться на лекцию.', 'error')
} }
} }
function isRegistered(id: string) {
return lecturesStore.isRegistered(id)
}
</script> </script>
<template> <template>
@@ -204,7 +208,7 @@ async function registerLecture(id: string) {
v-for="l in filtered" v-for="l in filtered"
:key="l.id" :key="l.id"
:lecture="l" :lecture="l"
:registered="lecturesStore.registeredIds.includes(l.id)" :registered="isRegistered(l.id)"
:show-rating="false" :show-rating="false"
@register="registerLecture" @register="registerLecture"
/> />
@@ -231,10 +235,10 @@ async function registerLecture(id: string) {
<template #action="{ row }"> <template #action="{ row }">
<button <button
class="btn-primary btn-sm" class="btn-primary btn-sm"
:disabled="row.freeSeats === 0 || row.registrationClosed || lecturesStore.registeredIds.includes(row.id)" :disabled="row.freeSeats === 0 || row.registrationClosed || isRegistered(row.id)"
@click="registerLecture(row.id)" @click="registerLecture(row.id)"
> >
{{ lecturesStore.registeredIds.includes(row.id) ? 'Записан' : 'Записаться' }} {{ isRegistered(row.id) ? 'Записан' : 'Записаться' }}
</button> </button>
</template> </template>
</DataTable> </DataTable>