增强筛选
This commit is contained in:
1 parent
b08b4cc4df
commit
eb20211f0e
5 files changed
+253
-18
No files matched your search
@@ -399,8 +399,22 @@ public sealed class CourseSelectionsController(
|
|||||||
|
|
||||||
[HttpGet("offerings/{id:guid}/roster")]
|
[HttpGet("offerings/{id:guid}/roster")]
|
||||||
[Authorize(Roles = RosterReaders)]
|
[Authorize(Roles = RosterReaders)]
|
||||||
public async Task<ActionResult> GetRoster(Guid id, CancellationToken cancellationToken)
|
public async Task<ActionResult> GetRoster(
|
||||||
|
Guid id,
|
||||||
|
string? keyword = null,
|
||||||
|
int? grade = null,
|
||||||
|
Guid? majorId = null,
|
||||||
|
Guid? administrativeClassId = null,
|
||||||
|
int studentPage = 1,
|
||||||
|
int studentPageSize = 20,
|
||||||
|
int waitlistPage = 1,
|
||||||
|
int waitlistPageSize = 20,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
studentPage = Math.Max(1, studentPage);
|
||||||
|
studentPageSize = Math.Clamp(studentPageSize, 10, 100);
|
||||||
|
waitlistPage = Math.Max(1, waitlistPage);
|
||||||
|
waitlistPageSize = Math.Clamp(waitlistPageSize, 10, 100);
|
||||||
var offering = await db.CourseSelectionOfferings.AsNoTracking()
|
var offering = await db.CourseSelectionOfferings.AsNoTracking()
|
||||||
.Where(x => x.Id == id)
|
.Where(x => x.Id == id)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
@@ -428,11 +442,64 @@ public sealed class CourseSelectionsController(
|
|||||||
if (!isAssignedTeacher && !scope.CanAccessCollege(offering.CollegeId))
|
if (!isAssignedTeacher && !scope.CanAccessCollege(offering.CollegeId))
|
||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
var students = await db.CourseEnrollments.AsNoTracking()
|
var enrolledSource = db.CourseEnrollments.AsNoTracking()
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.CourseSelectionOfferingId == id &&
|
x.CourseSelectionOfferingId == id &&
|
||||||
x.Status == CourseEnrollmentStatus.Enrolled)
|
x.Status == CourseEnrollmentStatus.Enrolled);
|
||||||
|
var waitlistSource = db.CourseEnrollments.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.CourseSelectionOfferingId == id &&
|
||||||
|
x.Status == CourseEnrollmentStatus.Waitlisted);
|
||||||
|
var enrolledCount = await enrolledSource.CountAsync(cancellationToken);
|
||||||
|
var waitlistedCount = await waitlistSource.CountAsync(cancellationToken);
|
||||||
|
var rosterSource = db.CourseEnrollments.AsNoTracking().Where(x =>
|
||||||
|
x.CourseSelectionOfferingId == id &&
|
||||||
|
(x.Status == CourseEnrollmentStatus.Enrolled ||
|
||||||
|
x.Status == CourseEnrollmentStatus.Waitlisted));
|
||||||
|
var filterOptions = await rosterSource
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
Grade = x.Student!.AdministrativeClass!.Grade,
|
||||||
|
MajorId = x.Student.AdministrativeClass.MajorId,
|
||||||
|
MajorName = x.Student.AdministrativeClass.Major!.Name,
|
||||||
|
ClassId = x.Student.AdministrativeClassId,
|
||||||
|
ClassName = x.Student.AdministrativeClass.Name
|
||||||
|
})
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
if (grade.HasValue)
|
||||||
|
{
|
||||||
|
enrolledSource = enrolledSource.Where(x => x.Student!.AdministrativeClass!.Grade == grade.Value);
|
||||||
|
waitlistSource = waitlistSource.Where(x => x.Student!.AdministrativeClass!.Grade == grade.Value);
|
||||||
|
}
|
||||||
|
if (majorId.HasValue)
|
||||||
|
{
|
||||||
|
enrolledSource = enrolledSource.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId.Value);
|
||||||
|
waitlistSource = waitlistSource.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId.Value);
|
||||||
|
}
|
||||||
|
if (administrativeClassId.HasValue)
|
||||||
|
{
|
||||||
|
enrolledSource = enrolledSource.Where(x => x.Student!.AdministrativeClassId == administrativeClassId.Value);
|
||||||
|
waitlistSource = waitlistSource.Where(x => x.Student!.AdministrativeClassId == administrativeClassId.Value);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
keyword = keyword.Trim();
|
||||||
|
enrolledSource = enrolledSource.Where(x =>
|
||||||
|
x.Student!.StudentNumber.Contains(keyword) ||
|
||||||
|
x.Student.Name.Contains(keyword) ||
|
||||||
|
x.Student.AdministrativeClass!.Name.Contains(keyword));
|
||||||
|
waitlistSource = waitlistSource.Where(x =>
|
||||||
|
x.Student!.StudentNumber.Contains(keyword) ||
|
||||||
|
x.Student.Name.Contains(keyword) ||
|
||||||
|
x.Student.AdministrativeClass!.Name.Contains(keyword));
|
||||||
|
}
|
||||||
|
var enrolledTotal = await enrolledSource.CountAsync(cancellationToken);
|
||||||
|
var waitlistedTotal = await waitlistSource.CountAsync(cancellationToken);
|
||||||
|
var students = await enrolledSource
|
||||||
.OrderBy(x => x.Student!.StudentNumber)
|
.OrderBy(x => x.Student!.StudentNumber)
|
||||||
|
.Skip((studentPage - 1) * studentPageSize)
|
||||||
|
.Take(studentPageSize)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
@@ -445,12 +512,11 @@ public sealed class CourseSelectionsController(
|
|||||||
x.EnrolledAt
|
x.EnrolledAt
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
var waitlistedRows = await db.CourseEnrollments.AsNoTracking()
|
var waitlistedRows = await waitlistSource
|
||||||
.Where(x =>
|
|
||||||
x.CourseSelectionOfferingId == id &&
|
|
||||||
x.Status == CourseEnrollmentStatus.Waitlisted)
|
|
||||||
.OrderBy(x => x.WaitlistedAt)
|
.OrderBy(x => x.WaitlistedAt)
|
||||||
.ThenBy(x => x.CreatedAt)
|
.ThenBy(x => x.CreatedAt)
|
||||||
|
.Skip((waitlistPage - 1) * waitlistPageSize)
|
||||||
|
.Take(waitlistPageSize)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
@@ -474,7 +540,7 @@ public sealed class CourseSelectionsController(
|
|||||||
item.MajorName,
|
item.MajorName,
|
||||||
item.Grade,
|
item.Grade,
|
||||||
item.WaitlistedAt,
|
item.WaitlistedAt,
|
||||||
Position = index + 1
|
Position = (waitlistPage - 1) * waitlistPageSize + index + 1
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
return Ok(new
|
return Ok(new
|
||||||
@@ -487,10 +553,24 @@ public sealed class CourseSelectionsController(
|
|||||||
offering.CourseName,
|
offering.CourseName,
|
||||||
offering.CourseNature,
|
offering.CourseNature,
|
||||||
offering.Capacity,
|
offering.Capacity,
|
||||||
EnrolledCount = students.Count,
|
EnrolledCount = enrolledCount,
|
||||||
Students = students,
|
Students = students,
|
||||||
WaitlistedCount = waitlist.Count,
|
StudentPage = studentPage,
|
||||||
|
StudentPageSize = studentPageSize,
|
||||||
|
StudentTotal = enrolledTotal,
|
||||||
|
WaitlistedCount = waitlistedCount,
|
||||||
Waitlist = waitlist,
|
Waitlist = waitlist,
|
||||||
|
WaitlistPage = waitlistPage,
|
||||||
|
WaitlistPageSize = waitlistPageSize,
|
||||||
|
WaitlistTotal = waitlistedTotal,
|
||||||
|
FilterOptions = new
|
||||||
|
{
|
||||||
|
Grades = filterOptions.Select(x => x.Grade).Distinct().OrderBy(x => x),
|
||||||
|
Majors = filterOptions.Select(x => new { x.MajorId, x.MajorName }).Distinct()
|
||||||
|
.OrderBy(x => x.MajorName),
|
||||||
|
Classes = filterOptions.Select(x => new { x.ClassId, x.ClassName, x.MajorId })
|
||||||
|
.Distinct().OrderBy(x => x.ClassName)
|
||||||
|
},
|
||||||
CanManageWaitlist =
|
CanManageWaitlist =
|
||||||
offering.RoundStatus == CourseSelectionRoundStatus.Open &&
|
offering.RoundStatus == CourseSelectionRoundStatus.Open &&
|
||||||
(scope.IsInRole(SystemRoles.SuperAdmin) ||
|
(scope.IsInRole(SystemRoles.SuperAdmin) ||
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using Jiaowu.Api.Contracts;
|
||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
@@ -20,10 +21,38 @@ public sealed class OtherExamsController(AppDbContext db, ICurrentUserDataScope
|
|||||||
|
|
||||||
[HttpGet("batches")]
|
[HttpGet("batches")]
|
||||||
[Authorize(Roles = Managers)]
|
[Authorize(Roles = Managers)]
|
||||||
public async Task<ActionResult> GetBatches(CancellationToken ct) => Ok(await db.OtherExamBatches
|
public async Task<ActionResult> GetBatches(
|
||||||
.AsNoTracking().OrderByDescending(x => x.ExamDate).ThenByDescending(x => x.CreatedAt)
|
string? keyword = null,
|
||||||
|
OtherExamBatchStatus? status = null,
|
||||||
|
OtherExamMetricKind? metricKind = null,
|
||||||
|
DateOnly? examDateFrom = null,
|
||||||
|
DateOnly? examDateTo = null,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
page = Math.Max(1, page);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||||
|
var source = db.OtherExamBatches.AsNoTracking();
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
keyword = keyword.Trim();
|
||||||
|
source = source.Where(x =>
|
||||||
|
(x.ExamCode != null && x.ExamCode.Contains(keyword)) ||
|
||||||
|
x.Name.Contains(keyword) ||
|
||||||
|
(x.Organizer != null && x.Organizer.Contains(keyword)));
|
||||||
|
}
|
||||||
|
if (status.HasValue) source = source.Where(x => x.Status == status.Value);
|
||||||
|
if (metricKind.HasValue) source = source.Where(x => x.MetricKind == metricKind.Value);
|
||||||
|
if (examDateFrom.HasValue) source = source.Where(x => x.ExamDate >= examDateFrom.Value);
|
||||||
|
if (examDateTo.HasValue) source = source.Where(x => x.ExamDate <= examDateTo.Value);
|
||||||
|
var total = await source.CountAsync(ct);
|
||||||
|
var items = await source.OrderByDescending(x => x.ExamDate).ThenByDescending(x => x.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize).Take(pageSize)
|
||||||
.Select(x => new { x.Id, x.ExamCode, x.Name, x.Organizer, x.ExamDate, x.MetricKind, x.MaxScore, x.LevelOptions, x.Status, x.PublicationCount, x.PublishedAt, ResultCount = x.Results.Count })
|
.Select(x => new { x.Id, x.ExamCode, x.Name, x.Organizer, x.ExamDate, x.MetricKind, x.MaxScore, x.LevelOptions, x.Status, x.PublicationCount, x.PublishedAt, ResultCount = x.Results.Count })
|
||||||
.ToListAsync(ct));
|
.ToListAsync(ct);
|
||||||
|
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("batches")]
|
[HttpPost("batches")]
|
||||||
[Authorize(Roles = Managers)]
|
[Authorize(Roles = Managers)]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Jiaowu.Api.Controllers;
|
using Jiaowu.Api.Controllers;
|
||||||
|
using Jiaowu.Api.Contracts;
|
||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
@@ -11,6 +12,37 @@ namespace Jiaowu.Api.Tests;
|
|||||||
|
|
||||||
public sealed class OtherExamsControllerTests
|
public sealed class OtherExamsControllerTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Batches_AreFilteredAndReturnedByPage()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
await using var db = new AppDbContext(
|
||||||
|
new DbContextOptionsBuilder<AppDbContext>().UseSqlite(connection).Options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
db.OtherExamBatches.AddRange(Enumerable.Range(1, 11).Select(number =>
|
||||||
|
new OtherExamBatch
|
||||||
|
{
|
||||||
|
ExamCode = $"CET-{number:00}",
|
||||||
|
Name = $"英语等级考试 {number}",
|
||||||
|
ExamDate = new DateOnly(2026, 6, number),
|
||||||
|
MetricKind = OtherExamMetricKind.Score,
|
||||||
|
MaxScore = 100,
|
||||||
|
Status = number == 11
|
||||||
|
? OtherExamBatchStatus.Published
|
||||||
|
: OtherExamBatchStatus.Draft
|
||||||
|
}));
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var controller = new OtherExamsController(db, new TestDataScope(Guid.NewGuid()));
|
||||||
|
var result = Assert.IsType<OkObjectResult>(await controller.GetBatches(
|
||||||
|
keyword: "英语", status: OtherExamBatchStatus.Draft, page: 1,
|
||||||
|
pageSize: 10, ct: CancellationToken.None));
|
||||||
|
var page = Assert.IsType<PagedResult<object>>(result.Value);
|
||||||
|
Assert.Equal(10, page.Total);
|
||||||
|
Assert.Equal(10, page.Items.Count);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReplaceResults_ReplacesExistingResultsInsideTransaction()
|
public async Task ReplaceResults_ReplacesExistingResultsInsideTransaction()
|
||||||
{
|
{
|
||||||
|
|||||||
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user