├── LICENSE ├── README.md ├── assignments ├── assignment_clo_worksheet.py ├── assignment_clo_worksheet.xlsx ├── assignments_lti_list.py ├── assignments_lti_msnonlinedev_list.py ├── assignments_turnitin_api_&_lti_list.py ├── assignments_turnitin_api_list.py ├── assignments_turnitin_list_by_faculty.py ├── assignments_turnitin_lti_list.py └── assignments_turnitin_msonline_list.py ├── core ├── accounts.py ├── api.py ├── assignments.py ├── config.json ├── config.py ├── courses.py ├── etc.py ├── io.py ├── outcome_groups.py ├── outcomes.py ├── terms.py └── users.py ├── enrollments ├── batch_enroll_nursing_community_courses_WIP.py ├── batch_enrollments.py ├── enrollments_duplicate_list.py └── fnpo_students_and_teachers.py ├── etc ├── alignments_summary.py ├── change_end_dates.py ├── course_best_practices_inventory.py ├── eportfolios.py ├── fnp_master_copies.py ├── grades_export_BROKEN.py ├── oneclass_conversations.py ├── page_text_replace.py ├── quiz_creation_TEST.py ├── replace_text_in_pages.py └── xlist_list.py ├── outcomes ├── clos_course_list.py ├── clos_course_sync.py ├── clos_program_refresh.py ├── clos_program_xlsx_from_canvas.py ├── clos_program_xlsx_from_cmi.py ├── cmi_scrub_alert.py ├── glos_duplicates_list.py ├── glos_push_to_courses.py └── glos_refresh.py ├── roles ├── admins_list.py ├── roles_in_accounts_list.py └── roles_in_courses_list.py └── syllabi ├── syllabi_download_OLD.py └── syllabot.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Samuel Merritt University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py3-canvaslms-api 2 | Python 3 API wrapper for Instructure's Canvas LMS with real-world examples of use. 3 | 4 | Simplifies tasks and reporting involving assignments, courses, enrollments, outcomes, roles, subaccounts, and users. 5 | 6 | Also includes functions that simplify performing SIS imports and exports, querying databases, and working with CSV and XSLX files. 7 | 8 | The "core" directory contains the scripts that the API wrapper functions are in, as well as config.json, which you'll need to edit to match your environment. (You might also need to edit config.py). 9 | 10 | All other directories contain scripts using the core functions to accomplish tasks, including these: 11 | 12 | * Sync subaccount-level learning outcomes with outcomes in an external repository. 13 | * Sync course-level learning outcomes with subaccount-level outcomes. 14 | * Import outcomes into a course from a formatted Word document. 15 | * Generate a syllabus for a course by wrangling data from Canvas, a SIS, and a learning outcomes repository into a Word template. 16 | * Download all syllabus files. 17 | * List assignments that use the Turnitin API. 18 | * Retrieve an SIS report. 19 | * Do an SIS import on a CSV file of enrollments created by running a SQL file against the SIS. 20 | * Assist in assessing Canvas course design best practices by generating an inventory of courses and the Canvas features they use. 21 | * Find and replace text in Canvas pages. 22 | * List all cross-listed courses. 23 | * List admins at the account and subaccount level. 24 | -------------------------------------------------------------------------------- /assignments/assignment_clo_worksheet.py: -------------------------------------------------------------------------------- 1 | # https://openpyxl.readthedocs.io/ 2 | # https://automatetheboringstuff.com/chapter12/ 3 | # https://www.ablebits.com/office-addins-blog/2014/09/24/excel-drop-down-list/ 4 | # http://stackoverflow.com/questions/18595686/how-does-operator-itemgetter-and-sort-work-in-python 5 | 6 | from canvas.core.courses import get_course_by_sis_id, validate_course 7 | from canvas.core.io import get_cmi_clos_by_course, tada 8 | from openpyxl import load_workbook 9 | from openpyxl.formatting.rule import CellIsRule 10 | from openpyxl.styles import Alignment, Font, colors, PatternFill 11 | from openpyxl.styles.borders import Border, Side 12 | from openpyxl.utils import get_column_letter 13 | from openpyxl.worksheet.datavalidation import DataValidation 14 | 15 | from canvas.core.assignments import get_assignments 16 | 17 | 18 | def assignment_clo_worksheet(): 19 | 20 | courses = { 21 | '2016-2SU-01-NDNP-714-LEC-ONL-O1': ['Paulina', 'Van'], 22 | '2016SS-OAK-UGAOAK1-NURSG-160-LEC1-1': ['Paulina', 'Van', 'NABSN'], 23 | '2016-3FA-02-NABSN-170-LEC-SFP-01': ['Jenny', 'Zettler Rhodes'], 24 | '2016-3FA-01-NBSN-164-LEC-OAK-01': ['Erik', 'Carter'], 25 | '2016-3FA-01-NELMSN-566-LEC-SAC-01': ['Erik', 'Carter'], 26 | '2016-3FA-01-NBSN-108-LEC-OAK-01': ['Christine', 'Rey'] 27 | } 28 | 29 | for course_sis_id in courses: 30 | 31 | template_file = load_workbook('assignment_clo_worksheet.xlsx') 32 | sheet = template_file.get_sheet_by_name(template_file.active.title) 33 | 34 | sheet.freeze_panes = 'B1' 35 | sheet.page_setup.fitToHeight = 1 36 | border = Border(left=Side(style='thin'), right=Side(style='thin'), 37 | top=Side(style='thin'), bottom=Side(style='thin')) 38 | sixteen_point = Font(size=16) 39 | dv = DataValidation(type="list", formula1='"Yes,No"', allow_blank=False) 40 | sheet.add_data_validation(dv) 41 | 42 | teacher_firstname = courses[course_sis_id][0] 43 | teacher_lastname = courses[course_sis_id][1] 44 | course = get_course_by_sis_id(course_sis_id) 45 | course_sis_info = validate_course(course) 46 | program, number, ctype, campus, section, term, session = \ 47 | [course_sis_info[i] for i in ['program', 'number', 'type', 'campus', 'section', 'term', 'session']] 48 | filename = '{}-{}-{}-{}-{}-{}-{}-{}.xlsx'\ 49 | .format(program, number, ctype, campus, section, term, session, teacher_lastname) 50 | 51 | # header 52 | sheet.cell(row=1, column=1).value = number 53 | sheet.cell(row=2, column=1).value = course_sis_id 54 | sheet.cell(row=3, column=1).value = course['name'] 55 | sheet.cell(row=1, column=2).value = term 56 | sheet.cell(row=2, column=2).value = teacher_firstname + ' ' + teacher_lastname 57 | 58 | # assignments (graded only) 59 | assignments = get_assignments(course['id']) 60 | for row, assignment in enumerate(sorted(assignments, key=lambda a: "" if not a['due_at'] else a['due_at'])): 61 | 62 | if 'not_graded' in assignment['submission_types'] or not assignment['points_possible'] \ 63 | or ('omit_from_final_grade' in assignment and assignment['omit_from_final_grade']): 64 | continue 65 | 66 | sheet.cell(row=7+row, column=1).value = assignment['name'] 67 | sheet.cell(row=7+row, column=1).hyperlink = assignment['html_url'] 68 | sheet.cell(row=7+row, column=1).border = border 69 | sheet.cell(row=7+row, column=1).font = sixteen_point 70 | sheet.cell(row=7+row, column=1).font = Font(color=colors.BLUE) 71 | sheet.row_dimensions[7+row].height = 27 72 | 73 | # rubric yes/no 74 | sheet.cell(row=7+row, column=2).border = border 75 | sheet.cell(row=7+row, column=2).font = sixteen_point 76 | dv.add(sheet.cell(row=7+row, column=2)) 77 | 78 | # improvement plan 79 | sheet.cell(row=7+row, column=3).border = border 80 | 81 | # plan complete yes/no 82 | sheet.cell(row=7+row, column=4).border = border 83 | sheet.cell(row=7+row, column=4).font = sixteen_point 84 | dv.add(sheet.cell(row=7+row, column=4)) 85 | 86 | # clos 87 | max_clo_desc_len = 0 88 | # kludge for old sis id format 89 | program = program if len(courses[course_sis_id]) == 2 else courses[course_sis_id][2] 90 | clos = get_cmi_clos_by_course(program, course_sis_info['number']) 91 | for col, clo in enumerate(clos): 92 | sheet.cell(row=6, column=5+col).alignment = Alignment(vertical='top', wrapText=True) 93 | sheet.cell(row=6, column=5+col).value = '{}: {}'.format(clo['clo_title'], clo['clo_description']) 94 | sheet.cell(row=6, column=5+col).border = border 95 | sheet.cell(row=6, column=5+col).font = sixteen_point 96 | max_clo_desc_len = max(len(clo['clo_description']), max_clo_desc_len) 97 | 98 | # clo headers [styling merged cells doesn't work in openpyxl] 99 | last_column = 4 + len(clos) 100 | sheet.merge_cells(start_row=4, start_column=5, end_row=4, end_column=last_column) 101 | sheet.merge_cells(start_row=5, start_column=5, end_row=5, end_column=last_column) 102 | 103 | # clo column width & row height 104 | sheet.row_dimensions[6].height = max_clo_desc_len / 50 * 36 105 | for column in range(5, last_column + 1): 106 | sheet.column_dimensions[get_column_letter(column)].width = 50 107 | 108 | # conditional formatting for x marks the spot 109 | clo_range = 'E7:{}{}'.format(get_column_letter(last_column), 6 + len(assignments)) 110 | sheet.conditional_formatting\ 111 | .add(clo_range, CellIsRule(operator='greaterThan', formula=['""'], fill=PatternFill(bgColor='70AD47'))) 112 | for row in range(7, 7 + len(assignments)): 113 | for column in range(5, last_column + 1): 114 | sheet.cell(row=row, column=column).border = border 115 | sheet.cell(row=row, column=column).alignment = Alignment(horizontal="center", vertical="center") 116 | 117 | template_file.save(filename) 118 | 119 | 120 | if __name__ == '__main__': 121 | assignment_clo_worksheet() 122 | tada() 123 | -------------------------------------------------------------------------------- /assignments/assignment_clo_worksheet.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgrobani/py3-canvaslms-api/c02c56a33dd196bdf779039c13bb52aa1e88699d/assignments/assignment_clo_worksheet.xlsx -------------------------------------------------------------------------------- /assignments/assignments_lti_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import get_courses 2 | from canvas.core.io import tada 3 | 4 | from canvas.core.assignments import get_assignments 5 | 6 | programs = ['NFNPO'] 7 | terms = ['2017-1SP'] 8 | synergis = True 9 | 10 | for course in get_courses(terms, programs, synergis): 11 | for assignment in get_assignments(course['id']): 12 | if 'external_tool' in assignment['submission_types']: 13 | print(course['sis_course_id'], assignment['external_tool_tag_attributes']['url'], assignment['name']) 14 | 15 | tada() 16 | -------------------------------------------------------------------------------- /assignments/assignments_lti_msnonlinedev_list.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from canvas.core.assignments import get_assignments 4 | from canvas.core.courses import get_courses_by_account_id 5 | from canvas.core.io import tada, write_xlsx_file 6 | 7 | accounts = {'DEV FNPO': '168920', 'DEV CMO': '168922'} 8 | # header = ['program', 'course name', 'lti', 'assignment name', 'assignment URL'] 9 | header = ['program', 'course name', 'assignment name', 'assignment URL', 'rubric length'] 10 | rows = [] 11 | for account in accounts: 12 | for course in get_courses_by_account_id(accounts[account], 'DEFAULT'): 13 | if course['account_id'] not in [168920, 168922]: 14 | continue 15 | course_id = course['id'] 16 | for assignment in get_assignments(course_id): 17 | # if 'external_tool' in assignment['submission_types']: 18 | if 'rubric' in assignment: 19 | print(assignment['rubric']) 20 | # lti_type = 'turnitin' if 'turnitin' in assignment['external_tool_tag_attributes']['url'] \ 21 | # else 'youseeu' if 'youseeu' in assignment['external_tool_tag_attributes']['url'] else '' 22 | # row = [account, course['name'], lti_type, assignment['name'], assignment['html_url']] 23 | row = [account, course['name'], assignment['name'], assignment['html_url'], len(assignment['rubric'])] 24 | rows.append(row) 25 | # print(row) 26 | 27 | # write_xlsx_file('lti_assignments_msonline_dev_{}' 28 | write_xlsx_file('lti_rubrics_msonline_dev_{}' 29 | .format(datetime.now().strftime('%Y.%m.%d.%H.%M.%S')), header, rows) 30 | 31 | tada() 32 | -------------------------------------------------------------------------------- /assignments/assignments_turnitin_api_&_lti_list.py: -------------------------------------------------------------------------------- 1 | # https://canvas.instructure.com/doc/api/assignments.html 2 | from datetime import datetime 3 | 4 | from canvas.core.courses import get_courses, get_course_people 5 | from canvas.core.io import write_xlsx_file, tada 6 | 7 | from canvas.core.assignments import get_assignments 8 | 9 | 10 | def turnitin_api_assignments(): 11 | terms = ['2016-3FA'] 12 | programs = [] 13 | synergis = True 14 | course_whitelist = [] 15 | header = ['term', 'program', 'SIS ID', 'assignment name', 'assignment URL', 'due date', 'submission types', 16 | 'points', 'rubric with grading criteria', 'rubric with CLOs', 'group assignment', 'faculty of record', 17 | 'lti', 'api'] 18 | rows = [] 19 | 20 | for course in get_courses(terms, programs, synergis, course_whitelist): 21 | course_id = course['id'] 22 | course_sis_id = course['sis_course_id'] 23 | program = course['course_sis_info']['program'] 24 | for assignment in get_assignments(course_id): 25 | api = 'turnitin_enabled' in assignment and assignment['turnitin_enabled'] 26 | lti = 'external_tool' in assignment['submission_types'] 27 | if api or lti: 28 | rubric_has_criteria = '' 29 | rubric_has_clos = '' 30 | if 'rubric' in assignment: 31 | for criterion in assignment['rubric']: 32 | if 'outcome_id' in criterion: 33 | rubric_has_clos = 'X' 34 | if 'id' in criterion: 35 | rubric_has_criteria = 'X' 36 | row = [terms[0], 37 | program, 38 | course_sis_id, 39 | assignment['name'], 40 | assignment['html_url'], 41 | assignment['due_at'][0:10] if assignment['due_at'] else '', 42 | ', '.join(assignment['submission_types']), 43 | assignment['points_possible'] if assignment['points_possible'] else '', 44 | rubric_has_criteria, 45 | rubric_has_clos, 46 | 'X' if 'group_category_id' in assignment and assignment['group_category_id'] else '', 47 | ', '.join([p['name'] for p in get_course_people(course_id, 'Faculty of record')]), 48 | 'X' if lti else '', 49 | 'X' if api else ''] 50 | rows.append(row) 51 | print(row) 52 | 53 | write_xlsx_file('turnitin_api_&_assignments_{}_{}' 54 | .format(terms[0], datetime.now().strftime('%Y.%m.%d.%H.%M.%S')), header, rows) 55 | 56 | if __name__ == '__main__': 57 | turnitin_api_assignments() 58 | tada() 59 | -------------------------------------------------------------------------------- /assignments/assignments_turnitin_api_list.py: -------------------------------------------------------------------------------- 1 | # https://canvas.instructure.com/doc/api/assignments.html 2 | from datetime import datetime 3 | 4 | from canvas.core.courses import get_courses, get_course_people 5 | from canvas.core.io import write_xlsx_file, tada 6 | 7 | from canvas.core.assignments import get_assignments 8 | 9 | 10 | def turnitin_api_assignments(): 11 | terms = ['2017-2SU'] 12 | programs = [] 13 | synergis = True 14 | course_whitelist = [] 15 | header = ['term', 'program', 'SIS ID', 'assignment name', 'assignment URL', 'due date', 'submission types', 16 | 'points', 'rubric with grading criteria', 'rubric with CLOs', 'group assignment', 'faculty of record'] 17 | rows = [] 18 | 19 | for course in get_courses(terms, programs, synergis, course_whitelist): 20 | course_id = course['id'] 21 | course_sis_id = course['sis_course_id'] 22 | program = course['course_sis_info']['program'] 23 | for assignment in get_assignments(course_id): 24 | if 'turnitin_enabled' in assignment and assignment['turnitin_enabled'] \ 25 | and 'external_tool' not in assignment['submission_types']: 26 | rubric_has_criteria = '' 27 | rubric_has_clos = '' 28 | if 'rubric' in assignment: 29 | for criterion in assignment['rubric']: 30 | if 'outcome_id' in criterion: 31 | rubric_has_clos = 'X' 32 | if 'id' in criterion: 33 | rubric_has_criteria = 'X' 34 | row = [terms[0], 35 | program, 36 | course_sis_id, 37 | assignment['name'], 38 | assignment['html_url'], 39 | assignment['due_at'][0:10] if assignment['due_at'] else '', 40 | ', '.join(assignment['submission_types']), 41 | assignment['points_possible'] if assignment['points_possible'] else '', 42 | rubric_has_criteria, 43 | rubric_has_clos, 44 | 'X' if 'group_category_id' in assignment and assignment['group_category_id'] else '', 45 | ', '.join([p['name'] for p in get_course_people(course_id, 'Faculty of record')])] 46 | rows.append(row) 47 | print(row) 48 | 49 | write_xlsx_file('turnitin_api_assignments_summer_{}' 50 | .format(datetime.now().strftime('%Y.%m.%d.%H.%M.%S')), header, rows) 51 | 52 | if __name__ == '__main__': 53 | turnitin_api_assignments() 54 | tada() 55 | -------------------------------------------------------------------------------- /assignments/assignments_turnitin_list_by_faculty.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from canvas.core.courses import get_courses, get_courses_whitelisted, get_course_people 4 | from canvas.core.io import write_xlsx_file, tada 5 | 6 | from canvas.core.assignments import get_assignments 7 | 8 | assignment_qtys = {} 9 | terms = ['2017-1SP'] 10 | programs = [] 11 | synergis = True 12 | course_whitelist = get_courses_whitelisted([]) 13 | 14 | for course in course_whitelist or get_courses(terms, programs, synergis): 15 | course_id = course['id'] 16 | if not get_course_people(course_id, 'student'): 17 | continue 18 | for assignment in get_assignments(course_id): 19 | if (assignment.get('turnitin_enabled', False)) or \ 20 | ('external_tool' in assignment['submission_types'] and 21 | 'turnitin' in (assignment['external_tool_tag_attributes']['url'] or '')): 22 | key = '{}|{}|{}'.format(terms[0], course['course_sis_info']['program'], 23 | ' & '.join([p['name'] for p in get_course_people(course_id, 'Faculty of record')])) 24 | assignment_qtys[key] = assignment_qtys.get(key, 0) + 1 25 | print(key) 26 | 27 | header = ['term', 'program', 'faculty', '# of TII assignments'] 28 | rows = [key.split('|') + [assignment_qtys[key]] for key in assignment_qtys] 29 | write_xlsx_file('turnitin_assignments_by_faculty_spring_{}' 30 | .format(datetime.now().strftime('%Y.%m.%d.%H.%M.%S')), header, rows) 31 | tada() 32 | -------------------------------------------------------------------------------- /assignments/assignments_turnitin_lti_list.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from canvas.core.courses import get_courses, get_courses_whitelisted, get_course_people 4 | from canvas.core.io import write_xlsx_file, tada 5 | 6 | from canvas.core.assignments import get_assignments 7 | 8 | 9 | def assignments_turnitin_list(): 10 | terms = ['2017-1SP'] 11 | programs = [] 12 | synergis = True 13 | course_whitelist = get_courses_whitelisted([]) 14 | header = ['term', 'program', 'SIS ID', 'course name', 'assignment name', 'assignment URL', 'due date', 'points', 15 | 'group assignment', 'faculty of record'] 16 | rows = [] 17 | 18 | for course in course_whitelist or get_courses(terms, programs, synergis): 19 | course_id = course['id'] 20 | if not get_course_people(course_id, 'student'): 21 | continue 22 | course_sis_id = course['sis_course_id'] 23 | program = course['course_sis_info']['program'] 24 | for assignment in get_assignments(course_id): 25 | if 'external_tool' in assignment['submission_types']: 26 | row = [terms[0], 27 | program, 28 | course_sis_id, 29 | course['name'], 30 | assignment['name'], 31 | assignment['html_url'], 32 | assignment['due_at'][0:10] if assignment['due_at'] else '', 33 | assignment['points_possible'] if assignment['points_possible'] else '', 34 | 'X' if 'group_category_id' in assignment and assignment['group_category_id'] else '', 35 | ', '.join([p['name'] for p in get_course_people(course_id, 'Faculty of record')])] 36 | rows.append(row) 37 | print(row) 38 | 39 | write_xlsx_file('turnitin_assignments_spring_{}' 40 | .format(datetime.now().strftime('%Y.%m.%d.%H.%M.%S')), header, rows) 41 | 42 | if __name__ == '__main__': 43 | assignments_turnitin_list() 44 | tada() 45 | -------------------------------------------------------------------------------- /assignments/assignments_turnitin_msonline_list.py: -------------------------------------------------------------------------------- 1 | # https://canvas.instructure.com/doc/api/assignments.html 2 | from datetime import datetime 3 | 4 | from canvas.core.courses import get_courses, get_courses_whitelisted, get_course_people, get_courses_by_account_id 5 | from canvas.core.io import write_xlsx_file, tada 6 | 7 | from canvas.core.assignments import get_assignments 8 | 9 | 10 | def assignments_turnitin_msonline_list(): 11 | terms = ['2017-1SP'] 12 | programs = ['NFNPO', 'NCMO'] 13 | synergis = True 14 | course_whitelist = get_courses_whitelisted([]) 15 | header = ['term', 'program', 'SIS ID', 'course name', 'assignment name', 'assignment URL', 'due date', 'points', 16 | 'group assignment', 'faculty of record'] 17 | rows = [] 18 | 19 | for course in course_whitelist or get_courses(terms, programs, synergis): 20 | course_id = course['id'] 21 | if not get_course_people(course_id, 'student'): 22 | continue 23 | course_sis_id = course['sis_course_id'] 24 | program = course['course_sis_info']['program'] 25 | for assignment in get_assignments(course_id): 26 | if 'external_tool' in assignment['submission_types']: 27 | row = [terms[0], 28 | program, 29 | course_sis_id, 30 | course['name'], 31 | assignment['name'], 32 | assignment['html_url'], 33 | assignment['due_at'][0:10] if assignment['due_at'] else '', 34 | assignment['points_possible'] if assignment['points_possible'] else '', 35 | 'X' if 'group_category_id' in assignment and assignment['group_category_id'] else '', 36 | ', '.join([p['name'] for p in get_course_people(course_id, 'Faculty of record')])] 37 | rows.append(row) 38 | print(row) 39 | 40 | write_xlsx_file('turnitin_assignments_spring_{}' 41 | .format(datetime.now().strftime('%Y.%m.%d.%H.%M.%S')), header, rows) 42 | 43 | 44 | def assignments_turnitin_msonline_list_dev(): 45 | accounts = {'DEV FNPO': '168920', 'DEV CMO': '168922'} 46 | header = ['program', 'course name', 'assignment name', 'assignment URL', 'points'] 47 | rows = [] 48 | for account in accounts: 49 | for course in get_courses_by_account_id(accounts[account], 'DEFAULT'): 50 | course_id = course['id'] 51 | for assignment in get_assignments(course_id): 52 | if 'external_tool' in assignment['submission_types']: 53 | row = [ 54 | account, 55 | course['name'], 56 | assignment['name'], 57 | assignment['html_url'], 58 | assignment['points_possible'] if assignment['points_possible'] else ''] 59 | rows.append(row) 60 | print(row) 61 | 62 | write_xlsx_file('turnitin_assignments_spring_dev_{}' 63 | .format(datetime.now().strftime('%Y.%m.%d.%H.%M.%S')), header, rows) 64 | 65 | if __name__ == '__main__': 66 | # assignments_turnitin_msonline_list() 67 | assignments_turnitin_msonline_list_dev() 68 | tada() 69 | -------------------------------------------------------------------------------- /core/accounts.py: -------------------------------------------------------------------------------- 1 | from sortedcontainers import SortedDict 2 | 3 | from canvas.core.api import get_list 4 | 5 | 6 | def get_subaccounts(account_id): 7 | return sorted(get_list('accounts/{}/sub_accounts'.format(account_id)), key=lambda s: s['name']) 8 | 9 | 10 | def get_admins(account_id): 11 | return sorted(get_list('accounts/{}/admins'.format(account_id)), key=lambda a: (a['role'], a['user']['name'])) 12 | 13 | 14 | def get_roles(account_id): 15 | return get_list('accounts/{}/roles'.format(account_id)) 16 | 17 | 18 | def get_account_grading_standard(account_id): 19 | """ return an account's grading standard(s?) """ 20 | return get_list('accounts/{}/grading_standards'.format(account_id)) 21 | 22 | 23 | def program_data(): 24 | return SortedDict({ 25 | # account, cmi, synergis, catalog, title 26 | 'BSCI': ['98327', 'BSCI', 'N', 'BSCI', 'Basic Sciences'], 27 | 'NABSN': ['98340', 'ABSN', 'N', 'NURSG', 'Accelerated Bachelor of Science in Nursing'], 28 | 'NBSN': ['98339', 'BSN', 'N', 'NURSG', 'Bachelor of Science in Nursing'], 29 | 'NCM': ['98332', 'MSN_CM', 'N', 'NURSG', 'Master of Science in Nursing: Case Management'], 30 | 'NCMO': ['145593', 'MSN_CM', 'Y', 'NURSG', 'Master of Science in Nursing: Case Management (Online)'], 31 | 'NCRNA': ['98334', 'MSN_CRNA', 'N', 'NURSG', 'Master of Science in Nursing: CRNA'], 32 | 'NDNP': ['98330', 'DNP', 'N', 'NURSG', 'Doctor of Nursing Practice'], 33 | 'NELMSN': ['98335', 'ELMSN', 'N', 'NURSG', 'Entry Level Master of Science in Nursing'], 34 | 'NFNP': ['98333', 'MSN_FNP', 'N', 'NURSG', 'Master of Science in Nursing: Family Nurse Practitioner'], 35 | 'NFNPO': ['145067', 'MSN_FNP', 'Y', 'NURSG', 'Master of Science in Nursing: Family Nurse Practitioner (Online)'], 36 | 'NR2B': ['167901', 'RN2BSN', 'N', 'NURSG', 'RN to BSN'], 37 | 'OT': ['98323', 'MOT', 'N', 'OCCTH', 'Doctor of Occupational Therapy'], 38 | 'PA': ['98324', 'MPA', 'N', 'PA', 'Master of Science in Physician Assistant'], 39 | 'PM': ['98325', 'DPM', 'N', 'PM', 'Doctor of Podiatric Medicine'], 40 | 'PT': ['98326', 'DPT', 'N', 'PHYTH', 'Doctor of Physical Therapy'] 41 | }) 42 | 43 | 44 | def program_account(program): 45 | return program_data().get(program, [program])[0] 46 | 47 | 48 | def cmi_program(program): 49 | return program_data()[program][1] 50 | 51 | 52 | def catalog_program(program): 53 | return program_data()[program][3] 54 | 55 | 56 | def program_formal_name(program): 57 | return program_data()[program][4] 58 | 59 | 60 | def all_programs(synergis): 61 | programs = program_data() 62 | return [program for program in programs if synergis or programs[program][2] == 'N'] 63 | -------------------------------------------------------------------------------- /core/api.py: -------------------------------------------------------------------------------- 1 | import time 2 | import winsound 3 | 4 | from requests import delete as api_delete, exceptions as api_exceptions, get as api_get, post as api_post, \ 5 | put as api_put 6 | 7 | from canvas.core import config 8 | 9 | 10 | def get(url): 11 | while True: 12 | try: 13 | return api_get(normalize_url(url), headers=config.auth_header, timeout=10, stream=False) 14 | except api_exceptions.RequestException as e: 15 | time_out(url, 'GET', e) 16 | 17 | 18 | def get_list(url): 19 | """ compile a paginated list up to 100 at a time (instead of default 10) and return the entire list """ 20 | paginated = [] 21 | r = get('{}{}per_page=100'.format(url, '&' if '?' in url else '?')) 22 | while 'next' in r.links: 23 | paginated.extend(r.json()) 24 | r = get(r.links['next']['url']) 25 | paginated.extend(r.json()) 26 | return paginated 27 | 28 | 29 | def get_file(url): 30 | while True: 31 | try: 32 | return api_get(url, headers=config.auth_header, timeout=10, stream=True) 33 | except api_exceptions.RequestException as e: 34 | time_out(url, 'GET', e) 35 | 36 | 37 | def post(url, r_data): 38 | while True: 39 | try: 40 | return api_post(normalize_url(url), headers=config.auth_header, timeout=5, stream=False, data=r_data) 41 | except api_exceptions.RequestException as e: 42 | time_out(url, 'POST', e) 43 | 44 | 45 | def put(url, r_data): 46 | while True: 47 | try: 48 | return api_put(normalize_url(url), headers=config.auth_header, timeout=5, stream=False, data=r_data) 49 | except api_exceptions.RequestException as e: 50 | time_out(url, 'PUT', e) 51 | 52 | 53 | def delete(url, r_data=''): 54 | while True: 55 | try: 56 | return api_delete(normalize_url(url), headers=config.auth_header, timeout=5, stream=False, data=r_data) 57 | except api_exceptions.RequestException as e: 58 | time_out(url, 'DELETE', e) 59 | 60 | 61 | def normalize_url(url): 62 | # paginated 'next' urls already start with base_url 63 | return url if url.startswith(config.base_url) else config.base_url + url 64 | 65 | 66 | def time_out(url, action, e): 67 | winsound.Beep(1200, 300) 68 | print('*' * 40, ' ERROR - RETRYING IN 10 SECONDS ', '*' * 40) 69 | print('\n***EXCEPTION:', e, '\n***ACTION:', action, '\n***URL:', url, '\n') 70 | time.sleep(10) 71 | -------------------------------------------------------------------------------- /core/assignments.py: -------------------------------------------------------------------------------- 1 | from canvas.core import api as api 2 | 3 | 4 | def assignment_is_graded(assignment): 5 | return 'not_graded' not in assignment['submission_types'] and assignment['points_possible'] \ 6 | and not ('omit_from_final_grade' in assignment and assignment['omit_from_final_grade']) 7 | 8 | 9 | def get_assignment(course_id, assignment_id): 10 | """ return one course assignment """ 11 | return api.get('courses/{}/assignments/{}'.format(course_id, assignment_id)).json() 12 | 13 | 14 | def get_assignments(course_id): 15 | """ return a list of a course's assignments """ 16 | return sorted(api.get_list('courses/{}/assignments'.format(course_id)), key=lambda a: a['position']) 17 | 18 | 19 | def get_assignment_grade_summaries(course_id): 20 | """ return a list of a course's assignments with a grade summary for each 21 | https://canvas.instructure.com/doc/api/analytics.html#method.analytics_api.course_assignments """ 22 | assignments = api.get_list('courses/{}/analytics/assignments'.format(course_id)) 23 | return [] if 'errors' in assignments else assignments 24 | 25 | 26 | def get_assignment_groups(course_id): 27 | """ return a list of a course's assignment groups with their assignments """ 28 | return sorted(api.get_list('courses/{}/assignment_groups?include[]=assignments'.format(course_id)), 29 | key=lambda g: g['position']) 30 | 31 | 32 | def get_assignment_student_grades(course_id, student_id): 33 | """ return a list of a student's assignments with grades 34 | https://canvas.instructure.com/doc/api/analytics.html#method.analytics_api.student_in_course_assignments """ 35 | return api.get_list('courses/{}/analytics/users/{}/assignments'.format(course_id, student_id)) 36 | 37 | 38 | def get_assignment_submissions(course_id, assignment_id): 39 | """ return a list of submissions for an assignment """ 40 | return api.get_list('courses/{}/assignments/{}/submissions'.format(course_id, assignment_id)) 41 | 42 | 43 | def get_discussions(course_id): 44 | """ return a list of a course's discussion topics """ 45 | return api.get_list('courses/{}/discussion_topics'.format(course_id)) 46 | 47 | 48 | def get_quizzes(course_id): 49 | """ return a list of a course's quizzes """ 50 | return api.get_list('courses/{}/quizzes'.format(course_id)) 51 | -------------------------------------------------------------------------------- /core/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "canvas": { 3 | "prod": "https://.instructure.com/api/v1/", 4 | "test": "https://.test.instructure.com/api/v1/", 5 | "beta": "https://.beta.instructure.com/api/v1/", 6 | "token": "" 7 | "root_account": "", 8 | "academic_account": "" 9 | }, 10 | "cmi": { 11 | "host": "", 12 | "user": "", 13 | "pw": "", 14 | "db": "" 15 | }, 16 | "powercampus": { 17 | "connection_string": "DRIVER={SQL Server};SERVER=POWERCAMPUS2;DATABASE=CAMPUS6;Trusted_Connection=yes" 18 | }, 19 | "directories": { 20 | "output_dir": "", 21 | "sql_dir": "" 22 | }, 23 | "aws": { 24 | "aws_access_key_id": "", 25 | "aws_secret_access_key": "" 26 | } 27 | } -------------------------------------------------------------------------------- /core/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | env = 'test' if input('Enter=PROD, or TEST? ').lower() == 'test' else 'prod' 4 | print(env) 5 | 6 | with open('..\\core\\config.json') as config_file: 7 | config = json.load(config_file) 8 | 9 | # canvas 10 | base_url = config['canvas'][env] 11 | token = config['canvas']['token'] 12 | auth_header = {'Authorization': 'Bearer {}'.format(token)} 13 | root_account = config['canvas']['root_account'] 14 | academic_account = config['canvas']['academic_account'] 15 | 16 | 17 | # CMI 18 | cmi_host = config['cmi']['host'] 19 | cmi_user = config['cmi']['user'] 20 | cmi_pw = config['cmi']['pw'] 21 | cmi_db = config['cmi']['db'] 22 | 23 | # PowerCampus 24 | powercampus_connection_string = config['powercampus']['connection_string'] 25 | 26 | # directories 27 | sql_dir = config['directories']['sql_dir'] 28 | output_dir = config['directories']['output_dir'] 29 | 30 | # AWS 31 | aws_access_key_id = config['aws']['aws_access_key_id'] 32 | aws_secret_access_key = config['aws']['aws_secret_access_key'] 33 | -------------------------------------------------------------------------------- /core/courses.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import re 3 | 4 | import canvas.core.api as api 5 | from canvas.core.accounts import all_programs, program_account 6 | from canvas.core.io import wait 7 | from canvas.core.terms import all_terms, term_id_from_name 8 | 9 | 10 | def course_front_page_set(course_id): 11 | """ return true if a front page has been set for a course """ 12 | for page in get_course_pages(course_id): 13 | if page['front_page']: 14 | return True 15 | return False 16 | 17 | 18 | def course_is_published(course): 19 | """ return true if a course is published """ 20 | return course['workflow_state'] in ['available', 'completed'] 21 | 22 | 23 | def get_course(course_id): 24 | """ return a course """ 25 | return api.get('courses/{}'.format(course_id)).json() 26 | 27 | 28 | def get_course_announcements(course_id): 29 | """ return a list of announcements """ 30 | return api.get_list('courses/{}/discussion_topics?only_announcements=true'.format(course_id)) 31 | 32 | 33 | def get_course_by_sis_id(sis_id): 34 | """ return one course for an sis id """ 35 | return api.get('courses/sis_course_id:{}'.format(sis_id)).json() 36 | 37 | 38 | def get_course_conferences(course_id): 39 | """ return a list of a course's conferences """ 40 | return api.get('courses/{}/conferences'.format(course_id)).json()['conferences'] 41 | 42 | 43 | def get_course_files(course_id): 44 | """ return a list of a course's files """ 45 | return api.get_list('courses/{}/files'.format(course_id)) 46 | 47 | 48 | def get_course_front_page(course_id): 49 | """ return a course's front page """ 50 | return api.get('courses/{}/front_page'.format(course_id)).json() 51 | 52 | 53 | def get_course_modules(course_id): 54 | """ return a list of a course's modules with its items [api won't return items if too many] """ 55 | return sorted(api.get_list('courses/{}/modules?include[]=items'.format(course_id)), key=lambda m: m['position']) 56 | 57 | 58 | def get_course_module_items(course_id, course_module): 59 | """ return a list of a module's items [if api didn't return items, make explicit call to get them] """ 60 | if 'items' in course_module: 61 | return course_module['items'] 62 | else: 63 | return sorted(api.get_list('courses/{}/modules/{}/items' 64 | .format(course_id, course_module['id'])), key=lambda m: m['position']) 65 | 66 | 67 | def get_course_page(course_id, page_url): 68 | """ get a course page """ 69 | return api.get('courses/{}/pages/{}'.format(course_id, page_url)).json() 70 | 71 | 72 | def get_course_pages(course_id): 73 | """ return a list of a course's pages 74 | NOTE: api doesn't include page['body'] when requesting list """ 75 | return api.get_list('courses/{}/pages'.format(course_id)) 76 | 77 | 78 | def get_course_people(course_id, role): 79 | """ return a list of a course's people with a given role 80 | https://canvas.instructure.com/doc/api/all_resources.html#method.courses.users """ 81 | if role in ['teacher', 'student', 'student_view', 'ta', 'observer', 'designer']: 82 | people = api.get_list('courses/{}/users?include[]=email&enrollment_type[]={}'.format(course_id, role)) 83 | return [] if 'errors' in people else people 84 | else: 85 | # e.g. Faculty of record 86 | people = [] 87 | for person in api.get_list('courses/{}/users?include[]=email&include[]=enrollments'.format(course_id)): 88 | for enrollment in person['enrollments']: 89 | if enrollment['role'] == role: 90 | del person['enrollments'] 91 | people.append(person) 92 | break 93 | return people 94 | 95 | 96 | def get_course_sections(course_id): 97 | """ return a list of a course's sections """ 98 | return sorted(api.get_list('courses/{}/sections'.format(course_id)), key=lambda s: s['name']) 99 | 100 | 101 | def get_course_syllabus_body(course_id): 102 | """ return a course's syllabus body html """ 103 | return api.get('courses/{}?include[]=syllabus_body'.format(course_id)).json()['syllabus_body'] 104 | 105 | 106 | def get_course_tabs(course_id): 107 | """ return a list of a course's navigation tabs """ 108 | return api.get_list('courses/{}/tabs'.format(course_id)) 109 | 110 | 111 | def get_courses(terms, programs, synergis, whitelist=None): 112 | """ yield a course augmented with sis info from a sorted list of courses for a list of terms & programs 113 | gratitude to http://nedbatchelder.com/text/iter.html#h_customizing_iteration 114 | NOTE: api returns courses in a subaccount AND ITS SUBACCOUNTS """ 115 | 116 | if whitelist: 117 | for course_sis_id in sorted(whitelist): 118 | course = get_course_by_sis_id(course_sis_id) 119 | course_sis_info = validate_course(course) 120 | if course_sis_info: 121 | course['course_sis_info'] = course_sis_info 122 | yield course 123 | else: 124 | print('>>> no course for {}'.format(course_sis_id)) 125 | else: 126 | for term in terms or all_terms(): 127 | print(term, '-' * 70) 128 | for program in programs or all_programs(synergis): 129 | print(program, '-' * 70) 130 | courses = api.get_list('accounts/{}/courses?enrollment_term_id={}' 131 | .format(program_account(program), term_id_from_name(term))) 132 | for course in sorted([course for course in courses if course['sis_course_id']], 133 | key=operator.itemgetter('sis_course_id')): 134 | course_sis_info = validate_course(course) 135 | if course_sis_info: 136 | course['course_sis_info'] = course_sis_info 137 | yield course 138 | 139 | 140 | def parse_course_sis(course_sis_id): 141 | """ return a dict of the info in a course sis id """ 142 | 143 | if not course_sis_id: 144 | return None 145 | 146 | # new format implemented 2016.02.08 147 | new_format_match = re.match(r'(.*)-(.*)-(.*)-(.*)-(.*)-(.*)-(.*)-(.*)', course_sis_id) 148 | if new_format_match: 149 | program = new_format_match.group(4) 150 | return {"term": '{}-{}'.format(new_format_match.group(1), new_format_match.group(2)), 151 | "session": new_format_match.group(3), 152 | "program": program, 153 | "number": ('N' if program.startswith('N') else program) + new_format_match.group(5), 154 | "type": new_format_match.group(6), 155 | "campus": new_format_match.group(7), 156 | "section": new_format_match.group(8)} 157 | 158 | old_format_match = re.match(r'(.*)-(.*)-(.*)-(.*)-(.*)-(.*)-(.*)', course_sis_id) 159 | if old_format_match: 160 | program = old_format_match.group(4) 161 | number_prefix = { 162 | 'BSCI': 'BSCI', 163 | 'GENED': 'NGE', 164 | 'IPE': 'IPE', 165 | 'NURSG': 'N', 166 | 'OCCTH': 'OT', 167 | 'PA': 'PA', 168 | 'PM': 'PM', 169 | 'PHYTH': 'PT' 170 | }[program] 171 | return {"term": old_format_match.group(1), 172 | "campus": old_format_match.group(2), 173 | "session": old_format_match.group(3), 174 | "program": program, 175 | "number": number_prefix + old_format_match.group(5), 176 | "type": old_format_match.group(6), 177 | "section": old_format_match.group(7)} 178 | 179 | return None 180 | 181 | 182 | def update_course_page_contents(course_id, page_url, body): 183 | """ update a course page's content """ 184 | req_data = {'wiki_page[body]': body} 185 | return api.put('courses/{}/pages/{}'.format(course_id, page_url), req_data) 186 | 187 | 188 | def update_tab(course_id, tab_id, position, hidden): 189 | """ update a course navigation tab and return the updated tab """ 190 | req_data = {'position': position, 'hidden': hidden} 191 | return api.put('courses/{}/tabs/{}'.format(course_id, tab_id), req_data).json() 192 | 193 | 194 | def validate_course(course): 195 | """ if valid, return dict of course info from SIS ID, else return None """ 196 | 197 | if 'sis_course_id' not in course: 198 | return None 199 | 200 | # get the course id & sis id 201 | course_sis_id = course['sis_course_id'] 202 | if not course_sis_id: 203 | print('>>> no sis id for https://samuelmerritt.instructure.com/courses/{}'.format(course['id'])) 204 | return None 205 | 206 | # derive course info from the sis id 207 | course_sis_info = parse_course_sis(course_sis_id) 208 | if not course_sis_info: 209 | print('>>> bad sis id: {}'.format(course_sis_id)) 210 | return None 211 | 212 | # alert & skip if course is cross-listed across programs 213 | for section in get_course_sections(course['id']): 214 | if section['nonxlist_course_id']: 215 | nonxlist_course_sis_id = get_course(section['nonxlist_course_id'])['sis_course_id'] 216 | section_course_info = parse_course_sis(nonxlist_course_sis_id) 217 | if section_course_info['program'] != course_sis_info['program']: 218 | print('{} >>> cross-listed across programs with {}'.format(course_sis_id, nonxlist_course_sis_id)) 219 | return None 220 | 221 | return course_sis_info 222 | 223 | 224 | def create_course(account_id, code, name): 225 | """ create a new course and return the new course """ 226 | req_data = { 227 | 'course[course_code]': code, 228 | 'course[name]': name 229 | } 230 | return api.post('accounts/{}/courses'.format(account_id), req_data).json() 231 | 232 | 233 | def create_enrollment(course_id, canvas_or_sis_user_id, user_id, role, notify=False): 234 | """ enroll a user in a course 235 | https://canvas.instructure.com/doc/api/enrollments.html#method.enrollments_api.create 236 | """ 237 | req_data = { 238 | 'enrollment[user_id]': user_id if canvas_or_sis_user_id == 'canvas' else 'sis_user_id:' + user_id, 239 | 'enrollment[enrollment_state]': 'active', 240 | 'enrollment[notify]': notify 241 | } 242 | role_translate = {'student': 'StudentEnrollment', 'teacher': 'TeacherEnrollment', 'ta': 'TaEnrollment', 243 | 'observer': 'ObserverEnrollment', 'designer': 'DesignerEnrollment'} 244 | if role in role_translate: 245 | req_data['enrollment[type]'] = role_translate[role] 246 | else: 247 | req_data['enrollment[role_id]'] = role 248 | 249 | print(api.post('courses/{}/enrollments'.format(course_id), req_data).json()) 250 | 251 | 252 | def copy_course(source_course_id, dest_course_id): 253 | """ copy course content from source to dest 254 | https://canvas.instructure.com/doc/api/content_migrations.html#method.content_migrations.create 255 | https://community.canvaslms.com/thread/2454#comment-9347 256 | """ 257 | req_data = { 258 | 'migration_type': 'course_copy_importer', 259 | 'settings[source_course_id]': source_course_id 260 | } 261 | progress_url = api.post('courses/{}/content_migrations'.format(dest_course_id), req_data).json()['progress_url'] 262 | progress = 0 263 | while progress < 100: 264 | progress = api.get(progress_url).json()['completion'] 265 | wait('\tcopy progress: {}%'.format(int(progress)), 5) 266 | print('\r\tcopy completed') 267 | 268 | 269 | def delete_course(course_id): 270 | """ delete a course """ 271 | req_data = {'event': 'delete'} 272 | api.delete('courses/{}'.format(course_id), req_data) 273 | 274 | 275 | def reset_course(course_id): 276 | """ reset a course's content (deletes the course and creates a new course); return the new course """ 277 | req_data = {} 278 | return api.post('courses/{}/reset_content'.format(course_id), req_data).json() 279 | 280 | 281 | def rename_course(course_id, name, code): 282 | """ update a course's name and/or code """ 283 | req_data = {} 284 | if name: 285 | req_data['course[name]'] = name 286 | if code: 287 | req_data['course[code]'] = code 288 | if req_data: 289 | api.put('courses/{}'.format(course_id), req_data) 290 | 291 | 292 | def get_courses_by_account_id(account_id, term): 293 | """ return a list of courses for a program by its account id """ 294 | return sorted(api.get_list('accounts/{}/courses?enrollment_term_id={}'.format(account_id, term_id_from_name(term))), 295 | key=lambda c: c['name']) 296 | 297 | 298 | def get_onl_masters(program): 299 | account_id = {'NFNPO': '168920', 'NCMO': '168922'}[program] 300 | return sorted([course for course in get_courses_by_account_id(account_id, 'DEFAULT') 301 | if course['name'].startswith('N') and course['name'].endswith(' - MASTER')], key=lambda c: c['name']) 302 | -------------------------------------------------------------------------------- /core/etc.py: -------------------------------------------------------------------------------- 1 | def scrub(text): 2 | """ return a string with targeted non-ascii characters replaced, all others replaced with *, white space trimmed """ 3 | # """ return a string with non-ascii characters and consecutive spaces converted to one space """ 4 | # if not text: 5 | # return '' 6 | # # text = ''.join([i if 31 < ord(i) < 127 else ' ' for i in text]) 7 | # # convert consecutive non-ascii characters to one space & clean up html characters 8 | # text = re.sub(r'[^\x1F-\x7F]+', ' ', text) \ 9 | # .replace('&', '&').replace('"', '"').replace('

