diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs index 4d1bbd78f..ef02af168 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs @@ -74,14 +74,6 @@ public async Task AddStudentInGroup(long courseId, long groupId, return Ok(); } - [HttpPost("{courseId}/removeStudentFromGroup/{groupId}")] - [Authorize(Roles = Roles.LecturerRole)] - public async Task RemoveStudentFromGroup(long courseId, long groupId, [FromQuery] string userId) - { - await _coursesClient.RemoveStudentFromGroup(courseId, groupId, userId); - return Ok(); - } - [HttpGet("get/{groupId}")] [ProducesResponseType(typeof(GroupViewModel), (int)HttpStatusCode.OK)] public async Task GetGroup(long groupId) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index 7ad6bf87a..c3c94084a 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -243,7 +243,7 @@ public async Task EditMentorWorkspace( return BadRequest("Пользователь с такой почтой не является преподавателем или экспертом"); var courseFilterModel = _mapper.Map(editMentorWorkspaceDto); - courseFilterModel.UserId = mentorId; + courseFilterModel.Id = mentorId; var courseFilterCreationResult = await _coursesClient.CreateOrUpdateCourseFilter(courseId, courseFilterModel); @@ -308,6 +308,7 @@ private async Task ToCourseViewModel(CourseDTO course) AcceptedStudents = acceptedStudents.ToArray(), NewStudents = newStudents.ToArray(), Homeworks = course.Homeworks, + Groups = course.Groups, IsCompleted = course.IsCompleted, IsOpen = course.IsOpen }; diff --git a/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs b/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs index 6ca399364..b1612eabe 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs @@ -4,14 +4,14 @@ namespace HwProj.Models.CoursesService { public class CreateCourseFilterModel { - public string UserId { get; set; } + public string Id { get; set; } public long CourseId { get; set; } - + public List StudentIds { get; set; } = new List(); - + public List HomeworkIds { get; set; } = new List(); - + public List MentorIds { get; set; } = new List(); } } \ No newline at end of file diff --git a/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs b/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs index a8c8a0fa2..a0838be69 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs @@ -4,7 +4,7 @@ namespace HwProj.Models.CoursesService.DTO { public class CreateCourseFilterDTO { - public string UserId { get; set; } + public string Id { get; set; } public List StudentIds { get; set; } = new List(); diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs index 18e2b51b6..d61937109 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs @@ -52,6 +52,7 @@ public class CourseViewModel public bool IsOpen { get; set; } public bool IsCompleted { get; set; } + public GroupViewModel[] Groups { get; set; } public AccountDataDto[] Mentors { get; set; } public AccountDataDto[] AcceptedStudents { get; set; } public AccountDataDto[] NewStudents { get; set; } diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs index 5f654924a..29794f467 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs @@ -6,6 +6,7 @@ namespace HwProj.Models.CoursesService.ViewModels public class GroupViewModel { public long Id { get; set; } + public string Name { get; set; } public string[] StudentsIds { get; set; } } diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs index 2c7b0a857..1a196e463 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs @@ -27,6 +27,8 @@ public class CreateHomeworkViewModel public List Tasks { get; set; } = new List(); public ActionOptions? ActionOptions { get; set; } + + public long? GroupId { get; set; } } public class HomeworkViewModel @@ -58,5 +60,7 @@ public class HomeworkViewModel public List Tags { get; set; } = new List(); public List Tasks { get; set; } = new List(); + + public long? GroupId { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs b/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs index 845b0e146..43646b989 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs @@ -21,6 +21,11 @@ public AutomapperProfile() CreateMap(); CreateMap(); + + CreateMap().ReverseMap(); + CreateMap(); + + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs index f99ce1d42..d248e5098 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs @@ -30,6 +30,7 @@ public async Task GetAll(long courseId) var result = groups.Select(t => new GroupViewModel { Id = t.Id, + Name = t.Name, StudentsIds = t.GroupMates.Select(s => s.StudentId).ToArray() }).ToArray(); @@ -80,15 +81,6 @@ public async Task AddStudentInGroup(long groupId, [FromQuery] str return Ok(); } - [HttpPost("{courseId}/removeStudentFromGroup/{groupId}")] - [ServiceFilter(typeof(CourseMentorOnlyAttribute))] - public async Task RemoveStudentFromGroup(long groupId, [FromQuery] string userId) - { - return await _groupsService.DeleteGroupMateAsync(groupId, userId) - ? Ok() - : NotFound() as IActionResult; - } - [HttpGet] public async Task Get([FromBody] long[] groupIds) { @@ -96,6 +88,7 @@ public async Task Get([FromBody] long[] groupIds) var result = groups.Select(group => new GroupViewModel { Id = group.Id, + Name = group.Name, StudentsIds = group.GroupMates.Select(g => g.StudentId).ToArray() }).ToArray(); return Ok(result) as IActionResult; diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index a6c321f66..152bf0410 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -32,6 +32,7 @@ public static HomeworkViewModel ToHomeworkViewModel(this Homework homework) IsDeferred = DateTime.UtcNow < homework.PublicationDate, Tasks = homework.Tasks.Select(t => t.ToHomeworkTaskViewModel()).ToList(), Tags = tags.ToList(), + GroupId = homework.GroupId, }; } @@ -147,6 +148,7 @@ public static Homework ToHomework(this CreateHomeworkViewModel homework) PublicationDate = homework.PublicationDate, Tasks = homework.Tasks.Select(t => t.ToHomeworkTask()).ToList(), Tags = string.Join(";", homework.Tags), + GroupId = homework.GroupId, }; public static CourseTemplate ToCourseTemplate(this CreateCourseViewModel createCourseViewModel) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs index 2a4dda861..6cade85ea 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs @@ -112,6 +112,11 @@ public static List ValidateHomework(CreateHomeworkViewModel homework, Ho errors.Add("Нельзя изменить дату публикации домашнего задания, если она уже показана студента"); } + if (previousState.GroupId != homework.GroupId) + { + errors.Add("Нельзя изменить группу для домашнего задания, если оно уже опубликовано"); + } + return errors; } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.Designer.cs new file mode 100644 index 000000000..191ea7625 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.Designer.cs @@ -0,0 +1,485 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260410190811_HomeworkGroupId")] + partial class HomeworkGroupId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("MentorId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupName") + .HasColumnType("nvarchar(max)"); + + b.Property("InviteCode") + .HasColumnType("nvarchar(max)"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("IsOpen") + .HasColumnType("bit"); + + b.Property("MentorIds") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FilterJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("IsAccepted") + .HasColumnType("bit"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("MaxPoints") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Criteria"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("StudentId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("HomeworkId") + .HasColumnType("bigint"); + + b.Property("IsBonusExplicit") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("MaxRating") + .HasColumnType("int"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId") + .HasColumnType("bigint"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsPrivate") + .HasColumnType("bit"); + + b.Property("LecturerId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Text") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CourseFilterId") + .HasColumnType("bigint"); + + b.HasKey("CourseId", "UserId"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") + .WithMany("Criteria") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Homework"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate", null) + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CourseFilter"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Navigation("Assignments"); + + b.Navigation("CourseMates"); + + b.Navigation("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Navigation("Characteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Navigation("GroupMates"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Navigation("Criteria"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.cs new file mode 100644 index 000000000..d0990772a --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + /// + public partial class HomeworkGroupId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GroupId", + table: "Homeworks", + type: "bigint", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GroupId", + table: "Homeworks"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.Designer.cs new file mode 100644 index 000000000..35446d065 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.Designer.cs @@ -0,0 +1,485 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260413171549_FilterId")] + partial class FilterId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("MentorId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupName") + .HasColumnType("nvarchar(max)"); + + b.Property("InviteCode") + .HasColumnType("nvarchar(max)"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("IsOpen") + .HasColumnType("bit"); + + b.Property("MentorIds") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FilterJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("IsAccepted") + .HasColumnType("bit"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("MaxPoints") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Criteria"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("StudentId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("HomeworkId") + .HasColumnType("bigint"); + + b.Property("IsBonusExplicit") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("MaxRating") + .HasColumnType("int"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId") + .HasColumnType("bigint"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsPrivate") + .HasColumnType("bit"); + + b.Property("LecturerId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Text") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("CourseFilterId") + .HasColumnType("bigint"); + + b.HasKey("CourseId", "Id"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") + .WithMany("Criteria") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Homework"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate", null) + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CourseFilter"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Navigation("Assignments"); + + b.Navigation("CourseMates"); + + b.Navigation("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Navigation("Characteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Navigation("GroupMates"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Navigation("Criteria"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.cs new file mode 100644 index 000000000..ae885fc27 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + /// + public partial class FilterId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "UserId", + table: "UserToCourseFilters", + newName: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Id", + table: "UserToCourseFilters", + newName: "UserId"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 106df5d62..4b3bb83b5 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -202,6 +202,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("nvarchar(max)"); + b.Property("GroupId") + .HasColumnType("bigint"); + b.Property("HasDeadline") .HasColumnType("bit"); @@ -343,13 +346,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CourseId") .HasColumnType("bigint"); - b.Property("UserId") + b.Property("Id") .HasColumnType("nvarchar(450)"); b.Property("CourseFilterId") .HasColumnType("bigint"); - b.HasKey("CourseId", "UserId"); + b.HasKey("CourseId", "Id"); b.HasIndex("CourseFilterId"); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs index 42587af31..c32220254 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using System; +using Microsoft.EntityFrameworkCore; namespace HwProj.CoursesService.API.Models { @@ -8,6 +9,7 @@ public sealed class CourseContext : DbContext public DbSet CourseMates { get; set; } public DbSet Groups { get; set; } public DbSet GroupMates { get; set; } + [Obsolete("Не используется")] public DbSet TasksModels { get; set; } public DbSet Homeworks { get; set; } public DbSet Tasks { get; set; } @@ -26,7 +28,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasAlternateKey(u => new { u.GroupId, u.StudentId }); modelBuilder.Entity().HasIndex(a => a.CourseId); - modelBuilder.Entity().HasKey(u => new { u.CourseId, u.UserId }); + modelBuilder.Entity().HasKey(u => new { u.CourseId, u.Id }); modelBuilder.Entity().HasIndex(t => t.TaskId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs index 7aa8229a2..d5e2f6f49 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using HwProj.Repositories.Net8; @@ -15,6 +16,7 @@ public class Group : IEntity public List GroupMates { get; set; } = new List(); + [Obsolete("Не используется")] public List Tasks { get; set; } = new List(); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs index 455c411a8..466bacaf2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs @@ -25,5 +25,7 @@ public class Homework : IEntity public long CourseId { get; set; } public List Tasks { get; set; } + + public long? GroupId { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs index 4f7ed5fb3..1b4431a9a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs @@ -5,7 +5,7 @@ namespace HwProj.CoursesService.API.Models public class UserToCourseFilter { public long CourseId { get; set; } - public string UserId { get; set; } + public string Id { get; set; } public CourseFilter CourseFilter { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs index 0ed63aa38..7f47e4f77 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs @@ -18,7 +18,7 @@ public CourseFilterRepository(CourseContext context) : base(context) var userToCourseFilter = await Context.Set() .Include(ucf => ucf.CourseFilter) .AsNoTracking() - .FirstOrDefaultAsync(u => u.UserId == userId && u.CourseId == courseId); + .FirstOrDefaultAsync(u => u.Id == userId && u.CourseId == courseId); return userToCourseFilter?.CourseFilter; } @@ -27,7 +27,7 @@ public async Task> GetAsync(string[] userIds, long cour { return await Context.Set() .AsNoTracking() - .Where(u => userIds.Contains(u.UserId) && u.CourseId == courseId) + .Where(u => userIds.Contains(u.Id) && u.CourseId == courseId) .Include(ucf => ucf.CourseFilter) .ToListAsync(); } @@ -36,7 +36,7 @@ public async Task> GetAsync(string userId, long[] cours { return await Context.Set() .AsNoTracking() - .Where(u => u.UserId == userId && courseIds.Contains(u.CourseId)) + .Where(u => u.Id == userId && courseIds.Contains(u.CourseId)) .Include(ucf => ucf.CourseFilter) .ToListAsync(); } @@ -53,7 +53,7 @@ public async Task AddAsync(CourseFilter courseFilter, string userId, long { CourseFilterId = filterId, CourseId = courseId, - UserId = userId + Id = userId }; Context.Set().Add(userToCourseFilter); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs index 6b0c8df45..3ed37a429 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs @@ -27,8 +27,6 @@ public IQueryable GetGroupsWithGroupMatesByCourse(long courseId) return Context.Set() .Where(c => c.CourseId == courseId) .Include(c => c.GroupMates) - .AsNoTracking() - .Include(c => c.Tasks) .AsNoTracking(); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs index d25c28238..375d2f953 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs @@ -7,7 +7,7 @@ namespace HwProj.CoursesService.API.Repositories.Groups { public interface IGroupsRepository : ICrudRepository { - Task GetGroupsWithGroupMatesAsync(long[] ids); + Task GetGroupsWithGroupMatesAsync(params long[] ids); IQueryable GetGroupsWithGroupMatesByCourse(long courseId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/ITaskModelsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/ITaskModelsRepository.cs deleted file mode 100644 index 3d5813b2d..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/ITaskModelsRepository.cs +++ /dev/null @@ -1,9 +0,0 @@ -using HwProj.CoursesService.API.Models; -using HwProj.Repositories.Net8; - -namespace HwProj.CoursesService.API.Repositories.Groups -{ - public interface ITaskModelsRepository : ICrudRepository - { - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/TaskModelsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/TaskModelsRepository.cs deleted file mode 100644 index 28609be69..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/TaskModelsRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using HwProj.CoursesService.API.Models; -using HwProj.Repositories.Net8; - -namespace HwProj.CoursesService.API.Repositories.Groups -{ - public class TaskModelsRepository : CrudRepository, ITaskModelsRepository - { - public TaskModelsRepository(CourseContext context) - : base(context) - { - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index b47139ee0..e5e51dfc8 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -6,11 +6,21 @@ using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Result; +using System.Collections.Generic; +using System; namespace HwProj.CoursesService.API.Services { + public enum ApplyFilterOperation + { + Intersect, + Union, + Subtract + } + public class CourseFilterService : ICourseFilterService { + private const string GlobalFilterId = ""; private readonly ICourseFilterRepository _courseFilterRepository; public CourseFilterService( @@ -24,14 +34,14 @@ public async Task> CreateOrUpdateCourseFilter(CreateCourseFilterMod var filter = CourseFilterUtils.CreateFilter(courseFilterModel); var existingCourseFilter = - await _courseFilterRepository.GetAsync(courseFilterModel.UserId, courseFilterModel.CourseId); + await _courseFilterRepository.GetAsync(courseFilterModel.Id, courseFilterModel.CourseId); if (existingCourseFilter != null) { await UpdateAsync(existingCourseFilter.Id, filter); return Result.Success(existingCourseFilter.Id); } - var filterId = await AddCourseFilter(filter, courseFilterModel.CourseId, courseFilterModel.UserId); + var filterId = await AddCourseFilter(filter, courseFilterModel.CourseId, courseFilterModel.Id); if (filterId == -1) { return Result.Failed(); @@ -57,49 +67,64 @@ await _courseFilterRepository.UpdateAsync(courseFilterId, f => public async Task ApplyFiltersToCourses(string userId, CourseDTO[] courses) { - var courseIds = courses.Select(c => c.Id).ToArray(); - - var filters = (await _courseFilterRepository.GetAsync(userId, courseIds)) - .ToDictionary(x => x.CourseId, x => x.CourseFilter); + var result = new List(); + foreach (var course in courses) + { + result.Add(await ApplyFilter(course, userId)); + } - return courses - .Select(course => - { - filters.TryGetValue(course.Id, out var courseFilter); - return ApplyFilterInternal(course, courseFilter); - }) - .ToArray(); + return result.ToArray(); } - public async Task ApplyFilter(CourseDTO courseDto, string userId) + public async Task ApplyFilter(CourseDTO course, string userId) { - var isMentor = courseDto.MentorIds.Contains(userId); - var isCourseStudent = courseDto.AcceptedStudents.Any(t => t.StudentId == userId); + var isMentor = course.MentorIds.Contains(userId); + var isCourseStudent = course.AcceptedStudents.Any(t => t.StudentId == userId); + + // Получаем группы пользователя из course + var studentGroups = course.Groups + .Where(g => g.StudentsIds.Contains(userId)) + .ToArray(); + var groupIds = studentGroups.Select(g => g.Id.ToString()).ToArray(); + var findFiltersFor = isMentor || !isCourseStudent - ? new[] { userId } - : courseDto.MentorIds.Concat(new[] { userId }).ToArray(); + ? new[] { userId, GlobalFilterId } + : course.MentorIds.Concat(new[] { userId, GlobalFilterId }).Concat(groupIds).ToArray(); var courseFilters = - (await _courseFilterRepository.GetAsync(findFiltersFor, courseDto.Id)) - .ToDictionary(x => x.UserId, x => x.CourseFilter); - - var course = courseFilters.TryGetValue(userId, out var userFilter) - ? ApplyFilterInternal(courseDto, userFilter) - : courseDto; - if (isMentor || !isCourseStudent) return course; - - var mentorIds = course.MentorIds - .Where(u => - // Фильтрация не настроена вообще - !courseFilters.TryGetValue(u, out var courseFilter) || - // Не отфильтрованы студенты - !courseFilter.Filter.StudentIds.Any() || - // Фильтр содержит студента - courseFilter.Filter.StudentIds.Contains(userId)) - .ToArray(); + (await _courseFilterRepository.GetAsync(findFiltersFor, course.Id)) + .ToDictionary(x => x.Id, x => x.CourseFilter); + + if (!isMentor) + { + var globalFilter = courseFilters.GetValueOrDefault(GlobalFilterId); + var globalCourse = globalFilter != null + ? ApplyFilterInternal(course, course, globalFilter, ApplyFilterOperation.Subtract) + : course; + + var studentCourse = studentGroups + .Select(g => courseFilters.GetValueOrDefault(g.Id.ToString())) + .Where(cf => cf != null) + .Aggregate(globalCourse, (current, groupCourseFilter) => + ApplyFilterInternal(course, current, groupCourseFilter, ApplyFilterOperation.Union)); + + var mentorIds = course.MentorIds + .Where(u => + // Фильтрация не настроена вообще + !courseFilters.TryGetValue(u, out var courseFilter) || + // Не отфильтрованы студенты + !courseFilter.Filter.StudentIds.Any() || + // Фильтр содержит студента + courseFilter.Filter.StudentIds.Contains(userId)) + .ToArray(); + + studentCourse.MentorIds = mentorIds; + return studentCourse; + } - courseDto.MentorIds = mentorIds; - return course; + return courseFilters.TryGetValue(userId, out var userFilter) + ? ApplyFilterInternal(course, course, userFilter, ApplyFilterOperation.Intersect) + : course; } public async Task GetAssignedStudentsIds(long courseId, string[] mentorsIds) @@ -110,7 +135,7 @@ public async Task GetAssignedStudentsIds(long cou .Where(u => u.CourseFilter.Filter.HomeworkIds.Count == 0) .Select(u => new MentorToAssignedStudentsDTO { - MentorId = u.UserId, + MentorId = u.Id, SelectedStudentsIds = u.CourseFilter.Filter.StudentIds }) .ToArray(); @@ -123,52 +148,91 @@ private async Task AddCourseFilter(Filter filter, long courseId, string us return courseFilterId; } - private CourseDTO ApplyFilterInternal(CourseDTO courseDto, CourseFilter? courseFilter) + private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO editingCourseDto, + CourseFilter? courseFilter, ApplyFilterOperation filterType) { var filter = courseFilter?.Filter; if (filter == null) - { - return courseDto; - } + return editingCourseDto; + + var homeworks = filter.HomeworkIds.Count != 0 + ? filterType switch + { + ApplyFilterOperation.Intersect => editingCourseDto.Homeworks + .Where(hw => filter.HomeworkIds.Contains(hw.Id)) + .ToArray(), + + ApplyFilterOperation.Subtract => editingCourseDto.Homeworks + .Where(hw => !filter.HomeworkIds.Contains(hw.Id)) + .ToArray(), + + ApplyFilterOperation.Union => editingCourseDto.Homeworks + .Concat(initialCourseDto.Homeworks + .Where(hw => filter.HomeworkIds.Contains(hw.Id))) + .ToArray(), + + _ => editingCourseDto.Homeworks + } + : editingCourseDto.Homeworks; return new CourseDTO { - Id = courseDto.Id, - Name = courseDto.Name, - GroupName = courseDto.GroupName, - IsCompleted = courseDto.IsCompleted, - IsOpen = courseDto.IsOpen, - InviteCode = courseDto.InviteCode, + Id = editingCourseDto.Id, + Name = editingCourseDto.Name, + GroupName = editingCourseDto.GroupName, + IsCompleted = editingCourseDto.IsCompleted, + IsOpen = editingCourseDto.IsOpen, + InviteCode = editingCourseDto.InviteCode, Groups = (filter.StudentIds.Any() - ? courseDto.Groups.Select(gs => + ? editingCourseDto.Groups.Select(gs => { var filteredStudentsIds = gs.StudentsIds.Intersect(filter.StudentIds).ToArray(); return filteredStudentsIds.Any() ? new GroupViewModel { Id = gs.Id, + Name = gs.Name, StudentsIds = filteredStudentsIds } : null; }) .Where(t => t != null) .ToArray() - : courseDto.Groups)!, + : editingCourseDto.Groups)!, MentorIds = filter.MentorIds.Any() - ? courseDto.MentorIds.Intersect(filter.MentorIds).ToArray() - : courseDto.MentorIds, + ? editingCourseDto.MentorIds.Intersect(filter.MentorIds).ToArray() + : editingCourseDto.MentorIds, CourseMates = filter.StudentIds.Any() - ? courseDto.CourseMates + ? editingCourseDto.CourseMates .Where(mate => !mate.IsAccepted || filter.StudentIds.Contains(mate.StudentId)).ToArray() - : courseDto.CourseMates, - Homeworks = - filter.HomeworkIds.Any() - ? courseDto.Homeworks.Where(hw => filter.HomeworkIds.Contains(hw.Id)).ToArray() - : courseDto.Homeworks + : editingCourseDto.CourseMates, + Homeworks = homeworks.OrderBy(hw => hw.PublicationDate).ToArray() }; } + + public async Task UpdateGroupFilters(long courseId, long homeworkId, Group group) + { + var filterIds = group != null + ? new[] { GlobalFilterId, group.Id.ToString() } + : new[] { GlobalFilterId }; + + var filters = await _courseFilterRepository.GetAsync(filterIds, courseId); + + foreach (var filterId in filterIds) + { + var existingCourseFilter = filters.SingleOrDefault(f => f.Id == filterId)?.CourseFilter; + var newFilter = existingCourseFilter?.Filter + ?? new Filter { StudentIds = new List(), HomeworkIds = new List(), MentorIds = new List() }; + newFilter.HomeworkIds.Add(homeworkId); + + if (existingCourseFilter != null) + await UpdateAsync(existingCourseFilter.Id, newFilter); + else + await AddCourseFilter(newFilter, courseId, filterId); + } + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 9293160d4..dca5492ec 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -74,12 +74,13 @@ public async Task GetAllAsync() CourseDomain.FillTasksInCourses(course); - var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToArrayAsync(); + var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToListAsync(); var courseDto = course.ToCourseDto(); courseDto.Groups = groups.Select(g => new GroupViewModel { Id = g.Id, + Name = g.Name, StudentsIds = g.GroupMates.Select(t => t.StudentId).ToArray() }).ToArray(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 37b1173d8..3ee1dae8b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using System.Collections.Generic; using AutoMapper; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Repositories.Groups; @@ -12,23 +13,23 @@ public class GroupsService : IGroupsService { private readonly IGroupsRepository _groupsRepository; private readonly IGroupMatesRepository _groupMatesRepository; - private readonly ITaskModelsRepository _taskModelsRepository; private readonly IMapper _mapper; public GroupsService(IGroupsRepository groupsRepository, IGroupMatesRepository groupMatesRepository, - ITaskModelsRepository taskModelsRepository, IMapper mapper) { _groupsRepository = groupsRepository; _groupMatesRepository = groupMatesRepository; - _taskModelsRepository = taskModelsRepository; _mapper = mapper; } public async Task GetAllAsync(long courseId) { - return await _groupsRepository.GetGroupsWithGroupMatesByCourse(courseId).ToArrayAsync().ConfigureAwait(false); + return await _groupsRepository.GetGroupsWithGroupMatesByCourse(courseId) + .AsNoTracking() + .ToArrayAsync() + .ConfigureAwait(false); } public async Task GetGroupsAsync(params long[] groupIds) @@ -56,64 +57,48 @@ public async Task DeleteGroupAsync(long groupId) { var group = await _groupsRepository.GetAsync(groupId); group.GroupMates.RemoveAll(cm => true); - group.Tasks.RemoveAll(cm => true); await _groupsRepository.DeleteAsync(groupId).ConfigureAwait(false); } public async Task UpdateAsync(long groupId, Group updated) { - var group = await _groupsRepository.GetAsync(groupId); - group.GroupMates.RemoveAll(cm => true); - group.Tasks.RemoveAll(cm => true); + var group = (await _groupsRepository.GetGroupsWithGroupMatesAsync(groupId)).SingleOrDefault(); - updated.GroupMates.ForEach(cm => cm.GroupId = groupId); - updated.Tasks.ForEach(cm => cm.GroupId = groupId); - var mateTasks = updated.GroupMates.Select(cm => _groupMatesRepository.AddAsync(cm)); - var idTasks = updated.Tasks.Select(cm => _taskModelsRepository.AddAsync(cm)); + if (group == null) return; - group.Name = updated.Name; + var updatedGroupMates = updated.GroupMates ?? new List(); - await Task.WhenAll(mateTasks); - await Task.WhenAll(idTasks); - } + var currentStudentIds = (group.GroupMates?.Select(gm => gm.StudentId) ?? Enumerable.Empty()).ToHashSet(); + var updatedStudentIds = updatedGroupMates.Select(gm => gm.StudentId).ToHashSet(); - public async Task DeleteGroupMateAsync(long groupId, string studentId) - { - var group = await _groupsRepository.GetAsync(groupId).ConfigureAwait(false); - if (group == null) - { - return false; - } + var studentsToAdd = updatedStudentIds.Except(currentStudentIds).ToList(); + var studentsToRemove = currentStudentIds.Except(updatedStudentIds).ToList(); - var getGroupMateTask = - await _groupMatesRepository.FindAsync(cm => cm.GroupId == groupId && cm.StudentId == studentId).ConfigureAwait(false); - - if (getGroupMateTask == null) - { - return false; - } + var groupMatesToAdd = updatedGroupMates.Where(x => studentsToAdd.Contains(x.StudentId)).ToArray(); + foreach (var groupMate in groupMatesToAdd) + groupMate.GroupId = groupId; + await _groupMatesRepository.AddRangeAsync(groupMatesToAdd); + await _groupMatesRepository + .FindAll(x => x.GroupId == groupId && studentsToRemove.Contains(x.StudentId)) + .DeleteFromQueryAsync(); - await _groupMatesRepository.DeleteAsync(getGroupMateTask.Id); - return true; + await _groupsRepository.UpdateAsync(groupId, g => new Group + { + Name = updated.Name + }); } public async Task GetStudentGroupsAsync(long courseId, string studentId) { - var studentGroupsIds = await _groupMatesRepository - .FindAll(cm => cm.StudentId == studentId) - .Select(cm => cm.GroupId) - .ToArrayAsync() - .ConfigureAwait(false); + var studentGroups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(courseId) + .Where(x => x.GroupMates.Any(g => g.StudentId == studentId)) + .ToListAsync(); - var getStudentGroupsTask = studentGroupsIds - .Select(async id => await _groupsRepository.GetAsync(id).ConfigureAwait(false)) - .Where(cm => cm.Result.CourseId == courseId) + return studentGroups + .Select(c => _mapper.Map(c)) .ToArray(); - var studentGroups = await Task.WhenAll(getStudentGroupsTask).ConfigureAwait(false); - - return studentGroups.Select(c => _mapper.Map(c)).ToArray(); } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 76844defc..a6816f1bd 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -16,13 +16,18 @@ public class HomeworksService : IHomeworksService private readonly IHomeworksRepository _homeworksRepository; private readonly IEventBus _eventBus; private readonly ICoursesRepository _coursesRepository; + private readonly IGroupsService _groupsService; + private readonly ICourseFilterService _courseFilterService; public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, - ICoursesRepository coursesRepository) + ICoursesRepository coursesRepository, + IGroupsService groupsService, ICourseFilterService courseFilterService) { _homeworksRepository = homeworksRepository; _eventBus = eventBus; _coursesRepository = coursesRepository; + _groupsService = groupsService; + _courseFilterService = courseFilterService; } public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel) @@ -32,14 +37,26 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo homework.CourseId = courseId; var course = await _coursesRepository.GetWithCourseMatesAndHomeworksAsync(courseId); - var studentIds = course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + var notifyStudentIds = + course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + + await _homeworksRepository.AddAsync(homework); + + if (homework.GroupId is { } groupId) + { + var group = (await _groupsService.GetGroupsAsync(groupId)).SingleOrDefault(); + var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); + + await _courseFilterService.UpdateGroupFilters(courseId, homework.Id, group); + notifyStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + } + if (DateTime.UtcNow >= homework.PublicationDate) { - _eventBus.Publish(new NewHomeworkEvent(homework.Title, course.Name, course.Id, studentIds, + _eventBus.Publish(new NewHomeworkEvent(homework.Title, course.Name, course.Id, notifyStudentIds, homework.DeadlineDate)); } - await _homeworksRepository.AddAsync(homework); return await GetHomeworkAsync(homework.Id, withCriteria: true); } @@ -72,9 +89,18 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV var homework = await _homeworksRepository.GetAsync(homeworkId); var course = await _coursesRepository.GetWithCourseMates(homework.CourseId); var studentIds = course!.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + var notifyStudentIds = studentIds; + + if (update.GroupId is { } groupId) + { + var group = (await _groupsService.GetGroupsAsync(groupId)).SingleOrDefault(); + var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); + + notifyStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + } if (options.SendNotification && update.PublicationDate <= DateTime.UtcNow) - _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, studentIds)); + _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, notifyStudentIds)); await _homeworksRepository.UpdateAsync(homeworkId, hw => new Homework() { @@ -84,7 +110,8 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV DeadlineDate = update.DeadlineDate, PublicationDate = update.PublicationDate, IsDeadlineStrict = update.IsDeadlineStrict, - Tags = update.Tags + Tags = update.Tags, + GroupId = update.GroupId, }); var updatedHomework = await _homeworksRepository.GetWithTasksAsync(homeworkId); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs index 36d5a2945..0d49f1805 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using HwProj.CoursesService.API.Models; using HwProj.Models.CoursesService; @@ -14,5 +15,6 @@ public interface ICourseFilterService Task ApplyFiltersToCourses(string userId, CourseDTO[] courses); Task ApplyFilter(CourseDTO courseDto, string userId); Task GetAssignedStudentsIds(long courseId, string[] mentorsIds); + Task UpdateGroupFilters(long courseId, long homeworkId, Group group); } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs index 2a3ce818c..60862f33e 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs @@ -8,7 +8,7 @@ public interface IGroupsService { Task GetAllAsync(long courseId); - Task GetGroupsAsync(long[] groupIds); + Task GetGroupsAsync(params long[] groupIds); Task AddGroupAsync(Group group); @@ -18,8 +18,6 @@ public interface IGroupsService Task AddGroupMateAsync(long groupId, string studentId); - Task DeleteGroupMateAsync(long groupId, string studentId); - Task GetStudentGroupsAsync(long courseId, string studentId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs index f6bb3f1d1..62461edc6 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs @@ -33,7 +33,6 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index 2cbd55fd2..c82dcdcbe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -481,17 +481,6 @@ public async Task AddStudentInGroup(long courseId, long groupId, string userId) await _httpClient.SendAsync(httpRequest); } - public async Task RemoveStudentFromGroup(long courseId, long groupId, string userId) - { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Post, - _coursesServiceUri + $"api/CourseGroups/{courseId}/removeStudentFromGroup/{groupId}?userId={userId}"); - - httpRequest.TryAddUserId(_httpContextAccessor); - - await _httpClient.SendAsync(httpRequest); - } - public async Task GetGroupsById(params long[] groupIds) { if (groupIds.Length == 0) return Array.Empty(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index da84eb73b..9bcb031c1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -42,7 +42,6 @@ Task UpdateStudentCharacteristics(long courseId, string studentId, Task UpdateCourseGroup(UpdateGroupViewModel model, long courseId, long groupId); Task GetCourseGroupsById(long courseId, string userId); Task AddStudentInGroup(long courseId, long groupId, string userId); - Task RemoveStudentFromGroup(long courseId, long groupId, string userId); Task GetGroupsById(params long[] groupIds); Task GetGroupTasks(long groupId); Task AcceptLecturer(long courseId, string lecturerEmail, string lecturerId); diff --git a/hwproj.front/src/api/ApiSingleton.ts b/hwproj.front/src/api/ApiSingleton.ts index df3527ba0..1886ef4ce 100644 --- a/hwproj.front/src/api/ApiSingleton.ts +++ b/hwproj.front/src/api/ApiSingleton.ts @@ -8,7 +8,8 @@ import { TasksApi, StatisticsApi, SystemApi, - FilesApi + FilesApi, + CourseGroupsApi } from "."; import AuthService from "../services/AuthService"; import CustomFilesApi from "./CustomFilesApi"; @@ -18,6 +19,7 @@ class Api { readonly accountApi: AccountApi; readonly expertsApi: ExpertsApi; readonly coursesApi: CoursesApi; + readonly courseGroupsApi: CourseGroupsApi; readonly solutionsApi: SolutionsApi; readonly notificationsApi: NotificationsApi; readonly homeworksApi: HomeworksApi; @@ -32,6 +34,7 @@ class Api { accountApi: AccountApi, expertsApi: ExpertsApi, coursesApi: CoursesApi, + courseGroupsApi: CourseGroupsApi, solutionsApi: SolutionsApi, notificationsApi: NotificationsApi, homeworksApi: HomeworksApi, @@ -45,6 +48,7 @@ class Api { this.accountApi = accountApi; this.expertsApi = expertsApi; this.coursesApi = coursesApi; + this.courseGroupsApi = courseGroupsApi; this.solutionsApi = solutionsApi; this.notificationsApi = notificationsApi; this.homeworksApi = homeworksApi; @@ -78,6 +82,7 @@ ApiSingleton = new Api( new AccountApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new ExpertsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new CoursesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), + new CourseGroupsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new SolutionsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new NotificationsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new HomeworksApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index e198db7c5..4e0c6dfec 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -477,6 +477,12 @@ export interface CourseViewModel { * @memberof CourseViewModel */ isCompleted?: boolean; + /** + * + * @type {Array} + * @memberof CourseViewModel + */ + groups?: Array; /** * * @type {Array} @@ -636,6 +642,12 @@ export interface CreateHomeworkViewModel { * @memberof CreateHomeworkViewModel */ actionOptions?: ActionOptions; + /** + * + * @type {number} + * @memberof CreateHomeworkViewModel + */ + groupId?: number; } /** * @@ -1058,6 +1070,12 @@ export interface GroupViewModel { * @memberof GroupViewModel */ id?: number; + /** + * + * @type {string} + * @memberof GroupViewModel + */ + name?: string; /** * * @type {Array} @@ -1340,6 +1358,12 @@ export interface HomeworkViewModel { * @memberof HomeworkViewModel */ tasks?: Array; + /** + * + * @type {number} + * @memberof HomeworkViewModel + */ + groupId?: number; } /** * @@ -4077,53 +4101,6 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config options: localVarRequestOptions, }; }, - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options: any = {}): FetchArgs { - // verify required parameter 'courseId' is not null or undefined - if (courseId === null || courseId === undefined) { - throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling courseGroupsRemoveStudentFromGroup.'); - } - // verify required parameter 'groupId' is not null or undefined - if (groupId === null || groupId === undefined) { - throw new RequiredError('groupId','Required parameter groupId was null or undefined when calling courseGroupsRemoveStudentFromGroup.'); - } - const localVarPath = `/api/CourseGroups/{courseId}/removeStudentFromGroup/{groupId}` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) - .replace(`{${"groupId"}}`, encodeURIComponent(String(groupId))); - const localVarUrlObj = url.parse(localVarPath, true); - const localVarRequestOptions = Object.assign({ method: 'POST' }, options); - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication Bearer required - if (configuration && configuration.apiKey) { - const localVarApiKeyValue = typeof configuration.apiKey === 'function' - ? configuration.apiKey("Authorization") - : configuration.apiKey; - localVarHeaderParameter["Authorization"] = localVarApiKeyValue; - } - - if (userId !== undefined) { - localVarQueryParameter['userId'] = userId; - } - - localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); - // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - localVarUrlObj.search = null; - localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - - return { - url: url.format(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {number} courseId @@ -4310,26 +4287,6 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }); }; }, - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = CourseGroupsApiFetchParamCreator(configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options); - return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { - return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { - if (response.status >= 200 && response.status < 300) { - return response; - } else { - throw response; - } - }); - }; - }, /** * * @param {number} courseId @@ -4426,17 +4383,6 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f courseGroupsGetGroupTasks(groupId: number, options?: any) { return CourseGroupsApiFp(configuration).courseGroupsGetGroupTasks(groupId, options)(fetch, basePath); }, - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options?: any) { - return CourseGroupsApiFp(configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options)(fetch, basePath); - }, /** * * @param {number} courseId @@ -4539,19 +4485,6 @@ export class CourseGroupsApi extends BaseAPI { return CourseGroupsApiFp(this.configuration).courseGroupsGetGroupTasks(groupId, options)(this.fetch, this.basePath); } - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CourseGroupsApi - */ - public courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options?: any) { - return CourseGroupsApiFp(this.configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options)(this.fetch, this.basePath); - } - /** * * @param {number} courseId diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx new file mode 100644 index 000000000..9d139a7d5 --- /dev/null +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -0,0 +1,207 @@ +import {FC, useEffect, useMemo, useState} from "react"; +import { + Grid, + TextField, + Autocomplete, + Button, + Stack, + CircularProgress, + Chip, + Alert, + AlertTitle, + Typography +} from "@mui/material"; +import ApiSingleton from "../../api/ApiSingleton"; +import {GroupViewModel, AccountDataDto} from "@/api"; + + +interface GroupSelectorProps { + courseId: number, + courseStudents: AccountDataDto[], + groups: GroupViewModel[], + onGroupIdChange: (groupId?: number) => void, + onGroupsUpdate: () => void, + selectedGroupId?: number, + choiceDisabled?: boolean, + onCreateNewGroup?: () => void, +} + +const GroupSelector: FC = (props) => { + const groups = [{id: -1, name: ""}, { + id: undefined, + name: "Все студенты" + }, ...(props.groups || []).filter(x => x.name)] + const selectedGroup = groups.find(g => g.id == props.selectedGroupId) + const [formState, setFormState] = useState<{ + name: string, + memberIds: string[] + }>({ + name: selectedGroup?.name || "", + memberIds: selectedGroup?.studentsIds || [] + }); + + useEffect(() => { + setFormState({ + name: selectedGroup?.name || "", + memberIds: selectedGroup?.studentsIds || [] + }) + }, [props.selectedGroupId, props.groups]) + + const [isSubmitting, setIsSubmitting] = useState(false); + + const studentToGroups = useMemo(() => { + const map = new Map(); + (props.groups || []).concat(formState).forEach(g => { + g.studentsIds?.forEach(stId => { + if (!map.has(stId)) map.set(stId, []); + map.get(stId)!.push(g.name!); + }); + }); + return map; + }, [props.groups, props.selectedGroupId, formState.memberIds]); + + const studentsInMultipleGroups = useMemo(() => { + const set = new Set(); + studentToGroups.forEach((groups, studentId) => { + if (groups.length > 1) set.add(studentId); + }); + return set; + }, [studentToGroups]); + + const handleSubmitEdit = async () => { + setIsSubmitting(true); + try { + if (selectedGroup && selectedGroup.id! > 0) { + await ApiSingleton.courseGroupsApi.courseGroupsUpdateCourseGroup( + props.courseId, + selectedGroup.id!, + { + name: formState.name, + groupMates: formState.memberIds.map(studentId => ({studentId})), + } + ); + props.onGroupsUpdate(); + } else { + const groupId = await ApiSingleton.courseGroupsApi.courseGroupsCreateCourseGroup(props.courseId, { + name: formState.name.trim(), + groupMatesIds: formState.memberIds, + courseId: props.courseId, + }); + props.onGroupsUpdate(); + props.onGroupIdChange(groupId); + } + } catch (error) { + console.error('Failed to update group:', error); + } finally { + setIsSubmitting(false); + } + } + + return ( + + + { + if (option.id === -1) + return
  • + Добавить новую + группу
  • + if (option.id == undefined) + return
  • {option.name}
  • + return
  • {option.name}
  • + }} + getOptionLabel={(option) => typeof option === 'string' ? option : option?.name!} + value={formState.name} + onChange={(_, newGroup) => { + if (typeof newGroup === 'string') return + if (props.selectedGroupId !== newGroup?.id) props.onGroupIdChange(newGroup?.id) + }} + onInputChange={(_, newInputValue, reason) => { + if (reason === 'input' && props.selectedGroupId != undefined) { + setFormState(prevState => ({...prevState, name: newInputValue})) + } + }} + renderInput={(params) => ( + + )} + /> +
    + {props.selectedGroupId && selectedGroup && + + formState.memberIds.includes(s.userId!)) || []} + getOptionLabel={(option) => { + const groups = studentToGroups.get(option.userId!); + const groupSuffix = groups && groups.length > 0 + ? ' — в группе: ' + groups[0] + : ''; + return `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}${groupSuffix}`.trim(); + }} + filterSelectedOptions + onChange={(_, value) => { + setFormState(prev => ({ + ...prev, + memberIds: value + .map(x => x.userId!) + .filter(Boolean) + })) + }} + disabled={isSubmitting} + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + + )) + } + renderInput={(params) => ( + + )} + noOptionsText={'Больше нет студентов для выбора'} + /> + {studentsInMultipleGroups.size > 0 && formState.memberIds.some(id => studentsInMultipleGroups.has(id)) && + + Синим выделены студенты, состоящие в нескольких группах + } + + + } + {props.selectedGroupId == undefined && + + Создайте или выберите группу + • Задание будет доступно только студентам из группы +
    + • Вы можете изменить состав группы в любое время +
    +
    } +
    ) +} + +export default GroupSelector diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 5440ded7c..c33122808 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -1,7 +1,7 @@ import * as React from "react"; -import {FC, useEffect, useState} from "react"; +import {FC, useEffect, useState, useMemo} from "react"; import {useNavigate, useParams, useSearchParams} from "react-router-dom"; -import {AccountDataDto, CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {AccountDataDto, CourseViewModel, GroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import StudentStats from "./StudentStats"; import NewCourseStudents from "./NewCourseStudents"; import ApiSingleton from "../../api/ApiSingleton"; @@ -10,6 +10,7 @@ import EditIcon from "@material-ui/icons/Edit"; import { Alert, AlertTitle, + Badge, Box, Chip, Dialog, @@ -21,6 +22,7 @@ import { Menu, MenuItem, Stack, + Tooltip, Typography } from "@mui/material"; import {CourseExperimental} from "./CourseExperimental"; @@ -30,10 +32,12 @@ import AssessmentIcon from '@mui/icons-material/Assessment'; import NameBuilder from "../Utils/NameBuilder"; import {QRCodeSVG} from 'qrcode.react'; import QrCode2Icon from '@mui/icons-material/QrCode2'; +import GroupIcon from '@mui/icons-material/Group'; import {MoreVert} from "@mui/icons-material"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; +import Utils from "@/services/Utils"; type TabValue = "homeworks" | "stats" | "applications" @@ -45,6 +49,7 @@ interface ICourseState { isFound: boolean; course: CourseViewModel; courseHomeworks: HomeworkViewModel[]; + groups: GroupViewModel[]; mentors: AccountDataDto[]; acceptedStudents: AccountDataDto[]; newStudents: AccountDataDto[]; @@ -66,6 +71,7 @@ const Course: React.FC = () => { course: {}, courseHomeworks: [], mentors: [], + groups: [], acceptedStudents: [], newStudents: [], studentSolutions: [], @@ -84,8 +90,17 @@ const Course: React.FC = () => { newStudents, acceptedStudents, courseHomeworks, + groups } = courseState + const loadGroups = async () => { + const groups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroups(course.id!) + setCourseState(prevState => ({ + ...prevState, + groups: groups + })) + }; + const userId = ApiSingleton.authService.getUserId() const isLecturer = ApiSingleton.authService.isLecturer() @@ -138,6 +153,7 @@ const Course: React.FC = () => { courseHomeworks: course.homeworks!, createHomework: false, mentors: course.mentors!, + groups: course.groups || [], acceptedStudents: course.acceptedStudents!, newStudents: course.newStudents!, })) @@ -170,6 +186,11 @@ const Course: React.FC = () => { const [lecturerStatsState, setLecturerStatsState] = useState(false); + const studentsWithoutGroup = useMemo(() => { + const inGroupIds = new Set(groups.flatMap(g => g.studentsIds)); + return acceptedStudents.filter(s => !inGroupIds.has(s.userId!)); + }, [groups, acceptedStudents]); + const CourseMenu: FC = () => { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -298,16 +319,30 @@ const Course: React.FC = () => { } + {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && + + + + + + + + } { if (value === 0 && !isExpert) navigate(`/courses/${courseId}/homeworks`) if (value === 1) navigate(`/courses/${courseId}/stats`) if (value === 2 && !isExpert) navigate(`/courses/${courseId}/applications`) + if (value === 3) navigate(`/courses/${courseId}/groups`) }} > {!isExpert && @@ -368,6 +403,8 @@ const Course: React.FC = () => { courseHomeworks: homeworks })) }} + onGroupsUpdate={loadGroups} + groups={groups} /> } {tabValue === "stats" && @@ -379,6 +416,7 @@ const Course: React.FC = () => { isMentor={isCourseMentor} course={courseState.course} solutions={studentSolutions} + groups={groups} /> } diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index 138b4f225..d16fbb3a1 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { - FileInfoDTO, + FileInfoDTO, GroupViewModel, HomeworkTaskViewModel, HomeworkViewModel, SolutionDto, StatisticsCourseMatesModel, } from "@/api"; @@ -36,6 +36,7 @@ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import SwitchAccessShortcutIcon from '@mui/icons-material/SwitchAccessShortcut'; import Lodash from "lodash"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; +import GroupIcon from '@mui/icons-material/Group'; interface ICourseExperimentalProps { homeworks: HomeworkViewModel[] @@ -60,6 +61,8 @@ interface ICourseExperimentalProps { previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; + onGroupsUpdate: () => void; + groups: GroupViewModel[]; } interface ICourseExperimentalState { @@ -440,6 +443,8 @@ export const CourseExperimental: FC = (props) => { }} isProcessing={props.processingFiles[homework.id!]?.isLoading || false} onStartProcessing={props.onStartProcessing} + onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} /> @@ -569,13 +574,17 @@ export const CourseExperimental: FC = (props) => { } })) }}> - - {isMentor && renderHomeworkStatus(x)} - {x.title}{getTip(x)} - + + {x.groupId && } + + {isMentor && renderHomeworkStatus(x)} + {x.title}{getTip(x)} + + {x.isDeferred && !x.publicationDateNotSet && {"🕘 " + renderDate(x.publicationDate!) + " " + renderTime(x.publicationDate!)} diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 61e38becb..e7e0b4a23 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -1,9 +1,9 @@ import React, {useEffect, useState, useRef} from "react"; -import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {CourseViewModel, GroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import {useNavigate, useParams} from 'react-router-dom'; import {LinearProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; -import {Alert, Button, Chip, IconButton, Typography} from "@mui/material"; +import {Alert, Button, Chip, IconButton, Stack, Typography} from "@mui/material"; import {grey} from "@material-ui/core/colors"; import StudentStatsUtils from "../../services/StudentStatsUtils"; import ShowChartIcon from "@mui/icons-material/ShowChart"; @@ -12,6 +12,7 @@ import Lodash from "lodash" import ApiSingleton from "@/api/ApiSingleton"; import FullscreenIcon from '@mui/icons-material/Fullscreen'; import FullscreenExitIcon from '@mui/icons-material/FullscreenExit'; +import GroupIcon from '@mui/icons-material/Group'; interface IStudentStatsProps { course: CourseViewModel; @@ -19,6 +20,7 @@ interface IStudentStatsProps { isMentor: boolean; userId: string; solutions: StatisticsCourseMatesModel[] | undefined; + groups: GroupViewModel[]; } interface IStudentStatsState { @@ -104,14 +106,8 @@ const StudentStats: React.FC = (props) => { const notTests = homeworks.filter(h => !h.tags!.includes(TestTag)) - const homeworksMaxSum = notTests - .filter(h => !h.tags!.includes(BonusTag)) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => { - return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); - }, 0) - - const testGroups = Lodash(homeworks.filter(h => h.tags!.includes(TestTag))) + const testHomeworks = homeworks.filter(h => h.tags!.includes(TestTag)) + const testGroups = Lodash(testHomeworks) .groupBy((h: HomeworkViewModel) => { const key = h.tags!.find(t => !DefaultTags.includes(t)) return key || h.id!.toString(); @@ -119,14 +115,22 @@ const StudentStats: React.FC = (props) => { .values() .value(); - const testsMaxSum = testGroups - .map(h => h[0]) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => - sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)), 0) + const homeworksWithGroups = notTests.filter(h => h.groupId) + const testsWithGroups = testHomeworks.filter(t => t.groupId != undefined) + + const getMaxSum = (studentId: string, isTests: boolean = false) => { + const works = isTests ? testHomeworks : notTests; + return works + .filter(h => (isTests || !h.tags!.includes(BonusTag)) && + (h.groupId == undefined || (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(studentId)))) + .flatMap(homework => homework.tasks) + .reduce((sum, task) => { + return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); + }, 0) + } - const hasHomeworks = homeworksMaxSum > 0 - const hasTests = testsMaxSum > 0 + const hasHomeworks = notTests.length > 0 + const hasTests = testHomeworks.length > 0 const showBestSolutions = isMentor && (hasHomeworks || hasTests) const bestTaskSolutions = new Map() @@ -217,7 +221,7 @@ const StudentStats: React.FC = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - ДЗ ({homeworksMaxSum}) + ДЗ {homeworksWithGroups.length === 0 && `(${getMaxSum("", false)})`} } {hasTests && = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - КР ({testsMaxSum}) + КР {testsWithGroups.length === 0 && `(${getMaxSum("", true)})`} } {showBestSolutions && @@ -259,6 +263,7 @@ const StudentStats: React.FC = (props) => { .flatMap(t => StudentStatsUtils.calculateLastRatedSolution(t.solutions || [])?.rating || 0) || 0 ) .reduce((sum, rating) => sum + rating, 0) + const studentHomeworksMaxSum = getMaxSum(cm.id!, false) const testsSum = testGroups .map(group => { @@ -277,10 +282,14 @@ const StudentStats: React.FC = (props) => { .flat() .reduce((sum, rating) => sum + rating, 0) + const studentTestsMaxSum = getMaxSum(cm.id!, true) + const bestSolutionsCount = bestTaskSolutions.values() .filter(x => x === cm.id) .toArray().length + const studentGroups = props.groups.filter(x => x.studentsIds!.includes(cm.id!)) + return ( = (props) => { variant={"head"} > {cm.surname} {cm.name} + {studentGroups.length > 0 && + + +
    {studentGroups + .map(r => r.name) + .join(', ')}
    +
    +
    } = (props) => { scope="row" variant={"body"} > - 0 && + label={`${homeworksSum} ${homeworksWithGroups.length > 0 ? `/ ${studentHomeworksMaxSum}` : ""}`}/>}
    } {hasTests && = (props) => { scope="row" variant={"body"} > - 0 && + label={`${testsSum} ${testsWithGroups.length > 0 ? `/ ${studentTestsMaxSum}` : ""}`}/>} } {showBestSolutions && = (props) => { {homeworks.map((homework, idx) => homework.tasks!.map((task, i) => { const additionalStyles = i === 0 && homeworkStyles(homeworks, idx) + const isDisabled = homework.groupId + ? !props.groups.find(g => g.id === homework.groupId)?.studentsIds?.includes(cm.id!) + : false return = (props) => { taskId={task.id!} taskMaxRating={task.maxRating!} isBestSolution={bestTaskSolutions.get(task.id!) === cm.id} + disabled={isDisabled} {...additionalStyles}/>; }) )} diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 512bf8872..cddde7ee3 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -1,5 +1,6 @@ import { Alert, + Badge, CardActions, CardContent, Chip, @@ -9,8 +10,10 @@ IconButton, Stack, TextField, + ToggleButton, + ToggleButtonGroup, Tooltip, - Typography + Typography, } from "@mui/material"; import {MarkdownEditor, MarkdownPreview} from "components/Common/MarkdownEditor"; import FilesPreviewList from "components/Files/FilesPreviewList"; @@ -18,7 +21,7 @@ import {IFileInfo} from "components/Files/IFileInfo"; import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { - HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel + HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto, GroupViewModel } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -37,6 +40,9 @@ import Lodash from "lodash"; import {CourseUnitType} from "../Files/CourseUnitType" import ProcessFilesUtils from "../Utils/ProcessFilesUtils"; import {FilesHandler} from "@/components/Files/FilesHandler"; +import GroupSelector from "../Common/GroupSelector"; +import GroupIcon from '@mui/icons-material/Group'; +import AssignmentIcon from '@mui/icons-material/Assignment'; export interface HomeworkAndFilesInfo { homework: HomeworkViewModel & { isModified?: boolean }, @@ -63,6 +69,8 @@ const CourseHomeworkEditor: FC<{ previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; + onGroupsUpdate: () => void; + groups: GroupViewModel[]; }> = (props) => { const homework = props.homeworkAndFilesInfo.homework const isNewHomework = homework.id! < 0 @@ -114,6 +122,21 @@ const CourseHomeworkEditor: FC<{ const [title, setTitle] = useState(loadedHomework.title!) const [tags, setTags] = useState(loadedHomework.tags!) const [description, setDescription] = useState(loadedHomework.description!) + const [selectedGroupId, setSelectedGroupId] = useState(loadedHomework.groupId) + const [courseStudents, setCourseStudents] = useState([]) + const [page, setPage] = useState<"homework" | "group">("homework") + + useEffect(() => { + const loadCourseStudents = async () => { + try { + const courseData = await ApiSingleton.coursesApi.coursesGetAllCourseData(courseId) + setCourseStudents(courseData.course?.acceptedStudents || []) + } catch (error) { + console.error('Failed to load course students:', error) + } + } + loadCourseStudents() + }, [courseId]) const [hasErrors, setHasErrors] = useState(false) @@ -164,13 +187,14 @@ const CourseHomeworkEditor: FC<{ title: title, description: description, tags: tags, + groupId: selectedGroupId, hasErrors: hasErrors, deadlineDateNotSet: metadata.hasDeadline && !metadata.deadlineDate, isModified: true, } props.onUpdate({homework: update}) - }, [title, description, tags, metadata, hasErrors, filesState.selectedFilesInfo]) + }, [title, description, tags, metadata, hasErrors, filesState.selectedFilesInfo, selectedGroupId]) useEffect(() => { setHasErrors(!title || metadata.hasErrors) @@ -228,6 +252,7 @@ const CourseHomeworkEditor: FC<{ deadlineDate: metadata.deadlineDate, isDeadlineStrict: metadata.isDeadlineStrict, publicationDate: metadata.publicationDate, + groupId: selectedGroupId, actionOptions: editOptions, tasks: isNewHomework ? homework.tasks!.map(t => { const task: PostTaskViewModel = { @@ -259,89 +284,110 @@ const CourseHomeworkEditor: FC<{ const isDisabled = hasErrors || !isLoaded || taskHasErrors - return ( - - - - { - e.persist() - setHasErrors(prevState => prevState || !e.target.value) - setTitle(e.target.value) - }} - /> - - - apiSingleton.coursesApi.coursesGetAllTagsForCourse(courseId)}/> - - - - {tags.includes(TestTag) && + return + { + if (x === "homework" || x === "group") setPage(x) + }} + > + + + + + + + + + + {page === "homework" &&
    + + - - Вы можете сгруппировать контрольные работы и переписывания с помощью - дополнительного тега. Например, 'КР 1' - - } - - { - setDescription(value) - }} - /> - - - - { - setFilesState((prevState) => ({ - ...prevState, - selectedFilesInfo: filesInfo - })); + { + e.persist() + setHasErrors(prevState => prevState || !e.target.value) + setTitle(e.target.value) }} - courseUnitType={CourseUnitType.Homework} - courseUnitId={homeworkId}/> - { - const conflictsWithTasks = changedTaskPublicationDates.some(d => d < metadata.publicationDate!) - setMetadata({ - hasDeadline: state.hasDeadline, - isDeadlineStrict: state.isDeadlineStrict, - publicationDate: state.publicationDate, - deadlineDate: state.deadlineDate, - hasErrors: state.hasErrors || conflictsWithTasks, - }) + /> + + + apiSingleton.coursesApi.coursesGetAllTagsForCourse(courseId)}/> + + + + {tags.includes(TestTag) && + + + Вы можете сгруппировать контрольные работы и переписывания с помощью + дополнительного тега. Например, 'КР 1' + + } + + { + setDescription(value) }} /> + + + { + setFilesState((prevState) => ({ + ...prevState, + selectedFilesInfo: filesInfo + })); + }} + courseUnitType={CourseUnitType.Homework} + courseUnitId={homeworkId}/> + { + const conflictsWithTasks = changedTaskPublicationDates.some(d => d < metadata.publicationDate!) + setMetadata({ + hasDeadline: state.hasDeadline, + isDeadlineStrict: state.isDeadlineStrict, + publicationDate: state.publicationDate, + deadlineDate: state.deadlineDate, + hasErrors: state.hasErrors || conflictsWithTasks, + }) + }} + /> + + + {taskHasErrors && + Одна или более вложенных задач содержат ошибки + } - {taskHasErrors && - Одна или более вложенных задач содержат ошибки - } - + {metadata.publicationDate && new Date() >= new Date(metadata.publicationDate) && - - ) +
    } + {page === "group" &&
    + + setSelectedGroupId(groupId)} + selectedGroupId={selectedGroupId} + choiceDisabled={!isNewHomework} + onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} + /> + + {!isNewHomework && + + } + loading={handleSubmitLoading} + > + {"Редактировать задание"} + + } +
    } +
    } const CourseHomeworkExperimental: FC<{ @@ -394,12 +470,15 @@ const CourseHomeworkExperimental: FC<{ previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; + onGroupsUpdate: () => void; + groups: GroupViewModel[]; }> = (props) => { const {homework, filesInfo} = props.homeworkAndFilesInfo const deferredTasks = homework.tasks!.filter(t => t.isDeferred!) const tasksCount = homework.tasks!.length const [showEditMode, setShowEditMode] = useState(false) const [editMode, setEditMode] = useState(false) + const group = props.groups.find(g => g.id === homework.groupId) useEffect(() => { setEditMode(props.initialEditMode) @@ -414,6 +493,8 @@ const CourseHomeworkExperimental: FC<{ props.onUpdate(update) }} onStartProcessing={props.onStartProcessing} + onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} /> return
    } + {group && + + + +
    {group.name}
    +
    +
    } diff --git a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx index a81501135..22405d4f6 100644 --- a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx +++ b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx @@ -17,13 +17,14 @@ interface ITaskStudentCellProps { taskMaxRating: number; isBestSolution: boolean; solutions?: SolutionDto[]; + disabled?: boolean; } const StudentStatsCell: FC = (props) => { const navigate = useNavigate() const {solutions, taskMaxRating, forMentor} = props - const cellState = StudentStatsUtils.calculateLastRatedSolutionInfo(solutions!, taskMaxRating) + const cellState = StudentStatsUtils.calculateLastRatedSolutionInfo(solutions!, taskMaxRating, props.disabled) const {ratedSolutionsCount, solutionsDescription} = cellState; @@ -41,6 +42,8 @@ const StudentStatsCell: FC ; const handleCellClick = (e: React.MouseEvent) => { + if(props.disabled) return; + // Формируем URL const url = forMentor ? `/task/${props.taskId}/${props.studentId}` @@ -71,7 +74,7 @@ const StudentStatsCell: FC style={{ backgroundColor: cellState.color, borderLeft: `1px solid ${props.borderLeftColor || grey[300]}`, - cursor: "pointer", + cursor: props.disabled ? "default" : "pointer", }}> {result}
    diff --git a/hwproj.front/src/services/StudentStatsUtils.ts b/hwproj.front/src/services/StudentStatsUtils.ts index 3597c9948..f2149f807 100644 --- a/hwproj.front/src/services/StudentStatsUtils.ts +++ b/hwproj.front/src/services/StudentStatsUtils.ts @@ -1,6 +1,7 @@ import {SolutionDto, SolutionState} from "@/api"; import {colorBetween} from "./JsUtils"; import Utils from "./Utils"; +import {grey} from "@material-ui/core/colors"; export default class StudentStatsUtils { @@ -17,12 +18,12 @@ export default class StudentStatsUtils { : "#ffffff" } - static calculateLastRatedSolution(solutions: SolutionDto[]){ + static calculateLastRatedSolution(solutions: SolutionDto[]) { const ratedSolutions = solutions!.filter(x => x.state !== SolutionState.NUMBER_0) return ratedSolutions.slice(-1)[0] } - static calculateLastRatedSolutionInfo(solutions: SolutionDto[], taskMaxRating: number) { + static calculateLastRatedSolutionInfo(solutions: SolutionDto[], taskMaxRating: number, disabled: boolean = false) { const ratedSolutions = solutions!.filter(x => x.state !== SolutionState.NUMBER_0) const ratedSolutionsCount = ratedSolutions.length const isFirstUnratedTry = ratedSolutionsCount === 0 @@ -30,7 +31,9 @@ export default class StudentStatsUtils { const lastRatedSolution = ratedSolutions.slice(-1)[0] let solutionsDescription: string - if (lastSolution === undefined) + if (disabled) + solutionsDescription = "Задача недоступна для этого студента" + else if (lastSolution === undefined) solutionsDescription = "Решение отсутствует" else if (isFirstUnratedTry) solutionsDescription = "Решение ожидает проверки" @@ -38,11 +41,17 @@ export default class StudentStatsUtils { solutionsDescription = `${lastSolution.rating}/${taskMaxRating} ${Utils.pluralizeHelper(["балл", "балла", "баллов"], taskMaxRating)}` else solutionsDescription = "Последняя оценка — " + `${lastRatedSolution.rating}/${taskMaxRating} ${Utils.pluralizeHelper(["балл", "балла", "баллов"], taskMaxRating)}\nНовое решение ожидает проверки` + let color: string + if (disabled) + color = grey[300] + else if (lastRatedSolution == undefined) + color = "#ffffff" + else + color = StudentStatsUtils.getCellBackgroundColor(lastRatedSolution.state, lastRatedSolution.rating, taskMaxRating, isFirstUnratedTry) + return { lastRatedSolution: lastRatedSolution, - color: lastSolution === undefined - ? "#ffffff" - : StudentStatsUtils.getCellBackgroundColor(lastSolution.state, lastSolution.rating, taskMaxRating, isFirstUnratedTry), + color: color, ratedSolutionsCount, lastSolution, solutionsDescription } }