├── 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": "
(.*)$', 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 |
--------------------------------------------------------------------------------