diff --git a/.github/workflows/app_production.yml b/.github/workflows/wigg-app-prod-course.yml similarity index 75% rename from .github/workflows/app_production.yml rename to .github/workflows/wigg-app-prod-course.yml index aba85f8..6d3ad3b 100644 --- a/.github/workflows/app_production.yml +++ b/.github/workflows/wigg-app-prod-course.yml @@ -9,9 +9,6 @@ on: - main workflow_dispatch: -env: - DOTNET_VERSION: '8.0.x' # set this to the dotnet version to use - jobs: build: runs-on: windows-latest @@ -19,17 +16,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment + - name: Set up .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ env.DOTNET_VERSION }} + dotnet-version: '8.x' include-prerelease: true - name: Build with dotnet run: dotnet build --configuration Release - name: dotnet publish - run: dotnet publish ProgramInformationV2 -c Release -r linux-x64 --property:PublishDir=${{env.DOTNET_ROOT}}/myapp + run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp - name: Upload artifact for deployment job uses: actions/upload-artifact@v4 @@ -54,7 +51,6 @@ jobs: id: deploy-to-webapp uses: azure/webapps-deploy@v2 with: - app-name: itp-app-programrepository - package: . + app-name: 'wigg-course-infra-winapp01' publish-profile: ${{ secrets.AZURE_WEB_PUBLISH_PROFILE_PRODUCTION }} - slot-name: 'deploy' + package: . \ No newline at end of file diff --git a/.github/workflows/wigg-app-staging-course.yml b/.github/workflows/wigg-app-staging-course.yml new file mode 100644 index 0000000..6ac00ba --- /dev/null +++ b/.github/workflows/wigg-app-staging-course.yml @@ -0,0 +1,57 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Staging WIGG Build and deploy ASP.Net Core app to Azure Web App + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.x' + include-prerelease: true + + - name: Build with dotnet + run: dotnet build --configuration Release + + - name: dotnet publish + run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: .net-app + path: ${{env.DOTNET_ROOT}}/myapp + + deploy: + runs-on: windows-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: .net-app + + - name: Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: 'wigg-course-infra-winapp01' + slot-name: 'staging' + publish-profile: ${{ secrets.AZURE_WEB_PUBLISH_PROFILE_STAGING }} + package: . \ No newline at end of file diff --git a/.github/workflows/fun_production.yml b/.github/workflows/wigg-fun-prod-course.yml similarity index 87% rename from .github/workflows/fun_production.yml rename to .github/workflows/wigg-fun-prod-course.yml index 7ef9d72..e8366e3 100644 --- a/.github/workflows/fun_production.yml +++ b/.github/workflows/wigg-fun-prod-course.yml @@ -12,10 +12,10 @@ on: env: AZURE_FUNCTIONAPP_PACKAGE_PATH: 'ProgramInformationV2.Function' # set this to the path to your web app project, defaults to the repository root DOTNET_VERSION: '8.0.x' # set this to the dotnet version to use - + jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: 'Checkout GitHub Action' @@ -25,20 +25,18 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ env.DOTNET_VERSION }} - include-prerelease: true - name: 'Resolve Project Dependencies Using Dotnet' shell: pwsh run: | pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}' - dotnet build --configuration Release --output ./output -r linux-x64 + dotnet build --configuration Release --output ./output popd - name: 'Run Azure Functions Action' uses: Azure/functions-action@v1 id: fa with: - app-name: 'itp-function-programrepository' + app-name: 'wigg-course-infra-funapp01' package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output' - publish-profile: ${{ secrets.AZURE_FUNCTION_PUBLISH_PROFILE_PRODUCTION }} - slot-name: 'deploy' + publish-profile: ${{ secrets.AZURE_FUNCTION_PUBLISH_PROFILE_PRODUCTION }} \ No newline at end of file diff --git a/.github/workflows/wigg-fun-staging-course.yml b/.github/workflows/wigg-fun-staging-course.yml new file mode 100644 index 0000000..860c382 --- /dev/null +++ b/.github/workflows/wigg-fun-staging-course.yml @@ -0,0 +1,43 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Staging WIGG Build and deploy ASP.Net Core app to Azure Function App + +on: + push: + branches: + - develop + workflow_dispatch: + +env: + AZURE_FUNCTIONAPP_PACKAGE_PATH: 'ProgramInformationV2.Function' # set this to the path to your web app project, defaults to the repository root + DOTNET_VERSION: '8.0.x' # set this to the dotnet version to use + +jobs: + build-and-deploy: + runs-on: windows-latest + + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: 'Resolve Project Dependencies Using Dotnet' + shell: pwsh + run: | + pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}' + dotnet build --configuration Release --output ./output + popd + + - name: 'Run Azure Functions Action' + uses: Azure/functions-action@v1 + id: fa + with: + app-name: 'wigg-course-infra-funapp01' + slot-name: 'staging' + package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output' + publish-profile: ${{ secrets.AZURE_FUNCTION_PUBLISH_PROFILE_STAGING }} \ No newline at end of file diff --git a/ProgramInformationV2.Data/CourseImport/ScheduleTranslator.cs b/ProgramInformationV2.Data/CourseImport/ScheduleTranslator.cs index 6448d61..deceb0f 100644 --- a/ProgramInformationV2.Data/CourseImport/ScheduleTranslator.cs +++ b/ProgramInformationV2.Data/CourseImport/ScheduleTranslator.cs @@ -12,6 +12,8 @@ public static Course Translate(ScheduleCourse scheduleCourse, string source, boo Rubric = scheduleCourse.Rubric, CourseTitle = scheduleCourse.Title, ExternalUrl = $"https://courses.illinois.edu/schedule/terms/{scheduleCourse.Rubric}/{scheduleCourse.CourseNumber}", + Url = $"https://courses.illinois.edu/schedule/terms/{scheduleCourse.Rubric}/{scheduleCourse.CourseNumber}", + UrlFull = $"https://courses.illinois.edu/schedule/terms/{scheduleCourse.Rubric}/{scheduleCourse.CourseNumber}", IsActive = true, CreditHours = scheduleCourse.CreditHours, Description = string.Empty, @@ -41,6 +43,9 @@ public static Course Translate(ScheduleCourse scheduleCourse, string source, boo Type = s.Type })]; } + course.FacultyNameList = course.Sections == null ? [] : course.Sections.SelectMany(s => s.FacultyNameList).Distinct().ToList(); + course.Faculty = string.Join(", ", course.FacultyNameList.Select(f => f.Name)); + course.Title = string.IsNullOrWhiteSpace(course.Rubric) ? course.CourseTitle : $"{course.Rubric} {course.CourseNumber}: {course.CourseTitle}"; return course; } diff --git a/ProgramInformationV2.Data/CourseImport/XmlImporter.cs b/ProgramInformationV2.Data/CourseImport/XmlImporter.cs index 01be563..75006c2 100644 --- a/ProgramInformationV2.Data/CourseImport/XmlImporter.cs +++ b/ProgramInformationV2.Data/CourseImport/XmlImporter.cs @@ -9,7 +9,7 @@ public static class XmlImporter { private static readonly int _yearsLookBack = 5; private static readonly int _yearsLookForward = 1; - public static IEnumerable GetAllCoursesBySemester(string rubric, string courseNumber) { + public static IEnumerable GetAllCoursesBySemester(string rubric, string courseNumber, bool shortCircuit = false) { var returnValue = new List(); var urls = new List>(); @@ -26,6 +26,9 @@ public static IEnumerable GetAllCoursesBySemester(string rubric, stri var courseUrl = new CourseUrl { CourseNumber = node.Attributes("id").First().Value, Rubric = rubric.ToUpperInvariant(), Semester = url.Item2, Url = FixUrl(node.Attributes("href").First().Value), Year = url.Item3 }; if (courseUrl != null && courseUrl.CourseNumber == courseNumber) { returnValue.Add(courseUrl); + if (shortCircuit) { + return returnValue; + } } } Console.WriteLine($"Found {url.Item1}"); diff --git a/ProgramInformationV2.Data/DataHelpers/FilterHelper.cs b/ProgramInformationV2.Data/DataHelpers/FilterHelper.cs index 57f312c..d572622 100644 --- a/ProgramInformationV2.Data/DataHelpers/FilterHelper.cs +++ b/ProgramInformationV2.Data/DataHelpers/FilterHelper.cs @@ -1,9 +1,9 @@ -using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; using ProgramInformationV2.Data.DataContext; using ProgramInformationV2.Data.DataModels; using ProgramInformationV2.Search.Helpers; using ProgramInformationV2.Search.JsonThinModels; -using System.Text.Json; namespace ProgramInformationV2.Data.DataHelpers { @@ -16,20 +16,20 @@ public async Task>> GetAllFilters(stri public async Task> GetFilterListForExport(string source) => [.. (await GetAllFilters(source)).Select(t => new TagList { Title = t.Key.ToString(), - List = [.. t.Select(l => l.Title)], + List = [.. t.OrderBy(l => l.Order).Select(l => l.Title)], })]; public async Task GetFilterJsonForExport(string source) => JsonSerializer.Serialize(await GetFilterListForExport(source)); public async Task ImportFiltersFromJson(string source, string json) { - List? importedTags = JsonSerializer.Deserialize>(json); - Source? sourceEntity = await _programRepository.ReadAsync(c => c.Sources.FirstOrDefault(s => s.Code == source)); + var importedTags = JsonSerializer.Deserialize>(json); + var sourceEntity = await _programRepository.ReadAsync(c => c.Sources.FirstOrDefault(s => s.Code == source)); if (sourceEntity == null || importedTags == null) { return "No tags loaded"; } - foreach (TagList tagList in importedTags) { + foreach (var tagList in importedTags) { var count = 1; - TagType tagType = Enum.TryParse(tagList.Title, out TagType parsedTagType) ? parsedTagType : TagType.None; + var tagType = Enum.TryParse(tagList.Title, out var parsedTagType) ? parsedTagType : TagType.None; if (tagType != TagType.None) { foreach (var tag in tagList.List) { var newTag = new TagSource { @@ -46,9 +46,9 @@ public async Task ImportFiltersFromJson(string source, string json) { } public async Task<(List TagSources, int SourceId)> GetFilters(string source, TagType tagType) { - List returnValue = await _programRepository.ReadAsync(c => c.TagSources.Include(c => c.Source).Where(ts => ts.Source != null && ts.Source.Code == source && ts.TagType == tagType).OrderBy(ts => ts.Order).ToList()); + var returnValue = await _programRepository.ReadAsync(c => c.TagSources.Include(c => c.Source).Where(ts => ts.Source != null && ts.Source.Code == source && ts.TagType == tagType).OrderBy(ts => ts.Order).ToList()); var sourceId = 0; - foreach (TagSource? item in returnValue) { + foreach (var item in returnValue) { item.OldTitle = item.Title; sourceId = item.SourceId; } @@ -60,7 +60,7 @@ public async Task ImportFiltersFromJson(string source, string json) { public async Task SaveFilters(IEnumerable tags, IEnumerable tagsForDeletion, string sourceName) { var i = 1; - foreach (TagSource tag in tags) { + foreach (var tag in tags) { tag.Order = i++; if (tag.Id == 0) { _ = await _programRepository.CreateAsync(tag); @@ -72,7 +72,7 @@ public async Task SaveFilters(IEnumerable tags, IEnumerable logger) { + private readonly ILogger _logger = logger; + + [Function("CampusCourse")] + [OpenApiOperation(operationId: "CampusCourse", tags: "Get Course Information", Description = "Get a course from the campus tools. This is using the same course object, but the source and many other fields not supported by the campus tools will be blank. Note that this is doing a lot of calls to see if the course exists in a particular semester, so it may take some time to run as the only way to check is to validate timeout.")] + [OpenApiParameter(name: "rubric", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "The rubric.")] + [OpenApiParameter(name: "coursenumber", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "The course number.")] + [OpenApiParameter(name: "singlesemester", In = ParameterLocation.Query, Required = true, Type = typeof(bool), Description = "If set to true, then only query latest semester -- used if you just need the description. Defaults to false.")] + [OpenApiParameter(name: "courseonly", In = ParameterLocation.Query, Required = true, Type = typeof(bool), Description = "If set to true, then do not include section information. Defaults to false.")] + [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(List), Description = "The list of courses")] + public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req) { + _logger.LogInformation("Called CampusCourse."); + var requestHelper = RequestHelperFactory.Create(); + requestHelper.Initialize(req); + var rubric = requestHelper.GetRequest(req, "rubric"); + var coursenumber = requestHelper.GetRequest(req, "coursenumber"); + var singlesemester = requestHelper.GetBoolean(req, "singlesemester"); + var courseonly = requestHelper.GetBoolean(req, "courseonly"); + requestHelper.Validate(); + + var itemGroups = XmlImporter.GetAllCoursesBySemester(rubric, coursenumber, singlesemester); + var scheduledCourse = XmlImporter.GetCourse(itemGroups ?? []); + var course = ScheduleTranslator.Translate(scheduledCourse, "", !courseonly); + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(course); + return response; + } +} \ No newline at end of file diff --git a/ProgramInformationV2.Function/GetCourses.cs b/ProgramInformationV2.Function/GetCourses.cs index eb66180..2525aa2 100644 --- a/ProgramInformationV2.Function/GetCourses.cs +++ b/ProgramInformationV2.Function/GetCourses.cs @@ -81,8 +81,8 @@ public async Task Id([HttpTrigger(AuthorizationLevel.Anonymous [OpenApiParameter(name: "formats", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "Either 'On-Campus', 'Online', 'Off-Campus', or 'Hybrid'. Can choose multiple by separating them with the characters '[-]'")] [OpenApiParameter(name: "rubric", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The course rubric.")] [OpenApiParameter(name: "terms", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "Either 'Fall', 'Spring', 'Summer', or 'Winter'. Can choose multiple by separating them with the characters '[-]'")] - [OpenApiParameter(name: "take", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "How many courses do you want? Defaults to 0.")] - [OpenApiParameter(name: "skip", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "A skip value to help with pagination. Defaults to 1000.")] + [OpenApiParameter(name: "take", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "How many courses do you want? Defaults to 1000.")] + [OpenApiParameter(name: "skip", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "A skip value to help with pagination. Defaults to 0.")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(IEnumerable), Description = "All programs that meet the search criteria. If you filter by credentials, it will filter the credential list for each program.")] public async Task Search([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req) { _logger.LogInformation("Called CourseSearch."); diff --git a/ProgramInformationV2.Function/GetPrograms.cs b/ProgramInformationV2.Function/GetPrograms.cs index 32c775c..2486ddd 100644 --- a/ProgramInformationV2.Function/GetPrograms.cs +++ b/ProgramInformationV2.Function/GetPrograms.cs @@ -1,4 +1,3 @@ -using System.Net; using Microsoft.AspNetCore.Http; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; @@ -7,6 +6,7 @@ using Microsoft.OpenApi.Models; using ProgramInformationV2.Function.Helper; using ProgramInformationV2.Search.Getters; +using System.Net; namespace ProgramInformationV2.Function { @@ -75,6 +75,8 @@ public async Task ProgramByCredentialId([HttpTrigger(Authoriza [OpenApiParameter(name: "q", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "A full text search string -- it will search the title and description for the search querystring.")] [OpenApiParameter(name: "formats", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "Either 'On-Campus', 'Online', 'Off-Campus', or 'Hybrid'. Can choose multiple by separating them with the characters '[-]'")] [OpenApiParameter(name: "credentials", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The credential type (BS, MS, EdM, PhD, Certificate, etc.) Can choose multiple by separating them with the characters '[-]'")] + [OpenApiParameter(name: "take", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "How many programs do you want? Defaults to 1000.")] + [OpenApiParameter(name: "skip", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "A skip value to help with pagination. Defaults to 0.")] [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(IEnumerable), Description = "All programs that meet the search criteria. If you filter by credentials, it will filter the credential list for each program.")] public async Task Search([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req) { _logger.LogInformation("Called ProgramSearch to check deployment."); @@ -90,9 +92,11 @@ public async Task Search([HttpTrigger(AuthorizationLevel.Anony var departments = requestHelper.GetArray(req, "departments"); var formats = requestHelper.GetArray(req, "formats"); var credentials = requestHelper.GetArray(req, "credentials"); + var take = requestHelper.GetInteger(req, "take", 1000); + var skip = requestHelper.GetInteger(req, "skip"); requestHelper.Validate(); var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(await _programGetter.GetPrograms(source, query, tags, tags2, tags3, skills, departments, formats, credentials)); + await response.WriteAsJsonAsync(await _programGetter.GetPrograms(source, query, tags, tags2, tags3, skills, departments, formats, credentials, take, skip)); return response; } catch (Exception ex) { _logger.LogInformation(ex.ToString()); diff --git a/ProgramInformationV2.Function/Program.cs b/ProgramInformationV2.Function/Program.cs index a1993eb..2853053 100644 --- a/ProgramInformationV2.Function/Program.cs +++ b/ProgramInformationV2.Function/Program.cs @@ -24,10 +24,10 @@ services.ConfigureFunctionsApplicationInsights(); _ = services.AddApplicationInsightsTelemetryWorkerService(); _ = services.ConfigureFunctionsApplicationInsights(); - _ = services.AddDbContextFactory(options => options.UseSqlServer(hostContext.Configuration["AppConnection"]).EnableSensitiveDataLogging(true)); + _ = services.AddDbContextFactory(options => options.UseSqlServer(hostContext.Configuration["Values:AppConnection"]).EnableSensitiveDataLogging(true)); _ = services.AddScoped(); - _ = services.AddSingleton(c => OpenSearchFactory.CreateLowLevelClient(hostContext.Configuration["SearchUrl"], hostContext.Configuration["AccessKey"], hostContext.Configuration["SecretKey"], hostContext.Configuration["Debug"] == "true")); - _ = services.AddSingleton(c => OpenSearchFactory.CreateClient(hostContext.Configuration["SearchUrl"], hostContext.Configuration["AccessKey"], hostContext.Configuration["SecretKey"], true)); + _ = services.AddSingleton(c => OpenSearchFactory.CreateLowLevelClient(hostContext.Configuration["Values:SearchUrl"], hostContext.Configuration["Values:AccessKey"], hostContext.Configuration["Values:SecretKey"], hostContext.Configuration["Values:SearchDebug"] == "true")); + _ = services.AddSingleton(c => OpenSearchFactory.CreateClient(hostContext.Configuration["Values:SearchUrl"], hostContext.Configuration["Values:AccessKey"], hostContext.Configuration["Values:SecretKey"], hostContext.Configuration["Values:SearchDebug"] == "true")); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); @@ -42,4 +42,4 @@ }) .Build(); -host.Run(); \ No newline at end of file +host.Run(); diff --git a/ProgramInformationV2.Search/Getters/CourseGetter.cs b/ProgramInformationV2.Search/Getters/CourseGetter.cs index e43b354..c28d87f 100644 --- a/ProgramInformationV2.Search/Getters/CourseGetter.cs +++ b/ProgramInformationV2.Search/Getters/CourseGetter.cs @@ -71,16 +71,15 @@ public async Task> GetCourses(string source, string search, f => isUpcoming ? f.Term(m => m.Field(fld => fld.IsUpcoming).Value(true)) : f.MatchAll(), f => isCurrent ? f.Term(m => m.Field(fld => fld.IsCurrent).Value(true)) : f.MatchAll()) .Must(m => !string.IsNullOrWhiteSpace(search) ? m.Match(m => m.Field(fld => fld.Title).Query(search)) : m.MatchAll()))) - .Sort(srt => srt.Ascending(f => f.TitleSortKeyword)) + .Sort(srt => string.IsNullOrWhiteSpace(search) ? srt.Ascending(f => f.TitleSortKeyword) : srt.Descending(f => SortSpecialField.Score)) .Suggest(a => a.Phrase("didyoumean", p => p.Text(search).Field(fld => fld.Title)))); LogDebug(response); - List documents = response.IsValid ? [.. response.Documents] : []; return new SearchObject() { Error = !response.IsValid ? response.ServerError.Error.ToString() : "", DidYouMean = response.Suggest["didyoumean"].FirstOrDefault()?.Options?.FirstOrDefault()?.Text ?? "", - Total = (int) response.Total, - Items = documents + Total = (int)response.Total, + Items = response.IsValid ? [.. response.Documents] : [] }; } diff --git a/ProgramInformationV2.Search/Getters/CredentialGetter.cs b/ProgramInformationV2.Search/Getters/CredentialGetter.cs index d0f54e2..8d566ff 100644 --- a/ProgramInformationV2.Search/Getters/CredentialGetter.cs +++ b/ProgramInformationV2.Search/Getters/CredentialGetter.cs @@ -34,7 +34,7 @@ public async Task> GetAllCredentialsBySource(string source, st public async Task GetCredential(string credentialId) => (await _programGetter.GetProgramByCredential(credentialId)).Credentials?.SingleOrDefault(c => c.Id == credentialId) ?? new(); public async Task> GetCredentials(string source, string search, IEnumerable tags, IEnumerable tags2, IEnumerable tags3, IEnumerable skills, IEnumerable departments, IEnumerable formats, IEnumerable credentials) { - var response = await _programGetter.GetPrograms(source, search, tags, tags2, tags3, skills, departments, formats, credentials); + var response = await _programGetter.GetPrograms(source, search, tags, tags2, tags3, skills, departments, formats, credentials, 1000, 0); var credentialList = response.Items.SelectMany(p => p.Credentials).Where(c => c.IsActive).OrderBy(c => c.Title).ToList(); if (tags.Any()) { credentialList = credentialList.Where(c => c.TagList.Any(t => tags.Contains(t))).ToList(); diff --git a/ProgramInformationV2.Search/Getters/ProgramGetter.cs b/ProgramInformationV2.Search/Getters/ProgramGetter.cs index 9fce747..16f72b9 100644 --- a/ProgramInformationV2.Search/Getters/ProgramGetter.cs +++ b/ProgramInformationV2.Search/Getters/ProgramGetter.cs @@ -53,9 +53,10 @@ public async Task GetProgramByCredential(string credentialId) { return response.IsValid ? response.Documents.FirstOrDefault() ?? new() : new(); } - public async Task> GetPrograms(string source, string search, IEnumerable tags, IEnumerable tags2, IEnumerable tags3, IEnumerable skills, IEnumerable departments, IEnumerable formats, IEnumerable credentials) { + public async Task> GetPrograms(string source, string search, IEnumerable tags, IEnumerable tags2, IEnumerable tags3, IEnumerable skills, IEnumerable departments, IEnumerable formats, IEnumerable credentials, int take, int skip) { var response = await _openSearchClient.SearchAsync(s => s.Index(UrlTypes.Programs.ConvertToUrlString()) - .Size(1000) + .Skip(skip) + .Size(take) .Query(q => q .Bool(b => b .Filter(f => f.Term(m => m.Field(fld => fld.Source).Value(source)), @@ -74,16 +75,16 @@ public async Task> GetPrograms(string source, string searc f => formats.Any() ? f.Terms(m => m.Field(fld => fld.Formats).Terms(formats)) : f.MatchAll(), f => departments.Any() ? f.Terms(m => m.Field(fld => fld.DepartmentList).Terms(departments)) : f.MatchAll(), f => skills.Any() ? f.Terms(m => m.Field(fld => fld.SkillList).Terms(skills)) : f.MatchAll()) - .Must(m => !string.IsNullOrWhiteSpace(search) ? m.Match(m => m.Field(fld => fld.Title).Query(search)) : m.MatchAll()))) + .Must(m => !string.IsNullOrWhiteSpace(search) ? m.MultiMatch(m => m.Fields(fld => fld.Field("title^10").Field("summarytext^5").Field("description^2").Field("whoshouldapply")).Query(search)) : m.MatchAll()))) + .Sort(srt => string.IsNullOrWhiteSpace(search) ? srt.Ascending(f => f.TitleSortKeyword) : srt.Descending(f => SortSpecialField.Score)) .Suggest(a => a.Phrase("didyoumean", p => p.Text(search).Field(fld => fld.Title)))); LogDebug(response); - List documents = response.IsValid ? (string.IsNullOrEmpty(search) ? [.. response.Documents.OrderBy(p => p.Title)] : [.. response.Documents]) : []; return new SearchObject() { Error = !response.IsValid ? response.ServerError.Error.ToString() : "", DidYouMean = response.Suggest["didyoumean"].FirstOrDefault()?.Options?.FirstOrDefault()?.Text ?? "", - Total = (int) response.Total, - Items = documents + Total = (int)response.Total, + Items = response.IsValid ? [.. response.Documents] : [] }; } } diff --git a/ProgramInformationV2.sln b/ProgramInformationV2.sln index cda617a..cf672ff 100644 --- a/ProgramInformationV2.sln +++ b/ProgramInformationV2.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34728.123 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProgramInformationV2", "ProgramInformationV2\ProgramInformationV2.csproj", "{B547452F-BE87-4536-80C0-230EA18AD030}" EndProject @@ -13,11 +13,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProgramInformationV2.Functi EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{64244983-1B95-4158-AAD6-313765A46B73}" ProjectSection(SolutionItems) = preProject - .github\workflows\app_production.yml = .github\workflows\app_production.yml - .github\workflows\app_staging.yml = .github\workflows\app_staging.yml - .github\workflows\fun_production.yml = .github\workflows\fun_production.yml - .github\workflows\fun_staging.yml = .github\workflows\fun_staging.yml README.md = README.md + .github\workflows\wigg-app-prod-course.yml = .github\workflows\wigg-app-prod-course.yml + .github\workflows\wigg-app-staging-course.yml = .github\workflows\wigg-app-staging-course.yml + .github\workflows\wigg-fun-prod-course.yml = .github\workflows\wigg-fun-prod-course.yml + .github\workflows\wigg-fun-staging-course.yml = .github\workflows\wigg-fun-staging-course.yml EndProjectSection EndProject Global diff --git a/ProgramInformationV2/Program.cs b/ProgramInformationV2/Program.cs index 9d4e238..e0f33c9 100644 --- a/ProgramInformationV2/Program.cs +++ b/ProgramInformationV2/Program.cs @@ -51,8 +51,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddSingleton(b => OpenSearchFactory.CreateClient(builder.Configuration["SearchUrl"], builder.Configuration["SearchAccessKey"], builder.Configuration["SearchSecretAccessKey"], bool.Parse(builder.Configuration["SearchDebug"] ?? "false"))); -builder.Services.AddSingleton(b => OpenSearchFactory.CreateLowLevelClient(builder.Configuration["SearchUrl"], builder.Configuration["SearchAccessKey"], builder.Configuration["SearchSecretAccessKey"], bool.Parse(builder.Configuration["SearchDebug"] ?? "false"))); +builder.Services.AddSingleton(b => OpenSearchFactory.CreateClient(builder.Configuration["SearchUrl"], builder.Configuration["AccessKey"], builder.Configuration["SecretKey"], bool.Parse(builder.Configuration["SearchDebug"] ?? "false"))); +builder.Services.AddSingleton(b => OpenSearchFactory.CreateLowLevelClient(builder.Configuration["SearchUrl"], builder.Configuration["AccessKey"], builder.Configuration["SecretKey"], bool.Parse(builder.Configuration["SearchDebug"] ?? "false"))); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped();