public void ThenICanSeeTheUserSCreditScore() { AppHome Ahp = new AppHome(driver); Ahp.MobileCreditscoreDisplays(); driver.Quit(); }
public static void Main() { var home = new AppHome("bulk_file_delete"); 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("data") { Items = { { "map_file", "RELATIVE_MAP_FILE_PATH_HERE" }, { "id_is_sis", true } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and map info."); 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 data = config.GetTable("data"); var mapFileName = data.Get <string>("map_file"); bool idIsSis = data.GetOr("id_is_sis", true); string mapFilePath = Path.Combine(home.NsDir, mapFileName); Console.WriteLine($"Sourcing map from {mapFilePath}"); var startedAt = DateTime.Now; // ------------------------------------------------------------------------ var list = File.ReadAllLines(mapFilePath).ToList(); List <string>[] taskLists = list.Chunk(Math.Min(Math.Max(list.Count / 7, 2), list.Count - 1)) .ToArray(); int nThreads = taskLists.Length; var apis = new Api[nThreads]; for (var i = 0; i < nThreads; i++) { apis[i] = new Api(token, "https://uview.instructure.com/api/v1/"); } Console.WriteLine($"Using {nThreads} threads."); Console.WriteLine(idIsSis ? "Interpeting userkey as SIS ID." : "Interpreting userkey as CANVAS ID."); var completed = new ConcurrentBag <string>(); var userNotFound = new ConcurrentBag <string>(); var fileNotFound = new ConcurrentBag <string>(); var error = new ConcurrentBag <string>(); var keys = new ConcurrentBag <string>(); using (var countdown = new CountdownEvent(nThreads)) { for (var i = 0; i < nThreads; i++) { ThreadPool.QueueUserWorkItem(async o => { try { var n = (int)o; foreach (string line in taskLists[n]) { string[] halves = line.Split(','); Debug.Assert(halves.Length == 2); var(userKey, userFile) = (halves[0], halves[1]); keys.Add(userKey); var api = apis[n]; try { var user = idIsSis switch { true => await api.GetUserBySis(userKey), _ => await api.GetUser(ulong.Parse(userKey)) }; if (user == null) { Console.WriteLine($"WARN: Couldn't find the user for userkey {userKey} !!"); userNotFound.Add(userKey); continue; } Console.WriteLine($"Preparing to delete filename(s) {userFile} from userkey " + $"{userKey}, Id {user.Id}, SIS {user.SisUserId}"); api.MasqueradeAs(user.Id); var anyDeleted = false; await foreach (var file in api.StreamPersonalFiles(searchTerm: userFile)) { if (file.Filename == userFile) { var deleted = await api.DeleteFile(file.Id, false); Console.WriteLine($"Deleted {userFile}, id {deleted.Id}, from userkey {userKey}!"); anyDeleted = true; } } if (anyDeleted) { completed.Add(userKey); } else { fileNotFound.Add(userKey); Console.WriteLine($"Userkey {userKey} has no {userFile} to delete."); } } catch (Exception e) { Console.WriteLine($"Caught an exception during upload for userkey {userKey}: {e}"); error.Add(userKey); } finally { api.StopMasquerading(); } } } finally { // ReSharper disable once AccessToDisposedClosure countdown.Signal(); } }, i); } countdown.Wait(); } var completedList = completed.Concat(fileNotFound).ToList(); var completedWithDeletionIds = completed.Distinct().ToList(); var completedWithoutDeletionIds = fileNotFound.Distinct().ToList(); var errorIds = error.Distinct().ToList(); var userNotFoundIds = userNotFound.Distinct().ToList(); Console.WriteLine($"{completedList.Count} out of {list.Count} operations were completed."); if (errorIds.Any()) { Console.WriteLine($"Operation failed for the following userkeys: {errorIds.ToPrettyString()}"); } if (userNotFoundIds.Any()) { Console.WriteLine($"The following userkeys could not be resolved: {userNotFoundIds.ToPrettyString()}"); } if (completedWithoutDeletionIds.Any()) { Console.WriteLine($"The following userkeys were missing at least one file (usually not an error): {completedWithoutDeletionIds.ToPrettyString()}"); } var document = new JObject { ["dateStarted"] = startedAt.ToIso8601Date(), ["dateCompleted"] = DateTime.Now.ToIso8601Date(), ["completedWithDeletion"] = new JArray(completedWithDeletionIds), ["completedWithoutDeletion"] = new JArray(completedWithoutDeletionIds), ["error"] = new JArray(errorIds), ["userNotFound"] = new JArray(userNotFoundIds) }; var outPath = Path.Combine(home.NsDir, $"BulkFileDelete_Log_{startedAt.Ticks}.json"); File.WriteAllText(outPath, document.ToString(Formatting.Indented) + "\n"); Console.WriteLine($"Wrote log to {outPath}"); }
public void WhenIClickOnDeals() { AppHome Ahp = new AppHome(driver); Ahp.ClickOffers(); }
public void WhenIClickOnReports() { AppHome Ahp = new AppHome(driver); Ahp.ClickReport(); }
public static async Task Main(string[] args) { var home = new AppHome("export_attendance_columns"); 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("options") { Items = { { "include_hidden", false } } }, new TableSyntax("debug") { Items = { { "limit_to", -1 } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and input path."); 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 includeHidden = config.GetTable("options") .Get <bool>("include_hidden"); var courseLimit = config.GetTable("debug") .Get <long>("limit_to"); var api = new Api(token, "https://uview.instructure.com/api/v1/"); if (courseLimit > 0) { Console.WriteLine($"[DEBUG] Limited to course id {courseLimit}"); } var coursesObj = new JObject(); var document = new JObject { ["metadata"] = new JObject { ["includeHidden"] = includeHidden, ["started"] = DateTime.Now.ToIso8601Date() }, ["courses"] = coursesObj }; try { var courses = courseLimit <= 0 ? api.StreamCourses() : AsyncEnumerable.Repeat(await api.GetCourse(Convert.ToUInt64(courseLimit)), 1); await foreach (var course in courses) { try { var columnsObj = new JObject(); await foreach (var col in api.StreamCustomGradebookColumns(course.Id, includeHidden)) { var thisColObj = new JObject(); await foreach (var datum in api.StreamColumnEntries(col.Id, course.Id)) { thisColObj[datum.UserId.ToString()] = datum.Content; } columnsObj[col.Title] = thisColObj; } coursesObj[course.Id.ToString()] = columnsObj; } catch (Exception e) { Console.WriteLine($"Threw up during course {course.Id}:\n{e}\nContinuing onwards."); } } var now = DateTime.Now; var outPath = Path.Combine(home.NsDir, $"AttendanceColumns_{now.Year}_{now.Month}_{now.Day}.json"); File.WriteAllText(outPath, document.ToString(Formatting.Indented)); Console.WriteLine($"Wrote report to {outPath}"); } catch (Exception e) { Console.WriteLine($"Threw up:\n{e}"); } }
public static async Task Main(string[] args) { var home = new AppHome("discover_ungraded_assignments"); 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("input") { Items = { { "user", -1 }, { "ignore", new int[] { } } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and input path."); 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 input = config.GetTable("input"); var userId = Convert.ToUInt64(input.Get <long>("user")); var ignoreIds = input.MaybeGet <TomlArray>("ignore") ?.Select(Convert.ToUInt64) .ToHashSet() ?? new HashSet <ulong>(); var sectionFilters = input.MaybeGet <TomlTableArray>("section_filters") ?.ToLookup(table => Convert.ToUInt64(table.Get <long>("course")), table => table.Get <string>("section_name")); var api = new Api(token, "https://uview.instructure.com/api/v1/"); try { var enrollments = api.StreamUserEnrollments(userId, new[] { TeacherEnrollment }); await foreach (var taughtCourse in enrollments) { if (ignoreIds.Contains(taughtCourse.CourseId)) { continue; } IEnumerable <Section> sections = null; if (sectionFilters != null && sectionFilters.Contains(taughtCourse.CourseId)) { sections = await api.StreamCourseSections(taughtCourse.CourseId) .Where(s => sectionFilters[taughtCourse.CourseId].Any(n => n == s.Name)) .ToListAsync(); } await foreach (var assignment in api.StreamCourseAssignments(taughtCourse.CourseId, Submission | AllDates)) { await foreach (var submission in api.StreamSubmissionVersions(taughtCourse.CourseId, assignment.Id)) { if (sections != null) { // need to make sure we want this student var ok = await api.StreamUserEnrollments(submission.UserId) .Where(e => e.CourseId == taughtCourse.CourseId) .AnyAsync(e => e.CourseSectionId != null && sections.Any(s => s.Id == e.CourseSectionId)); if (!ok) { continue; } } if (submission.Score == null) { Console.WriteLine($"Ungraded Assignment: {assignment.Name} ({assignment.Id}) submitted by user {submission.UserId} " + $"in course {taughtCourse.CourseId}."); } } } } } catch (Exception e) { Console.WriteLine($"Exception:\n{e}"); } }
public static async Task Main(string[] args) { var home = new AppHome("manage_attendance_columns_csv"); 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("debug") { Items = { { "limit_to", -1 } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and input path."); 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 courseLimit = config.GetTable("debug") .Get <long>("limit_to"); var api = new Api(token, "https://uview.instructure.com/api/v1/"); if (courseLimit > 0) { Console.WriteLine($"[DEBUG] Limited to course id {courseLimit}"); } try { var courses = courseLimit <= 0 ? api.StreamCourses() : AsyncEnumerable.Repeat(await api.GetCourse(Convert.ToUInt64(courseLimit)), 1); await foreach (var course in courses) { try { await foreach (var col in api.StreamCustomGradebookColumns(course.Id, true) .Where(col => col.Title.Contains("Attendance_"))) { var unhidden = await api.UpdateCustomColumn(col.Id, course.Id, hidden : false); Console.WriteLine($"Course {course.Id} - Unhid {unhidden.Title}"); } } catch (Exception e) { Console.WriteLine($"Threw up during course {course.Id}:\n{e}\nContinuing onwards."); } } } catch (Exception e) { Console.WriteLine($"Threw up:\n{e}"); } }
public static async Task Main(string[] args) { var home = new AppHome("export_attendance_columns_csv"); 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("options") { Items = { { "include_hidden", false } } }, new TableSyntax("debug") { Items = { { "limit_to", -1 } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and input path."); 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 includeHidden = config.GetTable("options") .Get <bool>("include_hidden"); var courseLimit = config.GetTable("debug") .Get <long>("limit_to"); var api = new Api(token, "https://uview.instructure.com/api/v1/"); if (courseLimit > 0) { Console.WriteLine($"[DEBUG] Limited to course id {courseLimit}"); } var table = new StringBuilder("course_id,column_name,student_id,enrollment_id,column_value\n"); try { var courses = courseLimit <= 0 ? api.StreamCourses() : (await api.GetCourse(Convert.ToUInt64(courseLimit))).YieldAsync(); await foreach (var course in courses) { try { var enrollments = api.StreamCourseEnrollments(course.Id, StudentEnrollment.Yield()) .ToDictionaryAsync(e => e.UserId); await foreach (var col in api.StreamCustomGradebookColumns(course.Id, includeHidden)) { await foreach (var datum in api.StreamColumnEntries(col.Id, course.Id)) { (await enrollments).TryGetValue(datum.UserId, out var thisEnrollment); table.Append($"{course.Id}," + $"\"{col.Title}\"," + $"{datum.UserId}," + $"{thisEnrollment?.Id.ToString() ?? "NULL"}" + $",\"{datum.Content}\"\n"); } } } catch (Exception e) { Console.WriteLine($"Threw up during course {course.Id}:\n{e}\nContinuing onwards."); } } var now = DateTime.Now; var outPath = Path.Combine(home.NsDir, $"AttendanceColumns_{now.Year}_{now.Month}_{now.Day}.csv"); File.WriteAllText(outPath, table.ToString()); Console.WriteLine($"Wrote report to {outPath}"); } catch (Exception e) { Console.WriteLine($"Threw up:\n{e}"); } }
public static async Task Main(string[] args) { var home = new AppHome("uva_quota_purger"); 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("options") { Items = { { "destroy", false }, { "send_message", false } } }, new TableSyntax("input") { Items = { { "input_file", "PUT_INPUT_FILE_HERE" } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and input file."); 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 options = config.GetTable("options"); var destroy = options.GetOr <bool>("destroy"); var sendMessage = destroy && options.GetOr <bool>("send_message"); var inputFilePath = config.GetTable("input").Get <string>("input_file"); if (!File.Exists(inputFilePath)) { if (File.Exists(Path.Combine(home.NsDir, inputFilePath))) { inputFilePath = Path.Combine(home.NsDir, inputFilePath); } else { Console.WriteLine($"The input file path {inputFilePath} was not found."); return; } } var input = JObject.Parse(File.ReadAllText(inputFilePath)); var detectedUsers = input["detectedUsers"].ToObject <Dictionary <string, JObject> >() .KeySelect(ulong.Parse) .ValSelect(v => new { userSis = v["userSis"], userFullName = v["userFullName"] }); Console.WriteLine($"Loaded input file {inputFilePath} with {input["usersInLog"]} entries."); Console.WriteLine($"Being destructive? {(destroy ? "YES" : "NO")}"); Console.WriteLine($"Sending message? {(sendMessage ? "YES" : "NO")}"); // -------------------------------------------------------------------- var api = new Api(token, "https://uview.instructure.com/api/v1/"); var started = DateTime.Now; var resolvedUsers = new JObject(); var actionTakenUsers = new JObject(); var actionFailedUsers = new JObject(); var inspectionFailedUsers = new JObject(); var actionLimitedUsers = new JObject(); var document = new JObject { ["resolvedUsers"] = resolvedUsers, ["actionTakenUsers"] = actionTakenUsers, ["actionLimitedUsers"] = actionLimitedUsers, ["actionFailedUsers"] = actionFailedUsers, ["inspectionFailedUsers"] = inspectionFailedUsers, ["dateStarted"] = started.ToIso8601Date(), ["sentMessages"] = sendMessage, ["deletedFiles"] = destroy }; foreach (var(userId, userData) in detectedUsers) { try { var isTeacher = await await api.GetUser(userId) .ThenApply(u => u.IsTeacher()); api.MasqueradeAs(userId); var(quota, used) = await api.GetPersonalQuotaMiB(); api.StopMasquerading(); var overLimit = used / quota >= .85m; if (!overLimit) { resolvedUsers[userId.ToString()] = JToken.FromObject(new { userData.userSis, userData.userFullName, usedMiB = used }); Console.WriteLine($"User {userId} is now under the limit. No action taken."); continue; } if (isTeacher) { actionLimitedUsers[userId.ToString()] = JToken.FromObject(new { userData.userSis, userData.userFullName, usedMiB = used }); Console.WriteLine($"User {userId} is still over the limit, but is a teacher. No action taken."); continue; } if (!destroy) { var mockDeletedFiles = new JObject(); api.MasqueradeAs(userId); var conversationAttachments = await api.StreamPersonalFolders() .FirstAsync(pf => pf.Name.ToLowerInvariant().Equals("conversation attachments")); await foreach (var file in api.StreamPersonalFiles(sortBy: Size, order: Descending) .Where(f => f.FolderId == conversationAttachments.Id)) { Console.WriteLine($"[MOCK] Would delete file {file.Id} for user {userId}."); mockDeletedFiles[file.Id.ToString()] = JToken.FromObject(new { fileName = file.Filename, displayName = file.DisplayName, fileSize = file.Size, url = file.Url, contentType = file.ContentType, createdAt = file.CreatedAt, modifiedAt = file.ModifiedAt, updatedAt = file.UpdatedAt }); } api.StopMasquerading(); actionTakenUsers[userId.ToString()] = JToken.FromObject(new { userData.userSis, userData.userFullName, filesWereDeleted = false, mockDeletedFiles }); Console.WriteLine($"User {userId} was over the limit, and his files would have been purged, but destructive mode is off."); continue; } try { var deletedFiles = new JObject(); api.MasqueradeAs(userId); var conversationAttachments = await api.StreamPersonalFolders() .FirstAsync(pf => pf.Name.ToLowerInvariant().Equals("conversation attachments")); await foreach (var file in api.StreamPersonalFiles(sortBy: Size, order: Descending) .Where(f => f.FolderId == conversationAttachments.Id)) { await api.DeleteFile(file.Id); Console.WriteLine($"Deleted file {file.Id} for user {userId}."); deletedFiles[file.Id.ToString()] = JToken.FromObject(new { fileName = file.Filename, displayName = file.DisplayName, fileSize = file.Size, url = file.Url, contentType = file.ContentType, createdAt = file.CreatedAt, modifiedAt = file.ModifiedAt, updatedAt = file.UpdatedAt }); } api.StopMasquerading(); actionTakenUsers[userId.ToString()] = JToken.FromObject(new { userData.userSis, userData.userFullName, filesWereDeleted = true, deletedFiles }); Console.WriteLine($"User {userId} was over the limit, and his files were purged."); if (sendMessage) { // todo } } catch (Exception e) { Console.WriteLine($"Exception when taking action for user {userId}.\n{e}\nContinuing as normal ------"); actionFailedUsers[userId.ToString()] = JToken.FromObject(new { userData.userSis, userData.userFullName, usedMiB = used }); } finally { api.StopMasquerading(); } } catch (Exception e) { Console.WriteLine($"Exception when inspecting user {userId}.\n{e}\nContinuing as normal ------"); inspectionFailedUsers[userId.ToString()] = JToken.FromObject(userData); } finally { api.StopMasquerading(); } } document["dateCompleted"] = DateTime.Now.ToIso8601Date(); document["usersInLog"] = detectedUsers.Count; var outPath = Path.Combine(home.NsDir, $"QuotaPurger_Log_{started.Ticks}.json"); File.WriteAllText(outPath, document.ToString(Indented) + "\n"); Console.WriteLine($"Wrote log to {outPath}"); }
public static async Task Main(string[] args) { var home = new AppHome("rolling_attendance_columns"); 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("debug") { Items = { { "limit_to", -1 } } }, new TableSyntax("filter") { Items = { { "new_column_terms", new string[] { }} } } } }); Console.WriteLine("Created a new config file. Please go put in your token and input path."); 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 courseLimit = config.GetTable("debug") .Get <long>("limit_to"); var filterTerms = config.GetTable("filter") .Get <TomlArray>("new_column_terms") .Cast <string>() .ToHashSet(); var termWhitelist = filterTerms.Count > 0; var api = new Api(token, "https://uview.instructure.com/api/v1/"); if (courseLimit > 0) { Console.WriteLine($"[DEBUG] Limited to course id {courseLimit}"); } if (termWhitelist) { Console.WriteLine("[FILTER] New column creation is limited to the following sections:"); foreach (var s in filterTerms) { Console.WriteLine($" - {s}"); } } else { Console.WriteLine("[FILTER] No section filter!"); } try { var courses = courseLimit <= 0 ? api.StreamCourses(includes: Term) : AsyncEnumerable.Repeat(await api.GetCourse(Convert.ToUInt64(courseLimit), includes: Api.IndividualLevelCourseIncludes.Term), 1); var nextMonday = NextWeekday(DateTime.Today, Monday); var nextMondayStr = FormatColumnName(nextMonday); var lastMonday = NextWeekday(DateTime.Today.AddDays(-7), Monday); var lastMondayStr = FormatColumnName(lastMonday); Console.WriteLine($"The new column will be called {nextMondayStr}"); Console.WriteLine($"The old column (if it exists) is called {lastMondayStr}"); await foreach (var course in courses) { try { var old = await api.StreamCustomGradebookColumns(course.Id) .FirstOrDefaultAsync(col => col.Title == lastMondayStr); if (old != null) { await api.UpdateCustomColumn(old.Id, course.Id, hidden : true); Console.WriteLine($"[Course {course.Id}] Hid old column id {old.Id}"); } if (termWhitelist) { var t = course.Term?.Name; if (t == null || !filterTerms.Contains(t)) { Console.WriteLine($"[Course {course.Id}] Skipping new column creation (term {t ?? "default"} not in whitelist)"); continue; } } var c = await api.CreateCustomColumn(course.Id, nextMondayStr); Console.WriteLine($"[Course {course.Id}] Created new column id {c.Id}"); var enrollments = api.StreamCourseEnrollments( course.Id, Api.CourseEnrollmentType.StudentEnrollment.Yield() ); var updates = await enrollments.Select(e => new Api.ColumnEntryUpdate { UserId = e.UserId, ColumnId = c.Id, Content = "N" }).ToListAsync(); await api.UpdateCustomColumnEntries(course.Id, updates); Console.WriteLine($"[Course {course.Id} - Column {c.Id}] Submitted bulk default update"); } catch (Exception e) { Console.WriteLine($"Threw up during course {course.Id}:\n{e}\nContinuing onwards."); } } Console.WriteLine("Done."); } catch (Exception e) { Console.WriteLine($"Threw up:\n{e}"); } }
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}"); }
public static async Task Main(string[] args) { var home = new AppHome("sis_importer"); 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("input") { Items = { { "import_file", "RELATIVE_IMPORT_FILE_PATH_HERE" } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and input path."); 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 inputFilePath = config.GetTable("input") .Get <string>("import_file"); if (!File.Exists(inputFilePath)) { if (File.Exists(Path.Combine(home.NsDir, inputFilePath))) { inputFilePath = Path.Combine(home.NsDir, inputFilePath); } else { Console.WriteLine($"The import file path {inputFilePath} was not found."); return; } } byte[] file = File.ReadAllBytes(inputFilePath); Console.WriteLine($"Successfully read import file at {inputFilePath}."); var api = new Api(token, "https://uview.instructure.com/api/v1/"); var started = DateTime.Now; try { var imp = await api.ImportSisData(file, Path.GetFileName(inputFilePath)); Console.WriteLine($"Import with ID {imp.Id} was submitted. Waiting until completion..."); do { await Task.Delay(500); // check the import status every N imp = await api.GetSisImport(imp.Id); } while (!imp.WorkflowState.IsHaltedState()); Console.WriteLine($"Import finished with status: {imp.WorkflowState}"); var completed = DateTime.Now; switch (imp.WorkflowState) { case Imported: case ImportedWithMessages: case FailedWithMessages: case Failed: { var document = new JObject { ["wasSuccessful"] = imp.WorkflowState == Imported || imp.WorkflowState == ImportedWithMessages, ["status"] = imp.WorkflowState.ToString(), ["importStarted"] = started.ToIso8601Date(), ["importCompleted"] = completed.ToIso8601Date(), ["counts"] = JToken.FromObject(imp.Data.Counts ?? new object()), ["errors"] = JToken.FromObject(imp.ProcessingErrors ?? new List <IEnumerable <string> >()), ["warnings"] = JToken.FromObject(imp.ProcessingWarnings ?? new List <IEnumerable <string> >()) }; var outPath = Path.Combine(home.NsDir, $"SisImport_{started.Year}-{started.Month}-{started.Day}.json"); File.WriteAllText(outPath, document.ToString(Indented) + "\n"); Console.WriteLine($"Wrote log to {outPath}"); return; } case PartiallyRestored: case Restored: case Aborted: return; case Initializing: case Created: case Importing: case CleanupBatch: case Restoring: case Invalid: Debug.Assert(false); return; default: throw new ArgumentOutOfRangeException(); } } catch (Exception e) { Console.WriteLine($"ImportSisData call threw up:\n{e}"); } }
public static async Task Main(string[] args) { var home = new AppHome("uva_quota_watcher"); 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("options") { Items = { { "send_message", 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 options = config.GetTable("options"); var sendMessage = options.GetOr <bool>("send_message"); Console.WriteLine($"Sending message? {(sendMessage ? "YES" : "NO")}"); // -------------------------------------------------------------------- var api = new Api(token, "https://uview.instructure.com/api/v1/"); var started = DateTime.Now; var detectedUsers = new JObject(); var document = new JObject { ["detectedUsers"] = detectedUsers, ["dateStarted"] = started.ToIso8601Date(), ["sentMessages"] = sendMessage }; await foreach (var user in api.StreamUsers()) { try { api.MasqueradeAs(user.Id); var(quota, used) = await api.GetPersonalQuotaMiB(); api.StopMasquerading(); if (used / quota < .85m) { continue; } var isTeacher = await await api.GetUser(user.Id) .ThenApply(u => u.IsTeacher()); if (isTeacher) { Console.WriteLine($"Noticed teacher {user.Id, -4} - {Math.Round(used / quota * 100, 3), 8:####.000}%"); } else { Console.WriteLine($"Noticed non-teacher {user.Id, -4} - {Math.Round(used / quota * 100, 3), 8:####.000}%"); } detectedUsers[user.Id.ToString()] = new JObject { ["userSis"] = user.SisUserId, ["userFullName"] = user.Name, ["quotaUsedMiB"] = used, ["quotaUsedPercent"] = used / quota * 100 }; if (!sendMessage) { continue; } if (isTeacher) { Console.WriteLine($"Didn't send a message to {user.Id} because they are a teacher."); continue; } var message = string.Format(WarningMessageTemplate, user.Name, Math.Round(used / quota * 100, 2), Math.Round(used, 3)); await foreach (var c in api.CreateConversation(new QualifiedId[] { user.Id }, message, "File Storage Alert", true)) { Console.WriteLine($"Sent the message to {user.Id}.\n{c.ToPrettyString()}\n------\n"); } } catch (Exception e) { Console.WriteLine($"Caught exception, skipping check for user {user.Id}.\n{e}\n"); } finally { api.StopMasquerading(); } } document["dateCompleted"] = DateTime.Now.ToIso8601Date(); document["usersInLog"] = detectedUsers.Count; var outPath = Path.Combine(home.NsDir, $"QuotaWatcher_Log_{started.Ticks}.json"); File.WriteAllText(outPath, document.ToString(Indented) + "\n"); Console.WriteLine($"Wrote log to {outPath}"); }
public static async Task Main(string[] args) { var home = new AppHome("file_map_uploader"); 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("data") { Items = { { "map_file", "RELATIVE_MAP_FILE_PATH_HERE" }, { "id_is_sis", true }, { "target_canvas_dir", "uploaded" }, { "send_message", true }, { "message", "An important file was pushed to your account." } } } } }); Console.WriteLine("Created a new config file. Please go put in your token and map info."); 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 data = config.GetTable("data"); var mapFileName = data.Get <string>("map_file"); var idIsSis = data.GetOr("id_is_sis", true); var targetFolder = data.Get <string>("target_canvas_dir"); var sendMessage = data.Get <bool>("send_message"); var message = data.Get <string>("message"); var mapFilePath = Path.Combine(home.NsDir, mapFileName); Console.WriteLine($"Sourcing map from {mapFilePath}"); var startedAt = DateTime.Now; // ------------------------------------------------------------------------ var list = File.ReadAllLines(mapFilePath).ToList(); List <string>[] taskLists = list.Chunk(Math.Min(Math.Max(list.Count / 7, 2), list.Count - 1)) .ToArray(); var nThreads = taskLists.Length; var apis = new Api[nThreads]; for (int i = 0; i < nThreads; i++) { apis[i] = new Api(token, "https://uview.instructure.com/api/v1/"); } Console.WriteLine($"Using {nThreads} threads."); var completed = new ConcurrentBag <string>(); var notFound = new ConcurrentBag <string>(); var error = new ConcurrentBag <string>(); var keys = new ConcurrentBag <string>(); using (var countdown = new CountdownEvent(nThreads)) { for (int i = 0; i < nThreads; i++) { ThreadPool.QueueUserWorkItem(o => { try { var n = (int)o; foreach (var line in taskLists[n]) { string[] halves = line.Split(','); Debug.Assert(halves.Length == 2); var(userKey, userFile) = (halves[0], halves[1]); keys.Add(userKey); var api = apis[n]; try { var user = idIsSis switch { true => api.GetUserBySis(userKey).Result, _ => api.GetUser(ulong.Parse(userKey)).Result }; if (user == null) { Console.WriteLine($"WARN: Couldn't find the user for sis {userKey} !!"); notFound.Add(userKey); continue; } var bytes = File.ReadAllBytes(Path.Combine(Path.GetDirectoryName(mapFilePath), userFile)); Console.WriteLine($"Preparing to upload filename {userFile} to user " + $"{userKey}, Id {user.Id}, SIS {user.SisUserId}"); api.MasqueradeAs(user.Id); var file = api.UploadPersonalFile(bytes, userFile, targetFolder) .Result; Console.WriteLine($"Uploaded as {file.Id}!"); completed.Add(userKey); if (sendMessage) { api.StopMasquerading(); api.CreateConversation(new QualifiedId(user.Id).Yield(), message, forceNew: true); } } catch (Exception e) { Console.WriteLine($"Caught an exception during upload for {userKey}: {e}"); error.Add(userKey); } finally { api.StopMasquerading(); } } } finally { countdown.Signal(); } }, i); } countdown.Wait(); } var completedE = completed.Distinct().ToList(); var errorE = error.Distinct().ToList(); var notFoundE = notFound.Distinct().ToList(); Console.WriteLine($"{completedE.Count} out of {list.Count} operations were completed."); if (errorE.Any()) { Console.WriteLine($"Operation failed for the following SIS IDs: {errorE.ToPrettyString()}"); } if (notFoundE.Any()) { Console.WriteLine($"The following SIS IDs could not be resolved: {notFoundE.ToPrettyString()}"); } var document = new JObject { ["dateStarted"] = startedAt.ToIso8601Date(), ["dateCompleted"] = DateTime.Now.ToIso8601Date(), ["completed"] = new JArray(completedE), ["error"] = new JArray(errorE), ["notFound"] = new JArray(notFoundE) }; var outPath = Path.Combine(home.NsDir, $"FileMapUploader_Log_{startedAt.Ticks}.json"); File.WriteAllText(outPath, document.ToString(Formatting.Indented) + "\n"); Console.WriteLine($"Wrote log to {outPath}"); }
public static async Task Main(string[] args) { var selected = -1; if (args.Length > 0) { int.TryParse(args[0], out selected); } var home = new AppHome("canvas_report_gen"); 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("sis") { Items = { { "use_sis", false }, { "current_year", "2021" }, { "conn_str", "PUT_CONN_STR_HERE" } } }, new TableSyntax("truancy") { Items = { { "sis_id_year", "2020" }, { "subaccounts", new string[] { } } } } } }); 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 sisTable = config.GetTable("sis"); Database.UseSis = sisTable.Get <bool>("use_sis"); if (Database.UseSis) { Database.ConnStr = sisTable.Get <string>("conn_str"); Database.CurrentYear = sisTable.Get <string>("current_year"); } var emailTable = config.GetTable("email"); var started = DateTime.Now; var outPath = Path.Combine(home.NsDir, "{0}" + $"_{started.Ticks}.csv"); for (;;) { Console.WriteLine("Which report? (* = needs SIS)"); Console.WriteLine("1: Zero Logins"); Console.WriteLine("2: Last Activity"); Console.WriteLine("3: *Truancy"); Console.WriteLine("4: *Truancy (Short Interval)"); Console.WriteLine("5: *Grade Levels"); Console.Write("?> "); await Console.Out.FlushAsync(); if (selected > -1 || int.TryParse(Console.ReadLine(), out selected)) { switch (selected) { case 1: await Reports.ZeroLogins(token, string.Format(outPath, "ZeroLogins")); Mailman.SendReport(emailTable, "ZeroLogins", outPath, started); return; case 2: await Reports.LastActivity(token, string.Format(outPath, "LastActivity")); Mailman.SendReport(emailTable, "LastActivity", outPath, started); return; case 3: await Reports.Truancy(token, string.Format(outPath, "Truancy"), config.GetTable("truancy")); Mailman.SendReport(emailTable, "Truancy", outPath, started); return; case 4: await Reports.Truancy(token, string.Format(outPath, "TruancyShort"), config.GetTable("truancy"), true); Mailman.SendReport(emailTable, "TruancyShort", outPath, started); return; case 5: await Reports.GradeLevels(token, string.Format(outPath, "GradeLevels")); Mailman.SendReport(emailTable, "GradeLevels", outPath, started); return; case 0: File.WriteAllText(string.Format(outPath, "Dummy"), "dummy1,dummy2\na,b\n"); Mailman.SendReport(emailTable, "Dummy", outPath, started); return; default: Console.WriteLine("Please choose a report.\n"); break; } } else { Console.WriteLine("Please choose a report.\n"); } } }