', '').replace('

', '') 10 | # # remove leading, trailing, and repeated spaces 11 | # return ' '.join(text.split()) 12 | 13 | # pairs = { 14 | # '&': '&', 15 | # '"': '"', 16 | # '

': '', 17 | # '

': '' 18 | # '\u00E2\u0080\u0099': "'", 19 | # '\u0009': ' ', # tab 20 | # '\u002D': '-', # - 21 | # '\u00AB': '"', # � 22 | # '\u00BB': '"', # � 23 | # '\u2013': '-', # � 24 | # '\u2014': '-', # � 25 | # '\u2018': "'", # � 26 | # '\u2019': "'", # � 27 | # '\u201A': "'", # � 28 | # '\u201B': "'", # ? 29 | # '\u201C': '"', # � 30 | # '\u201D': '"', # � 31 | # '\u201E': '"', # � 32 | # '\u201F': '"', # ? 33 | # '\u2022': '', # � 34 | # '\u2026': '...', # � 35 | # '\u2039': '<', # � 36 | # '\u203A': '>', # � 37 | # '\u2264': '<=' # ? 38 | # bullets? 39 | # } 40 | # scrubbed = text 41 | # for crap in pairs: 42 | # scrubbed = scrubbed.replace(crap, pairs[crap]) 43 | # scrubbed = re.sub(r'[^\x1F-\x7F]+', '*', scrubbed) 44 | return ' '.join(text.replace('&', '&').replace('"', '"').replace('

