public async Task VenueNameSpecifiedWithProviderVenueRef_ReturnsValidationError()
        {
            // Arrange
            var provider = await TestData.CreateProvider();

            var user = await TestData.CreateUser();

            var venue = await TestData.CreateVenue(
                providerId : provider.ProviderId,
                createdBy : user,
                venueName : "My Venue",
                providerVenueRef : "VENUE1");

            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DeliveryMode     = "classroom based",
                ProviderVenueRef = venue.ProviderVenueRef,
                VenueName        = venue.VenueName
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_VENUE_NAME_NOT_ALLOWED_WITH_REF");
        }
        public async Task FindVenue_RefProvidedAndMatches_ReturnsVenueId()
        {
            // Arrange
            var blobServiceClient = new Mock <BlobServiceClient>();

            blobServiceClient.Setup(mock => mock.GetBlobContainerClient(It.IsAny <string>())).Returns(Mock.Of <BlobContainerClient>());

            var fileUploadProcessor = new FileUploadProcessor(
                SqlQueryDispatcherFactory,
                blobServiceClient.Object,
                Clock,
                new RegionCache(SqlQueryDispatcherFactory),
                new ExecuteImmediatelyBackgroundWorkScheduler(Fixture.ServiceScopeFactory));

            var provider = await TestData.CreateProvider();

            var user = await TestData.CreateUser(providerId : provider.ProviderId);

            var venue = await TestData.CreateVenue(providerId : provider.ProviderId, createdBy : user, venueName : "My Venue", providerVenueRef : "VENUE1");

            var row = new CsvCourseRow()
            {
                DeliveryMode     = "classroom based",
                ProviderVenueRef = "venue1"
            };

            var rowInfo = new CourseDataUploadRowInfo(row, rowNumber: 2, courseId: Guid.NewGuid());

            // Act
            var result = fileUploadProcessor.FindVenue(rowInfo, new[] { venue });

            // Assert
            Assert.Equal(venue.VenueId, result?.VenueId);
        }
        public async Task <Response> Handle(Query request, CancellationToken cancellationToken)
        {
            var providerContext = _providerContextProvider.GetProviderContext();

            var courses = await _sqlQueryDispatcher.ExecuteQuery(new GetCoursesForProvider()
            {
                ProviderId = providerContext.ProviderInfo.ProviderId
            });

            var allRegions = await _regionCache.GetAllRegions();

            var rows = courses.OrderBy(x => x.LearnAimRef)
                       .ThenBy(x => x.CourseId)
                       .SelectMany(course => CsvCourseRow.FromModel(course, allRegions))
                       .ToList();

            var fileName = FileNameHelper.SanitizeFileName(
                $"{providerContext.ProviderInfo.ProviderName}_courses_{_clock.UtcNow:yyyyMMddHHmm}.csv");

            return(new Response()
            {
                FileName = fileName,
                Rows = rows
            });
        }
        public static ParsedCsvCourseRow FromCsvCourseRow(CsvCourseRow row, IEnumerable <Region> allRegions)
        {
            var parsedRow = row.Adapt(new ParsedCsvCourseRow());

            parsedRow.ResolvedDeliveryMode      = ResolveDeliveryMode(parsedRow.DeliveryMode);
            parsedRow.ResolvedStartDate         = ResolveStartDate(parsedRow.StartDate);
            parsedRow.ResolvedFlexibleStartDate = ResolveFlexibleStartDate(parsedRow.FlexibleStartDate);
            parsedRow.ResolvedNationalDelivery  = ResolveNationalDelivery(parsedRow.NationalDelivery, parsedRow.SubRegions);
            parsedRow.ResolvedCost              = ResolveCost(parsedRow.Cost);
            parsedRow.ResolvedDuration          = ResolveDuration(parsedRow.Duration);
            parsedRow.ResolvedDurationUnit      = ResolveDurationUnit(parsedRow.DurationUnit);
            parsedRow.ResolvedStudyMode         = ResolveStudyMode(parsedRow.StudyMode);
            parsedRow.ResolvedAttendancePattern = ResolveAttendancePattern(parsedRow.AttendancePattern);
            parsedRow.ResolvedSubRegions        = ResolveSubRegions(parsedRow.SubRegions, allRegions);
            return(parsedRow);
        }
        public static CourseDataUploadRowInfoCollection ToDataUploadRowCollection(this IEnumerable <CsvCourseRow> rows)
        {
            var rowInfos = new List <CourseDataUploadRowInfo>();

            foreach (var group in CsvCourseRow.GroupRows(rows))
            {
                var courseId = Guid.NewGuid();

                foreach (var row in group)
                {
                    rowInfos.Add(new CourseDataUploadRowInfo(row, rowNumber: rowInfos.Count + 2, courseId));
                }
            }

            return(new CourseDataUploadRowInfoCollection(rowInfos));
        }
        public async Task DurationUnitMissing_ReturnsValidationError()
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DurationUnit = string.Empty,
                Duration     = "10"
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_DURATION_UNIT_REQUIRED");
        }
        public async Task CostEmptyWithEmptyCostDescription_ReturnsSingleCostValidationError()
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                Cost            = string.Empty,
                CostDescription = string.Empty
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_COST_REQUIRED");
        }
        public async Task SubRegionsInvalidWithNonWorkBasedDeliveryMode_DoesNotReturnInvalidValidationError(string deliveryMode)
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DeliveryMode = deliveryMode,
                SubRegions   = "xxx"
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.DoesNotContain(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_SUBREGIONS_INVALID");
        }
        public async Task AttendancePatternNotEmptyWithNonClassroomBasedDeliveryMode_ReturnsValidationError(string deliveryMode)
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                AttendancePattern = "daytime",
                DeliveryMode      = deliveryMode
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_ATTENDANCE_PATTERN_NOT_ALLOWED");
        }
        public async Task StudyModeNotEmptyWithNonClassroomBasedDeliveryMode_ReturnsValidationError(string deliveryMode)
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DeliveryMode = deliveryMode,
                StudyMode    = "full time"
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_STUDY_MODE_NOT_ALLOWED");
        }
        public async Task StartDateNotEmptyWithFlexibleTrue_ReturnsValidationError()
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                StartDate         = "01/04/2021",
                FlexibleStartDate = "yes"
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_START_DATE_NOT_ALLOWED");
        }
        public async Task NationalDeliveryNotEmptyWithNonWorkBasedDeliveryMode_ReturnsValidationError(string nationalDelivery)
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DeliveryMode     = "classroom based",
                NationalDelivery = nationalDelivery
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_NATIONAL_DELIVERY_NOT_ALLOWED");
        }
        public async Task SubRegionsEmptyWithNational_DoesNotReturnValidationError()
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DeliveryMode     = "work based",
                NationalDelivery = "yes",
                SubRegions       = ""
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.DoesNotContain(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_SUBREGIONS_INVALID");
        }
        public async Task ProviderVenueRefEmptyButVenueNameNotEmpty_DoesNotReturnValidationError()
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DeliveryMode     = "classroom based",
                ProviderVenueRef = string.Empty,
                VenueName        = "venue"
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.DoesNotContain(
                validationResult.Errors,
                error => error.PropertyName == nameof(CsvCourseRow.ProviderVenueRef));
        }
        public async Task ProviderVenueRefAndVenueNameEmptyWithClassroomBasedDeliveryMode_ReturnsValidationError()
        {
            // Arrange
            var allRegions = await new RegionCache(SqlQueryDispatcherFactory).GetAllRegions();

            var row = new CsvCourseRow()
            {
                DeliveryMode     = "classroom based",
                ProviderVenueRef = string.Empty,
                VenueName        = string.Empty
            };

            var validator = new CourseUploadRowValidator(Clock, matchedVenueId: null);

            // Act
            var validationResult = validator.Validate(ParsedCsvCourseRow.FromCsvCourseRow(row, allRegions));

            // Assert
            Assert.Contains(
                validationResult.Errors,
                error => error.ErrorCode == "COURSERUN_VENUE_REQUIRED");
        }
