diff --git a/VueApp/src/CTS/pages/CourseStudents.vue b/VueApp/src/CTS/pages/CourseStudents.vue
index 42ec398bf..33d5dfc89 100644
--- a/VueApp/src/CTS/pages/CourseStudents.vue
+++ b/VueApp/src/CTS/pages/CourseStudents.vue
@@ -1,6 +1,14 @@
Students for VET430 Lab 1 Challenging Communication - Simulated Lab with Actors and Video
@@ -9,12 +17,12 @@ const student = ref("")
@@ -37,73 +45,10 @@ const student = ref("")
-
- |
-
- |
- Montserrat Armero |
- 10/16/24 2:29:01 PM |
-
-
- |
-
-
- |
-
-
- |
-
- |
- Hailey Atwood |
- 10/16/24 2:29:01 PM |
-
-
- |
-
-
- |
-
-
+
|
|
- Xander Avila |
- 10/16/24 2:29:01 PM |
+ {{ row.name }} |
+ {{ row.time }} |
|
diff --git a/web/Areas/CMS/Data/Codecs.cs b/web/Areas/CMS/Data/Codecs.cs
index eab1f4a21..4cd584275 100644
--- a/web/Areas/CMS/Data/Codecs.cs
+++ b/web/Areas/CMS/Data/Codecs.cs
@@ -51,164 +51,21 @@ public static class Codecs
};
public static void UUDecode(Stream input, Stream output)
- {
- if (input == null)
- throw new ArgumentNullException(nameof(input));
-
- if (output == null)
- throw new ArgumentNullException(nameof(output));
-
- long len = input.Length;
- if (len == 0)
- return;
-
- long didx = 0;
- int nextByte = input.ReadByte();
- while (nextByte >= 0)
- {
- // get line length (in number of encoded octets)
- int line_len = UUDecMap[nextByte];
-
- // ascii printable to 0-63 and 4-byte to 3-byte conversion
- long end = didx + line_len;
- byte A, B, C, D;
- if (end > 2)
- {
- while (didx < end - 2)
- {
- A = UUDecMap[input.ReadByte()];
- B = UUDecMap[input.ReadByte()];
- C = UUDecMap[input.ReadByte()];
- D = UUDecMap[input.ReadByte()];
-
- output.WriteByte((byte)(((A << 2) & 255) | ((B >> 4) & 3)));
- output.WriteByte((byte)(((B << 4) & 255) | ((C >> 2) & 15)));
- output.WriteByte((byte)(((C << 6) & 255) | (D & 63)));
- didx += 3;
- }
- }
-
- if (didx < end)
- {
- A = UUDecMap[input.ReadByte()];
- B = UUDecMap[input.ReadByte()];
- output.WriteByte((byte)(((A << 2) & 255) | ((B >> 4) & 3)));
- didx++;
-
- if (didx < end)
- {
- C = UUDecMap[input.ReadByte()];
- output.WriteByte((byte)(((B << 4) & 255) | ((C >> 2) & 15)));
- didx++;
- }
- }
-
- // skip padding
- do
- {
- nextByte = input.ReadByte();
- }
- while (nextByte >= 0 && nextByte != '\n' && nextByte != '\r');
-
- // skip end of line
- do
- {
- nextByte = input.ReadByte();
- }
- while (nextByte >= 0 && (nextByte == '\n' || nextByte == '\r'));
- }
- }
+ => DecodeWithMap(input, output, UUDecMap);
public static void UUEncode(Stream input, Stream output)
- {
- if (input == null)
- throw new ArgumentNullException(nameof(input));
-
- if (output == null)
- throw new ArgumentNullException(nameof(output));
-
- long len = input.Length;
- if (len == 0)
- return;
-
- int sidx = 0;
- int line_len = 45;
- byte[] nl = Encoding.ASCII.GetBytes(Environment.NewLine);
-
- byte A, B, C;
- // split into lines, adding line-length and line terminator
- while (sidx + line_len < len)
- {
- // line length
- output.WriteByte(UUEncMap[line_len]);
-
- // 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
- for (int end = sidx + line_len; sidx < end; sidx += 3)
- {
- A = (byte)input.ReadByte();
- B = (byte)input.ReadByte();
- C = (byte)input.ReadByte();
+ => EncodeWithMap(input, output, UUEncMap);
- output.WriteByte(UUEncMap[(A >> 2) & 63]);
- output.WriteByte(UUEncMap[(B >> 4) & 15 | (A << 4) & 63]);
- output.WriteByte(UUEncMap[(C >> 6) & 3 | (B << 2) & 63]);
- output.WriteByte(UUEncMap[C & 63]);
- }
-
- // line terminator
- for (int idx = 0; idx < nl.Length; idx++)
- output.WriteByte(nl[idx]);
- }
-
- // line length
- output.WriteByte(UUEncMap[len - sidx]);
-
- // 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
- while (sidx + 2 < len)
- {
- A = (byte)input.ReadByte();
- B = (byte)input.ReadByte();
- C = (byte)input.ReadByte();
-
- output.WriteByte(UUEncMap[(A >> 2) & 63]);
- output.WriteByte(UUEncMap[(B >> 4) & 15 | (A << 4) & 63]);
- output.WriteByte(UUEncMap[(C >> 6) & 3 | (B << 2) & 63]);
- output.WriteByte(UUEncMap[C & 63]);
- sidx += 3;
- }
-
- if (sidx < len - 1)
- {
- A = (byte)input.ReadByte();
- B = (byte)input.ReadByte();
-
- output.WriteByte(UUEncMap[(A >> 2) & 63]);
- output.WriteByte(UUEncMap[(B >> 4) & 15 | (A << 4) & 63]);
- output.WriteByte(UUEncMap[(B << 2) & 63]);
- output.WriteByte(UUEncMap[0]);
- }
- else if (sidx < len)
- {
- A = (byte)input.ReadByte();
-
- output.WriteByte(UUEncMap[(A >> 2) & 63]);
- output.WriteByte(UUEncMap[(A << 4) & 63]);
- output.WriteByte(UUEncMap[0]);
- output.WriteByte(UUEncMap[0]);
- }
+ public static void XXDecode(Stream input, Stream output)
+ => DecodeWithMap(input, output, XXDecMap);
- // line terminator
- for (int idx = 0; idx < nl.Length; idx++)
- output.WriteByte(nl[idx]);
- }
+ public static void XXEncode(Stream input, Stream output)
+ => EncodeWithMap(input, output, XXEncMap);
- public static void XXDecode(Stream input, Stream output)
+ private static void DecodeWithMap(Stream input, Stream output, byte[] decMap)
{
- if (input == null)
- throw new ArgumentNullException(nameof(input));
-
- if (output == null)
- throw new ArgumentNullException(nameof(output));
+ ArgumentNullException.ThrowIfNull(input);
+ ArgumentNullException.ThrowIfNull(output);
long len = input.Length;
if (len == 0)
@@ -219,7 +76,7 @@ public static void XXDecode(Stream input, Stream output)
while (nextByte >= 0)
{
// get line length (in number of encoded octets)
- int line_len = XXDecMap[nextByte];
+ int line_len = decMap[nextByte];
// ascii printable to 0-63 and 4-byte to 3-byte conversion
long end = didx + line_len;
@@ -228,10 +85,10 @@ public static void XXDecode(Stream input, Stream output)
{
while (didx < end - 2)
{
- A = XXDecMap[input.ReadByte()];
- B = XXDecMap[input.ReadByte()];
- C = XXDecMap[input.ReadByte()];
- D = XXDecMap[input.ReadByte()];
+ A = decMap[input.ReadByte()];
+ B = decMap[input.ReadByte()];
+ C = decMap[input.ReadByte()];
+ D = decMap[input.ReadByte()];
output.WriteByte((byte)(((A << 2) & 255) | ((B >> 4) & 3)));
output.WriteByte((byte)(((B << 4) & 255) | ((C >> 2) & 15)));
@@ -242,14 +99,14 @@ public static void XXDecode(Stream input, Stream output)
if (didx < end)
{
- A = XXDecMap[input.ReadByte()];
- B = XXDecMap[input.ReadByte()];
+ A = decMap[input.ReadByte()];
+ B = decMap[input.ReadByte()];
output.WriteByte((byte)(((A << 2) & 255) | ((B >> 4) & 3)));
didx++;
if (didx < end)
{
- C = XXDecMap[input.ReadByte()];
+ C = decMap[input.ReadByte()];
output.WriteByte((byte)(((B << 4) & 255) | ((C >> 2) & 15)));
didx++;
}
@@ -271,13 +128,10 @@ public static void XXDecode(Stream input, Stream output)
}
}
- public static void XXEncode(Stream input, Stream output)
+ private static void EncodeWithMap(Stream input, Stream output, byte[] encMap)
{
- if (input == null)
- throw new ArgumentNullException(nameof(input));
-
- if (output == null)
- throw new ArgumentNullException(nameof(output));
+ ArgumentNullException.ThrowIfNull(input);
+ ArgumentNullException.ThrowIfNull(output);
long len = input.Length;
if (len == 0)
@@ -292,7 +146,7 @@ public static void XXEncode(Stream input, Stream output)
while (sidx + line_len < len)
{
// line length
- output.WriteByte(XXEncMap[line_len]);
+ output.WriteByte(encMap[line_len]);
// 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
for (int end = sidx + line_len; sidx < end; sidx += 3)
@@ -301,10 +155,10 @@ public static void XXEncode(Stream input, Stream output)
B = (byte)input.ReadByte();
C = (byte)input.ReadByte();
- output.WriteByte(XXEncMap[(A >> 2) & 63]);
- output.WriteByte(XXEncMap[(B >> 4) & 15 | (A << 4) & 63]);
- output.WriteByte(XXEncMap[(C >> 6) & 3 | (B << 2) & 63]);
- output.WriteByte(XXEncMap[C & 63]);
+ output.WriteByte(encMap[(A >> 2) & 63]);
+ output.WriteByte(encMap[(B >> 4) & 15 | (A << 4) & 63]);
+ output.WriteByte(encMap[(C >> 6) & 3 | (B << 2) & 63]);
+ output.WriteByte(encMap[C & 63]);
}
// line terminator
@@ -313,7 +167,7 @@ public static void XXEncode(Stream input, Stream output)
}
// line length
- output.WriteByte(XXEncMap[len - sidx]);
+ output.WriteByte(encMap[len - sidx]);
// 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
while (sidx + 2 < len)
@@ -322,10 +176,10 @@ public static void XXEncode(Stream input, Stream output)
B = (byte)input.ReadByte();
C = (byte)input.ReadByte();
- output.WriteByte(XXEncMap[(A >> 2) & 63]);
- output.WriteByte(XXEncMap[(B >> 4) & 15 | (A << 4) & 63]);
- output.WriteByte(XXEncMap[(C >> 6) & 3 | (B << 2) & 63]);
- output.WriteByte(XXEncMap[C & 63]);
+ output.WriteByte(encMap[(A >> 2) & 63]);
+ output.WriteByte(encMap[(B >> 4) & 15 | (A << 4) & 63]);
+ output.WriteByte(encMap[(C >> 6) & 3 | (B << 2) & 63]);
+ output.WriteByte(encMap[C & 63]);
sidx += 3;
}
@@ -334,19 +188,19 @@ public static void XXEncode(Stream input, Stream output)
A = (byte)input.ReadByte();
B = (byte)input.ReadByte();
- output.WriteByte(XXEncMap[(A >> 2) & 63]);
- output.WriteByte(XXEncMap[(B >> 4) & 15 | (A << 4) & 63]);
- output.WriteByte(XXEncMap[(B << 2) & 63]);
- output.WriteByte(XXEncMap[0]);
+ output.WriteByte(encMap[(A >> 2) & 63]);
+ output.WriteByte(encMap[(B >> 4) & 15 | (A << 4) & 63]);
+ output.WriteByte(encMap[(B << 2) & 63]);
+ output.WriteByte(encMap[0]);
}
else if (sidx < len)
{
A = (byte)input.ReadByte();
- output.WriteByte(XXEncMap[(A >> 2) & 63]);
- output.WriteByte(XXEncMap[(A << 4) & 63]);
- output.WriteByte(XXEncMap[0]);
- output.WriteByte(XXEncMap[0]);
+ output.WriteByte(encMap[(A >> 2) & 63]);
+ output.WriteByte(encMap[(A << 4) & 63]);
+ output.WriteByte(encMap[0]);
+ output.WriteByte(encMap[0]);
}
// line terminator
diff --git a/web/Areas/ClinicalScheduler/Services/InstructorScheduleService.cs b/web/Areas/ClinicalScheduler/Services/InstructorScheduleService.cs
index e3bf22b33..4a96f3a49 100644
--- a/web/Areas/ClinicalScheduler/Services/InstructorScheduleService.cs
+++ b/web/Areas/ClinicalScheduler/Services/InstructorScheduleService.cs
@@ -42,43 +42,7 @@ public async Task> GetInstructorScheduleAsync(
.Include(s => s.Service)
.Include(s => s.Rotation)
.AsNoTracking()
- .AsQueryable();
-
- // Apply filters
- if (classYear.HasValue)
- {
- query = query.Where(s => s.Week.WeekGradYears.Any(gy => gy.GradYear == classYear));
- }
-
- if (!string.IsNullOrWhiteSpace(mothraId))
- {
- query = query.Where(s => s.MothraId == mothraId);
- }
-
- if (rotationId.HasValue)
- {
- query = query.Where(s => s.RotationId == rotationId);
- }
-
- if (serviceId.HasValue)
- {
- query = query.Where(s => s.ServiceId == serviceId);
- }
-
- if (weekId.HasValue)
- {
- query = query.Where(s => s.WeekId == weekId);
- }
-
- if (startDate.HasValue)
- {
- query = query.Where(s => s.DateEnd >= startDate);
- }
-
- if (endDate.HasValue)
- {
- query = query.Where(s => s.DateStart <= endDate);
- }
+ .ApplyScheduleFilters(classYear, mothraId, rotationId, serviceId, weekId, startDate, endDate);
if (active.HasValue && active.Value)
{
diff --git a/web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs b/web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs
new file mode 100644
index 000000000..c3da702ed
--- /dev/null
+++ b/web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs
@@ -0,0 +1,59 @@
+using Viper.Models.CTS;
+
+namespace Viper.Areas.ClinicalScheduler.Services;
+
+///
+/// LINQ extensions shared between InstructorScheduleService and StudentScheduleService
+/// for filtering schedule entities by the common rotation / service / week / date filters.
+///
+public static class ScheduleQueryExtensions
+{
+ public static IQueryable ApplyScheduleFilters(
+ this IQueryable query,
+ int? classYear,
+ string? mothraId,
+ int? rotationId,
+ int? serviceId,
+ int? weekId,
+ DateTime? startDate,
+ DateTime? endDate)
+ where T : class, IScheduleEntity
+ {
+ if (classYear.HasValue)
+ {
+ query = query.Where(s => s.Week.WeekGradYears.Any(gy => gy.GradYear == classYear));
+ }
+
+ if (!string.IsNullOrWhiteSpace(mothraId))
+ {
+ query = query.Where(s => s.MothraId == mothraId);
+ }
+
+ if (rotationId.HasValue)
+ {
+ query = query.Where(s => s.RotationId == rotationId);
+ }
+
+ if (serviceId.HasValue)
+ {
+ query = query.Where(s => s.ServiceId == serviceId);
+ }
+
+ if (weekId.HasValue)
+ {
+ query = query.Where(s => s.WeekId == weekId);
+ }
+
+ if (startDate.HasValue)
+ {
+ query = query.Where(s => s.DateEnd >= startDate);
+ }
+
+ if (endDate.HasValue)
+ {
+ query = query.Where(s => s.DateStart <= endDate);
+ }
+
+ return query;
+ }
+}
diff --git a/web/Areas/ClinicalScheduler/Services/StudentScheduleService.cs b/web/Areas/ClinicalScheduler/Services/StudentScheduleService.cs
index 5b2e9b93b..6b7c689ed 100644
--- a/web/Areas/ClinicalScheduler/Services/StudentScheduleService.cs
+++ b/web/Areas/ClinicalScheduler/Services/StudentScheduleService.cs
@@ -41,43 +41,7 @@ public async Task> GetStudentScheduleAsync(
.Include(s => s.Service)
.Include(s => s.Rotation)
.AsNoTracking()
- .AsQueryable();
-
- // Apply filters
- if (classYear.HasValue)
- {
- query = query.Where(s => s.Week.WeekGradYears.Any(gy => gy.GradYear == classYear));
- }
-
- if (!string.IsNullOrWhiteSpace(mothraId))
- {
- query = query.Where(s => s.MothraId == mothraId);
- }
-
- if (rotationId.HasValue)
- {
- query = query.Where(s => s.RotationId == rotationId);
- }
-
- if (serviceId.HasValue)
- {
- query = query.Where(s => s.ServiceId == serviceId);
- }
-
- if (weekId.HasValue)
- {
- query = query.Where(s => s.WeekId == weekId);
- }
-
- if (startDate.HasValue)
- {
- query = query.Where(s => s.DateEnd >= startDate);
- }
-
- if (endDate.HasValue)
- {
- query = query.Where(s => s.DateStart <= endDate);
- }
+ .ApplyScheduleFilters(classYear, mothraId, rotationId, serviceId, weekId, startDate, endDate);
// Apply ordering
query = query.OrderBy(s => s.LastName)
diff --git a/web/Areas/Students/Services/StudentGroupService.cs b/web/Areas/Students/Services/StudentGroupService.cs
index cf75c1a09..c0e95aa8a 100644
--- a/web/Areas/Students/Services/StudentGroupService.cs
+++ b/web/Areas/Students/Services/StudentGroupService.cs
@@ -1,8 +1,8 @@
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
-using Viper.Areas.Curriculum.Services;
-using Viper.Areas.Students.Models;
using Viper.Classes.SQLContext;
+using Viper.Areas.Students.Models;
+using Viper.Areas.Curriculum.Services;
using Viper.Classes.Utilities;
namespace Viper.Areas.Students.Services
@@ -21,6 +21,25 @@ public interface IStudentGroupService
public class StudentGroupService : IStudentGroupService
{
+ ///
+ /// Common projection shape used by the AAUD student queries before they are
+ /// turned into StudentPhoto results. Keeps the same column set across the
+ /// class-level / group / course query variants so the photo-building loop is
+ /// shared.
+ ///
+ private sealed record StudentBaseRecord(
+ string? MailId,
+ string? FirstName,
+ string LastName,
+ string? MiddleName,
+ string? IamId,
+ string? BannerId,
+ string? ClassLevel,
+ string? EighthsGroup,
+ string? TwentiethsGroup,
+ string? TeamNumber,
+ string? V3SpecialtyGroup);
+
private readonly AAUDContext _aaudContext;
private readonly SISContext _sisContext;
private readonly CoursesContext _coursesContext;
@@ -91,57 +110,22 @@ join sg in _aaudContext.Studentgrps on i.IdsPidm equals sg.StudentgrpPidm into s
from sg in sgGroup.DefaultIfEmpty()
where s.StudentsClassLevel == classLevel && i.IdsTermCode == currentTerm
&& (string.IsNullOrEmpty(i.IdsIamId) || !rossIamIds.Contains(i.IdsIamId!))
- select new
- {
- PersonId = p.PersonPKey,
- MailId = i.IdsMailid,
- FirstName = p.PersonDisplayFirstName ?? p.PersonFirstName,
- LastName = p.PersonLastName,
- MiddleName = p.PersonMiddleName,
- IamId = i.IdsIamId,
- BannerId = i.IdsClientid,
- ClassLevel = s.StudentsClassLevel,
- EighthsGroup = sg != null ? sg.StudentgrpGrp : null,
- TwentiethsGroup = sg != null ? sg.Studentgrp20 : null,
- TeamNumber = sg != null ? sg.StudentgrpTeamno : null,
- V3SpecialtyGroup = sg != null ? sg.StudentgrpV3grp : null
- };
+ select new StudentBaseRecord(
+ i.IdsMailid,
+ p.PersonDisplayFirstName ?? p.PersonFirstName,
+ p.PersonLastName,
+ p.PersonMiddleName,
+ i.IdsIamId,
+ i.IdsClientid,
+ s.StudentsClassLevel,
+ sg != null ? sg.StudentgrpGrp : null,
+ sg != null ? sg.Studentgrp20 : null,
+ sg != null ? sg.StudentgrpTeamno : null,
+ sg != null ? sg.StudentgrpV3grp : null);
var students = await query.OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToListAsync();
- var mailIds = students.Where(s => !string.IsNullOrWhiteSpace(s.MailId)).Select(s => s.MailId!).Distinct();
- var photoUrls = await _photoService.GetStudentPhotoUrlsBatchAsync(mailIds);
- var defaultPhotoUrl = _photoService.GetDefaultPhotoUrl();
-
- var photoStudents = new List();
- foreach (var student in students)
- {
- var displayName = FormatStudentDisplayName(student.LastName, student.FirstName, student.MiddleName);
-
- var (photoUrl, hasPhoto) = ResolvePhotoUrl(student.MailId, photoUrls, defaultPhotoUrl);
-
- // Combine Eighths and Twentieths groups in format "2B1 / 1AA"
- var groupAssignment = FormatGroupAssignment(student.EighthsGroup, student.TwentiethsGroup);
-
- var photoStudent = new StudentPhoto
- {
- MailId = student.MailId,
- FirstName = student.FirstName,
- LastName = student.LastName,
- DisplayName = displayName,
- PhotoUrl = photoUrl,
- GroupAssignment = groupAssignment,
- EighthsGroup = student.EighthsGroup?.Trim(),
- TwentiethsGroup = student.TwentiethsGroup?.Trim(),
- TeamNumber = student.ClassLevel == "V3" ? student.TeamNumber?.Trim() : null,
- V3SpecialtyGroup = student.ClassLevel == "V3" ? student.V3SpecialtyGroup?.Trim() : null,
- HasPhoto = hasPhoto,
- IsRossStudent = false,
- ClassLevel = student.ClassLevel
- };
-
- photoStudents.Add(photoStudent);
- }
+ var photoStudents = await BuildStudentPhotoListAsync(students);
// Add Ross students if requested
if (includeRossStudents)
@@ -320,28 +304,7 @@ public async Task> GetStudentsByGroupAsync(string groupType,
// Get list of Ross student IamIds to ALWAYS exclude from regular query
// This prevents duplicates - Ross students would need to be added separately if we supported includeRoss for groups
- List rossIamIds = new List();
- try
- {
- rossIamIds = await _sisContext.StudentDesignations
- .Where(sd => sd.DesignationType == "Ross")
- .Where(sd => (sd.EndTerm == null || currentTermInt <= sd.EndTerm) &&
- (sd.StartTerm == null || sd.StartTerm <= currentTermInt))
- .Select(sd => sd.IamId)
- .Where(id => !string.IsNullOrEmpty(id))
- .Distinct()
- .ToListAsync();
- }
- catch (InvalidOperationException ex)
- {
- _logger.LogError(ex, "Invalid operation querying SIS context for Ross students");
- // Continue with empty rossIamIds list - no Ross students will be excluded
- }
- catch (SqlException ex)
- {
- _logger.LogError(ex, "Database error querying SIS context for Ross students");
- // Continue with empty rossIamIds list - no Ross students will be excluded
- }
+ var rossIamIds = await GetActiveRossIamIdsAsync(currentTermInt, "by-group");
var queryBase = from i in _aaudContext.Ids
join p in _aaudContext.People on i.IdsPKey equals p.PersonPKey
@@ -375,56 +338,22 @@ join sg in _aaudContext.Studentgrps on i.IdsPidm equals sg.StudentgrpPidm
queryBase = queryBase.Where(x => x.sg.StudentgrpV3grp == groupId);
}
- var query = queryBase.Select(x => new
- {
- PersonId = x.p.PersonPKey,
- MailId = x.i.IdsMailid,
- FirstName = x.p.PersonDisplayFirstName ?? x.p.PersonFirstName,
- LastName = x.p.PersonLastName,
- MiddleName = x.p.PersonMiddleName,
- IamId = x.i.IdsIamId,
- BannerId = x.i.IdsClientid,
- ClassLevel = x.s.StudentsClassLevel,
- EighthsGroup = x.sg.StudentgrpGrp,
- TwentiethsGroup = x.sg.Studentgrp20,
- TeamNumber = x.sg.StudentgrpTeamno,
- V3SpecialtyGroup = x.sg.StudentgrpV3grp
- });
+ var query = queryBase.Select(x => new StudentBaseRecord(
+ x.i.IdsMailid,
+ x.p.PersonDisplayFirstName ?? x.p.PersonFirstName,
+ x.p.PersonLastName,
+ x.p.PersonMiddleName,
+ x.i.IdsIamId,
+ x.i.IdsClientid,
+ x.s.StudentsClassLevel,
+ x.sg.StudentgrpGrp,
+ x.sg.Studentgrp20,
+ x.sg.StudentgrpTeamno,
+ x.sg.StudentgrpV3grp));
var students = await query.OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToListAsync();
- var mailIds = students.Where(s => !string.IsNullOrWhiteSpace(s.MailId)).Select(s => s.MailId!).Distinct();
- var photoUrls = await _photoService.GetStudentPhotoUrlsBatchAsync(mailIds);
- var defaultPhotoUrl = _photoService.GetDefaultPhotoUrl();
-
- var photoStudents = new List();
- foreach (var student in students)
- {
- var displayName = FormatStudentDisplayName(student.LastName, student.FirstName, student.MiddleName);
-
- var (photoUrl, hasPhoto) = ResolvePhotoUrl(student.MailId, photoUrls, defaultPhotoUrl);
-
- // Combine Eighths and Twentieths groups in format "2B1 / 1AA"
- var groupAssignment = FormatGroupAssignment(student.EighthsGroup, student.TwentiethsGroup);
-
- photoStudents.Add(new StudentPhoto
- {
- MailId = student.MailId,
- FirstName = student.FirstName,
- LastName = student.LastName,
- DisplayName = displayName,
- PhotoUrl = photoUrl,
- GroupAssignment = groupAssignment,
- EighthsGroup = student.EighthsGroup?.Trim(),
- TwentiethsGroup = student.TwentiethsGroup?.Trim(),
- TeamNumber = student.ClassLevel == "V3" ? student.TeamNumber?.Trim() : null,
- V3SpecialtyGroup = student.ClassLevel == "V3" ? student.V3SpecialtyGroup?.Trim() : null,
- HasPhoto = hasPhoto,
- IsRossStudent = false
- });
- }
-
- return photoStudents;
+ return await BuildStudentPhotoListAsync(students);
}
catch (InvalidOperationException ex)
{
@@ -445,26 +374,7 @@ public async Task> GetStudentsByCourseAsync(string termCode,
var currentTermInt = int.Parse(termCode);
// Get list of Ross IAM IDs so we can always exclude them unless explicitly requested
- List rossIamIds = new();
- try
- {
- rossIamIds = await _sisContext.StudentDesignations
- .Where(sd => sd.DesignationType == "Ross")
- .Where(sd => (sd.EndTerm == null || currentTermInt <= sd.EndTerm) &&
- (sd.StartTerm == null || sd.StartTerm <= currentTermInt))
- .Select(sd => sd.IamId)
- .Where(id => !string.IsNullOrEmpty(id))
- .Distinct()
- .ToListAsync();
- }
- catch (InvalidOperationException ex)
- {
- _logger.LogError(ex, "Invalid operation querying SIS context for Ross students");
- }
- catch (SqlException ex)
- {
- _logger.LogError(ex, "Database error querying SIS context for Ross students");
- }
+ var rossIamIds = await GetActiveRossIamIdsAsync(currentTermInt, "by-course");
// Query students enrolled in the course
// Two-step approach to avoid multi-context joins (following legacy implementation pattern)
@@ -494,21 +404,18 @@ from sg in sgGroup.DefaultIfEmpty()
where enrolledPidms.Contains(i.IdsPidm)
&& i.IdsTermCode == termCode
&& (string.IsNullOrEmpty(i.IdsIamId) || !rossIamIds.Contains(i.IdsIamId!))
- select new
- {
- PersonId = p.PersonPKey,
- MailId = i.IdsMailid,
- FirstName = p.PersonDisplayFirstName ?? p.PersonFirstName,
- LastName = p.PersonLastName,
- MiddleName = p.PersonMiddleName,
- IamId = i.IdsIamId,
- BannerId = i.IdsClientid,
- ClassLevel = s.StudentsClassLevel,
- EighthsGroup = sg != null ? sg.StudentgrpGrp : null,
- TwentiethsGroup = sg != null ? sg.Studentgrp20 : null,
- TeamNumber = sg != null ? sg.StudentgrpTeamno : null,
- V3SpecialtyGroup = sg != null ? sg.StudentgrpV3grp : null
- };
+ select new StudentBaseRecord(
+ i.IdsMailid,
+ p.PersonDisplayFirstName ?? p.PersonFirstName,
+ p.PersonLastName,
+ p.PersonMiddleName,
+ i.IdsIamId,
+ i.IdsClientid,
+ s.StudentsClassLevel,
+ sg != null ? sg.StudentgrpGrp : null,
+ sg != null ? sg.Studentgrp20 : null,
+ sg != null ? sg.StudentgrpTeamno : null,
+ sg != null ? sg.StudentgrpV3grp : null);
// Apply group filtering if specified
if (!string.IsNullOrEmpty(groupType) && !string.IsNullOrEmpty(groupId))
@@ -525,39 +432,7 @@ where enrolledPidms.Contains(i.IdsPidm)
var students = await query.OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToListAsync();
- var mailIds = students.Where(s => !string.IsNullOrWhiteSpace(s.MailId)).Select(s => s.MailId!).Distinct();
- var photoUrls = await _photoService.GetStudentPhotoUrlsBatchAsync(mailIds);
- var defaultPhotoUrl = _photoService.GetDefaultPhotoUrl();
-
- var photoStudents = new List();
- foreach (var student in students)
- {
- var displayName = FormatStudentDisplayName(student.LastName, student.FirstName, student.MiddleName);
-
- var (photoUrl, hasPhoto) = ResolvePhotoUrl(student.MailId, photoUrls, defaultPhotoUrl);
-
- // Combine Eighths and Twentieths groups
- var groupAssignment = FormatGroupAssignment(student.EighthsGroup, student.TwentiethsGroup);
-
- var photoStudent = new StudentPhoto
- {
- MailId = student.MailId,
- FirstName = student.FirstName,
- LastName = student.LastName,
- DisplayName = displayName,
- PhotoUrl = photoUrl,
- GroupAssignment = groupAssignment,
- EighthsGroup = student.EighthsGroup?.Trim(),
- TwentiethsGroup = student.TwentiethsGroup?.Trim(),
- TeamNumber = student.ClassLevel == "V3" ? student.TeamNumber?.Trim() : null,
- V3SpecialtyGroup = student.ClassLevel == "V3" ? student.V3SpecialtyGroup?.Trim() : null,
- HasPhoto = hasPhoto,
- IsRossStudent = false,
- ClassLevel = student.ClassLevel
- };
-
- photoStudents.Add(photoStudent);
- }
+ var photoStudents = await BuildStudentPhotoListAsync(students);
// Add Ross students if requested
if (includeRossStudents && rossIamIds.Any())
@@ -590,8 +465,8 @@ join s in _aaudContext.Students on p.PersonPKey equals s.StudentsPKey
i.IdsMailid,
i.IdsTermCode,
PersonFirstName = p.PersonDisplayFirstName ?? p.PersonFirstName,
- p.PersonLastName,
- p.PersonMiddleName,
+ PersonLastName = p.PersonLastName,
+ PersonMiddleName = p.PersonMiddleName,
ClassLevel = s.StudentsClassLevel
})
.ToListAsync();
@@ -825,18 +700,87 @@ private static string FormatGroupAssignment(string? eighthsGroup, string? twenti
{
return $"{eighthsGroup} / {twentiethsGroup}";
}
-
- if (!string.IsNullOrEmpty(eighthsGroup))
+ else if (!string.IsNullOrEmpty(eighthsGroup))
{
return eighthsGroup;
}
-
- if (!string.IsNullOrEmpty(twentiethsGroup))
+ else if (!string.IsNullOrEmpty(twentiethsGroup))
{
return twentiethsGroup;
}
return string.Empty;
}
+ ///
+ /// Build StudentPhoto results from already-projected student records: batch-resolve
+ /// photo URLs once, then format display name and group assignment per row.
+ ///
+ private async Task> BuildStudentPhotoListAsync(
+ IReadOnlyList students,
+ bool isRoss = false)
+ {
+ var mailIds = students
+ .Where(s => !string.IsNullOrWhiteSpace(s.MailId))
+ .Select(s => s.MailId!)
+ .Distinct();
+ var photoUrls = await _photoService.GetStudentPhotoUrlsBatchAsync(mailIds);
+ var defaultPhotoUrl = _photoService.GetDefaultPhotoUrl();
+
+ var result = new List(students.Count);
+ foreach (var student in students)
+ {
+ var displayName = FormatStudentDisplayName(student.LastName, student.FirstName ?? string.Empty, student.MiddleName);
+ var (photoUrl, hasPhoto) = ResolvePhotoUrl(student.MailId, photoUrls, defaultPhotoUrl);
+ var groupAssignment = FormatGroupAssignment(student.EighthsGroup, student.TwentiethsGroup);
+
+ result.Add(new StudentPhoto
+ {
+ MailId = student.MailId!,
+ FirstName = student.FirstName ?? string.Empty,
+ LastName = student.LastName,
+ DisplayName = displayName,
+ PhotoUrl = photoUrl,
+ GroupAssignment = groupAssignment,
+ EighthsGroup = student.EighthsGroup?.Trim(),
+ TwentiethsGroup = student.TwentiethsGroup?.Trim(),
+ TeamNumber = student.ClassLevel == "V3" ? student.TeamNumber?.Trim() : null,
+ V3SpecialtyGroup = student.ClassLevel == "V3" ? student.V3SpecialtyGroup?.Trim() : null,
+ HasPhoto = hasPhoto,
+ IsRossStudent = isRoss,
+ ClassLevel = student.ClassLevel
+ });
+ }
+ return result;
+ }
+
+ ///
+ /// Look up Ross-program IamIds active for the given term so non-Ross queries can
+ /// exclude them. Swallows the recoverable SIS-database errors and returns an
+ /// empty list so the caller's main query still runs.
+ ///
+ private async Task> GetActiveRossIamIdsAsync(int currentTermInt, string contextLabel)
+ {
+ try
+ {
+ return await _sisContext.StudentDesignations
+ .Where(sd => sd.DesignationType == "Ross")
+ .Where(sd => (sd.EndTerm == null || currentTermInt <= sd.EndTerm) &&
+ (sd.StartTerm == null || sd.StartTerm <= currentTermInt))
+ .Select(sd => sd.IamId)
+ .Where(id => !string.IsNullOrEmpty(id))
+ .Distinct()
+ .ToListAsync();
+ }
+ catch (InvalidOperationException ex)
+ {
+ _logger.LogError(ex, "Invalid operation querying SIS context for Ross students ({Context})", LogSanitizer.SanitizeString(contextLabel));
+ return new List();
+ }
+ catch (SqlException ex)
+ {
+ _logger.LogError(ex, "Database error querying SIS context for Ross students ({Context})", LogSanitizer.SanitizeString(contextLabel));
+ return new List();
+ }
+ }
}
}
diff --git a/web/Models/CTS/IScheduleEntity.cs b/web/Models/CTS/IScheduleEntity.cs
new file mode 100644
index 000000000..e1fd52909
--- /dev/null
+++ b/web/Models/CTS/IScheduleEntity.cs
@@ -0,0 +1,16 @@
+namespace Viper.Models.CTS;
+
+///
+/// Common filterable fields exposed by clinical-schedule entities (instructor and student).
+/// Used by ScheduleQueryExtensions to apply shared LINQ filter clauses generically.
+///
+public interface IScheduleEntity
+{
+ string MothraId { get; }
+ int RotationId { get; }
+ int ServiceId { get; }
+ int WeekId { get; }
+ DateTime DateStart { get; }
+ DateTime DateEnd { get; }
+ Week Week { get; }
+}
diff --git a/web/Models/CTS/InstructorSchedule.cs b/web/Models/CTS/InstructorSchedule.cs
index 22244c8b0..3f6855d02 100644
--- a/web/Models/CTS/InstructorSchedule.cs
+++ b/web/Models/CTS/InstructorSchedule.cs
@@ -1,6 +1,6 @@
namespace Viper.Models.CTS
{
- public class InstructorSchedule
+ public class InstructorSchedule : IScheduleEntity
{
public int InstructorScheduleId { get; set; }
public string LastName { get; set; } = null!;
diff --git a/web/Models/CTS/StudentSchedule.cs b/web/Models/CTS/StudentSchedule.cs
index e28ecdf0d..ff9725064 100644
--- a/web/Models/CTS/StudentSchedule.cs
+++ b/web/Models/CTS/StudentSchedule.cs
@@ -1,6 +1,6 @@
namespace Viper.Models.CTS
{
- public class StudentSchedule
+ public class StudentSchedule : IScheduleEntity
{
public int StudentScheduleId { get; set; }
public int PersonId { get; set; }