feat: Добавил синхронизацию преподавателей из лекций
Backend CI / build-and-test (push) Successful in 55s
Frontend CI / build-and-check (push) Failing after 5m9s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12s
🚀 Create and publish a Docker image / Build & publish backend image (push) Has been skipped
🚀 Create and publish a Docker image / Build & publish frontend image (push) Has been skipped
🚀 Create and publish a Docker image / Update stack on Portainer (push) Has been skipped
Backend CI / build-and-test (push) Successful in 55s
Frontend CI / build-and-check (push) Failing after 5m9s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12s
🚀 Create and publish a Docker image / Build & publish backend image (push) Has been skipped
🚀 Create and publish a Docker image / Build & publish frontend image (push) Has been skipped
🚀 Create and publish a Docker image / Update stack on Portainer (push) Has been skipped
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using UniVerse.Application.DTOs.Sync;
|
using UniVerse.Application.DTOs.Sync;
|
||||||
using UniVerse.Application.Interfaces;
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
using UniVerse.Infrastructure.Data;
|
using UniVerse.Infrastructure.Data;
|
||||||
using UniVerse.Infrastructure.Services;
|
using UniVerse.Infrastructure.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -123,11 +124,107 @@ public class ScheduleSyncServiceTests
|
|||||||
Assert.Equal(48, lecture.MaxEnrollments);
|
Assert.Equal(48, lecture.MaxEnrollments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SyncScheduleAsync_UsesModeusEventAttendeeTeacher()
|
||||||
|
{
|
||||||
|
await using var db = CreateDbContext();
|
||||||
|
var modeus = new FakeModeusApiClient(BuildEventsResponse());
|
||||||
|
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
|
||||||
|
|
||||||
|
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
|
||||||
|
|
||||||
|
Assert.Null(result.Error);
|
||||||
|
Assert.Equal(1, result.Created);
|
||||||
|
|
||||||
|
var lecture = await db.Lectures.Include(item => item.Teacher).SingleAsync();
|
||||||
|
Assert.Equal("Иванов Иван Иванович", lecture.Teacher?.DisplayName);
|
||||||
|
Assert.Equal("modeus-b5a5cad8-60c2-4d94-9972-8a0c2e981440@modeus.local", lecture.Teacher?.Email);
|
||||||
|
|
||||||
|
var teacherProfile = await db.TeacherProfiles.Include(item => item.User).SingleAsync();
|
||||||
|
Assert.Equal("b5a5cad8-60c2-4d94-9972-8a0c2e981440", teacherProfile.ModeusId);
|
||||||
|
Assert.Equal(teacherProfile.UserId, lecture.TeacherId);
|
||||||
|
|
||||||
|
var teacherRole = await db.UserRoles.SingleAsync();
|
||||||
|
Assert.Equal(lecture.TeacherId, teacherRole.UserId);
|
||||||
|
Assert.Equal(UserRole.Teacher, teacherRole.Role);
|
||||||
|
}
|
||||||
|
|
||||||
private static AppDbContext CreateDbContext()
|
private static AppDbContext CreateDbContext()
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
.UseInMemoryDatabase($"ScheduleSyncTests_{Guid.NewGuid()}")
|
.UseInMemoryDatabase($"ScheduleSyncTests_{Guid.NewGuid()}")
|
||||||
.Options;
|
.Options;
|
||||||
|
|
||||||
return new AppDbContext(options);
|
return new AppDbContext(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ModeusEventsResponse BuildEventsResponse()
|
||||||
|
{
|
||||||
|
const string eventId = "48102128-2224-4cb9-ae8f-a91d0b7c512a";
|
||||||
|
const string courseId = "73aa6226-adbb-4e15-b264-e16fee19fd73";
|
||||||
|
const string attendeeId = "a894db4e-833f-4f52-a153-fdd7c7d32ca7";
|
||||||
|
const string personId = "b5a5cad8-60c2-4d94-9972-8a0c2e981440";
|
||||||
|
|
||||||
|
return new ModeusEventsResponse
|
||||||
|
{
|
||||||
|
Embedded = new ModeusEventsEmbedded
|
||||||
|
{
|
||||||
|
Events =
|
||||||
|
[
|
||||||
|
new ModeusEvent
|
||||||
|
{
|
||||||
|
Id = eventId,
|
||||||
|
Name = "Тема 20. Управление ресурсами проекта. Часть 2.",
|
||||||
|
TypeId = "LAB",
|
||||||
|
StartsAt = new DateTime(2026, 4, 14, 5, 0, 0, DateTimeKind.Utc),
|
||||||
|
EndsAt = new DateTime(2026, 4, 14, 6, 35, 0, DateTimeKind.Utc),
|
||||||
|
Links = new ModeusEventLinks
|
||||||
|
{
|
||||||
|
CourseUnitRealization = new ModeusHrefLink($"/{courseId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
CourseUnitRealizations =
|
||||||
|
[
|
||||||
|
new ModeusCourseUnitRealization(
|
||||||
|
courseId,
|
||||||
|
"Управление проектами разработки программного обеспечения",
|
||||||
|
"УПРПО")
|
||||||
|
],
|
||||||
|
EventTeams = [new ModeusEventTeam(eventId, 25)],
|
||||||
|
EventAttendees =
|
||||||
|
[
|
||||||
|
new ModeusEventAttendee
|
||||||
|
{
|
||||||
|
Id = attendeeId,
|
||||||
|
RoleId = "TEACH",
|
||||||
|
RoleName = "Преподаватель",
|
||||||
|
Links = new ModeusEventAttendeeLinks
|
||||||
|
{
|
||||||
|
Event = new ModeusHrefLink($"/{eventId}"),
|
||||||
|
Person = new ModeusHrefLink($"/{personId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Persons =
|
||||||
|
[
|
||||||
|
new ModeusPerson(
|
||||||
|
personId,
|
||||||
|
"Иванов",
|
||||||
|
"Иван",
|
||||||
|
"Иванович",
|
||||||
|
"Иванов Иван Иванович")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeModeusApiClient(ModeusEventsResponse events) : IModeusApiClient
|
||||||
|
{
|
||||||
|
public Task<ModeusEventsResponse> SearchEventsAsync(SyncScheduleRequest request) => Task.FromResult(events);
|
||||||
|
|
||||||
|
public Task<ModeusRoomsResponse> SearchRoomsAsync() => Task.FromResult(new ModeusRoomsResponse());
|
||||||
|
|
||||||
|
public Task<List<ModeusEmployee>> SearchEmployeeAsync(string fullname) => Task.FromResult(new List<ModeusEmployee>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ public class ModeusEventsEmbedded
|
|||||||
[JsonPropertyName("event-teams")]
|
[JsonPropertyName("event-teams")]
|
||||||
public List<ModeusEventTeam>? EventTeams { get; init; }
|
public List<ModeusEventTeam>? EventTeams { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("event-attendees")]
|
||||||
|
public List<ModeusEventAttendee>? EventAttendees { get; init; }
|
||||||
|
|
||||||
|
public List<ModeusPerson>? Persons { get; init; }
|
||||||
|
|
||||||
public List<ModeusRoom>? Rooms { get; init; }
|
public List<ModeusRoom>? Rooms { get; init; }
|
||||||
}
|
}
|
||||||
public record ModeusHrefLink(string? Href);
|
public record ModeusHrefLink(string? Href);
|
||||||
@@ -79,6 +84,21 @@ public class ModeusEventRoomLinks
|
|||||||
public ModeusHrefLink? Room { get; init; }
|
public ModeusHrefLink? Room { get; init; }
|
||||||
}
|
}
|
||||||
public record ModeusEventTeam(string EventId, int? Size);
|
public record ModeusEventTeam(string EventId, int? Size);
|
||||||
|
public class ModeusEventAttendee
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string? RoleId { get; init; }
|
||||||
|
public string? RoleName { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("_links")]
|
||||||
|
public ModeusEventAttendeeLinks? Links { get; init; }
|
||||||
|
}
|
||||||
|
public class ModeusEventAttendeeLinks
|
||||||
|
{
|
||||||
|
public ModeusHrefLink? Event { get; init; }
|
||||||
|
public ModeusHrefLink? Person { get; init; }
|
||||||
|
}
|
||||||
|
public record ModeusPerson(string Id, string? LastName, string? FirstName, string? MiddleName, string? FullName);
|
||||||
public record ModeusBuilding(string? Id, string? Name, string? NameShort, string? Address);
|
public record ModeusBuilding(string? Id, string? Name, string? NameShort, string? Address);
|
||||||
public record ModeusRoom(string Id, string Name, string? NameShort, ModeusBuilding? Building, int? TotalCapacity, int? WorkingCapacity);
|
public record ModeusRoom(string Id, string Name, string? NameShort, ModeusBuilding? Building, int? TotalCapacity, int? WorkingCapacity);
|
||||||
public record ModeusRoomsEmbedded(List<ModeusRoom>? Rooms);
|
public record ModeusRoomsEmbedded(List<ModeusRoom>? Rooms);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
|||||||
using UniVerse.Application.DTOs.Sync;
|
using UniVerse.Application.DTOs.Sync;
|
||||||
using UniVerse.Application.Interfaces;
|
using UniVerse.Application.Interfaces;
|
||||||
using UniVerse.Domain.Entities;
|
using UniVerse.Domain.Entities;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
using UniVerse.Infrastructure.Data;
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
namespace UniVerse.Infrastructure.Services;
|
namespace UniVerse.Infrastructure.Services;
|
||||||
@@ -44,6 +45,7 @@ public class ScheduleSyncService : IScheduleSyncService
|
|||||||
var courseExternalId = courseUnit?.Id ?? ev.TypeId ?? ev.Id;
|
var courseExternalId = courseUnit?.Id ?? ev.TypeId ?? ev.Id;
|
||||||
var courseName = courseUnit?.Name ?? ev.Name;
|
var courseName = courseUnit?.Name ?? ev.Name;
|
||||||
var location = await UpsertEventLocationAsync(events, ev.Id);
|
var location = await UpsertEventLocationAsync(events, ev.Id);
|
||||||
|
var teacher = await UpsertEventTeacherAsync(events, ev.Id);
|
||||||
var roomId = GetEventRoomId(events, ev.Id);
|
var roomId = GetEventRoomId(events, ev.Id);
|
||||||
var maxEnrollments = GetRoomCapacity(embeddedRoomCapacityById, roomId);
|
var maxEnrollments = GetRoomCapacity(embeddedRoomCapacityById, roomId);
|
||||||
if (maxEnrollments is null && !string.IsNullOrWhiteSpace(roomId))
|
if (maxEnrollments is null && !string.IsNullOrWhiteSpace(roomId))
|
||||||
@@ -64,6 +66,7 @@ public class ScheduleSyncService : IScheduleSyncService
|
|||||||
existing.StartsAt = startsAt;
|
existing.StartsAt = startsAt;
|
||||||
existing.EndsAt = endsAt;
|
existing.EndsAt = endsAt;
|
||||||
existing.LocationId = location?.Id;
|
existing.LocationId = location?.Id;
|
||||||
|
existing.TeacherId = teacher?.Id;
|
||||||
existing.MaxEnrollments = lectureCapacity;
|
existing.MaxEnrollments = lectureCapacity;
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
updated++;
|
updated++;
|
||||||
@@ -81,6 +84,7 @@ public class ScheduleSyncService : IScheduleSyncService
|
|||||||
_db.Lectures.Add(new Lecture
|
_db.Lectures.Add(new Lecture
|
||||||
{
|
{
|
||||||
CourseId = course.Id,
|
CourseId = course.Id,
|
||||||
|
TeacherId = teacher?.Id,
|
||||||
LocationId = location?.Id,
|
LocationId = location?.Id,
|
||||||
Title = ev.Name,
|
Title = ev.Name,
|
||||||
Description = ev.Description,
|
Description = ev.Description,
|
||||||
@@ -190,6 +194,79 @@ public class ScheduleSyncService : IScheduleSyncService
|
|||||||
|
|
||||||
public Task<SyncStatusDto> GetLastSyncStatusAsync() => Task.FromResult(_lastStatus);
|
public Task<SyncStatusDto> GetLastSyncStatusAsync() => Task.FromResult(_lastStatus);
|
||||||
|
|
||||||
|
private async Task<User?> UpsertEventTeacherAsync(ModeusEventsResponse events, string eventId)
|
||||||
|
{
|
||||||
|
var personId = events.Embedded?.EventAttendees?
|
||||||
|
.Where(attendee => string.Equals(attendee.RoleId, "TEACH", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(attendee => new
|
||||||
|
{
|
||||||
|
EventId = GetHrefId(attendee.Links?.Event?.Href),
|
||||||
|
PersonId = GetHrefId(attendee.Links?.Person?.Href)
|
||||||
|
})
|
||||||
|
.FirstOrDefault(link => link.EventId == eventId)
|
||||||
|
?.PersonId;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(personId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var person = events.Embedded?.Persons?.FirstOrDefault(item => item.Id == personId);
|
||||||
|
var fullName = BuildPersonFullName(person);
|
||||||
|
if (string.IsNullOrWhiteSpace(fullName))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var existingProfile = await _db.TeacherProfiles
|
||||||
|
.Include(profile => profile.User)
|
||||||
|
.ThenInclude(user => user.Roles)
|
||||||
|
.FirstOrDefaultAsync(profile => profile.ModeusId == personId);
|
||||||
|
|
||||||
|
if (existingProfile != null)
|
||||||
|
{
|
||||||
|
existingProfile.User.DisplayName = fullName;
|
||||||
|
existingProfile.User.UpdatedAt = DateTime.UtcNow;
|
||||||
|
EnsureTeacherRole(existingProfile.User);
|
||||||
|
return existingProfile.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = BuildModeusTeacherEmail(personId);
|
||||||
|
var user = await _db.Users
|
||||||
|
.Include(item => item.Roles)
|
||||||
|
.Include(item => item.TeacherProfile)
|
||||||
|
.FirstOrDefaultAsync(item => item.Email == email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
user = new User
|
||||||
|
{
|
||||||
|
Email = email,
|
||||||
|
DisplayName = fullName,
|
||||||
|
IsActive = true,
|
||||||
|
TeacherProfile = new TeacherProfile { ModeusId = personId }
|
||||||
|
};
|
||||||
|
user.Roles.Add(new UserRoleAssignment { User = user, Role = UserRole.Teacher });
|
||||||
|
_db.Users.Add(user);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.DisplayName = fullName;
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
if (user.TeacherProfile == null)
|
||||||
|
user.TeacherProfile = new TeacherProfile { UserId = user.Id, ModeusId = personId };
|
||||||
|
else
|
||||||
|
user.TeacherProfile.ModeusId = personId;
|
||||||
|
|
||||||
|
EnsureTeacherRole(user);
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureTeacherRole(User user)
|
||||||
|
{
|
||||||
|
if (!user.Roles.Any(role => role.Role == UserRole.Teacher))
|
||||||
|
user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = UserRole.Teacher });
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Location?> UpsertEventLocationAsync(ModeusEventsResponse events, string eventId)
|
private async Task<Location?> UpsertEventLocationAsync(ModeusEventsResponse events, string eventId)
|
||||||
{
|
{
|
||||||
var roomId = GetEventRoomId(events, eventId);
|
var roomId = GetEventRoomId(events, eventId);
|
||||||
@@ -274,6 +351,25 @@ public class ScheduleSyncService : IScheduleSyncService
|
|||||||
private static int? NormalizeCapacity(int? capacity) =>
|
private static int? NormalizeCapacity(int? capacity) =>
|
||||||
capacity is > 0 ? capacity : null;
|
capacity is > 0 ? capacity : null;
|
||||||
|
|
||||||
|
private static string BuildModeusTeacherEmail(string personId) =>
|
||||||
|
$"modeus-{personId}@modeus.local".ToLowerInvariant();
|
||||||
|
|
||||||
|
private static string? BuildPersonFullName(ModeusPerson? person)
|
||||||
|
{
|
||||||
|
if (person == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(person.FullName))
|
||||||
|
return person.FullName.Trim();
|
||||||
|
|
||||||
|
var parts = new[] { person.LastName, person.FirstName, person.MiddleName }
|
||||||
|
.Where(part => !string.IsNullOrWhiteSpace(part))
|
||||||
|
.Select(part => part!.Trim());
|
||||||
|
|
||||||
|
var fullName = string.Join(" ", parts);
|
||||||
|
return string.IsNullOrWhiteSpace(fullName) ? null : fullName;
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<string> BuildErrorDetails(
|
private static IReadOnlyList<string> BuildErrorDetails(
|
||||||
Exception exception,
|
Exception exception,
|
||||||
string stage,
|
string stage,
|
||||||
|
|||||||
Reference in New Issue
Block a user