Skip to content

Commit

Permalink
Add Semesters (#42)
Browse files Browse the repository at this point in the history
* Add `Semester` and `StudentSemester` entities

* Configure a many-to-many relationship between students and semesters

* Fix naming for DB tables and columns

* Add a name to `Semester` entity

* Fix outdated comment

* Add DTOs for semesters

* Replace `StudentGroupCreateDto` with `AssignStudentDto`

This change, in fact, only renames a record so it can be used in other services that require assigninging students to something.

* Add validation tests for the semester DTO

* Implement semester validation

* Implement `AssigningService` and `StudentGroupsService`

* Replace typeparams for `IStudentGroupsService`

* Add tests for assigning endpoints

* Remove `StudentGroup` logic from `GroupsService`

* Implement `SemestersService`

* Implement `StudentSemestersService`

* Add integration tests for semesters endpoints

* Implement endpoints for semesters

* Fix enrollments display

* Configure test data generation for semesters

* Fix a test
  • Loading branch information
romandykyi authored Nov 19, 2023
1 parent 32216f2 commit 83f6dc7
Show file tree
Hide file tree
Showing 47 changed files with 3,182 additions and 420 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
namespace EUniversity.Core.Dtos.University;

[ValidateNever] // Remove data annotations validation
public record StudentGroupCreateDto(string StudentId);
public record AssignStudentDto(string StudentId);
6 changes: 6 additions & 0 deletions Core/Dtos/University/SemesterCreateDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace EUniversity.Core.Dtos.University;

[ValidateNever] // Remove data annotations validation
public record SemesterCreateDto(string Name, DateTimeOffset DateFrom, DateTimeOffset DateTo);
3 changes: 3 additions & 0 deletions Core/Dtos/University/SemesterPreviewDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace EUniversity.Core.Dtos.University;

public record SemesterPreviewDto(int Id, string Name, DateTimeOffset DateFrom, DateTimeOffset DateTo);
5 changes: 5 additions & 0 deletions Core/Dtos/University/SemesterViewDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace EUniversity.Core.Dtos.University;

public record SemesterViewDto(int Id, string Name,
DateTimeOffset DateFrom, DateTimeOffset DateTo,
IEnumerable<StudentSemesterViewDto> StudentEnrollments);
5 changes: 5 additions & 0 deletions Core/Dtos/University/StudentSemesterViewDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using EUniversity.Core.Dtos.Users;

namespace EUniversity.Core.Dtos.University;

public record StudentSemesterViewDto(StudentPreviewDto Student, DateTimeOffset EnrollmentDate);
34 changes: 34 additions & 0 deletions Core/Models/University/Semester.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace EUniversity.Core.Models.University;

/// <summary>
/// An entity that represents a semester.
/// </summary>
public class Semester : IEntity<int>, IHasName
{
public const int MaxNameLength = 100;

[Key]
public int Id { get; set; }
/// <summary>
/// Name of the semester.
/// </summary>
[StringLength(MaxNameLength)]
public string Name { get; set; } = null!;
/// <summary>
/// Date when the semester starts.
/// </summary>
public DateTimeOffset DateFrom { get; set; }
/// <summary>
/// Date when the semester ends.
/// </summary>
public DateTimeOffset DateTo { get; set; }

/// <summary>
/// Navigation property to the students which are part of this semester.
/// </summary>
public ICollection<ApplicationUser> Students { get; set; } = null!;
/// <summary>
/// Navigation property to the student enrollments of this semester.
/// </summary>
public ICollection<StudentSemester> StudentEnrollments { get; set; } = null!;
}
38 changes: 38 additions & 0 deletions Core/Models/University/StudentSemester.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations.Schema;

namespace EUniversity.Core.Models.University;

/// <summary>
/// Represents the entity used for configuring many-to-many relationship
/// between students and semesters.
/// </summary>
public class StudentSemester : IEntity<int>
{
[Key]
public int Id { get; set; }

/// <summary>
/// Date when student was added to the semester.
/// </summary>
public DateTimeOffset EnrollmentDate { get; set; }

/// <summary>
/// Foreign key of the associated student.
/// </summary>
[ForeignKey(nameof(Student))]
public string StudentId { get; set; } = null!;
/// <summary>
/// Foreign key of the associated semester.
/// </summary>
[ForeignKey(nameof(Semester))]
public int SemesterId { get; set; }

/// <summary>
/// Navigation property for the student associated with this semester.
/// </summary>
public ApplicationUser Student { get; set; } = null!;
/// <summary>
/// Navigation property for the semester that the student is part of.
/// </summary>
public Semester Semester { get; set; } = null!;
}
53 changes: 53 additions & 0 deletions Core/Services/IAssigningService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Linq.Expressions;

namespace EUniversity.Core.Services;

/// <summary>
/// Represents an interface for assigning/unassigning entities in a many-to-many relationship.
/// </summary>
/// <typeparam name="TAssigningEntity">A type of entity that configures a many-to-many relationship.</typeparam>
/// <typeparam name="TId1">A type of the ID of the first entity.</typeparam>
/// <typeparam name="TId2">A type of the ID of the second entity.</typeparam>
public interface IAssigningService<TAssigningEntity, TId1, TId2>
where TAssigningEntity : class
where TId1 : IEquatable<TId1>
where TId2 : IEquatable<TId2>
{
/// <summary>
/// Gets a predicate that can be used for finding
/// an instance of assigning entity in the database.
/// </summary>
/// <param name="id1">ID of the first entity.</param>
/// <param name="id2">ID of the second entity.</param>
/// <returns>
/// A predicate that can be used for finding
/// an instance of assigning entity in the database.
/// </returns>
public Expression<Func<TAssigningEntity, bool>> AssigningEntityPredicate(TId1 id1, TId2 id2);

/// <summary>
/// Adds the first entity to the second based on their IDs.
/// </summary>
/// <param name="entity1Id">ID of the first entity.</param>
/// <param name="entity2Id">ID of the second entity.</param>
/// <returns>
/// A task that represents the asynchronous operation. If the first entity was
/// successfully assigned to the second, it returns <see langword="true" />.
/// If the first entity was already assigned to the second, then <see langword="false" />
/// is returned.
/// </returns>
public Task<bool> AssignAsync(TId1 entity1Id, TId2 entity2Id);

/// <summary>
/// Unasigns the first entity from the second based on their IDs.
/// </summary>
/// <param name="entity1Id">ID of the first entity.</param>
/// <param name="entity2Id">ID of the second entity.</param>
/// <returns>
/// A task that represents the asynchronous operation. If the first entity was
/// successfully unassigned from the second, it returns <see langword="true" />.
/// If the first entity was not assigned to the second, then <see langword="false" />
/// is returned.
/// </returns>
public Task<bool> UnassignAsync(TId1 entity1Id, TId2 entity2Id);
}
26 changes: 0 additions & 26 deletions Core/Services/University/IGroupsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,4 @@ namespace EUniversity.Core.Services.University;
public interface IGroupsService :
ICrudService<Group, int, GroupPreviewDto, GroupViewDto, GroupCreateDto, GroupCreateDto>
{
/// <summary>
/// Adds a student to a group based on the information.
/// </summary>
/// <param name="studentId">ID of the student to add to the group.</param>
/// <param name="groupId">ID of the group to which the user will be added.</param>
/// <returns>
/// A task that represents the asynchronous operation. If the student was
/// successfully added to the group, it returns <see langword="true" />.
/// If the student was already part of the group., then <see langword="false" />
/// is returned.
/// </returns>
Task<bool> AddStudentAsync(string studentId, int groupId);

/// <summary>
/// Removes a student from a group based on the information provided in the
/// <see cref="StudentGroupCreateDto" /> asynchronously.
/// </summary>
/// <param name="studentId">ID of the student to remove from the group.</param>
/// <param name="groupId">ID of the group from which the user will be removed.</param>
/// <returns>
/// A task that represents the asynchronous operation. If the student was
/// successfully removed from the group, it returns <see langword="true" />.
/// If the student was not part of the group., then <see langword="false" />
/// is returned.
/// </returns>
Task<bool> RemoveStudentAsync(string studentId, int groupId);
}
10 changes: 10 additions & 0 deletions Core/Services/University/ISemestersService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using EUniversity.Core.Dtos.University;
using EUniversity.Core.Models.University;

namespace EUniversity.Core.Services.University;

public interface ISemestersService :
ICrudService<Semester, int, SemesterPreviewDto, SemesterViewDto, SemesterCreateDto, SemesterCreateDto>
{

}
11 changes: 11 additions & 0 deletions Core/Services/University/IStudentGroupsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using EUniversity.Core.Models.University;

namespace EUniversity.Core.Services.University;

/// <summary>
/// Represents an interface of the service which configures
/// the 'Students->Groups' many-to-many relationship.
/// </summary>
public interface IStudentGroupsService : IAssigningService<StudentGroup, int, string>
{
}
7 changes: 7 additions & 0 deletions Core/Services/University/IStudentSemestersService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using EUniversity.Core.Models.University;

namespace EUniversity.Core.Services.University;

public interface IStudentSemestersService : IAssigningService<StudentSemester, int, string>
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

namespace EUniversity.Core.Validation.University;

public class StudentGroupCreateDtoValidator : AbstractValidator<StudentGroupCreateDto>
public class AssignStudentDtoValidator : AbstractValidator<AssignStudentDto>
{
public StudentGroupCreateDtoValidator(UserManager<ApplicationUser> userManager)
public AssignStudentDtoValidator(UserManager<ApplicationUser> userManager)
{
RuleFor(sg => sg.StudentId)
.NotEmpty()
Expand Down
29 changes: 29 additions & 0 deletions Core/Validation/University/SemesterCreateDtoValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using EUniversity.Core.Dtos.University;
using EUniversity.Core.Models.University;
using FluentValidation;

namespace EUniversity.Core.Validation.University;

public class SemesterCreateDtoValidator : AbstractValidator<SemesterCreateDto>
{
public SemesterCreateDtoValidator()
{
RuleFor(s => s.Name)
.NotEmpty()
.WithErrorCode(ValidationErrorCodes.PropertyRequired)
.WithMessage("Semester name is required");
RuleFor(s => s.Name)
.MaximumLength(Semester.MaxNameLength)
.WithErrorCode(ValidationErrorCodes.PropertyTooLarge)
.WithMessage($"Semester name cannot exceed {Semester.MaxNameLength} characters");

RuleFor(s => s.DateFrom)
.LessThan(s => s.DateTo)
.WithErrorCode(ValidationErrorCodes.InvalidRange)
.WithMessage("The from date must be earlier than the to date");
RuleFor(s => s.DateTo)
.GreaterThan(DateTimeOffset.Now)
.WithErrorCode(ValidationErrorCodes.InvalidRange)
.WithMessage("The to date must be in the future");
}
}
1 change: 1 addition & 0 deletions Core/Validation/ValidationErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public static class ValidationErrorCodes
public const string PropertyTooLarge = "PropertyTooLargeError";
public const string InvalidProperty = "InvalidPropertyError";
public const string InvalidEmail = "InvalidEmailError";
public const string InvalidRange = "InvalidRangeError";
public const string Equal = "EqualError";
public const string EmptyCollection = "EmptyCollectionError";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

namespace EUniversity.Tests.Validation.University;

public class StudentGroupCreateDtoValidatorTests : UsersValidatorTests
public class AssignStudentDtoValidatorTests : UsersValidatorTests
{
private StudentGroupCreateDtoValidator _validator;
private AssignStudentDtoValidator _validator;

[OneTimeSetUp]
public void OneTimeSetUp()
Expand All @@ -19,7 +19,7 @@ public void OneTimeSetUp()
public async Task Dto_Valid_Succeeds()
{
// Arrange
StudentGroupCreateDto dto = new(TestStudentId);
AssignStudentDto dto = new(TestStudentId);

// Act
var result = await _validator.TestValidateAsync(dto);
Expand All @@ -35,7 +35,7 @@ public async Task Dto_Valid_Succeeds()
public async Task StudentId_Empty_FailsWithPropertyRequiredError(string studentId)
{
// Arrange
StudentGroupCreateDto dto = new(studentId);
AssignStudentDto dto = new(studentId);

// Act
var result = await _validator.TestValidateAsync(dto);
Expand All @@ -49,7 +49,7 @@ public async Task StudentId_Empty_FailsWithPropertyRequiredError(string studentI
public async Task StudentId_UserDoesNotExist_FailsWithInvalidForeignKeyError()
{
// Arrange
StudentGroupCreateDto dto = new(NonExistentUserId);
AssignStudentDto dto = new(NonExistentUserId);

// Act
var result = await _validator.TestValidateAsync(dto);
Expand All @@ -65,7 +65,7 @@ public async Task StudentId_UserDoesNotExist_FailsWithInvalidForeignKeyError()
public async Task StudentId_UserWithoutStudentRole_FailsWithUserIsNotInRoleError(string userId)
{
// Arrange
StudentGroupCreateDto dto = new(userId);
AssignStudentDto dto = new(userId);

// Act
var result = await _validator.TestValidateAsync(dto);
Expand Down
Loading

0 comments on commit 83f6dc7

Please sign in to comment.