cornellGrading 2.16.2__tar.gz → 2.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {cornellGrading-2.16.2/cornellGrading.egg-info → cornellgrading-2.18.0}/PKG-INFO +1 -1
  2. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading/__init__.py +1 -1
  3. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading/cornellGrading.py +359 -65
  4. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading/cornellQualtrics.py +53 -25
  5. {cornellGrading-2.16.2 → cornellgrading-2.18.0/cornellGrading.egg-info}/PKG-INFO +1 -1
  6. cornellgrading-2.18.0/pyproject.toml +81 -0
  7. cornellGrading-2.16.2/pyproject.toml +0 -3
  8. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/LICENSE +0 -0
  9. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/MANIFEST.in +0 -0
  10. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/README.md +0 -0
  11. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading/cornellInterface.py +0 -0
  12. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading/dueDatesFromCSV.py +0 -0
  13. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading/pandocHTMLParser.py +0 -0
  14. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading/upload_MC_questions.py +0 -0
  15. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading.egg-info/SOURCES.txt +0 -0
  16. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading.egg-info/dependency_links.txt +0 -0
  17. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading.egg-info/entry_points.txt +0 -0
  18. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading.egg-info/requires.txt +0 -0
  19. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/cornellGrading.egg-info/top_level.txt +0 -0
  20. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/scripts/canvastex.py +0 -0
  21. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/scripts/matlabgrader.py +0 -0
  22. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/scripts/setupSelfGrading.py +0 -0
  23. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/setup.cfg +0 -0
  24. {cornellGrading-2.16.2 → cornellgrading-2.18.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cornellGrading
3
- Version: 2.16.2
3
+ Version: 2.18.0
4
4
  Summary: Routines for interacting with Cornell installations of Canvas and Qualtrics
5
5
  Home-page: https://github.com/dsavransky/grading
6
6
  Author: Dmitry Savransky
@@ -1,4 +1,4 @@
1
1
  name = "cornellGrading"
2
- __version__ = "2.16.2"
2
+ __version__ = "2.18.0"
3
3
  from .cornellGrading import cornellGrading
4
4
  from .cornellQualtrics import cornellQualtrics
@@ -15,6 +15,7 @@ from cornellGrading.cornellQualtrics import cornellQualtrics
15
15
  import urllib.parse
16
16
  import subprocess
17
17
  import shutil
18
+ import requests
18
19
 
19
20
  try:
20
21
  from cornellGrading.pandocHTMLParser import pandocHTMLParser
@@ -209,6 +210,7 @@ class cornellGrading:
209
210
  Name of assignment to return. Must be exact match.
210
211
  To see all assignments do:
211
212
  >> for a in c.course.get_assignments(): print(a)
213
+
212
214
  Returns:
213
215
  canvasapi.assignment.Assignment:
214
216
  The Assignment object
@@ -234,6 +236,7 @@ class cornellGrading:
234
236
  Name of assignment group to return. Must be exact match.
235
237
  To see all assignments do:
236
238
  >> for a in c.course.get_assignment_groups(): print(a)
239
+
237
240
  Returns:
238
241
  canvasapi.assignment.AssignmentGroup:
239
242
  The assignment group object
@@ -255,8 +258,9 @@ class cornellGrading:
255
258
  """Create assignment group by name
256
259
 
257
260
  Args:
258
- assignmentGroup (str):
261
+ groupName (str):
259
262
  Name of assignment group to create. Cannot be name of existing group
263
+
260
264
  Returns:
261
265
  canvasapi.assignment.AssignmentGroup:
262
266
  The assignment group object
@@ -277,10 +281,11 @@ class cornellGrading:
277
281
  """Locate folder by name
278
282
 
279
283
  Args:
280
- (str):
284
+ folderName (str):
281
285
  Name of folder to return. Must be exact match.
282
286
  To see all assignments do:
283
287
  >> for a in c.course.get_folders(): print(a.name)
288
+
284
289
  Returns:
285
290
  canvasapi.folder.Folder:
286
291
  The folder object
@@ -313,6 +318,7 @@ class cornellGrading:
313
318
  hidden (bool):
314
319
  Whether to toggle to hidden (false by default). If parent folder is
315
320
  hidden, you cannot make the subfolder visible.
321
+
316
322
  Returns:
317
323
  canvasapi.folder.Folder:
318
324
  The folder object
@@ -478,7 +484,9 @@ class cornellGrading:
478
484
 
479
485
  return pg
480
486
 
481
- def createPage(self, title, body, editing_roles="teachers", published=False):
487
+ def createPage(
488
+ self, title, body, editing_roles="teachers", published=False, publish_at=None
489
+ ):
482
490
  """Create a Page
483
491
 
484
492
  Args:
@@ -489,7 +497,10 @@ class cornellGrading:
489
497
  editing_roles (str):
490
498
  See canvas API. Comma sepeated string, defaults to "teachers"
491
499
  published (bool):
492
- Whether page is published on create (defaults True)
500
+ Whether page is published on create (defaults False)
501
+ publish_at (datetime.datetime):
502
+ Publish date (not included if None). Must be timezone aware and UTC! If
503
+ set, overrides any inputs for published. Defaults None.
493
504
 
494
505
  Returns:
495
506
  canvasapi.page.Page:
@@ -503,6 +514,8 @@ class cornellGrading:
503
514
  """
504
515
 
505
516
  assert isinstance(editing_roles, str), "editing_roles must be a string."
517
+ if publish_at is not None:
518
+ published = False
506
519
 
507
520
  wiki_page = {
508
521
  "title": title,
@@ -510,6 +523,8 @@ class cornellGrading:
510
523
  "editing_roles": editing_roles,
511
524
  "published": published,
512
525
  }
526
+ if publish_at:
527
+ wiki_page["publish_at"] = publish_at.strftime("%Y-%m-%dT%H:%M:%SZ")
513
528
 
514
529
  res = self.course.create_page(wiki_page=wiki_page)
515
530
 
@@ -523,6 +538,7 @@ class cornellGrading:
523
538
  hidden=True,
524
539
  editing_roles="teachers",
525
540
  published=False,
541
+ publish_at=None,
526
542
  insertPDF=False,
527
543
  ):
