Dev #11
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user