Example #1
0
        private async Task ApplyLine <T>(DistrictRepo repo, DataSyncLine line) where T : CsvBaseObject
        {
            switch (line.LoadStatus)
            {
            case LoadStatus.None:
                Logger.Here().LogWarning($"None should not be flagged for Sync: {line.RawData}");
                return;
            }

            ApiPostBase data;
            var         apiManager = new ApiManager(repo.District.LmsApiBaseUrl)
            {
                ApiAuthenticator = ApiAuthenticatorFactory.GetApiAuthenticator(repo.District.LmsApiAuthenticatorType,
                                                                               repo.District.LmsApiAuthenticationJsonData)
            };

            if (line.Table == nameof(CsvEnrollment))
            {
                var enrollment = new ApiEnrollmentPost(line.RawData);

                CsvEnrollment csvEnrollment = JsonConvert.DeserializeObject <CsvEnrollment>(line.RawData);
                DataSyncLine  cls           = repo.Lines <CsvClass>().SingleOrDefault(l => l.SourcedId == csvEnrollment.classSourcedId);
                DataSyncLine  usr           = repo.Lines <CsvUser>().SingleOrDefault(l => l.SourcedId == csvEnrollment.userSourcedId);

                var map = new EnrollmentMap
                {
                    classTargetId = cls?.TargetId,
                    userTargetId  = usr?.TargetId,
                };

                // this provides a mapping of LMS TargetIds (rather than sourcedId's)
                enrollment.EnrollmentMap = map;
                enrollment.ClassTargetId = cls?.TargetId;
                enrollment.UserTargetId  = usr?.TargetId;

                // cache map in the database (for display/troubleshooting only)
                line.EnrollmentMap = JsonConvert.SerializeObject(map);

                data = enrollment;
            }
            else if (line.Table == nameof(CsvClass))
            {
                var classCsv = JsonConvert.DeserializeObject <CsvClass>(line.RawData);

                // Get course & school of this class
                var course    = repo.Lines <CsvCourse>().SingleOrDefault(l => l.SourcedId == classCsv.courseSourcedId);
                var courseCsv = JsonConvert.DeserializeObject <CsvCourse>(course.RawData);

                // Get Term of this class
                // TODO: Handle multiple terms, termSourceIds can be a comma separated list of terms.
                var term = repo.Lines <CsvAcademicSession>().SingleOrDefault(s => s.SourcedId == classCsv.termSourcedIds);

                var org = repo.Lines <CsvOrg>().SingleOrDefault(o => o.SourcedId == classCsv.schoolSourcedId);

                var _class = new ApiClassPost(line.RawData)
                {
                    CourseTargetId = course.TargetId,
                    SchoolTargetId = org.TargetId,
                    TermTargetId   = string.IsNullOrWhiteSpace(term.TargetId) ? "2018" : term.TargetId,       //TODO: Add a default term setting in District Entity
                    Period         = classCsv.periods
                };

                data = _class;
            }
            else
            {
                data = new ApiPost <T>(line.RawData);
            }

            data.DistrictId   = repo.District.TargetId;
            data.DistrictName = repo.District.Name;
            data.LastSeen     = line.LastSeen;
            data.SourcedId    = line.SourcedId;
            data.TargetId     = line.TargetId;
            data.Status       = line.LoadStatus.ToString();

            var response = await apiManager.Post(GetEntityEndpoint(data.EntityType.ToLower(), repo), data);

            if (response.Success)
            {
                line.SyncStatus = SyncStatus.Applied;
                if (!string.IsNullOrEmpty(response.TargetId))
                {
                    line.TargetId = response.TargetId;
                }
                line.Error = null;
            }
            else
            {
                line.SyncStatus = SyncStatus.ApplyFailed;
                line.Error      = response.ErrorMessage;

                // The Lms can send false success if the entity already exist. In such a case we read the targetId
                if (!string.IsNullOrEmpty(response.TargetId))
                {
                    line.TargetId = response.TargetId;
                }
            }

            line.Touch();

            repo.PushLineHistory(line, isNewData: false);
        }