Ejemplo n.º 16
0
        public async Task ProcessCourseFile(Guid courseUploadId, Stream stream)
        {
            using (var dispatcher = _sqlQueryDispatcherFactory.CreateDispatcher(System.Data.IsolationLevel.ReadCommitted))
            {
                var setProcessingResult = await dispatcher.ExecuteQuery(new SetCourseUploadProcessing()
                {
                    CourseUploadId      = courseUploadId,
                    ProcessingStartedOn = _clock.UtcNow
                });

                if (setProcessingResult != SetCourseUploadProcessingResult.Success)
                {
                    await DeleteBlob();

                    return;
                }

                await dispatcher.Commit();
            }

            // At this point `stream` should be a CSV that's already known to conform to `CsvCourseRow`'s schema.
            // We read all the rows upfront because validation needs to group rows into courses.
            // We also don't expect massive files here so reading everything into memory is ok.
            List <CsvCourseRow> rows;

            using (var streamReader = new StreamReader(stream))
                using (var csvReader = CreateCsvReader(streamReader))
                {
                    rows = await csvReader.GetRecordsAsync <CsvCourseRow>().ToListAsync();
                }

            var rowsCollection = CreateCourseDataUploadRowInfoCollection();

            using (var dispatcher = _sqlQueryDispatcherFactory.CreateDispatcher(System.Data.IsolationLevel.ReadCommitted))
            {
                // If CourseName is empty, use the LearnAimRefTitle from LARS
                var learnAimRefs       = rowsCollection.Select(r => r.Data.LearnAimRef).Distinct();
                var learningDeliveries = await dispatcher.ExecuteQuery(new GetLearningDeliveries()
                {
                    LearnAimRefs = learnAimRefs
                });

                foreach (var row in rowsCollection)
                {
                    if (string.IsNullOrWhiteSpace(row.Data.CourseName))
                    {
                        row.Data.CourseName = learningDeliveries[row.Data.LearnAimRef].LearnAimRefTitle;
                    }
                }

                var venueUpload = await dispatcher.ExecuteQuery(new GetCourseUpload()
                {
                    CourseUploadId = courseUploadId
                });

                var providerId = venueUpload.ProviderId;

                await AcquireExclusiveCourseUploadLockForProvider(providerId, dispatcher);

                await ValidateCourseUploadRows(dispatcher, courseUploadId, providerId, rowsCollection);

                await dispatcher.Commit();
            }

            await DeleteBlob();

            Task DeleteBlob()
            {
                var blobName = $"{Constants.CoursesFolder}/{courseUploadId}.csv";

                return(_blobContainerClient.DeleteBlobIfExistsAsync(blobName));
            }

            CourseDataUploadRowInfoCollection CreateCourseDataUploadRowInfoCollection()
            {
                // N.B. It's important we maintain ordering here; RowNumber needs to match the input

                var grouped        = CsvCourseRow.GroupRows(rows);
                var groupCourseIds = grouped.Select(g => (CourseId: Guid.NewGuid(), Rows: g)).ToArray();

                var rowInfos = new List <CourseDataUploadRowInfo>(rows.Count);

                foreach (var row in rows)
                {
                    var courseId = groupCourseIds.Single(g => g.Rows.Contains(row)).CourseId;
                    row.LearnAimRef = NormalizeLearnAimRef(row.LearnAimRef);

                    rowInfos.Add(new CourseDataUploadRowInfo(row, rowNumber: rowInfos.Count + 2, courseId));
                }

                return(new CourseDataUploadRowInfoCollection(rowInfos));
            }
        }