-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathCanvasMyRubrics.py
461 lines (411 loc) · 17.5 KB
/
CanvasMyRubrics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
import pathlib
import sys
from operator import itemgetter
import canvasapi
import requests
import xlsxwriter
from canvasapi import Canvas
from cryptography.fernet import Fernet
def build_canvas():
"""
Instantiate the Canvas object using the LMS URL and the
APIKEY.enc file (you know to keep the key secret as best
we can).
"""
global canvas
global badCanvas
global courses
apiURL = None
while not apiURL:
try:
with open("APIURL.txt", "r") as urlfile:
apiURL = urlfile.read()
except FileNotFoundError:
print("File 'APIURL.txt' not found!")
create_urlfile() # If the APIURL.txt file isn't found, create it
urlfile.close()
fernet = Fernet(key) # This key is from the start of the main pgm
apiKey = None
while not apiKey:
try:
with open(apiKeyFile, 'rb') as apifile: # We need to decrypt the text from the APIKEY.enc file
encapiKEY = apifile.read()
apiKey = fernet.decrypt(encapiKEY) # We read this as bytes
apiKey = apiKey.decode() # So we need to decode back to a str
except FileNotFoundError:
print("File 'APIKEY.enc' not found!")
create_apikeyfile() # If the APIKEY.enc file isn't found, ask the user their key and save it
apifile.close()
try:
canvas = Canvas(apiURL, apiKey) # Create the canvas object
courses = canvas.get_courses()
print("\nHere is a list of courses available to the API Token provided:\n")
for crse in courses:
print(crse.name)
badCanvas = False
return
except requests.exceptions.ConnectionError:
yesno = input("A connection occurred creating the Canvas object. Maybe your\n"
"Canvas URL is incorrect or your internet connection is down.\n"
"Would you like to update your Canvas URL? (Yes/No): \n")
excpt = 'connErr'
fix_file(yesno,excpt)
return
except canvasapi.exceptions.InvalidAccessToken:
print("There is an invalid API key in APIKEY.enc.\n")
yesno = input("Would you like to change or update your API Token? (Yes/No): ")
excpt = 'tokenErr'
fix_file(yesno,excpt)
return
def ask_course():
"""
Ask which course they are looking to download from using the human readable
name, then return with it. Also, the user can type list for a list courses
available to the user object.
"""
global uniqueID
while True:
uniqueID = input("\nEnter the unique name for your course from above (i.e. 20-2 or 20-B)\n"
"or, type 'list' to display the list of courses available to you: ")
print('\n')
is_exit(uniqueID)
if "list" in uniqueID:
for crse in courses:
print(crse.name)
continue
else:
return uniqueID
def build_course():
"""
Instantiate a course object using the ask_course() function to search the courses
available to the user object.
"""
global badSearch
global course
ask_course()
count = 1
coursesLen = len(courses._elements)
for crse in courses:
if uniqueID in crse.name:
badSearch = False
courseID = crse.id
elif count == coursesLen:
print("No course matching", uniqueID, "found.\n")
return
else:
count += 1
course = canvas.get_course(courseID)
return
def select_assignment():
"""
List all of the assignments the course object has access to. Then, using the course
object, create an the assignment object requested by the user. Finally, call
get_rubric() do iterate, download, and write the requested rubrics to an xlsx file.
"""
global assignment
global assignmentID
global badAsgmt
global wantedSubmissions
print("Listing assignments for", course.name, ":\n")
for assignment in course.get_assignments():
if "fire" in assignment.name.lower():
pass
else:
print(assignment)
while True:
print('\n')
assignmentID = input("Enter the the assignment ID (in parentheses above) for the \n"
"grades you would like to retrieve, or type 'all' for all grades: ")
is_exit(assignmentID)
break
if "all" in str(assignmentID):
for asgmt in course.get_assignments():
assignmentID = str(asgmt.id)
assignment = course.get_assignment(assignmentID) # We're getting this for variable name uniqueness
wantedSubmissions = course.get_multiple_submissions(student_ids='all', assignment_ids=str(asgmt.id),
include='rubric_assessment')
badAsgmt = False
get_rubric()
else:
try:
assignment = course.get_assignment(assignmentID) # We're getting this for variable name uniqueness
wantedSubmissions = course.get_multiple_submissions(student_ids='all', assignment_ids=assignmentID,
include='rubric_assessment')
badAsgmt = False
get_rubric()
except canvasapi.exceptions.ResourceDoesNotExist:
print("\nCould not find the requested assignment", assignmentID)
return
return
def get_rubric():
"""
Get important rubric (not assignment or submission) information and map rubric
IDs to assignment IDs. This allows us to match the rubric scoring and item info
to an assignment (and later a submission).
"""
global rubrics
global rubric
rubric = None
rubrics = course.get_rubrics()
rubricsList = []
for rbrc in rubrics:
rubricsList.append(rbrc.title)
rbrcAsgmtMap = []
for asgmt in course.get_assignments():
count = 0
while count < len(rubricsList): # Below...current naming conventions are unambiguous in the first 10 chars
if "fire" in str(asgmt.name).lower():
# count += 1
break
elif rubrics[count].title[0:10] in asgmt.name[0:10]: # They also won't match if you go too far
rbrcAsgmtMap.append([rubrics[count].id, asgmt.id])
break
else:
count += 1
for rbrc in rbrcAsgmtMap:
if assignmentID in str(rbrc): # Assignment IDs are more unique than rubric IDs
rubric = course.get_rubric(rbrc[0])
break
if rubric:
canvas_rubrics() # If we're good, let's do this thing
elif "fire" in str(assignment).lower():
print("Skipping a re-fire assignment.")
return
else:
print("No match for", assignment)
return
def canvas_rubrics():
"""
Build list of lists containing rubric info, assignment info, student/flight info, and
submission grades for the selected assignment. Uses that list to write to the
xlsx file.
"""
global currworksheet
global studentList
global submission
global scoresAll
global xlsxOut
print("Grabbing", assignment.name, "scores.")
rubricItems = ['Student ID', 'Student Name', 'Flight'] # Establish column headers before rubric items
rubricRatings = [[], [], []] # List of lists containing rubric rating options/points for each item
count = 0
for item in rubric.data: # Go through the rubrics to finish column headers and rating points
rubricItemDesc = rubric.data[count]['description']
rubricItemPoints = rubric.data[count]['points']
rubricItems.append(rubricItemDesc + ' ' + str(rubricItemPoints))
for rbrc in range(3):
ratingPoints = rubric.data[count]['ratings'][rbrc]['points']
rubricRatings[rbrc].append(ratingPoints)
count += 1
rubricItems.append('Highest possible: ' + str(rubric.points_possible))
rubricRatingDesc = ['Exceeds', 'Meets', 'Does Not Meet'] # Place rating options in front of points
for rating in range(3):
rubricRatings[rating].insert(0, '')
rubricRatings[rating].insert(0, rubricRatingDesc[rating])
rubricRatings[rating].insert(0, '')
scoresAll = [] # Our full list of scores for each submission
try: # We will get an exception if the assignment isn't published
for sub in wantedSubmissions:
count = 0
stuScores = [sub.user_id] # Our list for the current student/submission, index[0] is the student ID
if hasattr(sub, 'rubric_assessment'): # Check if the student even has a submission/rubric assessment
while count < len(sub.rubric_assessment):
for key in sub.rubric_assessment.keys():
if 'points' in sub.rubric_assessment[key]:
stuScores.append(sub.rubric_assessment[key]['points']) # Append each rubric item score
count += 1
else:
stuScores.append('BLANK')
count += 1
if "BLANK" in stuScores:
scoresAll.append(stuScores) # Append this student's score list to the full list
else:
stuScores.append(int(sub.grade)) # Append this student's overall score
scoresAll.append(stuScores) # Append this student's score list to the full list
except: # Catch unpublished assignments - or other errors *shrug*
print("Error in processing", assignment.name, "... Is it published? Skipping.")
return
scoresAll = sorted(scoresAll, key=itemgetter(0)) # Sort this list by student ID
if len(scoresAll) == 0:
print(assignment.name, "has no graded rubrics. Skipping.")
return
flts = course.get_sections(include='students') # We need a student/flight(section) list
stdFltList = []
for flt in flts:
count = 0
for s in flt.students:
stdFltList.append([flt.students[count]['id'], flt.students[count]['sortable_name'], flt.name])
count += 1
xlsxOut = sorted(stdFltList, key=itemgetter(0)) # Sort this in the same manner as scoresALL
scoresCount = 0
xlsxCount = 0
for item in xlsxOut:
if scoresCount <= len(scoresAll) - 1:
if item[0] == scoresAll[scoresCount][0]: # Match student IDs before appending
scoresAll[scoresCount].pop(0) # Remove the redundant student ID
xlsxOut[xlsxCount].extend(scoresAll[scoresCount]) # Place student's scores next to their name
scoresCount += 1
xlsxCount += 1
else:
xlsxOut[xlsxCount].append('NO SCORED RUBRIC FOUND ON CANVAS')
xlsxCount += 1
else:
xlsxOut[xlsxCount].append('NO SCORED RUBRIC FOUND ON CANVAS')
xlsxCount += 1
xlsxOut.insert(0, rubricItems) # Inserting rubric info at the top
count = 0
for item in rubricRatings:
xlsxOut.insert(count, item) # Inserting more rubric info at the top
if count == 1:
xlsxOut[count].append('Minimum Passing Score')
count += 1
elif count == 2:
xlsxOut[count].append(int(rubric.points_possible * .7))
else:
count += 1
if len(rubric.title) >= 25: # Worksheets have a name length limit of 31 chars and we need unique names
worksheetName = str(assignment.name[0:24])
else:
worksheetName = str(assignment.name)
try:
currworksheet = workbook.add_worksheet(worksheetName) # Make a new worksheet
except xlsxwriter.exceptions.DuplicateWorksheetName:
currworksheet = workbook.add_worksheet(worksheetName + str(assignmentID))
row_writer(currworksheet, xlsxOut) # Write it out to the file
def row_writer(wsname, data):
"""
This builds and writes simple rows in the xlsx from python list of lists.
"""
row = 0
while row < len(data):
col = 0
for item in data[row]:
wsname.write(row, col, item)
col += 1
row += 1
return
def is_exit(ex):
"""
Check if the user types exit at any prompt.
"""
if "exit" in ex:
print("\nSee you later!")
exit(0)
else:
pass
def get_datadir() -> pathlib.Path:
"""
Returns a parent directory path where persistent application data can be stored.
This is the best location for the encryption key, all things considered.
"""
home = pathlib.Path.home()
if sys.platform == "win32":
return home / "AppData/Roaming"
elif sys.platform == "linux":
return home / ".local/share"
elif sys.platform == "darwin":
return home / "Library/Application Support"
def load_key():
"""
Returns the encryption/decryption key as a variable to use later.
"""
try:
return open(keyFile, "rb").read()
except FileNotFoundError:
print("Encryption key not found, generating a new one...")
gen_key()
def gen_key():
"""
Generate a new encryption key, and save it in the user's application
data directory (userDir).
"""
newKey = Fernet.generate_key()
with open(keyFile, "wb") as file:
file.write(newKey)
return
def create_urlfile():
"""
Write/overwrite the APIURL.txt file with a new LMS URL.
"""
while True:
apiURLIn = input("\nPlease enter the URL you use to access your Canvas system,\n"
"or type 'help' for instructions on getting your URL: ")
is_exit(apiURLIn)
if "help" in apiURLIn:
print("\nThe URL you use to access your Canvas system is the same as\n"
"the web address you enter into your browser. As an example: if\n"
"you type 'https://lms.yourschool.edu' in your browser to go\n"
"to your Canvas, 'https://lms.yourschool.edu' is what you will\n"
"type at the prompt asking for your URL.\n")
continue
break
with open("APIURL.txt", "w") as file:
file.write(apiURLIn)
file.close()
def create_apikeyfile():
"""
Write/overwrite the encrypted APIKEY.enc file with a new LMS API Key.
"""
global key
if not key:
key = load_key()
f = Fernet(key)
while True:
apiKeyIn = input("\nPlease enter the API key you generated on LMS or\n"
"type 'help' for instructions on generating a key: ")
is_exit(apiKeyIn)
if "help" in apiKeyIn:
print("\nTo generate an API Token/Key on LMS, login and click on\n"
"'Account'>'Settings'. Under 'Approved Integrations' click\n"
"the '+New Access Token' button. Fill in the form, and click\n"
"the 'Generate Token' button. A screen will pop up showing\n"
"your new token/key. *You will not see that key again in its\n"
"entirety after clicking the 'X' in the top corner!* Write it\n"
"down in a **safe** place. This token/key gives full LMS access\n"
"to any person that possesses it! CanvasMyRubrics encrypts your\n"
"token when you enter it at the prompt. So you may delete/toss\n"
"the copy of your token that you wrote down after entering it.\n")
continue
break
apiKeyIn = apiKeyIn.encode() # We need to encode the API key into bytes for encryption
encapiKeyIn = f.encrypt(apiKeyIn)
with open(apiKeyFile, "wb") as file:
file.write(encapiKeyIn)
file.close()
def fix_file(yesno,excpt):
is_exit(yesno)
if yesno.lower() == 'yes' or yesno.lower() == 'y':
if excpt == 'connErr':
create_urlfile()
elif excpt == 'tokenErr':
create_apikeyfile()
return
elif yesno.lower() == 'no' or yesno.lower() == 'n':
return
print("\nWelcome to CanvasMyRubrics.\n"
"Type exit at any prompt to exit the program.\n")
userDir = get_datadir() / "CanvasMyRubrics"
try:
userDir.mkdir(parents=True)
except FileExistsError:
pass
keyFile = pathlib.Path(userDir / "APIKEY.key")
apiKeyFile = "APIKEY.enc"
key = None
while not key:
key = load_key()
badCanvas = True
while badCanvas:
build_canvas() # Open the Canvas API and create the canvas object
key = None # Clear the key to deter nefarious characters, should be done with it here anyway
badSearch = True # This helps us continue to run ask_course() when unexpected input is given
while badSearch:
build_course() # Create a course object to get assignment info and rubrics
filename = uniqueID + ".xlsx" # Create a filename based on the course name given by the user
workbook = xlsxwriter.Workbook(filename) # Open a workbook - *this overwrites existing files with the same name*
badAsgmt = True
while badAsgmt:
select_assignment() # Get the user to select an assignment and call get_rubric and canvas_rubrics
workbook.close() # Close the workbook
print("\nAll requested grades written to", filename, "in the current directory.")
exit(0)