528
544
  """Generate a new canvas page out of a LaTex source file
@@ -546,6 +562,9 @@ class cornellGrading:
546
562
  See canvas API. Comma sepeated string, defaults to "teachers"
547
563
  published (bool):
548
564
  Whether page is published on create (defaults False)
565
+ publish_at (datetime.datetime):
566
+ Publish date (not included if None). Must be timezone aware and UTC! If
567
+ set, overrides any inputs for published. Defaults None.
549
568
  insertPDF (bool):
550
569
  If true, also include the original file in the page (this assumes that
551
570
  fname points at the compiled PDF and not the source).
@@ -558,6 +577,7 @@ class cornellGrading:
558
577
  Requires pandoc to be installed and callable!
559
578
 
560
579
  .. warning::
580
+
561
581
  Uploaded files will overwrite files of the same name in the upload folder.
562
582
 
563
583
  """
@@ -585,7 +605,11 @@ class cornellGrading:
585
605
  body += " " + out
586
606
 
587
607
  res = self.createPage(
588
- title, body, editing_roles=editing_roles, published=published
608
+ title,
609
+ body,
610
+ editing_roles=editing_roles,
611
+ published=published,
612
+ publish_at=publish_at,
589
613
  )
590
614
 
591
615
  return res
@@ -1011,6 +1035,8 @@ class cornellGrading:
1011
1035
  Due date in format: YYYY-MM-DD (5pm assumed local time)
1012
1036
  hwfile (str):
1013
1037
  Full path to homework file
1038
+ totscore (int, float):
1039
+ Total number of points for the assignment. Defaults to 10.
1014
1040
  unlockDelta (float):
1015
1041
  Unlock this number of days before due date. Defaults to None,
1016
1042
  which makes the assignment initially unlocked.
@@ -1211,36 +1237,31 @@ class cornellGrading:
1211
1237
  f.write(ll)
1212
1238
 
1213
1239
  # run pandoc
1214
- if hwd:
1215
- _ = subprocess.run(
1216
- [
1217
- "pandoc",
1218
- os.path.join(tmpdir, texf),
1219
- "-s",
1220
- "--webtex",
1221
- "-o",
1222
- htmlf,
1223
- "--default-image-extension=png",
1224
- ],
1225
- cwd=hwd,
1226
- check=True,
1227
- capture_output=True,
1228
- )
1240
+ if not (hwd):
1241
+ hwd = os.path.curdir
1242
+
1243
+ pandoc_comm = [
1244
+ "pandoc",
1245
+ os.path.join(tmpdir, texf),
1246
+ "-s",
1247
+ "--webtex",
1248
+ "-o",
1249
+ htmlf,
1250
+ "--default-image-extension=png",
1251
+ ]
1252
+
1253
+ if shutil.which("pandoc-crossref") is not None:
1254
+ pandoc_comm += ["--filter", "pandoc-crossref"]
1255
+ have_crossref = True
1229
1256
  else:
