Add student SGU profile synchronization

This commit is contained in:
2026-09-01 14:23:20 -06:00
parent 3b5cf723cc
commit fd3eb537a1
21 changed files with 345 additions and 43 deletions
@@ -12,6 +12,18 @@ public static class SguProfileParser
private const string JobTitleId = "ctl00_contenedor_decEncabezado_lblPuesto";
private const string DepartmentId = "ctl00_contenedor_decEncabezado_lblDependencia";
private const string MenuNameId = "ctl00_lblNombreUsuario";
private const string StudentNumberId = "ctl00_contenedor_HistorialAlumno1_lblClaveAlumnoHP";
private const string StudentGivenNameId = "ctl00_contenedor_HistorialAlumno1_lblNombreAlumnoHP";
private const string StudentPaternalSurnameId = "ctl00_contenedor_HistorialAlumno1_lblApPatAlumnoHP";
private const string StudentMaternalSurnameId = "ctl00_contenedor_HistorialAlumno1_lblApMatAlumnoHP";
private const string StudentEmailId = "ctl00_contenedor_HistorialAlumno1_lblCorreoAlumnoHP";
private const string StudentCareerId = "ctl00_contenedor_HistorialAlumno1_Header1_lblCarrera";
private const string StudentStreetId = "ctl00_contenedor_HistorialAlumno1_lblDomicilioAlumnoHP";
private const string StudentNeighborhoodId = "ctl00_contenedor_HistorialAlumno1_lblColoniaAlumnoHP";
private const string StudentStateId = "ctl00_contenedor_HistorialAlumno1_lblEstadoAlumnoHP";
private const string StudentCityId = "ctl00_contenedor_HistorialAlumno1_lblCiudadAlumnoHP";
private const string StudentMunicipalityId = "ctl00_contenedor_HistorialAlumno1_lblDeloMunAlumnoHP";
private const string StudentPostalCodeId = "ctl00_contenedor_HistorialAlumno1_lblCPAlumnoHP";
public static InstitutionalProfile? ParseAdministrative(string html, string expectedEmployeeNumber)
{
@@ -44,6 +56,44 @@ public static class SguProfileParser
return profile.HasValues ? profile : null;
}
public static InstitutionalProfile? ParseStudent(string html, string expectedStudentNumber)
{
ArgumentNullException.ThrowIfNull(html);
ArgumentException.ThrowIfNullOrWhiteSpace(expectedStudentNumber);
string? studentNumber = NormalizeInstitutionalNumber(ExtractSpanText(html, StudentNumberId));
if (!string.Equals(studentNumber, expectedStudentNumber, StringComparison.Ordinal))
{
return null;
}
string? givenName = NormalizeName(ExtractSpanText(html, StudentGivenNameId), 64);
string? paternalSurname = NormalizeSurname(ExtractSpanText(html, StudentPaternalSurnameId), 64);
string? maternalSurname = NormalizeSurname(ExtractSpanText(html, StudentMaternalSurnameId), 64);
string? surname = NormalizeSurname(JoinNonEmpty(" ", paternalSurname, maternalSurname), 64);
string? displayName = NormalizeName(JoinNonEmpty(" ", givenName, surname), 256);
string? street = NormalizeTitle(ExtractSpanText(html, StudentStreetId), 512);
string? neighborhood = NormalizeTitle(ExtractSpanText(html, StudentNeighborhoodId), 256);
string? city = NormalizeTitle(ExtractSpanText(html, StudentCityId), 128);
string? municipality = NormalizeTitle(ExtractSpanText(html, StudentMunicipalityId), 128);
string? streetAddress = BuildStreetAddress(street, neighborhood, municipality, city);
InstitutionalProfile profile = new(
EmployeeNumber: studentNumber,
DisplayName: displayName,
GivenName: givenName,
Surname: surname,
Email: NormalizeEmail(ExtractSpanText(html, StudentEmailId)),
EmployeeType: "Alumno",
JobTitle: BuildStudentJobTitle(ExtractSpanText(html, StudentCareerId)),
StreetAddress: streetAddress,
City: city ?? municipality,
State: NormalizeTitle(ExtractSpanText(html, StudentStateId), 128),
PostalCode: NormalizePostalCode(ExtractSpanText(html, StudentPostalCodeId)));
return profile.HasValues ? profile : null;
}
private static bool TrySplitAdministrativeIdentity(
string? value,
out string? employeeNumber,
@@ -176,6 +226,104 @@ public static class SguProfileParser
return candidate is null ? null : SpanishTextNormalizer.ToTitleCase(candidate);
}
private static string? NormalizeName(string? value, int maximumLength)
{
string? candidate = Limit(value, maximumLength);
return candidate is null ? null : SpanishTextNormalizer.ToTitleCase(candidate);
}
private static string? NormalizeSurname(string? value, int maximumLength)
{
string? candidate = Limit(value, maximumLength);
return candidate is null ? null : SpanishTextNormalizer.ToSurnameCase(candidate);
}
private static string? NormalizeInstitutionalNumber(string? value)
{
string? candidate = value?.Trim();
return candidate is { Length: 6 } && candidate.All(char.IsAsciiDigit)
? candidate
: null;
}
private static string? BuildStudentJobTitle(string? value)
{
string? candidate = Limit(value, 256);
if (candidate is null)
{
return null;
}
candidate = candidate.Replace(
"COMPUTACIO NALES",
"COMPUTACIONALES",
StringComparison.OrdinalIgnoreCase);
string career = SpanishTextNormalizer.ToTitleCase(candidate);
const string degreePrefix = "Licenciatura en ";
if (career.StartsWith(degreePrefix, StringComparison.OrdinalIgnoreCase))
{
career = career[degreePrefix.Length..];
}
return TruncateAtWordBoundary($"Estudiante de {career}", 64);
}
private static string? BuildStreetAddress(
string? street,
string? neighborhood,
string? municipality,
string? city)
{
List<string> lines = [];
AddDistinct(lines, street);
AddDistinct(lines, neighborhood);
if (!string.Equals(municipality, city, StringComparison.OrdinalIgnoreCase))
{
AddDistinct(lines, municipality);
}
return Limit(string.Join("\r\n", lines), 1024);
}
private static void AddDistinct(List<string> values, string? candidate)
{
if (!string.IsNullOrWhiteSpace(candidate) &&
!values.Contains(candidate, StringComparer.OrdinalIgnoreCase))
{
values.Add(candidate);
}
}
private static string? NormalizePostalCode(string? value)
{
string? candidate = value?.Trim();
if (candidate is null ||
candidate.Length is < 4 or > 5 ||
!candidate.All(char.IsAsciiDigit))
{
return null;
}
return candidate.PadLeft(5, '0');
}
private static string? JoinNonEmpty(string separator, params string?[] values)
{
string result = string.Join(separator, values.Where(value => !string.IsNullOrWhiteSpace(value)));
return string.IsNullOrWhiteSpace(result) ? null : result;
}
private static string TruncateAtWordBoundary(string value, int maximumLength)
{
if (value.Length <= maximumLength)
{
return value;
}
int boundary = value.LastIndexOf(' ', maximumLength - 1, maximumLength);
return value[..(boundary > 0 ? boundary : maximumLength)].TrimEnd();
}
private static string? NormalizeSentence(string? value, int maximumLength)
{
string? candidate = Limit(value, maximumLength);