', '').replace('

', '').split()) 45 | 46 | 47 | def quote(text): 48 | return '"{}"'.format(text) 49 | 50 | 51 | def make_leader(program, course_sis_id): 52 | return '{: <12}{: <40}'.format(program, course_sis_id) 53 | 54 | 55 | class DictDiffer(object): 56 | """ 57 | A dictionary difference calculator from http://www.stackoverflow.com/questions/1165352#1165552 58 | 59 | Calculate the difference between two dictionaries as: 60 | (1) items added 61 | (2) items deleted 62 | (3) keys same in both but changed values 63 | (4) keys same in both and unchanged values 64 | """ 65 | 66 | def __init__(self, current_dict, past_dict): 67 | self.current_dict, self.past_dict = current_dict, past_dict 68 | self.current_keys, self.past_keys = [set(d.keys()) for d in (current_dict, past_dict)] 69 | self.intersect = self.current_keys.intersection(self.past_keys) 70 | 71 | def added(self): 72 | return self.current_keys - self.intersect 73 | 74 | def deleted(self): 75 | return self.past_keys - self.intersect 76 | 77 | def changed(self): 78 | return set(o for o in self.intersect if self.past_dict[o] != self.current_dict[o]) 79 | 80 | def unchanged(self): 81 | return set(o for o in self.intersect if self.past_dict[o] == self.current_dict[o]) 82 | -------------------------------------------------------------------------------- /core/io.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import time 3 | import winsound 4 | 5 | import pymysql 6 | import pypyodbc 7 | import xlsxwriter 8 | 9 | from canvas.core import config 10 | from canvas.core.accounts import cmi_program, program_account 11 | from canvas.core.terms import term_id_from_name 12 | import canvas.core.api as api 13 | 14 | 15 | def get_db_query_results(db, query): 16 | """ connect to database; execute query; return a list of the results and a list of column names """ 17 | if db == 'cmi': 18 | connection = pymysql.connect(config.cmi_host, config.cmi_user, config.cmi_pw, config.cmi_db) 19 | elif db == 'powercampus': 20 | connection = pypyodbc.connect(config.powercampus_connection_string) 21 | else: 22 | return None 23 | cursor = connection.cursor() 24 | cursor.execute(query) 25 | column_names = [column[0] for column in cursor.description] 26 | rows = [list(row) for row in cursor.fetchall()] 27 | cursor.close() 28 | connection.close() 29 | # return [dict(zip(column_names, list(row))) for row in rows] 30 | return rows, column_names 31 | 32 | 33 | def get_cmi_clos_by_program(canvas_program): 34 | query = ('SELECT c.CourseID AS course_id, c.CLO_ID AS clo_title, c.CLO AS clo_description ' + 35 | 'FROM Courses c WHERE c.programID = "' + cmi_program(canvas_program) + 36 | '" AND c.Active = 1 AND c.CLO NOT LIKE "%deleted%"') 37 | return [{'course_id': clo[0], 'clo_title': clo[1], 'clo_description': clo[2]} 38 | for clo in get_db_query_results('cmi', query)[0]] 39 | 40 | 41 | def get_cmi_clos_by_course(canvas_program, course_name): 42 | query = ('SELECT REPLACE(c.CLO_ID, "-", "" ) AS clo_title, c.CLO AS clo_description ' + 43 | 'FROM Courses c WHERE c.programID = "' + cmi_program(canvas_program) + 44 | '" AND REPLACE(c.CourseID, "-", "" ) = "' + course_name + 45 | '" AND c.Active = 1 AND c.CLO NOT LIKE "%deleted%"') 46 | return [{'clo_title': clo[0], 'clo_description': clo[1]} for clo in get_db_query_results('cmi', query)[0]] 47 | 48 | 49 | def get_cmi_plos_by_program(canvas_program): 50 | query = ('SELECT p.SLOID AS plo_title, p.SLOName AS plo_description FROM programSLOList p WHERE p.programID = "' + 51 | cmi_program(canvas_program) + '" AND p.Active = 1') 52 | return [{'plo_title': plo[0], 'plo_description': plo[1]} for plo in get_db_query_results('cmi', query)[0]] 53 | 54 | 55 | def run_sql_file(db, sql_file_name, sql_dir=config.sql_dir): 56 | """ run sql from a file, return rows and column names """ 57 | with open(sql_dir + sql_file_name) as sql_file: 58 | query = sql_file.read() 59 | return get_db_query_results(db, query) 60 | 61 | 62 | def write_csv_file(file_name, header_row, data_rows, out_dir=config.output_dir): 63 | """ write a header row and all data rows to a csv file """ 64 | with open(out_dir + file_name, 'w', newline='') as csv_file: 65 | csv_writer = csv.writer(csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) 66 | csv_writer.writerow(header_row) 67 | for row in data_rows: 68 | csv_writer.writerow(row) 69 | 70 | 71 | def write_xlsx_file(file_name, header_row, data_rows): 72 | """ write a table to an xlsx file per http://xlsxwriter.readthedocs.org/working_with_tables.html """ 73 | workbook = xlsxwriter.Workbook('{}{}.xlsx'.format(config.output_dir, file_name)) 74 | workbook.add_worksheet().add_table(0, 0, len(data_rows), len(header_row) - 1, 75 | {'data': data_rows, 'columns': [{'header': i} for i in header_row]}) 76 | workbook.close() 77 | 78 | 79 | def sis_import(sis_data): 80 | # https://github.com/kajigga/canvas-contrib/blob/master/SIS%20Integration/python_requestlib/import_csv.py 81 | # https://github.com/bryanpearson/canvas-sis-python/blob/master/canvas_sis_import.py 82 | get_url = 'accounts/{}/sis_imports'.format(config.root_account) 83 | post_url = get_url + '?import_type=instructure_csv&extension=csv' 84 | r = api.post(post_url, sis_data) 85 | while not api.get('{}/{}'.format(get_url, str(r.json()['id']))).json()['ended_at']: 86 | wait('\twaiting for import confirmation', 30) 87 | 88 | 89 | def get_sis_report(report_type, account, term, mode): 90 | """ request sis report; return list of column names & list of results """ 91 | # https://github.com/unsupported/canvas/tree/master/api/run_reports/provisioning_report/python 92 | # https://community.canvaslms.com/thread/11942 93 | account_id = program_account(account) 94 | req_data = {'parameters[enrollment_term_id]': term_id_from_name(term), 'parameters[{}]'.format(report_type): 1} 95 | report_id = api.post('accounts/{}/reports/sis_export_csv'.format(account_id), req_data).json()['id'] 96 | progress = 0 97 | while progress < 100: 98 | response = api.get('accounts/{}/reports/sis_export_csv/{}'.format(account_id, report_id)) 99 | json = response.json() 100 | progress = json['progress'] 101 | wait('\tprogress: {}%'.format(progress), 5) 102 | content = api.get_file(json['attachment']['url']).content 103 | if mode == 'file': 104 | with open(config.output_dir + json['attachment']['filename'], 'wb') as file: 105 | file.write(content) 106 | rows = [x for x in sorted(csv.reader(str(content, 'utf-8').split('\n')))] 107 | # remove extra EOL as last line 108 | return rows[0], rows[1:-1] 109 | 110 | 111 | def wait(msg, interval): 112 | if msg: 113 | print('\r{}....'.format(msg), end="") 114 | time.sleep(interval) 115 | 116 | 117 | def tada(): 118 | winsound.Beep(700, 500) 119 | winsound.Beep(800, 500) 120 | winsound.Beep(900, 500) 121 | 122 | 123 | def print_pretty(*argv): 124 | print('{} {: >19} {: <17} {}'.format(*argv)) 125 | -------------------------------------------------------------------------------- /core/outcome_groups.py: -------------------------------------------------------------------------------- 1 | import canvas.core.api as api 2 | 3 | 4 | def get_outcome_group(context, context_id, group_id): 5 | """ return an outcome group """ 6 | return api.get('{}/{}/outcome_groups/{}'.format(context, context_id, group_id)).json() 7 | 8 | 9 | def get_program_groups(account): 10 | """ return a dict of course numbers and the id of their program-level outcome folders """ 11 | course_folders = {} 12 | program_root_group = get_root_group('accounts', account) 13 | for course_folder in get_subgroups('accounts', account, program_root_group): 14 | # # if "N129/129L" format, add both N129 & N129L 15 | # splits = course_folder['title'].split('/') 16 | # course_folders[splits[0]] = course_folder['id'] 17 | # if len(splits) == 2: 18 | # course_number_stem = re.match(r'^[a-zA-Z]+', splits[0]).group(0) 19 | # course_folders[course_number_stem + splits[1]] = course_folder['id'] 20 | course_folders[course_folder['title']] = course_folder['id'] 21 | return course_folders 22 | 23 | 24 | def get_root_group(context, context_id): 25 | """ return the id of a root group """ 26 | return str(api.get('{}/{}/root_outcome_group'.format(context, context_id)).json()['id']) 27 | 28 | 29 | def get_subgroups(context, context_id, group_id): 30 | """ return a list of a group's subgroups """ 31 | return api.get_list('{}/{}/outcome_groups/{}/subgroups/'.format(context, context_id, group_id)) 32 | 33 | 34 | def create_group(context, context_id, parent_group_id, req_data): 35 | """ create a group and return the group id """ 36 | return str(api.post('{}/{}/outcome_groups/{}/subgroups/'.format(context, context_id, parent_group_id), 37 | req_data).json()['id']) 38 | 39 | 40 | def link_group(context, context_id, parent_group_id, child_group_id): 41 | """ link a group into a group and return the linked group id """ 42 | req_data = {'source_outcome_group_id': child_group_id} 43 | return str(api.post('{}/{}/outcome_groups/{}/import/'.format(context, context_id, parent_group_id), 44 | req_data).json()['id']) 45 | 46 | 47 | def relink_group(context, context_id, group_id, new_parent_group_id): 48 | """ relink a group into another group and return the group id """ 49 | req_data = {'parent_outcome_group_id': str(new_parent_group_id)} 50 | return str(api.put('{}/{}/outcome_groups/{}/'.format(context, context_id, group_id), req_data).json()['id']) 51 | 52 | 53 | def delete_group(context, context_id, group_id): 54 | """ delete a group and return the group """ 55 | return api.delete('{}/{}/outcome_groups/{}/'.format(context, context_id, group_id)).json() 56 | -------------------------------------------------------------------------------- /core/outcomes.py: -------------------------------------------------------------------------------- 1 | import canvas.core.api as api 2 | 3 | 4 | def get_outcome(outcome_id): 5 | """ return an outcome """ 6 | return api.get('outcomes/{}'.format(outcome_id)).json() 7 | 8 | 9 | def get_outcomes(context, context_id, group_id): 10 | """ return a list of outcomes 11 | calls get_outcome() for full outcome, as get_outcome_links() returns outcome links with abbreviated outcomes """ 12 | links = get_outcome_links(context, context_id, group_id) 13 | return [get_outcome(link['outcome']['id']) for link in links] if links else [] 14 | 15 | 16 | def get_all_outcome_links(context, context_id): 17 | """ return a list of all outcome links for an entire context 18 | BETA: https://canvas.instructure.com/doc/api/outcome_groups.html#method.outcome_groups_api.link_index """ 19 | return api.get_list('{}/{}/outcome_group_links'.format(context, context_id)) 20 | 21 | 22 | def get_outcome_links(context, context_id, group_id): 23 | """ return a list of outcome links for a group """ 24 | return api.get_list('{}/{}/outcome_groups/{}/outcomes'.format(context, context_id, group_id)) 25 | 26 | 27 | def link_new_outcome(context, context_id, group_id, title, description): 28 | """ create a new outcome, link it, and return the outcome id """ 29 | req_data = { 30 | 'title': 'CLO ' + title, 31 | 'description': description, 32 | 'mastery_points': 1, 33 | 'ratings[][description]': 'Aligned', 34 | 'ratings[][points]': 1 35 | } 36 | return str(api.post('{}/{}/outcome_groups/{}/outcomes'.format(context, context_id, group_id), req_data) 37 | .json()['outcome']['id']) 38 | 39 | 40 | def link_outcome(context, context_id, group_id, outcome_id): 41 | """ link an existing outcome into a group and return the outcome link """ 42 | return api.put('{}/{}/outcome_groups/{}/outcomes/{}'.format(context, context_id, group_id, outcome_id), '').json() 43 | 44 | 45 | def outcome_came_from_cmi(outcome_title, course_number): 46 | return course_number + '_' in outcome_title 47 | 48 | 49 | def update_outcome_title(outcome_id, title): 50 | """ update an outcome's title and return the request result """ 51 | req_data = {'title': title} 52 | result = api.put('outcomes/{}'.format(outcome_id), req_data).json() 53 | return 'errors' not in result and 'message' not in result 54 | 55 | 56 | def update_outcome_desc(outcome_id, description): 57 | """ update an outcome's description and return the request result """ 58 | req_data = {'description': description} 59 | result = api.put('outcomes/{}'.format(outcome_id), req_data).json() 60 | return 'errors' not in result and 'message' not in result 61 | 62 | 63 | def unlink_outcome(context, context_id, group_id, outcome_id): 64 | """ unlink an outcome from a group and return true if successful, false if not """ 65 | result = api.delete('{}/{}/outcome_groups/{}/outcomes/{}'.format(context, context_id, group_id, outcome_id)).json() 66 | return 'errors' not in result and 'message' not in result 67 | -------------------------------------------------------------------------------- /core/terms.py: -------------------------------------------------------------------------------- 1 | from sortedcontainers import SortedDict 2 | 3 | import canvas.core.api as api 4 | from canvas.core import config 5 | 6 | 7 | def term_data(): 8 | return SortedDict({ 9 | 'DEFAULT': 286, 10 | '2012-3FA': 2909, 11 | '2013-1SP': 4514, '2013-2SU': 4515, '2013-3FA': 4854, 12 | '2014-1SP': 5180, '2014-2SU': 5489, '2014-3FA': 5562, 13 | '2015-1SP': 6423, '2015-2SU': 6864, '2015-3FA': 6921, 14 | '2016-1SP': 7518, '2016-2SU': 10200, '2016-3FA': 10398, 15 | '2017-1SP': 10815, '2017-2SU': 11023, '2017-3FA': 11299, 16 | }) 17 | 18 | 19 | def term_id_from_name(term_name): 20 | """ return the id for a term name """ 21 | return term_data()[term_name] 22 | 23 | 24 | def all_terms(): 25 | return [term for term in term_data() if term != 'DEFAULT'] 26 | 27 | 28 | def get_canvas_terms(): 29 | return api.get('accounts/{}/terms?per_page=100'.format(config.root_account)).json()['enrollment_terms'] 30 | -------------------------------------------------------------------------------- /core/users.py: -------------------------------------------------------------------------------- 1 | from canvas.core import api as api 2 | 3 | 4 | def get_page_views(user, start_date, end_date): 5 | return api.get_list('users/{}/page_views?start_time="{}"&end_time="{}"'.format(user, start_date, end_date)) 6 | 7 | 8 | def get_user(user_id): 9 | return api.get('users/{}'.format(user_id)).json() 10 | 11 | 12 | def get_user_by_sis_id(sis_user_id): 13 | return api.get('users/sis_user_id:{}'.format(sis_user_id)).json() 14 | -------------------------------------------------------------------------------- /enrollments/batch_enroll_nursing_community_courses_WIP.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import create_enrollment, get_course_by_sis_id, get_course_people 2 | from canvas.core.io import tada 3 | from canvas.core.terms import term_id_from_name 4 | 5 | from canvas.core.api import get_list 6 | 7 | 8 | def batch_enroll_nursing_community_courses(): 9 | 10 | term = '2016-1SP' 11 | nursing_account = '98328' 12 | nursing_community_course_sis_ids = ['COMM-NURSG-DEPT', 'COMM-NURSG-FAC-RESOURCE', 'COMM-NURSG-GERI-PRO'] 13 | 14 | # get teachers enrolled in nursing courses 15 | account_courses = \ 16 | get_list('accounts/{}/courses?enrollment_term_id={}&enrollment_type[]=teacher&include[]=teachers' 17 | .format(nursing_account, term_id_from_name(term))) 18 | nursing_teachers = set() 19 | for course in account_courses: 20 | # print(course['sis_course_id']) 21 | # for teacher in course['teachers']: 22 | # print(teacher['display_name']) 23 | # nursing_teachers.add(teacher['id']) 24 | nursing_teachers.add([t['id'] for t in course['teachers']]) 25 | 26 | # enroll as student in each community course those nursing teachers not already enrolled 27 | for course_sis_id in nursing_community_course_sis_ids: 28 | course_id = get_course_by_sis_id(course_sis_id)['id'] 29 | # community_students = set() 30 | # for community_student in get_course_people(course_id, 'student'): 31 | # community_students.add(community_student['id']) 32 | community_students = set([s['id'] for s in get_course_people(course_id, 'student')]) 33 | # for teacher_id in nursing_teachers: 34 | # if teacher_id not in community_students: 35 | # # create_enrollment(course_id, user_id, 'student') 36 | # print(course_sis_id, nursing_teachers[teacher_id]) 37 | for teacher_id in nursing_teachers - community_students: 38 | create_enrollment(course_id, teacher_id, 'student') 39 | # print(course_sis_id, nursing_teachers[teacher_id]) 40 | 41 | if __name__ == '__main__': 42 | batch_enroll_nursing_community_courses() 43 | tada() 44 | -------------------------------------------------------------------------------- /enrollments/batch_enrollments.py: -------------------------------------------------------------------------------- 1 | from canvas.core import config 2 | from canvas.core.io import run_sql_file, tada, sis_import, write_csv_file 3 | 4 | 5 | def batch_enrollments(sql_filename, csv_filename): 6 | rows, column_names = run_sql_file('powercampus', sql_filename) 7 | write_csv_file(csv_filename, column_names, rows) 8 | records = open(config.output_dir + csv_filename, 'r').read() 9 | print('sending {} records returned by {}...'.format(len(rows), sql_filename)) 10 | sis_import(records) 11 | print('\n\timported!') 12 | 13 | 14 | batch_enrollments('batch_enrollments_1.sql', 'batch_enrollments_1.csv') 15 | batch_enrollments('batch_enrollments_2.sql', 'batch_enrollments_2.csv') 16 | tada() 17 | -------------------------------------------------------------------------------- /enrollments/enrollments_duplicate_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core import api 2 | from canvas.core.courses import get_courses 3 | 4 | course_whitelist = [] 5 | terms = ['2017-1SP'] 6 | programs = [] 7 | synergis = True 8 | for course in course_whitelist or get_courses(terms, programs, synergis): 9 | for person in api.get_list('courses/{}/users?include[]=enrollments'.format(course['id'])): 10 | if len(person['enrollments']) > 1: 11 | for enrollment in person['enrollments']: 12 | print('{}\t{}\t{}\t{}'.format(course['sis_course_id'], person['name'], enrollment['role'], 13 | enrollment['created_at'])) 14 | -------------------------------------------------------------------------------- /enrollments/fnpo_students_and_teachers.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import get_courses, get_course_people 2 | 3 | terms = ['2017-1SP'] 4 | programs = ['NFNPO'] 5 | synergis = True 6 | print('course number, course sis id, student, teachers') 7 | for course in get_courses(terms, programs, synergis): 8 | if course['course_sis_info']['number'] in ['N678L', 'N679L', 'N680L']: 9 | teachers = ', '.join([teacher['name'] for teacher in get_course_people(course['id'], 'teacher')]) 10 | for student in sorted(get_course_people(course['id'], 'student'), key=lambda s: s['name']): 11 | print('{}, {}, {}, {}'.format(course['course_sis_info']['number'], course['sis_course_id'], 12 | student['name'], teachers)) 13 | -------------------------------------------------------------------------------- /etc/alignments_summary.py: -------------------------------------------------------------------------------- 1 | from canvas.core.accounts import program_account, all_programs 2 | from canvas.core.courses import get_course_people, get_courses, parse_course_sis, validate_course 3 | from canvas.core.io import tada, write_csv_file, write_xlsx_file 4 | from canvas.core.terms import all_terms 5 | 6 | from canvas.core.assignments import get_assignment_groups 7 | 8 | 9 | def alignments_summarize(): 10 | # whitelist = ['2015FS-OAK-GRAD-PHYTH-756-LEC-1'] 11 | course_whitelist = [] 12 | programs = [] 13 | terms = ['2016SPRING'] 14 | rows_summary = [] 15 | rows_detail = [] 16 | max_teachers = 0 17 | for term in terms or all_terms(): 18 | term_id = term_id(term) 19 | for program in programs or all_programs('fnp_online_yes'): 20 | account = program_account(program) 21 | courses = get_courses(account, term_id) 22 | course_qty = len(courses) 23 | rubricked_course_qty = 0 24 | aligned_course_qty = 0 25 | total_assignment_qty = 0 26 | total_rubricked_assignment_qty = 0 27 | total_aligned_assignment_qty = 0 28 | rows_csv = [] 29 | for course in courses: 30 | 31 | if course_whitelist and course['sis_course_id'] not in course_whitelist or not validate_course(course): 32 | continue 33 | 34 | assignment_qty = 0 35 | rubricked_assignment_qty = 0 36 | aligned_assignment_qty = 0 37 | course_id = course['id'] 38 | for assignment_group in get_assignment_groups(course_id): 39 | assignment_qty += len(assignment_group['assignments']) 40 | for assignment in assignment_group['assignments']: 41 | if 'rubric' in assignment: 42 | rubricked_assignment_qty += 1 43 | for criterion in assignment['rubric']: 44 | if 'outcome_id' in criterion: 45 | aligned_assignment_qty += 1 46 | break 47 | 48 | course_sis_info = parse_course_sis(course['sis_course_id']) 49 | teachers = get_course_people(course_id, 'teacher') 50 | max_teachers = max(max_teachers, len(teachers)) 51 | row = [term, program, course_sis_info['number'], 52 | ', '.join(teacher['sortable_name'] for teacher in teachers), 53 | assignment_qty, rubricked_assignment_qty, aligned_assignment_qty] 54 | rows_csv.append(row) 55 | print(row) 56 | 57 | rubricked_course_qty += (rubricked_assignment_qty > 0) 58 | aligned_course_qty += (aligned_assignment_qty > 0) 59 | total_assignment_qty += assignment_qty 60 | total_rubricked_assignment_qty += rubricked_assignment_qty 61 | total_aligned_assignment_qty += aligned_assignment_qty 62 | 63 | rows_detail.append({ 64 | 'term': term, 65 | 'program': program, 66 | 'course_number': course_sis_info['number'], 67 | 'course_campus': course_sis_info['campus'], 68 | 'course_type': course_sis_info['type'], 69 | 'course_section': course_sis_info['section'], 70 | 'teachers': teachers, 71 | 'quantities': [assignment_qty, rubricked_assignment_qty, aligned_assignment_qty], 72 | 'booleans': ['No' if assignment_qty == 0 else 'Yes', 73 | 'No' if rubricked_assignment_qty == 0 else 'Yes', 74 | 'No' if aligned_assignment_qty == 0 else 'Yes']}) 75 | 76 | # write csv file with totals 77 | rows_csv.append([term, program, '', 'totals', total_assignment_qty, total_rubricked_assignment_qty, 78 | total_aligned_assignment_qty, course_qty, rubricked_course_qty, aligned_course_qty]) 79 | header = ['term', 'program', 'course', 'teachers', 'assignments', 'rubricked assignments', 80 | 'aligned assignments', 'courses', 'rubricked courses', 'aligned courses'] 81 | write_csv_file('alignments_{}_{}.csv'.format(term, program), header, rows_csv) 82 | 83 | rows_summary.append([term, program, 84 | total_assignment_qty, 85 | '{0:.0f}'.format(total_rubricked_assignment_qty / total_assignment_qty * 100) 86 | if total_assignment_qty > 0 else '0', 87 | '{0:.0f}'.format(total_aligned_assignment_qty / total_assignment_qty * 100) 88 | if total_assignment_qty > 0 else '0']) 89 | 90 | # write summary xlsx file [penny] 91 | header = ['term', 'program', 'assignments', '% with rubrics', '% aligned'] 92 | write_xlsx_file('alignments_{}_summary'.format(term), rows_summary, header) 93 | 94 | # write detail xlsx file without totals [nandini] 95 | rows_detail_out = [] 96 | for row in rows_detail: 97 | rows_detail_out.append([row['term'], row['program'], row['course_number'], row['course_campus'], 98 | row['course_type'], row['course_section']]) 99 | rows_detail_out[-1].extend([t['sortable_name'] for t in row['teachers']]) 100 | rows_detail_out[-1].extend([''] * (max_teachers - len(row['teachers']))) 101 | rows_detail_out[-1].extend(row['quantities']) 102 | rows_detail_out[-1].extend(row['booleans']) 103 | header_detail = ['term', 'program', 'course', 'campus', 'type', 'section'] 104 | header_detail.extend('teacher{0:0>2}'.format(i + 1) for i in range(max_teachers)) 105 | header_detail.extend(['assignments', 'rubricked assignments', 'aligned assignments', 106 | 'course has assignments in canvas', 'course has assignments with rubrics', 107 | 'course has assignments aligned to CLOs']) 108 | write_xlsx_file('alignments_{}_detail'.format(term), rows_detail_out, header_detail) 109 | 110 | 111 | if __name__ == '__main__': 112 | alignments_summarize() 113 | tada() 114 | -------------------------------------------------------------------------------- /etc/change_end_dates.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import get_courses 2 | import canvas.core.api as api 3 | 4 | 5 | terms = ['2017-2SU'] 6 | programs = ['NFNPO', 'NCMO'] 7 | synergis = True 8 | 9 | for course in get_courses(terms, programs, synergis, []): 10 | print(course['sis_course_id']) 11 | req_data = {'course[end_at]': '2017-08-21T23:59:59-07:00', 'course[restrict_enrollments_to_course_dates]': False} 12 | api.put('courses/{}/'.format(course['id']), req_data) 13 | -------------------------------------------------------------------------------- /etc/course_best_practices_inventory.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from canvas.core.accounts import program_account, all_programs 4 | from canvas.core.courses import get_course_people, get_courses, validate_course, get_course_modules, \ 5 | get_course_announcements, get_course_files, get_course_pages, get_course_syllabus_body, get_course_conferences, \ 6 | get_course_tabs 7 | from canvas.core.io import tada, write_xlsx_file 8 | from canvas.core.terms import term_id_from_name, all_terms 9 | 10 | from canvas.core.assignments import get_assignment_groups, assignment_is_graded 11 | 12 | 13 | ### add course_front_page_set() 14 | 15 | def course_inventory_and_alignments(): 16 | course_whitelist = [] 17 | terms = ['DEFAULT'] 18 | programs = [] 19 | 20 | for term in terms or all_terms(): 21 | 22 | term_id = term_id_from_name(term) 23 | 24 | for program in programs or all_programs('synergis_yes'): 25 | 26 | account = program_account(program) 27 | courses = get_courses(account, term_id) 28 | rows = [] 29 | max_teachers = 0 30 | 31 | for course in courses: 32 | 33 | course_sis_id = course['sis_course_id'] 34 | if course_whitelist and course_sis_id not in course_whitelist: 35 | continue 36 | 37 | # omit unpublished and clinical courses 38 | course_sis_info = validate_course(course) 39 | # if not course_sis_info or not course_is_published(course) or course_sis_info['number'].endswith('L'): 40 | if not course_sis_info: 41 | continue 42 | 43 | course_id = course['id'] 44 | 45 | # course-level conditions 46 | syllabus = get_course_syllabus_body(course_id) 47 | syllabus_has_link_or_length = syllabus is not None and ('href' in syllabus or len(syllabus) > 600) 48 | assignment_group_weights = course['apply_assignment_group_weights'] 49 | 50 | # course-level counts 51 | announcement_qty = len(get_course_announcements(course_id)) 52 | conference_qty = len(get_course_conferences(course_id)) 53 | file_qty = len(get_course_files(course_id)) 54 | page_qty = len(get_course_pages(course_id)) 55 | student_qty = len(get_course_people(course_id, 'student')) 56 | teachers = get_course_people(course_id, 'teacher') 57 | teacher_qty = len(teachers) 58 | 59 | # tabs 60 | tab_qty = 0 61 | tabs_pages_and_files_hidden = True 62 | for tab in get_course_tabs(course_id): 63 | if 'hidden' not in tab: 64 | tab_qty += 1 65 | if tab['label'] in ['Pages', 'Files']: 66 | tabs_pages_and_files_hidden = False 67 | 68 | # modules 69 | modules = get_course_modules(course_id) 70 | module_qty = len(modules) 71 | module_has_items_qty = 0 72 | for module in modules: 73 | module_has_items_qty += (module['items_count'] > 0) 74 | 75 | # assignment groups 76 | assignment_groups = get_assignment_groups(course_id) 77 | assignment_group_qty = len(assignment_groups) 78 | assignment_group_has_assignments_qty = 0 79 | 80 | # assignments 81 | assignment_qty = 0 82 | quiz_qty = 0 83 | discussion_qty = 0 84 | assignment_or_discussion_with_ok_body_qty = 0 85 | assignment_with_rubric_qty = 0 86 | assignment_with_alignments_qty = 0 87 | 88 | for assignment_group in assignment_groups: 89 | assignments = assignment_group['assignments'] 90 | assignment_group_has_assignments_qty += (len(assignments) > 0) 91 | 92 | for assignment in assignments: 93 | # print(course_sis_id, assignment['submission_types'], assignment['name'], 94 | # assignment['points_possible']) 95 | if not assignment_is_graded(assignment): 96 | continue 97 | if 'online_quiz' in assignment['submission_types']: 98 | assignment_type = 'quiz' 99 | quiz_qty += 1 100 | elif 'discussion_topic' in assignment['submission_types']: 101 | assignment_type = 'discussion' 102 | discussion_qty += 1 103 | else: 104 | assignment_type = 'assignment' 105 | assignment_qty += 1 106 | 107 | assignment_body = assignment['description'] 108 | if assignment_type != 'quiz' and assignment_body is not None and \ 109 | (len(assignment_body) > 100 or 'href' in assignment_body): 110 | assignment_or_discussion_with_ok_body_qty += 1 111 | 112 | if 'rubric' in assignment: 113 | assignment_with_rubric_qty += 1 114 | for criterion in assignment['rubric']: 115 | if 'outcome_id' in criterion: 116 | assignment_with_alignments_qty += 1 117 | break 118 | 119 | row = [ 120 | program, 121 | term, 122 | course_sis_info['session'], 123 | course_sis_info['campus'], 124 | course_sis_info['number'], 125 | course_sis_info['type'], 126 | course_sis_info['section'], 127 | course_sis_id, 128 | 'http://samuelmerritt.instructure.com/courses/{}'.format(course_id), 129 | tab_qty, 130 | tabs_pages_and_files_hidden, 131 | syllabus_has_link_or_length, 132 | module_qty, 133 | module_has_items_qty, 134 | assignment_group_weights, 135 | assignment_group_qty, 136 | assignment_group_has_assignments_qty, 137 | quiz_qty, 138 | assignment_qty, 139 | discussion_qty, 140 | assignment_or_discussion_with_ok_body_qty, 141 | assignment_with_rubric_qty, 142 | assignment_with_alignments_qty, 143 | announcement_qty, 144 | conference_qty, 145 | page_qty, 146 | file_qty, 147 | student_qty, 148 | teacher_qty 149 | ] 150 | row.extend([t['sortable_name'] for t in teachers]) 151 | print(row) 152 | rows.append(row) 153 | max_teachers = max(max_teachers, teacher_qty) 154 | 155 | header = [ 156 | 'program', 157 | 'term', 158 | 'session', 159 | 'campus', 160 | 'course number', 161 | 'type', 162 | 'section', 163 | 'course sis id', 164 | 'URL', 165 | 'tabs', 166 | 'tabs hidden: Pages & Files', 167 | 'syllabus has link', 168 | 'modules', 169 | 'modules with items', 170 | 'assignment group weights affect final grade', 171 | 'assignment groups', 172 | 'assignment groups with assignments', 173 | 'graded quizzes', 174 | 'graded assignments', 175 | 'graded discussions', 176 | 'graded assignments/discussions with body > 100 characters or link', 177 | 'graded assignments/discussions/quizzes with rubrics', 178 | 'graded assignments/discussions/quizzes with alignments', 179 | 'announcements', 180 | 'conferences', 181 | 'pages', 182 | 'files', 183 | 'students', 184 | 'teachers' 185 | ] 186 | # add teacher column headers and pad data rows 187 | header.extend('teacher{0:0>2}'.format(i + 1) for i in range(max_teachers)) 188 | for row in rows: 189 | row.extend([''] * (len(header) - len(row))) 190 | 191 | write_xlsx_file('canvas_course_inventory_{}_{}_{}' 192 | .format(term, program, datetime.now().strftime("%Y%m%d.%H%M%S")), header, rows) 193 | 194 | 195 | if __name__ == '__main__': 196 | course_inventory_and_alignments() 197 | tada() 198 | -------------------------------------------------------------------------------- /etc/eportfolios.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import create_course, create_enrollment, copy_course 2 | from canvas.core.io import tada 3 | 4 | from canvas.core.users import get_user_by_sis_id 5 | 6 | r_and_p_account_id = '146275' 7 | r_and_p_template_course_id = '1963445' 8 | sis_user_ids = ['P000040481', 'P000055601', 'P000068861', 'P000040372', 'P000069505', 'P000040283', 'P000040412', 9 | 'P000003248', 'P000075574', 'P000056769', 'P000064217', 'P000049558', 'P000064103', 'P000069775', 10 | 'P000006331', 'P000040574', 'P000106163', 'P000059358', 'P000040606', 'P000043808', 'P000108803', 11 | 'P000000212', 'P000105812', 'P000086630', 'P000064215', 'P000070340', 'P000069860', 'P000105448', 12 | 'P000054690', 'P000004541', 'P000040378', 'P000009891', 'P000106070', 'P000040504', 'P000040613', 13 | 'P000056504', 'P000040298', 'P000063249', 'P000056603', 'P000097470', 'P000040736', 'P000040402', 14 | 'P000040386', 'P000039252', 'P000039519', 'P000040633', 'P000105863', 'P000000205', 'P000023419', 15 | 'P000056600', 'P000040302', 'P000085673', 'P000059725', 'P000049423', 'P000050098', 'P000103897', 16 | 'P000002939', 'P000067085', 'P000040311', 'P000069288', 'P000069771', 'P000100575', 'P000040737', 17 | 'P000068849', 'P000098871', 'P000072676', 'P000001403', 'P000068101', 'P000082406', 'P000108497', 18 | 'P000041263', 'P000111981', 'P000075546', 'P000082208', 'P000059748', 'P000040539', 'P000039450', 19 | 'P000040430', 'P000098449', 'P000092634', 'P000083197', 'P000058289', 'P000000117', 'P000066320', 20 | 'P000082623', 'P000040636', 'P000062385', 'P000065050', 'P000063248', 'P000095767', 'P000088167', 21 | 'P000040557', 'P000047590', 'P000040531', 'P000040748', 'P000099972', 'P000074485', 'P000068631', 22 | 'P000058155', 'P000004225', 'P000088203', 'P000093330', 'P000056766', 'P000064484', 'P000082347', 23 | 'P000040573', 'P000076569', 'P000040532', 'P000000080', 'P000061604', 'P000107470', 'P000043871', 24 | 'P000070466', 'P000074484', 'P000062126', 'P000034023', 'P000088215', 'P000059786', 'P000040724', 25 | 'P000066056', 'P000110227', 'P000052981', 'P000082712', 'P000069214'] 26 | for sis_user_id in sis_user_ids: 27 | course_code = 'R&P ePortfolio: ' + get_user_by_sis_id(sis_user_id)['name'] 28 | print(course_code) 29 | #### changed create_course() to return course instead of course_id 2017.02.22, added "['id']", needs testing 30 | new_course_id = create_course(r_and_p_account_id, course_code, course_code)['id'] 31 | create_enrollment(new_course_id, 'sis', sis_user_id, 'teacher') 32 | copy_course(r_and_p_template_course_id, new_course_id) 33 | tada() 34 | -------------------------------------------------------------------------------- /etc/fnp_master_copies.py: -------------------------------------------------------------------------------- 1 | from canvas.core.accounts import program_account 2 | from canvas.core.courses import get_courses_by_account_id, create_course, copy_course, get_course_people, \ 3 | create_enrollment, delete_course, get_course, get_courses 4 | from canvas.core.io import tada 5 | 6 | term = '2017-2SU' 7 | course_whitelist = ['N626', 'N671', 'N675L'] 8 | 9 | masters_account_id = '168920' # DEV/FNPONL subaccount 10 | copies_account_id = '170513' # DEV/FNPONL/MASTER COPIES subaccount 11 | master_copier_role_id = '3630' 12 | 13 | # get FNP courses for the term [convert generator to list for multiple use in fnp_targets] 14 | all_fnp_courses = list(get_courses([term], ['NFNP'], False)) 15 | 16 | # delete existing master copies 17 | for copy in get_courses_by_account_id(copies_account_id, 'DEFAULT'): 18 | if not course_whitelist: 19 | # delete_course(copy['id']) 20 | print('deleted ' + copy['name']) 21 | else: 22 | for course_name in course_whitelist: 23 | if course_name == copy['name'].split(' ')[4]: 24 | # delete_course(copy['id']) 25 | print('deleted ' + copy['name']) 26 | break 27 | 28 | for master in get_courses_by_account_id(masters_account_id, 'DEFAULT'): 29 | 30 | # get the master details 31 | master_name = master['name'] 32 | if not master_name.startswith('N') or not master_name.endswith(' - MASTER'): 33 | continue 34 | 35 | course_number = master_name.split(' ')[0] 36 | if course_whitelist and course_number not in course_whitelist: 37 | continue 38 | 39 | print('master: ' + master_name) 40 | 41 | # create a master copy & import the master's content into it 42 | # copy_name = 'MASTER COPY FNP ONL ' + master['name'] 43 | # copy_id = create_course(copies_account_id, copy_name, copy_name)['id'] 44 | # copy_course(master['id'], copy_id) 45 | 46 | # get the courses in the FNP subaccount with the same course number 47 | fnp_targets = [f for f in all_fnp_courses if course_number + ' ' in f['course_code']] 48 | if not fnp_targets: 49 | print('\t>>> NO FNP COURSES') 50 | continue 51 | 52 | # enroll the teachers of each FNP course in the master copy with notification 53 | for fnp_target in fnp_targets: 54 | fnp_target_id = fnp_target['id'] 55 | fnp_target_sis_id = fnp_target['sis_course_id'] 56 | teachers = get_course_people(fnp_target_id, 'teacher') 57 | fors = get_course_people(fnp_target_id, 'Faculty of record') 58 | teachers.extend([f for f in fors if f['id'] not in [t['id'] for t in teachers]]) 59 | if not teachers: 60 | print('>>> NO TEACHERS: ' + fnp_target_sis_id) 61 | for teacher in teachers: 62 | print('\tenrolling {} from {}'.format(teacher['name'], fnp_target_sis_id)) 63 | # create_enrollment(copy_id, 'canvas', teacher['id'], master_copier_role_id, True) 64 | 65 | tada() -------------------------------------------------------------------------------- /etc/grades_export_BROKEN.py: -------------------------------------------------------------------------------- 1 | # 2 | # review https://canvas.instructure.com/doc/api/enrollments.html#method.enrollments_api.index, which returns grades 3 | # 4 | 5 | from canvas.core.accounts import program_account, all_programs 6 | from canvas.core.courses import get_course_people, get_courses, validate_course 7 | from canvas.core.io import tada, write_csv_file 8 | 9 | from canvas.core.assignments import get_assignment_student_grades, get_assignments 10 | from canvas.core.assignments import get_assignments 11 | 12 | 13 | def grades_export(): 14 | 15 | #course_whitelist = ['2015SS-OAK-GENERAL-NURSG-128-LEC1-1'] 16 | course_whitelist = [] 17 | programs = ['BSN'] 18 | term = '2015SPRING' 19 | term_id = term_id(term) 20 | 21 | for program in programs or all_programs('fnp_online_yes'): 22 | rows = [] 23 | account = program_account(program) 24 | for course in get_courses(account, term_id): 25 | 26 | # skip unpublished courses 27 | if course['workflow_state'] not in ['available', 'completed']: 28 | continue 29 | 30 | if course_whitelist and course['sis_course_id'] not in course_whitelist: 31 | continue 32 | 33 | course_number = validate_course(course, program) 34 | if not course_number: 35 | continue 36 | 37 | course_id = course['id'] 38 | 39 | # store each assignment's type 40 | assignment_type = {} 41 | for assignment in get_assignments(course_id): 42 | assignment_type[assignment['id']] = assignment["submission_types"] 43 | 44 | # get each student's assignments & grades 45 | for student in get_course_people(course_id, 'student'): 46 | assignment_grades = get_assignment_student_grades(course_id, student['id']) 47 | for assignment_grade in assignment_grades: 48 | if 'submission' in assignment_grade: 49 | submission_time = assignment_grade['submission']['submitted_at'] 50 | submission_score = assignment_grade['submission']['score'] 51 | else: 52 | submission_score = 'NULL' 53 | submission_time = 'NULL' 54 | 55 | row = [program, 56 | course['sis_course_id'], 57 | student['sortable_name'], 58 | student['sis_user_id'], 59 | assignment_type[assignment_grade['assignment_id']], 60 | assignment_grade['title'], 61 | submission_time, 62 | submission_score, 63 | assignment_grade['points_possible'] 64 | ] 65 | print(row) 66 | rows.append(row) 67 | ################# 68 | break 69 | ################# 70 | write_csv_file('canvas_grades_{}_{}.csv'.format(term, program), 71 | ['program', 72 | 'course', 73 | 'student name', 74 | 'student id', 75 | 'activity type', 76 | 'activity name', 77 | 'activity datetime', 78 | 'points received', 79 | 'points possible'], 80 | rows) 81 | 82 | 83 | if __name__ == '__main__': 84 | grades_export() 85 | tada() 86 | -------------------------------------------------------------------------------- /etc/oneclass_conversations.py: -------------------------------------------------------------------------------- 1 | # https://community.canvaslms.com/message/57773 2 | # https://community.canvaslms.com/groups/canvas-developers/blog/2016/12/19/delete-all-canvas-conversations 3 | # https://canvas.instructure.com/doc/api/conversations.html 4 | 5 | 6 | from canvas.core.courses import get_courses, get_course_people 7 | 8 | from canvas.core.api import get 9 | 10 | terms = ['2017-1SP'] 11 | programs = ['NFNP'] 12 | synergis = False 13 | 14 | for course in get_courses(terms, programs, synergis): 15 | print(course['sis_course_id']) 16 | for user_id in [user['id'] for user in get_course_people(course['id'], 'student')]: 17 | for convo_id in get('conversations?as_user_id={}&include_all_conversation_ids=true&filter=user_{}' 18 | .format(user_id, user_id)).json()['conversation_ids']: 19 | try: 20 | for message in get('conversations/{}?as_user_id={}'.format(convo_id, user_id)).json()['messages']: 21 | if 'oneclass' in message['body'].lower(): 22 | print(user_id, convo_id, message['id'], message['body']) 23 | except Exception as e: 24 | print('>>> exception @ user {}, convo {}: {}'.format(user_id, convo_id, e)) 25 | -------------------------------------------------------------------------------- /etc/page_text_replace.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from requests import exceptions as api_exceptions, get as api_get, put as api_put 4 | 5 | token = 'YOUR API TOKEN' 6 | subdomain = 'YOUR INSTRUCTURE.COM SUBDOMAIN' 7 | 8 | text_old = 'OLD TEXT' 9 | text_new = 'NEW TEXT' 10 | 11 | account_ids = ['ACCOUNT', 'IDS', 'TO', 'SEARCH'] # if empty, all accounts 12 | term_ids = ['TERM', 'IDS', 'TO', 'LIMIT', 'SEARCH', 'TO'] # if empty, all terms 13 | 14 | include_subaccounts = True # if True, traverse subaccounts of specified account IDs 15 | 16 | base_url = f'https://{subdomain}.instructure.com/api/v1/' 17 | auth_header = {'Authorization': f"Bearer {token}"} 18 | 19 | 20 | def get(url, r_data=None): 21 | for _ in range(5): 22 | try: 23 | return api_get(normalize_url(url), headers=auth_header, timeout=5, stream=False, data=r_data) 24 | except api_exceptions.RequestException as e: 25 | time_out(url, 'GET', e) 26 | print('*** GIVING UP ***') 27 | return None 28 | 29 | 30 | def put(url, r_data): 31 | for _ in range(5): 32 | try: 33 | return api_put(normalize_url(url), headers=auth_header, timeout=5, stream=False, data=r_data) 34 | except api_exceptions.RequestException as e: 35 | time_out(url, 'PUT', e) 36 | print('*** GIVING UP ***') 37 | return None 38 | 39 | 40 | def normalize_url(url): 41 | # paginated 'next' urls already start with base_url 42 | return url if url.startswith(base_url) else base_url + url 43 | 44 | 45 | def time_out(url, action, e): 46 | print('*' * 40, ' ERROR - RETRYING IN 5 SECONDS ', '*' * 40) 47 | print('\n***EXCEPTION:', e, '\n***ACTION:', action, '\n***URL:', url, '\n') 48 | time.sleep(5) 49 | 50 | 51 | def get_list(url, r_data=None): 52 | """ compile a paginated list up to 100 at a time (instead of default 10) and return the entire list """ 53 | r = get(f'{url}{"&" if "?" in url else "?"}per_page=100', r_data) 54 | paginated = r.json() 55 | while 'next' in r.links: 56 | r = get(r.links['next']['url'], r_data) 57 | paginated.extend(r.json()) 58 | return paginated 59 | 60 | 61 | def get_courses_for_accounts_and_terms(accounts=None, subaccounts=True, terms=None): 62 | """ yield a course for lists of terms & subaccounts """ 63 | 64 | if accounts: 65 | for account in sorted(accounts): 66 | if terms: 67 | for term in sorted(terms): 68 | yield from get_courses_by_account_id(account, subaccounts, term) 69 | else: 70 | yield from get_courses_by_account_id(account, subaccounts) 71 | else: 72 | for term in sorted(terms): 73 | for account in get_all_accounts(): 74 | yield from get_courses_by_account_id(account['id'], subaccounts, term) 75 | 76 | 77 | def get_courses_by_account_id(account_id, subaccounts, term=''): 78 | """ return a list of courses in an account, including courses in subaccounts of account if specified 79 | NOTE: API returns courses in an account AND ITS SUBACCOUNTS """ 80 | term_string = f'&enrollment_term_id={term}' if term else '' 81 | return [c for c in get_list(f'accounts/{account_id}/courses?sort=sis_course_id{term_string}') 82 | if subaccounts or str(c['account_id']) == account_id] 83 | 84 | 85 | def get_all_accounts(): 86 | """ return a list of all accounts """ 87 | return sorted(get_list(f'accounts')) 88 | 89 | 90 | def get_course_pages(course_id): 91 | """ return a list of a course's pages 92 | NOTE: API doesn't include page['body'] when requesting list, so must request each page """ 93 | return get_list(f'courses/{course_id}/pages') 94 | 95 | 96 | def get_course_page(course_id, page_url): 97 | """ get a course page """ 98 | response = get(f'courses/{course_id}/pages/{page_url}') 99 | return response.json() if response else {'body': ''} 100 | 101 | 102 | def update_course_page_contents(course_id, page_url, body): 103 | """ update a course page's content """ 104 | req_data = {'wiki_page[body]': body} 105 | return put(f'courses/{course_id}/pages/{page_url}', req_data) 106 | 107 | 108 | def replace_text_in_pages(): 109 | for course in get_courses_for_accounts_and_terms(account_ids, include_subaccounts, term_ids): 110 | print('searching', course['sis_course_id'] or course['name']) 111 | course_id = course['id'] 112 | for page in get_course_pages(course_id): 113 | body_old = get_course_page(course_id, page['url'])['body'] 114 | if body_old and text_old in body_old: 115 | update_course_page_contents(course_id, page['url'], body_old.replace(text_old, text_new)) 116 | print('updated', page['html_url']) 117 | 118 | 119 | if __name__ == '__main__': 120 | replace_text_in_pages() 121 | -------------------------------------------------------------------------------- /etc/quiz_creation_TEST.py: -------------------------------------------------------------------------------- 1 | # https://canvas.instructure.com/doc/api/quizzes.html#method.quizzes/quizzes_api.create 2 | # https://canvas.instructure.com/doc/api/quiz_questions.html#method.quizzes/quiz_questions.create 3 | 4 | import canvas.core.api as api 5 | 6 | 7 | # def create_formative_survey(course_id): 8 | # """ create a formative survey by creating a quiz and its questions """ 9 | course_id = '1826505' 10 | 11 | quiz_text = 'Ongoing feedback is essential for learning and improvement. ' \ 12 | 'Please give me feedback so that we can work together to improve learning.\n\n' \ 13 | 'Your feedback is anonymous. If you would like me to get in touch with you personally ' \ 14 | 'about your feedback, please add your name and email address to your comments.' 15 | 16 | question_descriptions = [ 17 | 'What’s working for you in this class? Which activities or strategies are most helpful to your learning?', 18 | 'What’s not working for you? Which activities or strategies could I change to improve your learning?', 19 | 'Are there content, concepts, or skills we\'ve covered that you don’t fully understand or ' 20 | 'that are giving you trouble?', 21 | 'Would you like to share any other comments with me?' 22 | ] 23 | 24 | quiz_data = { 25 | 'quiz[title]': 'Formative Evaluation Survey', 26 | 'quiz[description]': quiz_text, 27 | 'quiz[quiz_type]': 'survey', 28 | 'quiz[allowed_attempts]': -1, 29 | 'quiz[published]': False 30 | } 31 | quiz_id = api.post('courses/{}/quizzes'.format(course_id), quiz_data).json()['id'] 32 | 33 | for question_position, question_description in enumerate(question_descriptions): 34 | question_data = { 35 | 'question[question_name]': question_position + 1, 36 | 'question[question_text]': question_description, 37 | 'question[question_type]': 'essay_question', 38 | 'question[position]': question_position + 1 39 | } 40 | api.post('courses/{}/quizzes/{}/questions'.format(course_id, quiz_id), question_data).json() 41 | -------------------------------------------------------------------------------- /etc/replace_text_in_pages.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from canvas.core.courses import get_course_page, get_course_pages, update_course_page_contents, get_courses, \ 4 | get_courses_whitelisted 5 | 6 | from canvas.core.io import tada 7 | 8 | 9 | def replace_text_in_pages(): 10 | 11 | course_whitelist = get_courses_whitelisted([]) 12 | terms = ['2016-2SU'] 13 | programs = ['NFNPO'] 14 | synergis = True 15 | 16 | text_find = '(.*)http://(www.)*synergiseducation.com/academics/schools/SMU/(.*)' 17 | text_replace = 'http://media.samuelmerritt.edu/' 18 | 19 | for course in course_whitelist or get_courses(terms, programs, synergis): 20 | 21 | course_id = course['course_id'] 22 | print(course['course_sis_id'], course['course_code']) 23 | 24 | # pages; api doesn't include page['body'] when requesting page list, so must request each page 25 | for page in get_course_pages(course_id): 26 | page_url = page['url'] 27 | body_old = get_course_page(course_id, page_url)['body'] 28 | if not body_old: 29 | continue 30 | body_new = re.sub(text_find, r'\1' + text_replace + r'\3', body_old) 31 | if body_new != body_old: 32 | update_course_page_contents(course_id, page_url, body_new) 33 | # print(page_url, '\n', get_course_page(course_id, page_url)['body']) 34 | print(page_url) 35 | 36 | # assignments 37 | # discussions 38 | # to do; modify from old\mediacore_find_refs.py 39 | 40 | if __name__ == '__main__': 41 | replace_text_in_pages() 42 | tada() 43 | -------------------------------------------------------------------------------- /etc/xlist_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import get_course_sections, get_course, get_courses 2 | 3 | from canvas.core.io import tada 4 | 5 | 6 | course_whitelist = [] 7 | terms = ['2017-3FA'] 8 | programs = ['OT'] 9 | synergis = False 10 | 11 | for course in get_courses(terms, programs, synergis, course_whitelist): 12 | # rolled back code golf: 13 | # for xlist in sorted([get_course(section['nonxlist_course_id'])['sis_course_id'] 14 | # for section in get_course_sections(course['id']) if section['nonxlist_course_id']]): 15 | # print('{:<40} --> {}'.format(xlist, course['course_sis_id'])) 16 | for section in get_course_sections(course['id']): 17 | if section['nonxlist_course_id']: 18 | xlist = get_course(section['nonxlist_course_id'])['sis_course_id'] 19 | print('{:<40} --> {}'.format(xlist, course['sis_course_id'])) 20 | 21 | tada() 22 | -------------------------------------------------------------------------------- /outcomes/clos_course_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import get_courses, get_courses_whitelisted 2 | from canvas.core.outcome_groups import get_root_group 3 | from canvas.core.outcomes import get_outcomes 4 | 5 | from canvas.core.io import tada 6 | 7 | 8 | def course_clos_list(): 9 | 10 | terms = ['2017-1SP'] 11 | programs = [] 12 | synergis = True 13 | course_whitelist = get_courses_whitelisted([]) 14 | 15 | for course in course_whitelist or get_courses(terms, programs, synergis): 16 | print(course['sis_course_id']) 17 | for clo in get_outcomes('courses', course['id'], get_root_group('courses', course['id'])): 18 | print(clo['title']) 19 | print() 20 | 21 | if __name__ == '__main__': 22 | course_clos_list() 23 | tada() 24 | -------------------------------------------------------------------------------- /outcomes/clos_course_sync.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from canvas.core.accounts import program_account 4 | from canvas.core.courses import get_courses 5 | from canvas.core.io import tada 6 | from canvas.core.outcome_groups import delete_group, get_program_groups, get_root_group, get_subgroups 7 | from canvas.core.outcomes import get_outcomes, link_outcome, unlink_outcome 8 | 9 | from canvas.core.etc import DictDiffer, scrub 10 | 11 | 12 | def clos_course_sync(): 13 | 14 | def print_pretty(arg1, arg2='', arg3=''): 15 | print('{:<40}{: <20} {} {}'.format(course_sis_id, arg1, arg2, arg3)) 16 | 17 | def load_clos(context, context_id, root_group, clo_descs, clo_ids): 18 | for clo in get_outcomes(context, context_id, root_group): 19 | if 'Genomics' not in clo['title']: 20 | load_key = clo['title'] + '|' + scrub(clo['description']) 21 | clo_descs[load_key] = scrub(clo['description']) 22 | clo_ids[load_key] = clo['id'] 23 | 24 | def add(label): 25 | print_pretty(label, *key.split('|')) 26 | link_outcome('courses', course_id, course_root_group, program_clo_ids[key]) 27 | 28 | def delete(verb): 29 | print_pretty(verb, *key.split('|')) 30 | results = unlink_outcome('courses', course_id, course_root_group, course_clo_ids[key]) 31 | if not results: 32 | print_pretty('>>> FAILED--ALIGNED') 33 | 34 | # ---- HERE WE GO! ---- # 35 | 36 | course_whitelist = [] 37 | terms = ['2017-2SU'] 38 | programs = [] 39 | synergis = False 40 | 41 | previous_program = '' 42 | for course in get_courses(terms, programs, synergis, course_whitelist): 43 | 44 | course_id = course['id'] 45 | course_sis_id = course['sis_course_id'] 46 | course_sis_info = course['course_sis_info'] 47 | course_number = course_sis_info['number'] 48 | program = course_sis_info['program'] 49 | 50 | # load program's outcome groups ("folders") 51 | if program != previous_program: 52 | course_folders = get_program_groups(program_account(program)) 53 | previous_program = program 54 | 55 | # delete legacy folders 56 | course_root_group = get_root_group('courses', course_id) 57 | subgroups = get_subgroups('courses', course_id, course_root_group) 58 | if subgroups: 59 | for course_group in subgroups: 60 | delete_group('courses', course_id, course_group['id']) 61 | print_pretty('deleted folder', course_group['title']) 62 | for course_group in get_subgroups('courses', course_id, course_root_group): 63 | print_pretty(course_sis_id, '>>> STUCK FOLDER', course_group['title']) 64 | 65 | # skip if no program outcomes for course 66 | if course_number not in course_folders: 67 | print_pretty('>>> NO PROGRAM CLOS') 68 | continue 69 | 70 | # load course CLO titles & descriptions 71 | course_clo_descs = collections.OrderedDict() 72 | course_clo_ids = collections.OrderedDict() 73 | load_clos('courses', course_id, course_root_group, course_clo_descs, course_clo_ids) 74 | 75 | # load program CLO titles & descriptions 76 | program_clo_descs = collections.OrderedDict() 77 | program_clo_ids = collections.OrderedDict() 78 | load_clos('accounts', program_account(program), course_folders[course_number], program_clo_descs, 79 | program_clo_ids) 80 | 81 | # synchronize course & program CLOs 82 | diffs = DictDiffer(program_clo_descs, course_clo_descs) 83 | for key in sorted(diffs.added()): 84 | add('added') 85 | for key in sorted(diffs.deleted()): 86 | delete('deleted') 87 | for key in sorted(diffs.changed()): 88 | delete('changed') 89 | add('to') 90 | 91 | # actions = {'added': diffs.added(), 'deleted': diffs.deleted(), 'changed': diffs.changed()} 92 | # for action in actions: 93 | # for key in sorted(actions[action]): 94 | # print_pretty(action, *key.split('|')) 95 | 96 | 97 | if __name__ == '__main__': 98 | clos_course_sync() 99 | tada() 100 | -------------------------------------------------------------------------------- /outcomes/clos_program_refresh.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import time 3 | 4 | from canvas.core.accounts import program_account, all_programs 5 | from canvas.core.io import get_cmi_clos_by_program, tada 6 | from canvas.core.outcome_groups import create_group, get_root_group, get_subgroups 7 | from canvas.core.outcomes import get_outcomes, link_new_outcome, link_outcome, unlink_outcome, update_outcome_desc, \ 8 | update_outcome_title 9 | 10 | from canvas.core.etc import DictDiffer, scrub, quote 11 | 12 | 13 | def clos_program_refresh(): 14 | def add(key): 15 | prog, course_number, clo_title = key.split('|') # ELMSN|N520|N520_04 16 | # create course subgroup if it doesn't exist 17 | if course_number in program_course_groups: 18 | course_group_id = program_course_groups[course_number] 19 | else: 20 | request_data = {'title': course_number, 'description': 'CLOs for ' + course_number} 21 | course_group_id = create_group('accounts', account, program_root_group_id, request_data) 22 | program_course_groups[course_number] = course_group_id 23 | print('added course:', course_number) 24 | # create new CLO 25 | link_new_outcome('accounts', account, course_group_id, clo_title, cmi_clos[key]) 26 | 27 | def delete(key, verb): 28 | outcome_id = canvas_clo_id[key] 29 | prog, course_number, clo_title = key.split('|') # ELMSN|N520|N520_04 30 | course_group_id = program_course_groups[course_number] 31 | 32 | # # try deleting CLO; success means it was unaligned and doesn't need archiving 33 | # if unlink_outcome('accounts', account, course_group_id, outcome_id): 34 | # return 35 | 36 | # find archive ('OLD') group 37 | # search for archive group in ones already encountereds 38 | if course_number in course_archive_group_ids: 39 | archive_group_id = course_archive_group_ids[course_number] 40 | else: 41 | # if archive group not already encountered, search by traversing course subgroups 42 | for course_subgroup in get_subgroups('accounts', account, course_group_id): 43 | if course_subgroup['title'] == 'OLD': 44 | archive_group_id = course_subgroup['id'] 45 | course_archive_group_ids[course_number] = archive_group_id 46 | break 47 | else: 48 | # if archive group still not found, create it 49 | request_data = {'title': 'OLD', 'description': 'Old CLOs for ' + course_number} 50 | archive_group_id = create_group('accounts', account, course_group_id, request_data) 51 | course_archive_group_ids[course_number] = archive_group_id 52 | 53 | # link CLO into OLD subgroup 54 | link_outcome('accounts', account, archive_group_id, outcome_id) 55 | 56 | # unlink CLO from original subgroup 57 | if not unlink_outcome('accounts', account, course_group_id, outcome_id): 58 | print('>>>> failed to unlink {} from {}'.format(clo_title, course_number)) 59 | 60 | # append 'OLD' to title 61 | if not update_outcome_title(outcome_id, 'CLO {} OLD'.format(clo_title)): 62 | print('>>>> failed to append OLD to {} title'.format(clo_title)) 63 | 64 | # append replacement / retirement date to description 65 | if not update_outcome_desc(outcome_id, 66 | '{} [{} {}]'.format(canvas_clo_desc[key], verb, time.strftime("%Y-%m-%d"))): 67 | print('>>>> failed to append date to {} description'.format(clo_title)) 68 | 69 | programs = [] 70 | for program in programs or all_programs(synergis=True): 71 | print('\n' + program) 72 | 73 | # load CMI CLOs 74 | cmi_clos = collections.OrderedDict() 75 | for clo in get_cmi_clos_by_program(program): 76 | clo_key = '{}|{}|{}'.format(program, clo['course_id'].replace('-', ''), clo['clo_title'].replace('-', '')) 77 | cmi_clos[clo_key] = scrub(clo['clo_description']) 78 | # print('\nCMI:', json.dumps(cmi_clos, sort_keys=True, indent=4, separators=(',', ': '))) 79 | 80 | # load canvas CLOs 81 | # create lookups while traversing course subgroups: 82 | program_course_groups = collections.OrderedDict() # key = course title; value = subgroup id 83 | # next two are separate to enable diffing on description 84 | canvas_clo_desc = collections.OrderedDict() # key = program + course + clo title; value = clo description 85 | canvas_clo_id = collections.OrderedDict() # key = program + course + clo title; value = clo id 86 | # print('\nCANVAS:') 87 | account = program_account(program) 88 | program_root_group_id = get_root_group('accounts', account) 89 | for course_group in get_subgroups('accounts', account, program_root_group_id): 90 | print(course_group['title']) 91 | program_course_groups[course_group['title']] = course_group['id'] 92 | for clo in get_outcomes('accounts', account, course_group['id']): 93 | clo_key = '{}|{}|{}'.format(program, course_group['title'], clo['title'].replace('CLO ', '')) 94 | canvas_clo_desc[clo_key] = scrub(clo['description']) 95 | canvas_clo_id[clo_key] = clo['id'] 96 | # print(clo_key, ':', quote(canvas_clo_desc[clo_key])) 97 | 98 | # compare CMI & canvas CLOs 99 | diffs = DictDiffer(cmi_clos, canvas_clo_desc) 100 | course_archive_group_ids = collections.OrderedDict() 101 | 102 | added = diffs.added() 103 | if added: 104 | print('\nADDED:') 105 | for clo_key in sorted(added): 106 | # add(clo_key) 107 | print(' added:', clo_key, quote(cmi_clos[clo_key])) 108 | 109 | deleted = diffs.deleted() 110 | if deleted: 111 | print('\nDELETED:') 112 | # TODO: investigate how course_archive_group_ids[] works when removing is part of changing 113 | for clo_key in sorted(deleted): 114 | # delete(clo_key, 'retired') 115 | print(' deleted:', clo_key, quote(canvas_clo_desc[clo_key])) 116 | 117 | changed = diffs.changed() 118 | if changed: 119 | print('\nCHANGED:') 120 | for clo_key in sorted(changed): 121 | # delete(clo_key, 'replaced') 122 | print(' changed:', clo_key, quote(canvas_clo_desc[clo_key])) 123 | # add(clo_key) 124 | print(' to:', clo_key, quote(cmi_clos[clo_key])) 125 | 126 | 127 | if __name__ == '__main__': 128 | clos_program_refresh() 129 | tada() 130 | -------------------------------------------------------------------------------- /outcomes/clos_program_xlsx_from_canvas.py: -------------------------------------------------------------------------------- 1 | import xlsxwriter 2 | from canvas.core.outcome_groups import get_root_group, get_subgroups 3 | from canvas.core.outcomes import get_outcomes 4 | 5 | from canvas.core.accounts import program_account, all_programs 6 | 7 | programs = [] 8 | for program in programs or all_programs('fnp_online_no'): 9 | program_workbook = xlsxwriter.Workbook('CLOs_{}_2015.09.09.xlsx'.format(program)) 10 | account = program_account(program) 11 | program_root_group_id = get_root_group('accounts', account) 12 | for course_group in get_subgroups('accounts', account, program_root_group_id): 13 | course_worksheet = program_workbook.add_worksheet(course_group['title'].replace('/', '-')) 14 | for column_number, column_header in enumerate(['course', 'CLO name', 'CLO description']): 15 | course_worksheet.write(0, column_number, column_header) 16 | for row, clo in enumerate(get_outcomes('accounts', account, course_group['id'])): 17 | for column_number, column_data in enumerate([course_group['title'], clo['title'], clo['description']]): 18 | course_worksheet.write(row + 1, column_number, column_data) 19 | print(course_group['title'], clo['title'], clo['description']) 20 | program_workbook.close() 21 | -------------------------------------------------------------------------------- /outcomes/clos_program_xlsx_from_cmi.py: -------------------------------------------------------------------------------- 1 | import canvas.core.io as io 2 | import xlsxwriter 3 | from canvas.core.accounts import all_programs 4 | 5 | from canvas.core.etc import scrub 6 | 7 | programs = [] 8 | for program in programs or all_programs('fnp_online_no'): 9 | query = ('SELECT c.CourseID AS course_id, c.CLO_ID AS clo_title, c.CLO AS clo_description ' + 10 | 'FROM Courses c WHERE c.programID = "' + program + '" AND c.Active = 1 AND c.CLO NOT LIKE "%deleted%"') 11 | cmi_records = io.get_db_query_results('cmi', query)[0] 12 | workbook = xlsxwriter.Workbook('CLOs_{}_2015.09.09.xlsx'.format(program)) 13 | previous_course_id = '' 14 | for row_number, cmi_record in enumerate(cmi_records): 15 | current_course_id = cmi_record['course_id'].replace('-', '') 16 | course_id = cmi_record['course_id'] 17 | clo_title = cmi_record['clo_title'].replace('-', '') 18 | clo_desc = scrub(cmi_record['clo_description']) 19 | if current_course_id != previous_course_id: 20 | worksheet = workbook.add_worksheet(cmi_record['clo_title'].replace('-', '').replace('/', '-')) 21 | for column_number, column_header in enumerate(['course', 'CLO name', 'CLO description']): 22 | worksheet.write(0, column_number, column_header) 23 | print(program, course_id) 24 | for column_number, column_data in enumerate([course_id, clo_title, clo_desc]): 25 | worksheet.write(row_number + 1, column_number, column_data) 26 | print(clo_title, clo_desc) 27 | previous_course_id = current_course_id 28 | workbook.close() 29 | -------------------------------------------------------------------------------- /outcomes/cmi_scrub_alert.py: -------------------------------------------------------------------------------- 1 | # http://stackoverflow.com/a/21686937 & http://stackoverflow.com/a/1262210 2 | 3 | from canvas.core.accounts import all_programs 4 | from canvas.core.io import get_cmi_clos_by_program # BROKEN? 5 | 6 | from canvas.core.etc import scrub 7 | 8 | programs = [] 9 | for program in programs or all_programs('synergis_yes'): 10 | print('\n' + program) 11 | for clo in get_cmi_clos_by_program(program): 12 | scrubbed = scrub(clo['clo_description']) 13 | if clo['clo_description'] != scrubbed: 14 | print(clo['clo_title']) 15 | print(clo['clo_description']) 16 | print(scrubbed) 17 | -------------------------------------------------------------------------------- /outcomes/glos_duplicates_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core.accounts import program_account, all_programs 2 | from canvas.core.constants import all_programs, term_id_from_name 3 | from canvas.core.courses import get_courses, validate_course 4 | from canvas.core.outcome_groups import get_root_group 5 | from canvas.core.outcomes import get_outcomes 6 | from canvas.core.utility import make_leader 7 | 8 | from canvas.core.io import tada 9 | 10 | 11 | def list_duplicate_glos(): 12 | 13 | programs = ['BSN', 'DNP'] 14 | term = '2015FALL' 15 | term_id = term_id(term) 16 | for program in programs or all_programs('fnp_online_yes'): 17 | print(program, '-' * 70) 18 | account = program_account(program) 19 | for course in get_courses(account, term_id): 20 | if not validate_course(course, program): 21 | continue 22 | leader = make_leader(program, course['sis_course_id']) 23 | glos = [] 24 | for clo in get_outcomes('courses', course['id'], get_root_group('courses', course['id'])): 25 | if 'Genomics' in clo['title']: 26 | if clo['title'] in glos: 27 | print(leader, clo['title']) 28 | else: 29 | glos.append(clo['title']) 30 | 31 | 32 | if __name__ == '__main__': 33 | list_duplicate_glos() 34 | tada() 35 | -------------------------------------------------------------------------------- /outcomes/glos_push_to_courses.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import core.config as config 4 | import mammoth 5 | from canvas.core.accounts import program_account 6 | from canvas.core.constants import term_id_from_name 7 | from canvas.core.courses import get_courses, validate_course 8 | from canvas.core.outcome_groups import get_root_group 9 | from canvas.core.outcomes import get_outcomes, link_new_outcome 10 | 11 | from canvas.core.io import tada 12 | 13 | 14 | def push_genomics_to_courses(): 15 | term = '2016SPRING' 16 | program = 'DNP' 17 | document_title = 'glos_dnp.docx' 18 | 19 | # read genomic resources from Word document, convert to HTML, and load dict 20 | glos = {} 21 | with open(document_title, 'rb') as docx_file: 22 | html = mammoth.convert_to_html(docx_file).value 23 | for page in html.split('!!!'): 24 | match_result = re.match(r'^(.*?)