Example #2
0
		/// <summary>
		/// Analyze the records to determine which should be included in the feed
		/// based on dependencies.
		/// </summary>
		public async Task Analyze()
		{
			// load some small tables into memory for performance
			var cache = new DataLineCache();
			await cache.Load(Repo.Lines(), new[] { nameof(CsvOrg), nameof(CsvCourse), nameof(CsvClass) });

			var orgIds = new List<string>();
			// include Orgs that have been selected for sync
			foreach (var org in cache.GetMap<CsvOrg>().Values.Where(IsUnappliedChange))
			{
				orgIds.Add(org.SourcedId);
				IncludeReadyTouch(org);
			}
			await Repo.Committer.Invoke();

			// courses are manually marked for sync, so choose only those
			foreach (var course in cache.GetMap<CsvCourse>().Values.Where(l => l.IncludeInSync).Where(IsUnappliedChange))
				IncludeReadyTouch(course);
			await Repo.Committer.Invoke();

			// now walk the classes and include those which map to an included course and are part of the selected orgs.
			var classMap = cache.GetMap<CsvClass>();
			var courseIds = cache.GetMap<CsvCourse>()
				.Values
				.Where(l => l.IncludeInSync)
				.Select(l => l.SourcedId)
				.ToHashSet();

			foreach (var _class in classMap.Values.Where(IsUnappliedChangeWithoutIncludedInSync))
			{
				CsvClass csvClass = JsonConvert.DeserializeObject<CsvClass>(_class.RawData);
				if (courseIds.Contains(csvClass.courseSourcedId) && orgIds.Contains(csvClass.schoolSourcedId))
					IncludeReadyTouch(_class);
				await Repo.Committer.InvokeIfChunk();
			}
			await Repo.Committer.InvokeIfAny();

			// process enrollments in the database associated with the District based on the conditions below (in chunks of 200)
			await Repo.Lines<CsvEnrollment>().ForEachInChunksAsync(chunkSize: 200,
				action: async (enrollment) =>
				{
					CsvEnrollment csvEnrollment = JsonConvert.DeserializeObject<CsvEnrollment>(enrollment.RawData);

					// figure out if we need to process this enrollment
					if (!classMap.ContainsKey(csvEnrollment.classSourcedId) ||      // look up class associated with enrollment
						!classMap[csvEnrollment.classSourcedId].IncludeInSync ||    // only include enrollment if the class is included
						!IsUnappliedChangeWithoutIncludedInSync(enrollment))        // only include if unapplied change in enrollment
						return;

					var user = await Repo.Lines<CsvUser>().SingleOrDefaultAsync(l => l.SourcedId == csvEnrollment.userSourcedId);
					if (user == null) // should never happen
					{
						enrollment.Error = $"Missing user for {csvEnrollment.userSourcedId}";
						Logger.Here().LogError($"Missing user for enrollment for line {enrollment.DataSyncLineId}");
						return;
					}

					// mark enrollment for sync
					IncludeReadyTouch(enrollment);

					// mark user for sync
					//DataSyncLine user = userMap[csvEnrollment.userSourcedId];
					if (IsUnappliedChangeWithoutIncludedInSync(user))
						IncludeReadyTouch(user);
				},
				onChunkComplete: async () => await Repo.Committer.Invoke());

			// now process any user changes we may have missed
			await Repo.Lines<CsvUser>().Where(u => u.IncludeInSync
				&& u.LoadStatus != LoadStatus.NoChange
				&& u.SyncStatus != SyncStatus.ReadyToApply)
				.ForEachInChunksForShrinkingList(chunkSize: 200,
					#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
					action: async (user) => IncludeReadyTouch(user),
					#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
					onChunkComplete: async () => await Repo.Committer.Invoke());


			await Repo.Committer.Invoke();
		}