Example #1
0
        public async Task <OneOf <ProblemDetails, SearchViewModel> > Handle(Query request, CancellationToken cancellationToken)
        {
            var filters = new List <string>();

            // If either lat or lng is specified then both must be specified
            if (request.Latitude.HasValue != request.Longitude.HasValue)
            {
                return(new ProblemDetails()
                {
                    Detail = "Latitude & longitude must both be specified.",
                    Status = 400,
                    Title = "InvalidLatLng"
                });
            }

            if (request.SortBy == SearchSortBy.Distance &&
                (string.IsNullOrWhiteSpace(request.Postcode) && (!request.Latitude.HasValue || !request.Longitude.HasValue)))
            {
                return(new ProblemDetails()
                {
                    Detail = "Postcode is required to sort by Distance.",
                    Status = 400,
                    Title = "PostcodeRequired"
                });
            }

            Postcode postcode = null;

            if (!string.IsNullOrWhiteSpace(request.Postcode))
            {
                if (!Postcode.TryParse(request.Postcode, out postcode))
                {
                    return(new ProblemDetails()
                    {
                        Detail = "Postcode is not valid.",
                        Status = 400,
                        Title = "InvalidPostcode"
                    });
                }
            }

            var geoFilterRequired = request.Distance.GetValueOrDefault(0) > 0 &&
                                    (postcode != null || (request.Latitude.HasValue && request.Longitude.HasValue));

            // lat/lng required if Distance filter is specified *or* sorting by Distance
            var    getPostcodeCoords = (geoFilterRequired || request.SortBy == SearchSortBy.Distance) && !request.Latitude.HasValue;
            double?latitude          = request.Latitude;
            double?longitude         = request.Longitude;

            if (getPostcodeCoords)
            {
                var coords = await TryGetCoordinatesForPostcode(postcode);

                if (!coords.HasValue)
                {
                    return(new ProblemDetails()
                    {
                        Detail = "Specified postcode cannot be found.",
                        Status = 400,
                        Title = "PostcodeNotFound"
                    });
                }

                latitude  = coords.Value.lat;
                longitude = coords.Value.lng;
            }

            if (request.StartDateFrom.HasValue ||
                request.StartDateTo.HasValue ||
                request.HideOutOfDateCourses.HasValue ||
                request.HideFlexiCourses.HasValue)
            {
                var dateFilter = TryGetDateFilters(
                    request.StartDateFrom,
                    request.StartDateTo,
                    request.HideOutOfDateCourses,
                    request.HideFlexiCourses);

                if (!string.IsNullOrEmpty(dateFilter))
                {
                    filters.Add(dateFilter);
                }
            }

            if (request.AttendancePatterns?.Any() ?? false)
            {
                // TODO Validate AttendancePatterns? Consider using enum instead of int

                filters.Add($"({string.Join(" or ", request.AttendancePatterns.Select(ap => $"{nameof(FindACourseOffering.AttendancePattern)} eq {ap}"))} or {nameof(FindACourseOffering.DeliveryMode)} ne {(int)CourseDeliveryMode.ClassroomBased})");
            }

            if (request.QualificationLevels?.Any() ?? false)
            {
                // TODO Validate QualificationLevels?

                filters.Add($"search.in({nameof(FindACourseOffering.NotionalNVQLevelv2)}, '{string.Join("|", request.QualificationLevels.Select(EscapeFilterValue))}', '|')");
            }

            if (geoFilterRequired)
            {
                var distanceInKm = Convert.ToDecimal(GeoHelper.MilesToKilometers(request.Distance.Value));

                filters.Add(
                    $"(geo.distance({nameof(FindACourseOffering.Position)}, geography'POINT({longitude.Value} {latitude.Value})') le {distanceInKm}" +
                    $" or {nameof(FindACourseOffering.National)} eq true" +
                    $" or {nameof(FindACourseOffering.DeliveryMode)} eq 2)");
            }

            if (!string.IsNullOrWhiteSpace(request.Town))
            {
                filters.Add($"search.ismatch('{EscapeFilterValue(request.Town)}', '{nameof(FindACourseOffering.VenueTown)}')");
            }

            if (request.StudyModes?.Any() ?? false)
            {
                filters.Add($"({string.Join(" or ", request.StudyModes.Select(sm => $"{nameof(FindACourseOffering.StudyMode)} eq {sm}"))} or {nameof(FindACourseOffering.DeliveryMode)} ne {(int)CourseDeliveryMode.ClassroomBased})");
            }

            if (request.DeliveryModes?.Any() ?? false)
            {
                filters.Add($"({string.Join(" or ", request.DeliveryModes.Select(dm => $"{nameof(FindACourseOffering.DeliveryMode)} eq {dm}"))})");
            }

            if (!string.IsNullOrWhiteSpace(request.ProviderName))
            {
                filters.Add($"search.ismatchscoring('{EscapeFilterValue(request.ProviderName)}', '{nameof(FindACourseOffering.ProviderDisplayName)}', 'simple', 'any')");
            }

            if (!string.IsNullOrWhiteSpace(request.CampaignCode))
            {
                filters.Add($"{nameof(FindACourseOffering.CampaignCodes)}/any(c: c eq '{EscapeFilterValue(request.CampaignCode)}')");
            }

            var orderBy = request.SortBy == SearchSortBy.StartDateDescending ?
                          "StartDate desc" : request.SortBy == SearchSortBy.StartDateAscending ?
                          "StartDate asc" : request.SortBy == SearchSortBy.Distance ?
                          $"geo.distance({nameof(FindACourseOffering.Position)}, geography'POINT({longitude.Value} {latitude.Value})')" :
                          "search.score() desc";

            if (!TryResolvePagingParams(request.Limit, request.Start, out var size, out var skip, out var problem))
            {
                return(problem);
            }

            var searchText = TranslateCourseSearchSubjectText(request.SubjectKeyword);

            var query = new FindACourseOfferingSearchQuery()
            {
                Facets = new[]
                {
                    "NotionalNVQLevelv2,count:100",
                    "StudyMode",
                    "AttendancePattern",
                    "DeliveryMode",
                    "ProviderDisplayName,count:100",
                    "RegionName,count:100"
                },
                Filters    = filters,
                CourseName = searchText,
                Size       = size,
                Skip       = skip,
                OrderBy    = orderBy
            };

            var result = await _courseSearchClient.Search(query);

            return(new SearchViewModel()
            {
                Limit = size,
                Start = skip,
                Total = Convert.ToInt32(result.TotalCount.Value),
                Facets = result.Facets.ToDictionary(
                    f => _courseSearchFacetMapping.GetValueOrDefault(f.Key, f.Key),
                    f => f.Value.Select(v => new FacetCountResultViewModel()
                {
                    Value = v.Key.ToString(),
                    Count = v.Value.Value
                })),
                Results = result.Items.Select(i =>
                {
                    return new SearchResultViewModel()
                    {
                        Cost = !string.IsNullOrEmpty(i.Record.Cost) ?
                               Convert.ToInt32(decimal.Parse(i.Record.Cost)) :
                               (int?)null,
                        CostDescription = HtmlEncode(i.Record.CostDescription),
                        CourseDescription = HtmlEncode(NormalizeCourseDataEncodedString(i.Record.CourseDescription)),
                        CourseName = NormalizeCourseRunDataEncodedString(i.Record.CourseName),
                        CourseId = i.Record.CourseId,
                        CourseRunId = i.Record.CourseRunId,
                        CourseText = HtmlEncode(NormalizeCourseDataEncodedString(i.Record.CourseDescription)),
                        DeliveryMode = ((int)i.Record.DeliveryMode).ToString(),
                        DeliveryModeDescription = (i.Record.DeliveryMode.GetValueOrDefault(0)).ToDescription(),
                        Distance = GetDistanceFromLatLngForResult(i),
                        DurationUnit = i.Record.DurationUnit ?? 0,
                        DurationValue = i.Record.DurationValue,
                        FlexibleStartDate = i.Record.FlexibleStartDate,
                        LearnAimRef = i.Record.LearnAimRef,
                        National = i.Record.National,
                        QualificationLevel = i.Record.NotionalNVQLevelv2,
                        OfferingType = i.Record.OfferingType,
                        ProviderName = i.Record.ProviderDisplayName,
                        QualificationCourseTitle = HtmlEncode(i.Record.QualificationCourseTitle),
                        Region = i.Record.RegionName,
                        SearchScore = i.Score.Value,
                        StartDate = !i.Record.FlexibleStartDate.GetValueOrDefault() ? i.Record.StartDate : null,
                        TLevelId = i.Record.TLevelId,
                        TLevelLocationId = i.Record.TLevelLocationId,
                        Ukprn = i.Record.ProviderUkprn.ToString(),
                        UpdatedOn = i.Record.UpdatedOn,
                        VenueAddress = HtmlEncode(i.Record.VenueAddress),
                        VenueAttendancePattern = ((int?)i.Record.AttendancePattern)?.ToString(),
                        VenueAttendancePatternDescription = i.Record.DeliveryMode == CourseDeliveryMode.ClassroomBased ?
                                                            i.Record.AttendancePattern?.ToDescription() :
                                                            null,
                        VenueLocation = i.Record.Position != null ?
                                        new CoordinatesViewModel()
                        {
                            Latitude = i.Record.Position.Latitude,
                            Longitude = i.Record.Position.Longitude
                        } :
                        null,
                        VenueName = HtmlEncode(i.Record.VenueName),
                        VenueStudyMode = ((int?)i.Record.StudyMode)?.ToString(),
                        VenueStudyModeDescription = i.Record.DeliveryMode == CourseDeliveryMode.ClassroomBased ?
                                                    i.Record.StudyMode?.ToDescription() :
                                                    null,
                        VenueTown = HtmlEncode(i.Record.VenueTown)
                    };