(.*)

$', page) 25 | if match_result: 26 | glo_id, text = match_result.group(1), match_result.group(2) 27 | course_number = glo_id.split('_')[0] 28 | if course_number not in glos: 29 | glos[course_number] = [] 30 | glos[course_number].append([glo_id, text]) 31 | else: 32 | print('>>>>', page) 33 | 34 | title_roots = {} 35 | term_id = term_id(term) 36 | account = program_account(program) 37 | for course in get_courses(account, term_id): 38 | 39 | # validate course 40 | course_number = validate_course(course, program) 41 | if not course_number: 42 | continue 43 | 44 | # skip if no GLOs 45 | if course_number not in glos: 46 | continue 47 | 48 | # print(leader, 'https://samuelmerritt.{}instructure.com/courses/{}/outcomes' 49 | # .format('test.' if config.env == 'test' else '', course_id)) 50 | 51 | course_id, course_sis_id = course['id'], course['sis_course_id'] 52 | leader = '{: <10}{: <40}'.format(program, course_sis_id) 53 | 54 | # get course CLOs 55 | course_root_group = get_root_group('courses', course_id) 56 | course_clos = get_outcomes('courses', course_id, course_root_group) 57 | if not course_clos: 58 | print(leader, '>>> no clos found in course:') 59 | continue 60 | 61 | # skip if course already has GLOs ************* DOES THIS WORK? FALL HAD DUPLICATE GLOS! *************** 62 | pushed = False 63 | for course_clo in course_clos: 64 | if 'Genomics' in course_clo['title']: 65 | pushed = True 66 | print(leader, '>>> {} already pushed'.format(course_clo['title'])) 67 | break 68 | if pushed: 69 | continue 70 | 71 | # determine whether existing CLO title format is Nxxx or Nxxx/xxxL 72 | if course_number not in title_roots: 73 | # get CLO title root from first CLO 74 | clo_title = course_clos[0]['title'] 75 | match_result = re.match(r'^CLO (.*)_.*$', clo_title) 76 | title_roots[course_number] = match_result.group(1) 77 | 78 | # push the GLOs 79 | for glo in glos[course_number]: 80 | glo_title, text = (i for i in glo) 81 | glo_title = '{}_{} Genomics'.format(title_roots[course_number], glo_title.split('_')[1]) 82 | print(leader, glo_title) 83 | link_new_outcome('courses', course_id, course_root_group, glo_title, text) 84 | 85 | 86 | if __name__ == '__main__': 87 | push_genomics_to_courses() 88 | tada() 89 | -------------------------------------------------------------------------------- /outcomes/glos_refresh.py: -------------------------------------------------------------------------------- 1 | from canvas.core import * 2 | 3 | 4 | def add(key): 5 | prog, course_number, clo_title = key.split('|') # ELMSN|N520|N520_04 6 | # create course subgroup if it doesn't exist 7 | if course_number in program_course_groups: 8 | course_group_id = program_course_groups[course_number] 9 | else: 10 | request_data = {'title': course_number, 'description': 'CLOs for ' + course_number} 11 | course_group_id = create_group('accounts', account, program_root_group_id, request_data) 12 | program_course_groups[course_number] = course_group_id 13 | print('added course:', course_number) 14 | # create new CLO 15 | link_new_outcome('accounts', account, course_group_id, clo_title, cmi_clos[key]) 16 | 17 | 18 | def remove(key, verb): 19 | clo_id = canvas_other[key] 20 | prog, course_number, clo_title = key.split('|') # ELMSN|N520|N520_04 21 | course_group_id = program_course_groups[course_number] 22 | # find archive ('OLD') group 23 | # look for archive group in lookup 24 | if course_number in course_archive_group_ids: 25 | archive_group_id = course_archive_group_ids[course_number] 26 | else: 27 | # look for archive group by traversing course subgroups 28 | course_subgroups = get_subgroups('accounts', account, course_group_id) 29 | for course_subgroup in course_subgroups: 30 | if course_subgroup['title'] == 'OLD': 31 | archive_group_id = course_subgroup['id'] 32 | course_archive_group_ids[course_number] = archive_group_id 33 | break 34 | else: 35 | # archive group not found, so create it 36 | request_data = {'title': 'OLD', 'description': 'Old CLOs for ' + course_number} 37 | archive_group_id = create_group('accounts', account, course_group_id, request_data) 38 | course_archive_group_ids[course_number] = archive_group_id 39 | # link CLO into OLD subgroup 40 | link_outcome('accounts', account, archive_group_id, clo_id) 41 | # unlink CLO from original subgroup 42 | unlink_outcome('accounts', account, course_group_id, clo_id) 43 | # append replacement/retirement date to description 44 | update_outcome_description(clo_id, canvas_clos[key] + ' [' + verb + ' ' + time.strftime("%Y-%m-%d") + ']') 45 | 46 | 47 | def quote(text): 48 | return '"' + text + '"' 49 | 50 | #for program in ['ABSN', 'BSN', 'DNP', 'DPM', 'DPT', 'ELMSN', 'MOT', 'MPA', 'MSN_CM', 'MSN_CRNA', 'MSN_FNP']: 51 | for program in ['BSN']: 52 | print(program) 53 | 54 | # query SMU CLOs 55 | db = pymysql.connect("OAKDB03", "dgrobani", "Welcome#13", "SMU") 56 | cur = db.cursor(pymysql.cursors.DictCursor) 57 | #query = ('SELECT CourseID AS course_id, CLO_ID AS clo_title, CLO AS clo_description ' + 58 | #'FROM Courses WHERE programID = "' + program + '" AND Active = 1 ORDER BY CourseID, CLO_ID') 59 | query = ('SELECT c.CourseID AS course_id, c.CLO_ID AS clo_title, c.CLO AS clo_description ' 60 | 'FROM Courses c ' 61 | 'WHERE c.programID = "' + program + '" AND c.Active = 1 ' 62 | 'UNION ' 63 | 'SELECT c.CourseID AS course_id, g.genomicsID AS clo_title, ' 64 | 'CONCAT(\'