1230
- _ = subprocess.run(
1231
- [
1232
- "pandoc",
1233
- os.path.join(tmpdir, texf),
1234
- "-s",
1235
- "--webtex",
1236
- "-o",
1237
- htmlf,
1238
- "--default-image-extension=png",
1239
- ],
1240
- cwd=os.path.curdir,
1241
- check=True,
1242
- capture_output=True,
1243
- )
1257
+ have_crossref = False
1258
+
1259
+ _ = subprocess.run(
1260
+ pandoc_comm,
1261
+ cwd=hwd,
1262
+ check=True,
1263
+ capture_output=True,
1264
+ )
1244
1265
 
1245
1266
  assert os.path.exists(htmlf), "Cannot locate html output %s" % htmlf
1246
1267
 
@@ -1250,13 +1271,7 @@ class cornellGrading:
1250
1271
 
1251
1272
  upfolder = self.createFolder(folder, hidden=hidden)
1252
1273
 
1253
- # global replacements
1254
- repdict = {
1255
- r'class="math display"': r'style="display: block; text-align: center; margin: 0.5rem auto;"',
1256
- r'img style="vertical-align:middle"': r'img class="equation_image"',
1257
- r'src="https://latex.codecogs.com/png.latex\?': r'src="https://canvas.cornell.edu/equation_images/',
1258
- }
1259
-
1274
+ # latex conversion
1260
1275
  p = re.compile(r'src="https://latex.codecogs.com/png.latex\?(.*?)"')
1261
1276
 
1262
1277
  def convlatex(x):
@@ -1269,21 +1284,10 @@ class cornellGrading:
1269
1284
  parser.feed(line)
1270
1285
  if parser.inBody:
1271
1286
  tmp = p.sub(convlatex, line)
1272
- for k, v in repdict.items():
1273
- tmp = re.sub(k, v, tmp)
1274
-
1275
- while parser.imagesUploaded:
1276
- imup = parser.imagesUploaded.pop()
1277
- canvasimurl = (
1278
- r'src="https://canvas.cornell.edu{0}/preview" '
1279
- r'data-api-endpoint="https://canvas.cornell.edu/api/'
1280
- r'v1{0}" data-api-returntype="File" '.format(imup["url"])
1281
- )
1282
- tmp = re.sub(r'src="{0}"'.format(imup["orig"]), canvasimurl, tmp)
1283
1287
 
1284
- if parser.figcaptions:
1285
- figcap = parser.figcaptions.pop()
1286
- tmp = re.sub(r'alt=""', r'alt="{0}"'.format(figcap), tmp)
1288
+ if (len(parser.figcaptions) > 0) and not (have_crossref):
1289
+ figcap = parser.figcaptions.pop()
1290
+ tmp = re.sub(r'alt=""', r'alt="{0}"'.format(figcap), tmp)
1287
1291
 
1288
1292
  if parser.spanDefs:
1289
1293
  for cl, val in parser.spanDefs.items():
@@ -1294,10 +1298,43 @@ class cornellGrading:
1294
1298
 
1295
1299
  out.append(tmp)
1296
1300
 
1301
+ # put everything together into a isngle string
1297
1302
  out = out[1:]
1298
- # out = ' #strdelim# '.join(out)
1299
1303
  out = " ".join(out)
1300
1304
 
