// Note: make sure to comment out the LIMIT lines below to get a full report public async Task Run() { IAsyncEnumerable <User> sample = _api.StreamUsers() .WhereAwait(u => u.IsTeacher()) .Where(u => !u.Name.ToLowerInvariant().Contains("test")) .Where(u => !u.SisUserId?.StartsWith("pG") ?? false) .Take(15); // LIMITS number of teachers reported, comment this for full report uint teachersInReport = 0; var teachersObj = new JObject(); var document = new JObject { ["dateGenerated"] = DateTime.Now.ToIso8601Date(), ["teachers"] = teachersObj }; await foreach (var teacher in sample) { var advisoryCourse = await _api.StreamUserEnrollments(teacher.Id, TeacherEnrollment.Yield()) .Select(e => _api.GetCourse(e.CourseId).Result) .Where(c => c.WorkflowState == "available") .Where(c => c.Name.ToLowerInvariant().Contains("advisory")) .FirstOrDefaultAsync(); if (advisoryCourse == default) { continue; } var teacherObj = new JObject { ["teacherSisId"] = teacher.SisUserId, ["teacherName"] = teacher.Name, ["advisoryCourseId"] = advisoryCourse.Id }; try { _api.MasqueradeAs(teacher.Id); var conversations = await _api.StreamConversations(filter : new QualifiedId(advisoryCourse.Id, Course).Yield()) .Where(c => c.ContextName == advisoryCourse.Name) .Distinct(c => c.Audience) .CountAsync(); teacherObj["distinctAdvisoryConversations"] = conversations; } catch (Exception e) { _testOutputHelper.WriteLine(e.Message); continue; } finally { _api.StopMasquerading(); } teachersObj[teacher.Id.ToString()] = teacherObj; ++teachersInReport; } document["teachersInReport"] = teachersInReport; _testOutputHelper.WriteLine(document.ToString(Formatting.Indented)); }
public static async Task Main(string[] args) { var home = new AppHome("uva_super_report"); Console.WriteLine($"Using config path: {home.ConfigPath}"); if (!home.ConfigPresent()) { Console.WriteLine("Need to generate a config file."); home.CreateConfig(new DocumentSyntax { Tables = { new TableSyntax("tokens") { Items = { { "token", "PUT_TOKEN_HERE" } } }, new TableSyntax("limits") { Items = { { "sample_skip", 0 }, { "sample_take", 0 }, { "courses_per_teacher", 0 }, { "assignments_per_course", 0 }, { "submissions_per_assignment", 0 }, { "teachers_only", false } } } } }); Console.WriteLine("Created a new config file. Please go put in your token."); return; } Console.WriteLine("Found config file."); var config = home.GetConfig(); Debug.Assert(config != null, nameof(config) + " != null"); var token = config.GetTable("tokens").Get <string>("token"); var limits = config.GetTable("limits"); var sampleTake = (int)limits.GetOr <long>("sample_take"); var sampleSkip = (int)limits.GetOr <long>("sample_skip"); var coursesPerTeacher = (int)limits.GetOr <long>("courses_per_teacher"); var assignmentsPerCourse = (int)limits.GetOr <long>("assignments_per_course"); var submissionsPerAssignment = (int)limits.GetOr <long>("submissions_per_assignment"); var teachersOnly = limits.GetOr <bool>("teachers_only"); Console.WriteLine($"SKIPPING {sampleSkip} users."); Console.WriteLine($"TAKING {(sampleTake == default ? "ALL" : sampleTake.ToString())} users."); Console.WriteLine($"TAKING {(coursesPerTeacher == default ? "ALL" : coursesPerTeacher.ToString())} courses per teacher."); Console.WriteLine($"TAKING {(assignmentsPerCourse == default ? "ALL" : assignmentsPerCourse.ToString())} assignments per course."); Console.WriteLine($"TAKING {(submissionsPerAssignment == default ? "ALL" : submissionsPerAssignment.ToString())} submissions per assignment."); // -------------------------------------------------------------------- var api = new Api(token, "https://uview.instructure.com/api/v1/"); var studentsObj = new JObject(); var teachersObj = new JObject(); var coursesObj = new JObject(); var submissionsObj = new JObject(); var assignmentsOverallObj = new JObject(); var assignmentsIndividualObj = new JObject(); var individualCoursePerformanceObj = new JObject(); var overallCoursePerformanceObj = new JObject(); var teacherPerformanceObj = new JObject(); var started = DateTime.Now; var document = new JObject { ["teachers"] = teachersObj, ["students"] = studentsObj, ["courses"] = coursesObj, ["submissions"] = submissionsObj, ["assignmentsOverall"] = assignmentsOverallObj, ["assignmentsIndividual"] = assignmentsIndividualObj, ["individualCoursePerformance"] = individualCoursePerformanceObj, ["overallCoursePerformance"] = overallCoursePerformanceObj, ["teacherPerformance"] = teacherPerformanceObj, ["limits"] = new JObject { ["sampleTake"] = sampleTake, ["sampleSkip"] = sampleSkip, ["coursesPerTeacher"] = coursesPerTeacher, ["assignmentsPerCourse"] = assignmentsPerCourse, ["submissionsPerAssignment"] = submissionsPerAssignment, ["teachersOnly"] = teachersOnly }, ["dateStarted"] = started.ToIso8601Date() }; var sample = api.StreamUsers() .Where(u => !u.Name.ToLowerInvariant().Contains("test")) .Where(u => !u.SisUserId?.StartsWith("pG") ?? false); if (teachersOnly) { Console.WriteLine("TAKING TEACHERS ONLY."); sample = sample.WhereAwait(async u => await u.IsTeacher()); } sample = sample.Skip(sampleSkip); if (sampleTake != default) { sample = sample.Take(sampleTake); } await foreach (var user in sample) { try { if (await user.IsTeacher()) { #region CurrentUserIsTeacher if (!teachersObj.ContainsKey(user.Id.ToString())) { teachersObj[user.Id.ToString()] = new JObject { ["sisId"] = user.SisUserId, ["fullName"] = user.Name }; } var enrollmentsStream = api.StreamUserEnrollments(user.Id, TeacherEnrollment.Yield()) .SelectAwait(async e => (e, await api.GetCourse(e.CourseId))) .Where(ec => !ec.Item2.Name.ToLowerInvariant().Contains("advisory")); if (coursesPerTeacher != default) { enrollmentsStream = enrollmentsStream.Take(coursesPerTeacher); } await foreach (var(enrollment, course) in enrollmentsStream) { if (!coursesObj.ContainsKey(course.Id.ToString())) { coursesObj[course.Id.ToString()] = new JObject { ["sisId"] = course.SisCourseId, ["name"] = course.Name }; } if (!overallCoursePerformanceObj.ContainsKey(course.Id.ToString())) { var studentEnrollments = api.StreamCourseEnrollments(course.Id, StudentEnrollment.Yield()); var courseScores = new List <decimal>(); await foreach (var studentEnrollment in studentEnrollments) { var grades = studentEnrollment.Grades; if (!individualCoursePerformanceObj.ContainsKey(enrollment.Id.ToString())) { individualCoursePerformanceObj[enrollment.Id.ToString()] = new JObject { ["studentId"] = user.Id, ["courseId"] = enrollment.CourseId, ["currentLetterGrade"] = grades.CurrentGrade, ["finalLetterGrade"] = grades.FinalGrade, ["currentScore"] = grades.CurrentScore, ["finalScore"] = grades.FinalScore, ["activitySecondsSpent"] = studentEnrollment.TotalActivityTime, ["lastAttendedAt"] = studentEnrollment.LastAttendedAt?.ToIso8601Date(), ["lastActivityAt"] = studentEnrollment.LastActivityAt?.ToIso8601Date() }; } var currentScore = grades.CurrentScore; courseScores.Add(string.IsNullOrEmpty(currentScore) ? 0 : Convert.ToDecimal(grades.CurrentScore)); } if (!courseScores.Any()) { continue; } var courseScoreStats = new Stats(courseScores); var pass = courseScores.Count(s => s > 66.5m); overallCoursePerformanceObj[course.Id.ToString()] = new JObject { ["gradesInSample"] = courseScores.Count, ["meanCourseScore"] = courseScoreStats.Mean, ["modeCourseScore"] = courseScoreStats.Mode, ["25thPercentileCourseScore"] = courseScoreStats.Q1, ["medianCourseScore"] = courseScoreStats.Median, ["75thPercentileCourseScore"] = courseScoreStats.Q3, ["courseScoreStandardDeviation"] = courseScoreStats.Sigma, ["coursePassCount"] = pass, ["courseFailCount"] = courseScores.Count - pass }; } var assignments = (await api.StreamCourseAssignments(course.Id) .Where(a => a.Published) .ToListAsync()) .DistinctBy(a => a.Id); if (assignmentsPerCourse != default) { assignments = assignments.Take(assignmentsPerCourse); } foreach (var assignment in assignments) { var allSubmissionsStream = api.StreamSubmissionVersions(course.Id, assignment.Id); if (submissionsPerAssignment != default) { allSubmissionsStream = allSubmissionsStream.Take(submissionsPerAssignment); } var allSubmissions = await allSubmissionsStream.ToListAsync(); // just listing every submission here foreach (var submission in allSubmissions) { var submitter = await api.GetUser(submission.UserId); submissionsObj[$"c_{course.Id}|a_{assignment.Id}|u_{submitter.Id}|n_{submission.Attempt??0}"] = new JObject { ["assignmentId"] = assignment.Id, ["courseId"] = course.Id, ["userId"] = submitter.Id, ["versionNumber"] = submission.Attempt, ["submissionDate"] = submission.SubmittedAt, ["pointsEarned"] = submission.Score, ["dateSubmitted"] = submission.SubmittedAt, ["dateGraded"] = submission.GradedAt, ["gradeMatchesCurrent"] = submission.GradeMatchesCurrentSubmission, ["late"] = submission.Late ?? false, ["missing"] = submission.Missing ?? false }; } // now narrow down for assignment performance var submissions = allSubmissions.Where(s => s.Score != null) .GroupBy(s => s.UserId) .Select(sg => sg.First()) .ToList(); if (!submissions.Any()) { continue; } var scores = submissions.Select(s => s.Score) .Cast <decimal>() .ToList(); var stats = new Stats(scores); if (assignmentsOverallObj.ContainsKey($"c_{course.Id}|a_{assignment.Id}")) { Console.WriteLine($"WARN: Duplicated assignment?\nc={course.Id}\na={assignment.Id}\n\n{assignment.ToPrettyString()}\nSkipping!------\n"); continue; } assignmentsOverallObj[$"c_{course.Id}|a_{assignment.Id}"] = new JObject { ["assignmentName"] = assignment.Name, ["assignmentId"] = assignment.Id, ["courseId"] = course.Id, ["teacherId"] = user.Id, ["countedInFinalGrade"] = !(assignment.OmitFromFinalGrade ?? false), ["pointsPossible"] = assignment.PointsPossible, ["createdDate"] = assignment.CreatedAt.ToIso8601Date(), ["dueDate"] = assignment.DueAt?.ToIso8601Date(), ["gradesInSample"] = submissions.Count, ["meanScore"] = stats.Mean, ["modeScore"] = stats.Mode, ["25thPercentileScore"] = stats.Q1, ["medianScore"] = stats.Median, ["75thPercentileScore"] = stats.Q3, ["scoreStandardDeviation"] = stats.Sigma }; // here we're listing the submissions per user that actually count for grades foreach (var submission in submissions) { var submitter = await api.GetUser(submission.UserId); Debug.Assert(!assignmentsIndividualObj.ContainsKey($"c_{course.Id}|a_{assignment.Id}|u_{submitter.Id}")); var score = submission.Score.Value; var z = Convert.ToDouble(score - stats.Mean) / stats.Sigma; var iqr = stats.Q3 - stats.Q1; TimeSpan?minutesSubmittedBeforeDueDate = null; if (submission.SubmittedAt != null && assignment.DueAt != null) { minutesSubmittedBeforeDueDate = assignment.DueAt.Value - submission.SubmittedAt.Value; } assignmentsIndividualObj[$"c_{course.Id}|a_{assignment.Id}|u_{submitter.Id}"] = new JObject { ["assignmentId"] = assignment.Id, ["courseId"] = course.Id, ["userId"] = submitter.Id, ["submissionDate"] = submission.SubmittedAt, ["pointsEarned"] = score, ["z"] = z, ["isUnusual"] = Math.Abs(z) > 1.96, ["isMinorOutlier"] = score <stats.Q1 - iqr * 1.5m || score> stats.Q3 + iqr * 1.5m, ["isMajorOutlier"] = score <stats.Q1 - iqr * 3m || score> stats.Q3 + iqr * 3m, ["minutesSubmittedBeforeDueDate"] = minutesSubmittedBeforeDueDate?.TotalMinutes }; } } } teacherPerformanceObj[user.Id.ToString()] = new JObject { }; #endregion } else { #region CurrentUserIsStudent if (!studentsObj.ContainsKey(user.Id.ToString())) { var lastLogin = api.StreamUserAuthenticationEvents(user.Id) .Where(e => e.Event == EventType.Login) .Select(e => e.CreatedAt) .DefaultIfEmpty() .MaxAsync(); studentsObj[user.Id.ToString()] = new JObject { ["sisId"] = user.SisUserId, ["fullName"] = user.Name, ["lastLogin"] = (await lastLogin).ToIso8601Date() }; } await foreach (var enrollment in api.StreamUserEnrollments(user.Id)) { if (!coursesObj.ContainsKey(enrollment.CourseId.ToString())) { var course = await api.GetCourse(enrollment.CourseId); coursesObj[course.Id.ToString()] = new JObject { ["sisId"] = course.SisCourseId, ["name"] = course.Name }; } } #endregion } } catch (Exception e) { Console.WriteLine($"Caught an exception while processing user id {user.Id}\n{e}\n-------\n"); } } document["dateCompleted"] = DateTime.Now.ToIso8601Date(); document["usersInReport"] = studentsObj.Count + teachersObj.Count; var outPath = Path.Combine(home.NsDir, $"SuperReport_{started.Ticks}.json"); File.WriteAllText(outPath, document.ToString(Indented) + "\n"); Console.WriteLine($"Wrote report to {outPath}"); }