\', g.genomicsDescription, \'

\') ' 65 | 'AS clo_description ' 66 | 'FROM CLOGenomics g ' 67 | 'LEFT JOIN Courses c ON g.programID = c.programID AND g.cloID = c.CLO_ID ' 68 | 'WHERE c.programID = "' + program + '" AND c.Active = 1') 69 | cur.execute(query) 70 | rows = cur.fetchall() 71 | cur.close() 72 | db.close() 73 | 74 | # load SMU CLOs 75 | cmi_clos = collections.OrderedDict() 76 | for row in rows: 77 | clo_key = program + '|' + row['course_id'].replace('-', '') + '|' + row['clo_title'].replace('-', '') 78 | cmi_clos[clo_key] = scrub(row['clo_description']) 79 | 80 | print('\nSMU:') 81 | print(json.dumps(cmi_clos, sort_keys=True, indent=4, separators=(',', ': '))) 82 | 83 | # load canvas CLOs 84 | # create lookups while traversing course subgroups: 85 | # lookup: program + course + clo title -> clo description 86 | canvas_clos = collections.OrderedDict() 87 | # lookup: program + course + clo title -> clo id [separate from canvas_clos to enable diffing on description] 88 | canvas_other = collections.OrderedDict() 89 | # lookup : course title -> subgroup id 90 | program_course_groups = collections.OrderedDict() 91 | print('\nCANVAS:') 92 | account = account_for_program(program) 93 | program_root_group_id = get_root_group('accounts', account) 94 | course_groups = get_subgroups('accounts', account, program_root_group_id) 95 | for course_group in course_groups: 96 | program_course_groups[course_group['title']] = course_group['id'] 97 | course_clo_links = get_outcome_links('accounts', account, course_group['id']) 98 | for course_clo_link in course_clo_links: 99 | clo_id = course_clo_link['outcome']['id'] 100 | clo = get_outcome(clo_id) 101 | clo_key = program + '|' + course_group['title'] + '|' + clo['title'].replace('CLO ', '') 102 | canvas_clos[clo_key] = scrub(clo['description']) 103 | canvas_other[clo_key] = clo['id'] 104 | print(clo_key, ':', quote(canvas_clos[clo_key])) 105 | 106 | # compare SMU & canvas CLOs 107 | diffs = DictDiffer(cmi_clos, canvas_clos) 108 | 109 | print('\nADDED:') 110 | added = sorted(diffs.added()) 111 | for clo_key in added: 112 | add(clo_key) 113 | print(' added:', clo_key, quote(cmi_clos[clo_key])) 114 | 115 | print('\nREMOVED:') 116 | course_archive_group_ids = collections.OrderedDict() 117 | removed = sorted(diffs.removed()) 118 | for clo_key in removed: 119 | remove(clo_key, ' retired') 120 | print(' removed:', clo_key, quote(canvas_clos[clo_key])) 121 | 122 | print('\nCHANGED:') 123 | changed = sorted(diffs.changed()) 124 | for clo_key in changed: 125 | remove(clo_key, 'replaced') 126 | print(' removed:', clo_key, quote(canvas_clos[clo_key])) 127 | add(clo_key) 128 | print(' added:', clo_key, quote(cmi_clos[clo_key])) 129 | -------------------------------------------------------------------------------- /roles/admins_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core import config 2 | 3 | from canvas.core.accounts import get_subaccounts, get_admins 4 | from canvas.core.io import tada 5 | 6 | 7 | def admins_list(account_id, account_name): 8 | for admin in get_admins(account_id): 9 | print('{},{}{},{},{},{}'.format(account_id, account_name, ',' * (5 - account_name.count(',')), 10 | admin['role'], admin['user']['name'].replace(',', ''), admin['user']['login_id'])) 11 | for subaccount in get_subaccounts(account_id): 12 | admins_list(subaccount['id'], account_name + ',' + subaccount['name']) 13 | 14 | 15 | if __name__ == '__main__': 16 | admins_list(config.root_account, 'root') 17 | tada() 18 | -------------------------------------------------------------------------------- /roles/roles_in_accounts_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core import config 2 | 3 | from canvas.core.accounts import get_subaccounts, get_roles 4 | from canvas.core.io import tada 5 | 6 | 7 | def roles_in_accounts_list(account_id, account_name): 8 | for role in get_roles(account_id): 9 | if role['label'] not in ('Account Admin', 'Student', 'Teacher', 'TA', 'Designer', 'Observer'): 10 | print('{},{}{},{}'.format(account_id, account_name, ',' * (5 - account_name.count(',')), role['label'])) 11 | for subaccount in get_subaccounts(account_id): 12 | roles_in_accounts_list(subaccount['id'], account_name + ',' + subaccount['name']) 13 | 14 | 15 | if __name__ == '__main__': 16 | roles_in_accounts_list(config.root_account, 'root') 17 | tada() 18 | -------------------------------------------------------------------------------- /roles/roles_in_courses_list.py: -------------------------------------------------------------------------------- 1 | from canvas.core.courses import get_courses, get_course_people 2 | 3 | from canvas.core.io import tada 4 | 5 | 6 | def find_courses_for_role(): 7 | terms = ['2016-2SU'] 8 | programs = ['NFNPO'] 9 | role = 'Teacher Read-Only' 10 | 11 | print(role) 12 | course_whitelist = [] 13 | synergis = False 14 | for course in course_whitelist or get_courses(terms, programs, synergis): 15 | people = get_course_people(course['id'], role) 16 | if 'errors' not in people: 17 | for person in people: 18 | print(course['sis_course_id'], person['name']) 19 | 20 | if __name__ == '__main__': 21 | find_courses_for_role() 22 | tada() 23 | -------------------------------------------------------------------------------- /syllabi/syllabi_download_OLD.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import bs4.BeautifulSoup 4 | 5 | 6 | def get_syllabus(course): 7 | # returns course syllabus_body 8 | url = base_url + 'courses/' + str(course) + '?include[]=syllabus_body' 9 | return requests.get(url, headers=auth_headers).json()['syllabus_body'] 10 | 11 | 12 | def get_root_folder(course): 13 | # returns folder id 14 | url = base_url + 'courses/' + str(course) + '/folders/root' 15 | return requests.get(url, headers=auth_headers).json()['id'] 16 | 17 | 18 | def get_subfolders(folder): 19 | # returns list of subfolders 20 | # https://canvas.instructure.com/doc/api/file.pagination.html 21 | url = base_url + 'folders/' + str(folder) + '/folders?per_page=100' 22 | return requests.get(url, headers=auth_headers).json() 23 | 24 | 25 | def get_files(folder): 26 | # returns list of files 27 | url = base_url + 'folders/' + str(folder) + '/files?per_page=100' 28 | return requests.get(url, headers=auth_headers).json() 29 | 30 | 31 | def get_file_url(file): 32 | # returns url from file object 33 | url = base_url + 'files/' + str(file) 34 | r = requests.get(url, headers=auth_headers).json() 35 | return r['url'] if 'url' in r else '' 36 | 37 | 38 | def download_file(source, url, program, course, file): 39 | print(url) 40 | r = requests.get(url) 41 | print(source, r, program, course, file, '\n') 42 | if not os.path.exists(program): 43 | os.makedirs(program) 44 | with open(program + '/' + course + ' ' + file, 'wb') as f: 45 | for chunk in r.iter_content(): 46 | f.write(chunk) 47 | 48 | base_url = 'https://samuelmerritt.test.instructure.com/api/v1/' 49 | auth_headers = {'Authorization': 'Bearer '} 50 | canvas_courses_file = 'syllabi courses.csv' 51 | 52 | for line in open(canvas_courses_file): 53 | fields = line.split(',') 54 | course_id = fields[0] 55 | cn = fields[1].split('-') # 2013FS-OAK-GENERAL-NURSG-108-LEC1-1 56 | course_name = cn[3] + '-' + cn[4] + '-' + cn[5] + '-' + cn[6] + '-' + cn[1] + '-' + cn[0] 57 | program = fields[5] # ACADEMIC_NURS_U_BSN 58 | 59 | # files 60 | course_root_folder = get_root_folder(course_id) 61 | folders = get_subfolders(course_root_folder) 62 | for i in folders: 63 | files = get_files(i['id']) 64 | for j in files: 65 | if 'syl' in j['display_name'].lower(): 66 | download_file('F', j['url'], program, course_name, j['display_name']) 67 | 68 | # syllabus 69 | syllabus = get_syllabus(course_id) 70 | if syllabus is not None: 71 | soup = BeautifulSoup(str(syllabus)) 72 | for link in soup.find_all('a'): 73 | url = link.get('href') 74 | file_name = link.get('title') 75 | if 'download?verifier' in url and file_name not in ('Preview the document', 'View in a new window'): 76 | print(url) 77 | if 'courses' in url: 78 | url = get_file_url(url.split('/')[6]) 79 | if url == '': 80 | continue 81 | download_file('S', url, program, course_name, file_name) 82 | -------------------------------------------------------------------------------- /syllabi/syllabot.py: -------------------------------------------------------------------------------- 1 | # http://pbpython.com/python-word-template.html 2 | # https://github.com/awslabs/aws-python-sample/blob/master/s3_sample.py 3 | # http://boto3.readthedocs.io/en/latest/guide/migrations3.html 4 | # https://vverma.net/scrape-the-web-using-css-selectors-in-python.html 5 | # https://www.crummy.com/software/BeautifulSoup/bs4/doc/ 6 | 7 | import datetime 8 | import re 9 | from collections import defaultdict 10 | 11 | import boto3 12 | from bs4 import BeautifulSoup 13 | from mailmerge import MailMerge 14 | 15 | from canvas.core import config 16 | from canvas.core.accounts import catalog_program, program_formal_name, get_account_grading_standard, \ 17 | program_account 18 | from canvas.core.assignments import get_assignment_groups 19 | from canvas.core.courses import get_courses, get_course_modules, get_onl_masters, \ 20 | get_course_module_items, get_course_front_page 21 | from canvas.core.io import get_cmi_plos_by_program, get_db_query_results, tada, get_cmi_clos_by_course 22 | from canvas.core.outcomes import get_outcome 23 | 24 | 25 | def main(): 26 | course_whitelist = [ 27 | '2017-1SP-01-PT-743-LEC-OAK-01', 28 | '2017-1SP-01-NFNP-676-LEC-SAC-01' 29 | ] 30 | terms = ['2017-2SU'] 31 | programs = [] 32 | 33 | if 'NFNPO' in programs or 'NCMO' in programs: 34 | for term in terms: 35 | for program in programs: 36 | for master in get_onl_masters(program): 37 | syllabus_template(master, program, term) 38 | else: 39 | for course in get_courses(terms, programs, False, course_whitelist): 40 | syllabus_template(course) 41 | 42 | 43 | def syllabus_template(l_course, l_program='', l_term=''): 44 | course_id = l_course['id'] 45 | basics = get_basics(l_course, l_program, l_term) 46 | required_materials = get_onl_course_materials(course_id) if l_program else [] 47 | clos = get_cmi_clos_by_course(basics['program'], basics['course_number']) 48 | plos = get_cmi_plos_by_program(basics['program']) 49 | assignment_groups, assignment_group_names, assignment_assignment_groups, assignment_clos = \ 50 | get_assignments(course_id) 51 | module_assignments = get_modules(course_id, assignment_group_names, assignment_assignment_groups, assignment_clos) 52 | group_weights, group_weights_total = get_assignment_group_weights(assignment_groups) 53 | grading_standard = get_grading_standard(l_course['account_id']) 54 | write_file(basics, required_materials, clos, plos, module_assignments, group_weights, group_weights_total, 55 | grading_standard) 56 | write_file(basics, required_materials, [], [], module_assignments, group_weights, group_weights_total, 57 | grading_standard) 58 | upload_file(basics['out_filename']) 59 | 60 | 61 | def get_basics(l_course, l_program, l_term): 62 | 63 | basics = {} 64 | 65 | if l_program: 66 | basics['program'] = l_program 67 | basics['term'] = l_term 68 | basics['course_number'] = l_course['name'].split(' ')[0].replace(':', '') 69 | print(l_program, basics['course_number']) 70 | basics['footer_section'] = '' 71 | basics['in_filename'] = 'syllabot_onl.docx' 72 | basics['out_filename'] = 'syllabot_ONL_{}_{}.docx'.format(l_program, basics['course_number']) 73 | 74 | else: 75 | sis_id = l_course['sis_course_id'] 76 | print(sis_id) 77 | course_sis_info = l_course['course_sis_info'] 78 | basics['program'] = course_sis_info['program'] 79 | basics['term'] = course_sis_info['term'] 80 | basics['course_number'] = course_sis_info['number'] 81 | basics['section'] = 'Section ' + course_sis_info['section'] 82 | basics['footer_section'] = '' 83 | 84 | basics['office_location'] = 'Office location:' 85 | basics['meeting_times'] = 'Meeting times:' 86 | basics['class_location'] = 'Location:' 87 | basics['in_filename'] = 'syllabot.docx' 88 | basics['out_filename'] = 'syllabot_{}.docx'.format(sis_id) 89 | 90 | basics['term'] = ['Spring', 'Summer', 'Fall'][int(basics['term'][5]) - 1] + ' ' + basics['term'][0:4] 91 | basics['program_name'] = program_formal_name(basics['program']) 92 | basics['program_prefix'] = \ 93 | 'GENED' if basics['course_number'].startswith('GE') else catalog_program(basics['program']) 94 | basics['catalog_course_number'] = derive_catalog_course_number(basics['program_prefix'], basics['course_number']) 95 | basics['name'], basics['description'], basics['credits'] = get_powercampus_data(basics['catalog_course_number']) 96 | 97 | return basics 98 | 99 | 100 | def derive_catalog_course_number(program_prefix, course_number): 101 | exceptions = {'NURSG 100': 'IPE 100', 'NURSG 104': 'GENED 104', 'NURSG 433': 'GENED 433', 102 | 'NURSG 442': 'GENED 442', 'NURSG 456': 'GENED 456'} 103 | catalog_course_number = program_prefix + ' ' + re.sub(r'^\D*', '', course_number) 104 | return exceptions[catalog_course_number] if catalog_course_number in exceptions else catalog_course_number 105 | 106 | 107 | def get_powercampus_data(catalog_course_number): 108 | 109 | TODO: begin, end, drop dates 110 | query = 'SELECT start_date, end_date FROM sections WHERE academic_year = {} AND academic_term = {} AND ' \ 111 | 'event_id = {} AND event_sub_type = {} AND section = {} AND curriculum = {}' \ 112 | .format(catalog_course_number) 113 | pc_data = get_db_query_results('powercampus', query)[0][0] 114 | 115 | query = 'SELECT event_long_name, description, credits FROM event WHERE EVENT_ID = \'{}\''\ 116 | .format(catalog_course_number) 117 | results = get_db_query_results('powercampus', query)[0] 118 | if not results: 119 | print('>>>>> NO POWERCAMPUS EVENT FOR ' + catalog_course_number) 120 | return '???', '???', '???' 121 | pc_data = results[0] 122 | name = pc_data[0] 123 | description = pc_data[1] 124 | creditz = '{0:.2f}'.format(pc_data[2]) 125 | return name, description, creditz 126 | 127 | 128 | def get_assignments(course_id): 129 | assignment_assignment_groups = defaultdict(str) 130 | assignment_group_names = defaultdict(str) 131 | assignment_clos = defaultdict(list) 132 | assignment_groups = get_assignment_groups(course_id) 133 | for assignment_group in assignment_groups: 134 | assignment_group_names[assignment_group['id']] = assignment_group['name'] 135 | for assignment in assignment_group['assignments']: 136 | assignment_assignment_groups[assignment['id']] = assignment_group['id'] 137 | if 'rubric' in assignment: 138 | assignment_clos[assignment['id']] = \ 139 | [get_outcome(criterion['outcome_id'])['title'] 140 | for criterion in assignment['rubric'] if 'outcome_id' in criterion] 141 | return assignment_groups, assignment_group_names, assignment_assignment_groups, assignment_clos 142 | 143 | 144 | def get_modules(course_id, assignment_group_names, assignment_assignment_groups, assignment_clos): 145 | module_assessments = [] 146 | for course_module in get_course_modules(course_id): 147 | printed_module_name = False 148 | for item in get_course_module_items(course_id, course_module): 149 | if item['type'] in ['Discussion', 'Assignment', 'Quiz', 'ExternalTool']: 150 | if printed_module_name: 151 | module_name = '' 152 | else: 153 | module_name = course_module['name'] 154 | printed_module_name = True 155 | module_assessments.append( 156 | {'module_name': module_name, 157 | 'assignment_name': item['title'], 158 | 'assignment_group': assignment_group_names[assignment_assignment_groups[item['content_id']]], 159 | 'assignment_clos': ', '.join(assignment_clos[item['content_id']]) 160 | }) 161 | if not printed_module_name: 162 | module_assessments.append({'module_name': course_module['name']}) 163 | return module_assessments 164 | 165 | 166 | def get_assignment_group_weights(l_assignment_groups): 167 | weights = [{'assignment_group_name': group['name'], 'assignment_group_percent': str(group['group_weight'])} 168 | for group in l_assignment_groups] 169 | total = str(sum([group['group_weight'] for group in l_assignment_groups])) 170 | return weights, total 171 | 172 | 173 | def get_grading_standard(account_id): 174 | l_grading_standard = [] 175 | high_value = 100 176 | for scheme_item in get_account_grading_standard(account_id)[0]['grading_scheme']: 177 | low_value = int(scheme_item['value'] * 100) 178 | l_grading_standard.append({'grading_scale_high': str(high_value), 179 | 'grading_scale_low': str(low_value), 180 | 'grading_scale_letter': scheme_item['name']}) 181 | high_value = low_value - 1 182 | return l_grading_standard 183 | 184 | 185 | def get_onl_course_materials(course_id): 186 | required_materials = [] 187 | return required_materials 188 | 189 | 190 | def write_file(l_basics, l_materials, l_clos, l_plos, assignments, weights, weights_total, standard): 191 | template = MailMerge(l_basics['in_filename']) 192 | template.merge( 193 | program_name=l_basics['program_name'], 194 | course_number=l_basics['catalog_course_number'], 195 | course_name=l_basics['name'], 196 | term=l_basics['term'], 197 | course_description=l_basics['description'], 198 | credits=l_basics['credits'], 199 | assignment_group_total=weights_total, 200 | footer_course_number=l_basics['catalog_course_number'], 201 | footer_term=l_basics['term'], 202 | footer_section=l_basics['footer_section'], 203 | footer_date=datetime.date.today().strftime('%m/%d/%Y')) 204 | if l_basics['program'] not in ['NFNPO', 'NCMO']: 205 | template.merge( 206 | section=l_basics['section'], 207 | office_location=l_basics['office_location'], 208 | meeting_times=l_basics['meeting_times'], 209 | class_location=l_basics['class_location']) 210 | template.merge_rows('required_title', l_materials) 211 | template.merge_rows('clo_title', l_clos) 212 | template.merge_rows('plo_title', l_plos) 213 | template.merge_rows('module_name', assignments) 214 | template.merge_rows('assignment_group_name', weights) 215 | template.merge_rows('grading_scale_high', standard) 216 | template.write(config.output_dir + l_basics['out_filename']) 217 | 218 | 219 | def upload_file(syllabus_filename): 220 | s3 = boto3.client('s3', aws_access_key_id=config.aws_access_key_id, 221 | aws_secret_access_key=config.aws_secret_access_key) 222 | s3.upload_file(config.output_dir + syllabus_filename, 'smu-aii', 'syllabi/' + syllabus_filename) 223 | 224 | 225 | if __name__ == '__main__': 226 | main() 227 | tada() 228 | --------------------------------------------------------------------------------