1305
+ # global replacements
1306
+ repdict = {
1307
+ r'class="math display"': r'style="display: block; text-align: center; margin: 0.5rem auto;"',
1308
+ r'img[\s]*style="vertical-align:middle"': r'img class="equation_image"',
1309
+ r'src="https://latex.codecogs.com/png.latex\?': r'src="https://canvas.cornell.edu/equation_images/',
1310
+ r"<figcaption": '<figcaption style="text-align: center;"',
1311
+ }
1312
+
1313
+ for k, v in repdict.items():
1314
+ out = re.sub(k, v, out)
1315
+
1316
+ # handle all uploaded figures:
1317
+ while parser.imagesUploaded:
1318
+ imup = parser.imagesUploaded.pop()
1319
+ canvasimurl = (
1320
+ r'src="https://canvas.cornell.edu{0}/preview" '
1321
+ r'data-api-endpoint="https://canvas.cornell.edu/api/'
1322
+ r'v1{0}" data-api-returntype="File" '.format(imup["url"])
1323
+ )
1324
+ out = re.sub(r'src="{0}"'.format(imup["orig"]), canvasimurl, out)
1325
+
1326
+ # figures less than 100% width get centered:
1327
+ p2 = re.compile(r'style="([\S]*)?(?=")"')
1328
+
1329
+ def convwidth(x):
1330
+ return re.sub(
1331
+ x.group(1),
1332
+ f"{x.group(1)}; display: block; margin-left: auto; margin-right: auto;",
1333
+ x.group(),
1334
+ )
1335
+
1336
+ out = p2.sub(convwidth, out)
1337
+
1301
1338
  # handle any labeled equations
1302
1339
  if parser.eqlabels:
1303
1340
  for label, (eqnum, labelstr) in parser.eqlabels.items():
@@ -1306,14 +1343,10 @@ class cornellGrading:
1306
1343
  # for every equation image with a label, find it, and replace with a
1307
1344
  # span with an equation number
1308
1345
  imtag = re.search(
1309
- # r'<p>(<img class="equation_image"(.*?)(?={})([\s\S]*?)(?=/></p>)/>)</p>'.format(
1310
- # r'(<img class="equation_image"(.*?)(?={})([\s\S]*?)(?=/>)/>)'.format(
1311
- # r'(<img class="equation_image" src="https://canvas.cornell.edu/equation_images/([\w%]*?)(?={})([\s\S]*?)(?=/>)/>)'.format(
1312
- r'(<img class="equation_image" src="https://canvas.cornell.edu/equation_images/([\w%.-]*?)(?={})([\s\S]*?)(?=/>)/>)'.format(
1313
- enclabel
1314
- ),
1346
+ rf'(<img class="equation_image"[\s]*src="https://canvas.cornell.edu/equation_images/([\w%.-]*?)(?={enclabel})([\s\S]*?)(?=/>)/>)',
1315
1347
  out,
1316
1348
  )
1349
+
1317
1350
  imspan = (
1318
1351
  r'<span style="margin: 1ex auto; display: table; '
1319
1352
  r'text-align: center; width: 100%; vertical-align: middle;"> '
@@ -1332,11 +1365,10 @@ class cornellGrading:
1332
1365
  out = out.replace("[{}]".format(label), "{}".format(eqnum))
1333
1366
 
1334
1367
  # handle any figure labels
1335
- if parser.figLabels:
1368
+ if (len(parser.figLabels) > 0) and not (have_crossref):
1336
1369
  for cl, val in parser.figLabels.items():
1337
1370
  out = out.replace(cl, val)
1338
1371
 
1339
- # out = out.split(' #strdelim# ')
1340
1372
  return out
1341
1373
 
1342
1374
  def genPrivateHWSurvey(self, surveyname, nprobs, scoreOptions=None, ecprobs=[]):
@@ -1599,7 +1631,7 @@ class cornellGrading:
1599
1631
  Number of assignment. Name of survey will be
1600
1632
  "self.coursename HW# Self-Grade"
1601
1633
  Name of assignment will be HW#
1602
- escore (int):
1634
+ ecscore (int):
1603
1635
  Extra credit score (defaults to 3)
1604
1636
  checkLate (bool):
1605
1637
  Check for late submissions (defaults true)
@@ -1962,3 +1994,265 @@ class cornellGrading:
1962
1994
  item["position"] = position
1963
1995
 
1964
1996
  module.create_module_item(item)
1997
+
1998
+ def addNewQuizItem(self, quizid, item):
1999
+ """
2000
+
2001
+ Args:
2002
+ quizid (int):
2003
+ ID of New Quiz to add item to.\
2004
+ item (dict):
2005
+ Item payload dictionary. This is the contents of the 'item' key in the
2006
+ JSON payload. For details, see:
2007
+ https://canvas.instructure.com/doc/api/new_quiz_items.html#Question+Types-appendix
2008
+
2009
+ """
2010
+
2011
+ full_url = "{}{}".format(
2012
+ self.course._requester.new_quizzes_url,
2013
+ "courses/{}/quizzes/{}/items".format(self.course.id, quizid),
2014
+ )
2015
+ headers = {
2016
+ "Authorization": "Bearer {}".format(self.course._requester.access_token)
2017
+ }
2018
+
2019
+ r = requests.post(full_url, json={"item": item}, headers=headers)
2020
+ assert r.status_code == 200
2021
+
2022
+ def getHomework(self, assignmentNum, preamble=""):
2023
+ """Retrieve assignment based on number and optional preamble
2024
+
2025
+ Args:
2026
+ assignmentNum (int):
2027
+ Assignment names will be of the form "[preamble] HW#"
2028
+ where # is `assignmentNum`. See also `preamble` input.
2029
+
2030
+ preamble (str):
2031
+ If not "", then assignment name will be "[preamble] HW#". Otherwise,
2032
+ name will be "HW#". Defaults to ""
2033
+
2034
+ Returns:
2035
+ tuple:
2036
+ hwname (str):
2037
+ Full assignment name
2038
+ hw (canvasapi.assignment.Assignment):
2039
+ Assignment object
2040
+ """
2041
+
2042
+ # if preamble set, make sure it ends in a space
2043
+ if preamble != "":
2044
+ preamble = f"{preamble.strip()} "
2045
+
2046
+ # generate the name and grab the assignment
2047
+ hwname = f"{preamble}HW{assignmentNum}"
2048
+ hw = self.getAssignment(hwname)
2049
+
2050
+ return hwname, hw
2051
+
2052
+ def setupNewQuizSelfAssessment(
2053
+ self,
2054
+ assignmentNum,
2055
+ nprobs,
2056
+ solfile,
2057
+ item,
2058
+ preamble="",
2059
+ selfGradeDueDelta=7,
2060
+ selfGradeReleasedDelta=3,
2061
+ ):
2062
+ """Create a page with reference solutions and a New Quiz linking to the page for
2063
+ student self-assemssent.
2064
+
2065
+ Args:
2066
+ assignmentNum (int):
2067
+ Number of assignment.
2068
+ See :py:meth:`cornellGrading.cornellGrading.getHomework` for details.
2069
+ nprobs (int):
2070
+ Number of howmework problems
2071
+ solfile (str):
2072
+ Full path to solutions file to upload
2073
+ item (dict):
2074
+ New Quiz item payload dictionary. See `addNewQuizItem`
2075
+ preamble (str):
2076
+ Preamble for all naming.
2077
+ See :py:meth:`cornellGrading.cornellGrading.getHomework` for details.
2078
+ selfGradeDueDelta (float):
2079
+ Days after initial hw duedate that self-grading is due
2080
+ selfGradeReleasedDelta (float):
2081
+ Days after initial hw duedate that self-grading (and solutions) are
2082
+ released.
2083
+
2084
+ Returns:
2085
+ None
2086
+
2087
+ """
2088
+
2089
+ # ensure solutions file exists
2090
+ assert os.path.exists(solfile)
2091
+
2092
+ # get original assignment
2093
+ hwname, hw = self.getHomework(assignmentNum, preamble=preamble)
2094
+
2095
+ # figure out all dates
2096
+ duedate = datetime.strptime(hw.due_at, """%Y-%m-%dT%H:%M:%S%z""")
2097
+ unlockdate = duedate + timedelta(days=selfGradeReleasedDelta)
2098
+ selfgradeduedate = unlockdate + timedelta(days=selfGradeDueDelta)
2099
+
2100
+ # generate reference solutions page
2101
+ pagename = f"{hwname} Solutions"
2102
+
2103
+ hwfoldername = "Homeworks/HW%d" % assignmentNum
2104
+ _ = self.createFolder(hwfoldername, hidden=True)
2105
+
2106
+ p = self.latex2page(
2107
+ solfile,
2108
+ pagename,
2109
+ folder=hwfoldername,
2110
+ publish_at=unlockdate,
2111
+ )
2112
+
2113
+ # get self-assessment assignment group and generate new quiz there
2114
+ assgrp = self.getAssignmentGroup("Homework Self-Assessment")
2115
+
2116
+ instructions = (
2117
+ rf"<p>{hwname} Solutions are available here: "
2118
+ rf'<a title={pagename} href="{p.html_url.split(".edu")[-1]}" '
2119
+ rf'data-course-type="wikiPages">{pagename}</a>. Check your work and score '
2120
+ r" yourself based on the rubric in the syllabus.</p>"
2121
+ )
2122
+
2123
+ nq = self.course.create_new_quiz(
2124
+ quiz={
2125
+ "title": f"{hwname} Self-Assessment",
2126
+ "assignment_group_id": assgrp.id,
2127
+ "points_possible": hw.points_possible,
2128
+ "due_at": selfgradeduedate,
2129
+ "unlock_at": unlockdate,
2130
+ "grading_type": "points",
2131
+ "instructions": instructions,
2132
+ }
2133
+ )
2134
+
2135
+ # add new quiz items
2136
+ for j in range(nprobs):
2137
+ tmp = item.copy()
2138
+ tmp["position"] = j + 1
2139
+ tmp["entry"]["title"] = f"Problem {j+1} score"
2140
+ self.addNewQuizItem(nq.id, tmp)
2141
+
2142
+ # publish quiz
2143
+ ass = self.getAssignment(nq.title)
2144
+ ass.edit(assignment={"published": True})
2145
+
2146
+ def linearTimePenalty(self, hw, latePenalty=0.25, maxDaysLate=3):
2147
+ """Compute late penalty as linearly increasing in time.
2148
+
2149
+ Args:
2150
+ hw (canvasapi.assignment.Assignment):
2151
+ Assignment object
2152
+ latePenalty (float):
2153
+ Maximum fraction of score to remove for lateness (defaults to 0.25).
2154
+ Must be in (0,1).
2155
+ maxDaysLate (float):
2156
+ After this number of days past deadline, HW gets zero. Defaults to 3.
2157
+ Students submitting at exactly the maximum time will recieve the maximum
2158
+ late penalty.
2159
+
2160
+ Returns:
2161
+ dict:
2162
+ Dictionary of late penalties in absolute points. Keys are netids.
2163
+ The dictionary will only contain values for students with submissions.
2164
+
2165
+ """
2166
+
2167
+ totscore = hw.points_possible
2168
+ duedate = datetime.strptime(hw.due_at, """%Y-%m-%dT%H:%M:%S%z""")
2169
+
2170
+ lates = {}
2171
+ for t in hw.get_submissions():
2172
+ if t.user_id in self.ids:
2173
+ netid = self.netids[self.ids == t.user_id][0]
2174
+
2175
+ if t.submitted_at:
2176
+ subtime = datetime.strptime(
2177
+ t.submitted_at, """%Y-%m-%dT%H:%M:%S%z"""
2178
+ )
2179
+ if not t.late:
2180
+ tdelta = 0
2181
+ else:
2182
+ tdelta = np.abs((duedate - subtime).total_seconds()) / 86400
2183
+
2184
+ if tdelta > maxDaysLate:
2185
+ lates[netid] = totscore
2186
+ else:
2187
+ lates[netid] = tdelta / maxDaysLate * totscore * latePenalty
2188
+
2189
+ return lates
2190
+
2191
+ def importNewQuizSelfAssessment(
2192
+ self,
2193
+ assignmentNum,
2194
+ latePenalty=0.25,
2195
+ maxDaysLate=3,
2196
+ preamble="",
2197
+ ):
2198
+ """
2199
+
2200
+ Args:
2201
+ assignmentNum (int):
2202
+ Number of assignment.
2203
+ See :py:meth:`cornellGrading.cornellGrading.getHomework` for details.
2204
+ latePenalty (float):
2205
+ Maximum fraction of score to remove for lateness (defaults to 0.25).
2206
+ Must be in (0,1).
2207
+ maxDaysLate (float):
2208
+ After this number of days past deadline, HW gets zero. Defaults to 3.
2209
+ Students submitting at exactly the maximum time will recieve the maximum
2210
+ late penalty.
2211
+ preamble (str):
2212
+ Preamble for all naming.
2213
+ See :py:meth:`cornellGrading.cornellGrading.getHomework` for details.
2214
+
2215
+ Returns:
2216
+ tuple:
2217
+ submittedScoreNoAssignment (list):
2218
+ netids of students who submitted scores but not the original
2219
+ assignment
2220
+ submittedAssignmentNoScore (list):
2221
+ netids of students who submitted original assignments but not
2222
+ self-assessment scores
2223
+
2224
+ """
2225
+
2226
+ # get original assignments
2227
+ hwname, hw = self.getHomework(assignmentNum, preamble=preamble)
2228
+
2229
+ # compute late penalties
2230
+ lates = self.linearTimePenalty(
2231
+ hw, latePenalty=latePenalty, maxDaysLate=maxDaysLate
2232
+ )
2233
+
2234
+ # get the self-grading assignment and process submissions
2235
+ sg = self.getAssignment(f"{hwname} Self-Assessment")
2236
+ netids = []
2237
+ scores = []
2238
+ submittedScoreNoAssignment = []
2239
+ for sub in sg.get_submissions():
2240
+ if sub.user_id not in self.ids:
2241
+ continue
2242
+ if sub.grade is None:
2243
+ continue
2244
+ netid = self.netids[self.ids == sub.user_id][0]
2245
+ if netid not in lates:
2246
+ print(f"{netid} submitted self-grading but not the assignment!")
2247
+ submittedScoreNoAssignment.append(netid)
2248
+ continue
2249
+
2250
+ netids.append(netid)
2251
+ scores.append(float(sub.grade) - lates[netid])
2252
+
2253
+ # identify students with submitted assignments but no self-assessments
2254
+ submittedAssignmentNoScore = list(set(lates.keys()) - set(netids))
2255
+
2256
+ self.uploadScores(hw, netids, scores)
2257
+
2258
+ return submittedScoreNoAssignment, submittedAssignmentNoScore
@@ -659,6 +659,34 @@ class cornellQualtrics:
659
659
 
660
660
  return response.json()["result"]["elements"]
661
661
 
662
+ def getDistribution(self, surveyId, distributionId):
663
+ """Get a distribution
664
+
665
+ Args:
666
+ surveyId (str):
667
+ Unique id string of survey. Get either from web interface or via
668
+ getSurveyId
669
+ distributionId (str):
670
+ Unique id string of distribution.
671
+
672
+ Returns:
673
+ dict:
674
+ Dictionary encoding distribution definition and stats
675
+
676
+ Notes:
677
+ https://api.qualtrics.com/f5b1d8775d803-get-distribution
678
+
679
+ """
680
+
681
+ baseUrl = "https://{0}{1}distributions/{2}?surveyId={3}".format(
682
+ self.dataCenter, self.qualtricsapi, distributionId, surveyId
683
+ )
684
+
685
+ response = requests.get(baseUrl, headers=self.headers_tokenOnly)
686
+ assert response.status_code == 200
687
+
688
+ return response.json()["result"]
689
+
662
690
  def exportSurvey(self, surveyId, fileFormat="csv", useLabels="true", saveDir=None):
663
691
  """Download and extract survey results
664
692
 
@@ -816,31 +844,31 @@ class cornellQualtrics:
816
844
  "userId": sharewith,
817
845
  "permissions": {
818
846
  "surveyDefinitionManipulation": {
819
- "copySurveyQuestions": True,
820
- "editSurveyFlow": True,
821
- "useBlocks": True,
822
- "useSkipLogic": True,
823
- "useConjoint": True,
824
- "useTriggers": True,
825
- "useQuotas": True,
826
- "setSurveyOptions": True,
827
- "editQuestions": True,
828
- "deleteSurveyQuestions": True,
829
- "useTableOfContents": True,
830
- "useAdvancedQuotas": True,
847
+ "copySurveyQuestions": False,
848
+ "editSurveyFlow": False,
849
+ "useBlocks": False,
850
+ "useSkipLogic": False,
851
+ "useConjoint": False,
852
+ "useTriggers": False,
853
+ "useQuotas": False,
854
+ "setSurveyOptions": False,
855
+ "editQuestions": False,
856
+ "deleteSurveyQuestions": False,
857
+ "useTableOfContents": False,
858
+ "useAdvancedQuotas": False,
831
859
  },
832
860
  "surveyManagement": {
833
- "editSurveys": True,
834
- "activateSurveys": True,
835
- "deactivateSurveys": True,
836
- "copySurveys": True,
837
- "distributeSurveys": True,
838
- "deleteSurveys": True,
839
- "translateSurveys": True,
861
+ "editSurveys": False,
862
+ "activateSurveys": False,
863
+ "deactivateSurveys": False,
864
+ "copySurveys": False,
865
+ "distributeSurveys": False,
866
+ "deleteSurveys": False,
867
+ "translateSurveys": False,
840
868
  },
841
869
  "response": {
842
- "editSurveyResponses": True,
843
- "createResponseSets": True,
870
+ "editSurveyResponses": False,
871
+ "createResponseSets": False,
844
872
  "viewResponseId": True,
845
873
  "useCrossTabs": True,
846
874
  "useScreenouts": True,
@@ -991,7 +1019,7 @@ class cornellQualtrics:
991
1019
  return response.json()["result"]
992
1020
 
993
1021
  def updateSurveyOptions(self, surveyId, data):
994
- """Return survey options
1022
+ """Update survey options
995
1023
 
996
1024
  Args:
997
1025
  surveyId (str):
@@ -1199,8 +1227,8 @@ class cornellQualtrics:
1199
1227
  Message ID string
1200
1228
 
1201
1229
  Returns:
1202
- list:
1203
- message dictionaries
1230
+ dict:
1231
+ message contents
1204
1232
 
1205
1233
  """
1206
1234
  baseUrl = "https://{0}{2}libraries/{1}/messages/{3}".format(
@@ -1210,7 +1238,7 @@ class cornellQualtrics:
1210
1238
  response = requests.get(baseUrl, headers=self.headers_tokenOnly)
1211
1239
  assert response.status_code == 200, "Couldn't get messages"
1212
1240
 
1213
- return response.json()["result"]["elements"]
1241
+ return response.json()["result"]
1214
1242
 
1215
1243
  def createLibraryMessage(self, libraryId, subject, msg, category="thankYou"):
1216
1244
  """Create message in a library
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cornellGrading
3
- Version: 2.16.2
3
+ Version: 2.18.0
4
4
  Summary: Routines for interacting with Cornell installations of Canvas and Qualtrics
5
5
  Home-page: https://github.com/dsavransky/grading
6
6
  Author: Dmitry Savransky
@@ -0,0 +1,81 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.ruff]
6
+ # Exclude a variety of commonly ignored directories.
7
+ exclude = [
8
+ ".bzr",
9
+ ".direnv",
10
+ ".eggs",
11
+ ".git",
12
+ ".git-rewrite",
13
+ ".hg",
14
+ ".ipynb_checkpoints",
15
+ ".mypy_cache",
16
+ ".nox",
17
+ ".pants.d",
18
+ ".pyenv",
19
+ ".pytest_cache",
20
+ ".pytype",
21
+ ".ruff_cache",
22
+ ".svn",
23
+ ".tox",
24
+ ".venv",
25
+ ".vscode",
26
+ "__pypackages__",
27
+ "_build",
28
+ "buck-out",
29
+ "build",
30
+ "dist",
31
+ "node_modules",
32
+ "site-packages",
33
+ "venv",
34
+ "tests",
35
+ ]
36
+
37
+ # Same as Black.
38
+ line-length = 88
39
+ indent-width = 4
40
+
41
+ # Assume Python 3.10
42
+ target-version = "py310"
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E4", "E7", "E9", "F", "D101", "D102", "D103", "D105", "D106", "D207", "D208", "D416", "D417","D419"]
46
+ ignore = ["E741", "E731", "E203"]
47
+
48
+ # Allow fix for all enabled rules (when `--fix`) is provided.
49
+ fixable = ["ALL"]
50
+ unfixable = []
51
+
52
+ # Allow unused variables when underscore-prefixed.
53
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
54
+
55
+ [tool.ruff.format]
56
+ # Like Black, use double quotes for strings.
57
+ quote-style = "double"
58
+
59
+ # Like Black, indent with spaces, rather than tabs.
60
+ indent-style = "space"
61
+
62
+ # Like Black, respect magic trailing commas.
63
+ skip-magic-trailing-comma = false
64
+
65
+ # Like Black, automatically detect the appropriate line ending.
66
+ line-ending = "auto"
67
+
68
+ # Enable auto-formatting of code examples in docstrings. Markdown,
69
+ # reStructuredText code/literal blocks and doctests are all supported.
70
+ #
71
+ # This is currently disabled by default, but it is planned for this
72
+ # to be opt-out in the future.
73
+ docstring-code-format = false
74
+
75
+ # Set the line length limit used when formatting code snippets in
76
+ # docstrings.
77
+ #
78
+ # This only has an effect when the `docstring-code-format` setting is
79
+ # enabled.
80
+ docstring-code-line-length = "dynamic"
81
+
@@ -1,3 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools", "wheel"]
3
- build-backend = "setuptools.build_meta"
File without changes