kattis2canvas 0.1.2__tar.gz → 0.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kattis2canvas
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: CLI tool to integrate Kattis offerings with Canvas LMS courses
5
5
  Author: bcr33d
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kattis2canvas"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "CLI tool to integrate Kattis offerings with Canvas LMS courses"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """kattis2canvas - CLI tool to integrate Kattis offerings with Canvas LMS courses."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
@@ -29,10 +29,11 @@ class Config(NamedTuple):
29
29
  kattis_hostname: str
30
30
  canvas_url: str
31
31
  canvas_token: str
32
+ kattis_password: str = "" # Optional password for web access
32
33
 
33
34
 
34
35
  config: Optional[Config] = None
35
- login_cookies: Optional[requests.cookies.RequestsCookieJar] = None
36
+ kattis_session: Optional[requests.Session] = None
36
37
 
37
38
 
38
39
  class Student(NamedTuple):
@@ -87,17 +88,24 @@ def introspect(o):
87
88
 
88
89
 
89
90
  def web_get(url: str) -> requests.Response:
90
- rsp: requests.Response = requests.get(url, cookies=login_cookies, headers=HEADERS)
91
+ rsp: requests.Response = kattis_session.get(url)
91
92
  check_status(rsp)
92
93
  return rsp
93
94
 
94
95
 
95
- @click.group()
96
- def top():
97
- config_ini = click.get_app_dir("kattis2canvas.ini")
96
+ def get_config_path():
97
+ return click.get_app_dir("kattis2canvas.ini")
98
+
99
+
100
+ def load_config():
101
+ """Load config and login to Kattis. Called by commands that need it."""
102
+ global config, kattis_session
103
+ if config is not None:
104
+ return # Already loaded
105
+
106
+ config_ini = get_config_path()
98
107
  parser = configparser.ConfigParser()
99
108
  parser.read([config_ini])
100
- global config
101
109
  try:
102
110
  config = Config(
103
111
  kattis_username=parser['kattis']['username'],
@@ -106,6 +114,7 @@ def top():
106
114
  kattis_loginurl=parser['kattis']['loginurl'],
107
115
  canvas_url=parser['canvas']['url'],
108
116
  canvas_token=parser['canvas']['token'],
117
+ kattis_password=parser['kattis'].get('password', ''),
109
118
  )
110
119
  except:
111
120
  print(f"""problem getting configuration from {config_ini}. should have the following lines:
@@ -113,21 +122,218 @@ def top():
113
122
  [kattis]
114
123
  username=kattis_username
115
124
  token=kattis_token
125
+ password=kattis_password # optional, for web access to submissions
116
126
  hostname: something_like_sjsu.kattis.com
117
- loginurl: https://something_like_sjsu.kattis.com
127
+ loginurl: https://something_like_sjsu.kattis.com/login
118
128
  [canvas]
119
129
  url=https://something_like_sjsu.instructure.com
120
- token=convas_token
130
+ token=canvas_token
131
+
132
+ Run 'kattis2canvas setup' to configure.
121
133
  """)
122
134
  exit(2)
123
135
 
124
- global login_cookies
125
- args = {'user': config.kattis_username, 'script': 'true', 'token': config.kattis_token}
126
- rsp = requests.post(config.kattis_loginurl, data=args, headers=HEADERS)
127
- if rsp.status_code != 200:
128
- error(f"Kattis login failed. Status: {rsp.status_code}")
136
+ # Create session and login to Kattis
137
+ kattis_session = requests.Session()
138
+ kattis_session.headers.update(HEADERS)
139
+
140
+ # Get CSRF token from login page
141
+ rsp = kattis_session.get(config.kattis_loginurl)
142
+ bs = BeautifulSoup(rsp.content, 'html.parser')
143
+ csrf_input = bs.find('input', {'name': 'csrf_token'})
144
+ csrf_token = csrf_input['value'] if csrf_input else None
145
+
146
+ # Login with password if available, otherwise try token
147
+ if config.kattis_password:
148
+ args = {'user': config.kattis_username, 'password': config.kattis_password, 'csrf_token': csrf_token}
149
+ else:
150
+ args = {'user': config.kattis_username, 'token': config.kattis_token, 'csrf_token': csrf_token}
151
+ rsp = kattis_session.post(config.kattis_loginurl, data=args)
152
+
153
+ # Verify login by checking if we can access a protected page
154
+ rsp = kattis_session.get(f"https://{config.kattis_hostname}/")
155
+ bs = BeautifulSoup(rsp.content, 'html.parser')
156
+ login_link = bs.find('a', string='Log in')
157
+ if login_link:
158
+ error("Kattis web login failed.")
159
+ if not config.kattis_password:
160
+ error("The API token only works for submissions, not web access.")
161
+ error("Add 'password=your_kattis_password' to the [kattis] section in your config file.")
162
+ else:
163
+ error("Check your username and password.")
129
164
  exit(2)
130
- login_cookies = rsp.cookies
165
+
166
+
167
+ @click.group()
168
+ def top():
169
+ pass
170
+
171
+
172
+ def test_kattis_login(username, password, loginurl, hostname):
173
+ """Test if Kattis credentials work for web access. Returns True if successful."""
174
+ if not all([username, password, loginurl, hostname]):
175
+ return False
176
+ try:
177
+ session = requests.Session()
178
+ session.headers.update(HEADERS)
179
+
180
+ # Get CSRF token
181
+ rsp = session.get(loginurl)
182
+ bs = BeautifulSoup(rsp.content, 'html.parser')
183
+ csrf_input = bs.find('input', {'name': 'csrf_token'})
184
+ csrf_token = csrf_input['value'] if csrf_input else None
185
+
186
+ # Login with password
187
+ args = {'user': username, 'password': password, 'csrf_token': csrf_token}
188
+ session.post(loginurl, data=args)
189
+
190
+ # Check if actually logged in
191
+ rsp = session.get(f"https://{hostname}/")
192
+ bs = BeautifulSoup(rsp.content, 'html.parser')
193
+ login_link = bs.find('a', string='Log in')
194
+ return login_link is None
195
+ except:
196
+ return False
197
+
198
+
199
+ def test_canvas_login(url, token):
200
+ """Test if Canvas credentials work. Returns True if successful."""
201
+ if not all([url, token]):
202
+ return False
203
+ try:
204
+ canvas = Canvas(url, token)
205
+ # Try to get current user - this will fail if credentials are bad
206
+ canvas.get_current_user()
207
+ return True
208
+ except:
209
+ return False
210
+
211
+
212
+ @top.command()
213
+ def setup():
214
+ """
215
+ Set up or update Kattis and Canvas credentials.
216
+ """
217
+ config_ini = get_config_path()
218
+ parser = configparser.ConfigParser()
219
+ parser.read([config_ini])
220
+
221
+ # Ensure sections exist
222
+ if 'kattis' not in parser:
223
+ parser['kattis'] = {}
224
+ if 'canvas' not in parser:
225
+ parser['canvas'] = {}
226
+
227
+ config_changed = False
228
+
229
+ # Test existing Kattis credentials
230
+ kattis_username = parser['kattis'].get('username', '')
231
+ kattis_password = parser['kattis'].get('password', '')
232
+ kattis_hostname = parser['kattis'].get('hostname', '')
233
+ kattis_loginurl = parser['kattis'].get('loginurl', '')
234
+
235
+ info("=== Kattis Configuration ===")
236
+ if test_kattis_login(kattis_username, kattis_password, kattis_loginurl, kattis_hostname):
237
+ info(f"Kattis login OK (user: {kattis_username}, host: {kattis_hostname})")
238
+ else:
239
+ if kattis_username:
240
+ warn(f"Kattis web login failed for {kattis_username}")
241
+
242
+ if kattis_hostname:
243
+ kattisrc_url = f"https://{kattis_hostname}/download/kattisrc"
244
+ else:
245
+ kattisrc_url = "https://<your-school>.kattis.com/download/kattisrc"
246
+
247
+ info(f"Go to: {kattisrc_url}")
248
+ info("(Log in if needed, then copy the entire contents of the file)")
249
+ info("")
250
+ info("Paste the contents of your .kattisrc file below, then press Enter twice:")
251
+
252
+ lines = []
253
+ while True:
254
+ try:
255
+ line = input()
256
+ if line == "" and lines and lines[-1] == "":
257
+ break
258
+ lines.append(line)
259
+ except EOFError:
260
+ break
261
+
262
+ kattisrc_content = "\n".join(lines)
263
+
264
+ if kattisrc_content.strip():
265
+ kattisrc = configparser.ConfigParser()
266
+ kattisrc.read_string(kattisrc_content)
267
+
268
+ try:
269
+ parser['kattis']['username'] = kattisrc['user']['username']
270
+ parser['kattis']['token'] = kattisrc['user']['token']
271
+ parser['kattis']['hostname'] = kattisrc['kattis']['hostname']
272
+ # Ensure loginurl ends with /login
273
+ loginurl = kattisrc['kattis']['loginurl']
274
+ if not loginurl.endswith('/login'):
275
+ loginurl = loginurl.rstrip('/') + '/login'
276
+ parser['kattis']['loginurl'] = loginurl
277
+ config_changed = True
278
+ info("Kattis config from .kattisrc saved.")
279
+ except KeyError as e:
280
+ error(f"Could not parse .kattisrc content: missing {e}")
281
+ return
282
+ else:
283
+ info("No .kattisrc content provided.")
284
+
285
+ # Ask for password for web access
286
+ info("")
287
+ info("The Kattis API token only works for submissions, not web access.")
288
+ info("To access the submissions page, you need your Kattis password.")
289
+ kattis_password = click.prompt("Kattis password (for web access)",
290
+ default=parser['kattis'].get('password', ''),
291
+ hide_input=True)
292
+ if kattis_password:
293
+ parser['kattis']['password'] = kattis_password
294
+ config_changed = True
295
+
296
+ # Verify the credentials work
297
+ if test_kattis_login(parser['kattis'].get('username', ''), kattis_password,
298
+ parser['kattis'].get('loginurl', ''), parser['kattis'].get('hostname', '')):
299
+ info("Kattis web login verified.")
300
+ else:
301
+ warn("Kattis web login failed. Check your password.")
302
+
303
+ info("")
304
+ info("=== Canvas Configuration ===")
305
+ canvas_url = parser['canvas'].get('url', '')
306
+ canvas_token = parser['canvas'].get('token', '')
307
+
308
+ if test_canvas_login(canvas_url, canvas_token):
309
+ info(f"Canvas login OK ({canvas_url})")
310
+ else:
311
+ if canvas_url:
312
+ warn(f"Canvas login failed for {canvas_url}")
313
+
314
+ canvas_url = click.prompt("Canvas URL (e.g., https://sjsu.instructure.com)",
315
+ default=canvas_url)
316
+ canvas_token = click.prompt("Canvas API token (from Account > Settings > New Access Token)",
317
+ default=canvas_token)
318
+
319
+ parser['canvas']['url'] = canvas_url
320
+ parser['canvas']['token'] = canvas_token
321
+ config_changed = True
322
+
323
+ # Verify the new credentials work
324
+ if test_canvas_login(canvas_url, canvas_token):
325
+ info("Canvas credentials verified and saved.")
326
+ else:
327
+ error("Canvas login still failing with new credentials.")
328
+
329
+ # Write config if changed
330
+ if config_changed:
331
+ os.makedirs(os.path.dirname(config_ini), exist_ok=True)
332
+ with open(config_ini, 'w') as f:
333
+ parser.write(f)
334
+ info(f"Configuration saved to {config_ini}")
335
+ else:
336
+ info("All credentials OK, no changes needed.")
131
337
 
132
338
 
133
339
  def get_offerings(offering_pattern: str) -> str:
@@ -146,7 +352,7 @@ def list_offerings(name: str):
146
352
  list the possible offerings.
147
353
  :param name: a substring of the offering name
148
354
  """
149
-
355
+ load_config()
150
356
  for offering in get_offerings(name):
151
357
  info(str(offering))
152
358
 
@@ -168,12 +374,14 @@ TZINFOS = {
168
374
  }
169
375
 
170
376
 
171
- # reformat kattis date format to canvas format
377
+ # reformat kattis date format to canvas format (ISO 8601 UTC)
172
378
  def extract_kattis_date(element: str) -> str:
173
379
  if element == "infinity":
174
380
  element = "2100-01-01 00:00 UTC"
175
381
  dt = dateparser.parse(element, tzinfos=TZINFOS)
176
- return dt.strftime("%Y-%m-%dT%H:%M:00%z")
382
+ # Convert to UTC and format with Z suffix for Canvas API
383
+ dt_utc = dt.astimezone(datetime.timezone.utc)
384
+ return dt_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
177
385
 
178
386
 
179
387
  # convert canvas UTC to datetime
@@ -226,6 +434,7 @@ def list_assignments(offering):
226
434
  list the assignments for the given offering.
227
435
  :param offering: a substring of the offering name
228
436
  """
437
+ load_config()
229
438
  for offering in get_offerings(offering):
230
439
  for assignment in get_assignments(offering):
231
440
  info(
@@ -240,6 +449,7 @@ def download_submissions(offering, assignment):
240
449
  download the submissions for an assignment in an offering. offerings and assignments that have the given substring
241
450
  will match.
242
451
  """
452
+ load_config()
243
453
  for o in get_offerings(offering):
244
454
  for a in get_assignments(o):
245
455
  if assignment in a.title:
@@ -282,6 +492,32 @@ def get_course(canvas, name, is_active=True) -> Course:
282
492
  return course_list[0]
283
493
 
284
494
 
495
+ def get_section(course: Course, section_name: str):
496
+ """Find a section by name or unique substring. Returns the section or exits with error."""
497
+ sections = list(course.get_sections())
498
+
499
+ # First try exact match
500
+ for section in sections:
501
+ if section.name == section_name:
502
+ return section
503
+
504
+ # Try substring match
505
+ matching = [s for s in sections if section_name in s.name]
506
+
507
+ if len(matching) == 1:
508
+ return matching[0]
509
+ elif len(matching) > 1:
510
+ error(f"multiple sections match '{section_name}':")
511
+ for section in matching:
512
+ error(f" {section.name}")
513
+ exit(6)
514
+ else:
515
+ error(f"section '{section_name}' not found in {course.name}. available sections:")
516
+ for section in sections:
517
+ error(f" {section.name}")
518
+ exit(6)
519
+
520
+
285
521
  def get_courses(canvas: Canvas, name: str, is_active=True, is_finished=False) -> [Course]:
286
522
  """ find the courses based on partial match """
287
523
  courses = canvas.get_courses(enrollment_type="teacher")
@@ -306,10 +542,13 @@ def get_courses(canvas: Canvas, name: str, is_active=True, is_finished=False) ->
306
542
  @click.option("--dryrun/--no-dryrun", default=True, help="show planned actions, do not make them happen.")
307
543
  @click.option("--force/--no-force", default=False, help="force an update of an assignment if it already exists.")
308
544
  @click.option("--add-to-module", help="the module to add the assignment to.")
309
- def course2canvas(offering, canvas_course, dryrun, force, add_to_module):
545
+ @click.option("--assignment-group", default="kattis", help="the canvas assignment group to use (default: kattis).")
546
+ @click.option("--section", help="only create assignments for this specific section.")
547
+ def course2canvas(offering, canvas_course, dryrun, force, add_to_module, assignment_group, section):
310
548
  """
311
549
  create assignments in canvas for all the assignments in kattis.
312
550
  """
551
+ load_config()
313
552
  offerings = list(get_offerings(offering))
314
553
  if len(offerings) == 0:
315
554
  error(f"no offerings found for {offering}")
@@ -321,18 +560,27 @@ def course2canvas(offering, canvas_course, dryrun, force, add_to_module):
321
560
  canvas = Canvas(config.canvas_url, config.canvas_token)
322
561
  course = get_course(canvas, canvas_course)
323
562
 
324
- kattis_group = None
325
- for ag in course.get_assignment_groups():
326
- if ag.name == 'kattis':
327
- kattis_group = ag
563
+ # Get section if specified
564
+ canvas_section = None
565
+ if section:
566
+ canvas_section = get_section(course, section)
567
+
568
+ canvas_group = None
569
+ available_groups = list(course.get_assignment_groups())
570
+ for ag in available_groups:
571
+ if ag.name == assignment_group:
572
+ canvas_group = ag
328
573
  break
329
- if not kattis_group:
330
- # create assignment group if not present on canvas
574
+ if not canvas_group:
331
575
  if dryrun:
332
- info("would create assignment group 'kattis'.")
576
+ error(f"assignment group '{assignment_group}' not found in {course.name}. available groups:")
577
+ for ag in available_groups:
578
+ error(f" {ag.name}")
579
+ error(f"use --no-dryrun to create the assignment group, or specify an existing group with --assignment-group.")
580
+ exit(5)
333
581
  else:
334
- kattis_group = course.create_assignment_group(name='kattis')
335
- info(f"created assignment group {kattis_group}.")
582
+ canvas_group = course.create_assignment_group(name=assignment_group)
583
+ info(f"created assignment group '{assignment_group}'.")
336
584
 
337
585
  if add_to_module:
338
586
  modules = {m.name: m for m in course.get_modules()}
@@ -350,9 +598,9 @@ def course2canvas(offering, canvas_course, dryrun, force, add_to_module):
350
598
  add_to_module.edit(module=args)
351
599
  info(f"published module {add_to_module}.")
352
600
 
353
- # In dryrun mode without existing kattis group, get all assignments; otherwise filter by group
354
- if kattis_group:
355
- canvas_assignments = {a.name: a for a in course.get_assignments(assignment_group_id=kattis_group.id)}
601
+ # In dryrun mode without existing group, get all assignments; otherwise filter by group
602
+ if canvas_group:
603
+ canvas_assignments = {a.name: a for a in course.get_assignments(assignment_group_id=canvas_group.id)}
356
604
  else:
357
605
  canvas_assignments = {}
358
606
 
@@ -361,41 +609,49 @@ def course2canvas(offering, canvas_course, dryrun, force, add_to_module):
361
609
  sorted_assignments.sort(key=lambda a: a.start)
362
610
  for assignment in sorted_assignments:
363
611
  description = assignment.description if assignment.description else ""
612
+
613
+ # Base assignment data
614
+ assignment_data = {
615
+ 'assignment_group_id': canvas_group.id,
616
+ 'name': assignment.title,
617
+ 'description': f'Solve the problems found at <a href="{assignment.url}">{assignment.url}</a>. {description}',
618
+ 'points_possible': 100,
619
+ 'published': True,
620
+ }
621
+
622
+ # If section specified, use assignment overrides instead of base dates
623
+ if canvas_section:
624
+ assignment_data['only_visible_to_overrides'] = True
625
+ assignment_data['assignment_overrides'] = [{
626
+ 'course_section_id': canvas_section.id,
627
+ 'due_at': assignment.end,
628
+ 'lock_at': assignment.end,
629
+ 'unlock_at': assignment.start,
630
+ }]
631
+ else:
632
+ assignment_data['due_at'] = assignment.end
633
+ assignment_data['lock_at'] = assignment.end
634
+ assignment_data['unlock_at'] = assignment.start
635
+
364
636
  if assignment.title in canvas_assignments:
365
637
  info(f"{assignment.title} already exists.")
366
638
  if force:
367
639
  if dryrun:
368
640
  info(f"would update {assignment.title}.")
369
641
  else:
370
- canvas_assignments[assignment.title].edit(assignment={
371
- 'assignment_group_id': kattis_group.id,
372
- 'name': assignment.title,
373
- 'description': f'Solve the problems found at <a href="{assignment.url}">{assignment.url}</a>. {description}',
374
- 'points_possible': 100,
375
- 'due_at': assignment.end,
376
- 'lock_at': assignment.end,
377
- 'unlock_at': assignment.start,
378
- 'published': True,
379
- })
642
+ canvas_assignments[assignment.title].edit(assignment=assignment_data)
380
643
  info(f"updated {assignment.title}.")
381
644
  else:
382
645
  if dryrun:
383
- info(f"would create {assignment}")
646
+ section_info = f" (section: {section})" if canvas_section else ""
647
+ info(f"would create {assignment}{section_info}")
384
648
  elif 'late' in assignment.title and assignment.title.replace("-late", "") in canvas_assignments:
385
649
  info(f"no new assignment created as --late assignment for {assignment.title.replace('-late', '')}.")
386
650
  continue
387
651
  else:
388
- canvas_assignments[assignment.title] = course.create_assignment({
389
- 'assignment_group_id': kattis_group.id,
390
- 'name': assignment.title,
391
- 'description': f'Solve the problems found at <a href="{assignment.url}">{assignment.url}</a>. {description}',
392
- 'points_possible': 100,
393
- 'due_at': assignment.end,
394
- 'lock_at': assignment.end,
395
- 'unlock_at': assignment.start,
396
- 'published': True,
397
- })
398
- info(f"created {assignment.title}.")
652
+ canvas_assignments[assignment.title] = course.create_assignment(assignment_data)
653
+ section_info = f" for section {section}" if canvas_section else ""
654
+ info(f"created {assignment.title}{section_info}.")
399
655
  if add_to_module:
400
656
  if assignment.title not in [i.title for i in add_to_module.get_module_items()]:
401
657
  add_to_module.create_module_item(module_item={
@@ -425,14 +681,21 @@ class KattisLink(NamedTuple):
425
681
  kattis_user: str
426
682
 
427
683
 
428
- def get_kattis_links(course: Course) -> [KattisLink]:
684
+ def get_kattis_links(course: Course, section_id: int = None) -> [KattisLink]:
429
685
  # this is so terribly slow because of all the requests, we need threads
430
686
  with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
431
687
  futures = []
432
688
  for u in course.get_users(include=["enrollments"]):
433
- if "StudentEnrollment" not in [e['type'] for e in u.enrollments]:
689
+ # Check for student enrollment
690
+ student_enrollments = [e for e in u.enrollments if e['type'] == "StudentEnrollment"]
691
+ if not student_enrollments:
434
692
  continue
435
693
 
694
+ # If section specified, filter by section
695
+ if section_id:
696
+ if not any(e.get('course_section_id') == section_id for e in student_enrollments):
697
+ continue
698
+
436
699
  def get_profile(user: User) -> Optional[KattisLink]:
437
700
  profile = user.get_profile(include=["links"])
438
701
  kattis_url = find_kattis_link(profile)
@@ -452,6 +715,7 @@ def kattislinks(canvas_course):
452
715
  """
453
716
  list the students in the class with their email and kattis links.
454
717
  """
718
+ load_config()
455
719
  canvas = Canvas(config.canvas_url, config.canvas_token)
456
720
  course = get_course(canvas, canvas_course)
457
721
 
@@ -468,10 +732,13 @@ def kattislinks(canvas_course):
468
732
  @click.argument("offering")
469
733
  @click.argument("canvas_course")
470
734
  @click.option("--dryrun/--no-dryrun", default=True, help="show planned actions, do not make them happen.")
471
- def submissions2canvas(offering, canvas_course, dryrun):
735
+ @click.option("--assignment-group", default="kattis", help="the canvas assignment group to use (default: kattis).")
736
+ @click.option("--section", help="only process submissions for students in this specific section.")
737
+ def submissions2canvas(offering, canvas_course, dryrun, assignment_group, section):
472
738
  """
473
739
  mirror summary of submission from kattis into canvas as a submission comment.
474
740
  """
741
+ load_config()
475
742
  offerings = list(get_offerings(offering))
476
743
  if len(offerings) == 0:
477
744
  error(f"no offerings found for {offering}")
@@ -483,26 +750,34 @@ def submissions2canvas(offering, canvas_course, dryrun):
483
750
  canvas = Canvas(config.canvas_url, config.canvas_token)
484
751
  course = get_course(canvas, canvas_course)
485
752
 
753
+ # Get section if specified
754
+ canvas_section = None
755
+ section_id = None
756
+ if section:
757
+ canvas_section = get_section(course, section)
758
+ section_id = canvas_section.id
759
+ info(f"filtering students by section: {section}")
760
+
486
761
  kattis_user2canvas_id = {}
487
762
  canvas_id2kattis_user = {}
488
- for link in get_kattis_links(course):
763
+ for link in get_kattis_links(course, section_id=section_id):
489
764
  if link.kattis_user:
490
765
  kattis_user2canvas_id[link.kattis_user] = link.canvas_user
491
766
  canvas_id2kattis_user[link.canvas_user.id] = link.kattis_user
492
767
  else:
493
768
  warn(f"kattis link missing for {link.canvas_user.name} {link.canvas_user.email}.")
494
769
 
495
- kattis_group = None
770
+ canvas_group = None
496
771
  for ag in course.get_assignment_groups():
497
- if ag.name == 'kattis':
498
- kattis_group = ag
772
+ if ag.name == assignment_group:
773
+ canvas_group = ag
499
774
  break
500
775
 
501
- if not kattis_group:
502
- error(f"no kattis assignment group in {canvas_course}")
776
+ if not canvas_group:
777
+ error(f"no '{assignment_group}' assignment group in {canvas_course}")
503
778
  exit(4)
504
779
 
505
- assignments = {a.name: a for a in course.get_assignments(assignment_group_id=kattis_group.id)}
780
+ assignments = {a.name: a for a in course.get_assignments(assignment_group_id=canvas_group.id)}
506
781
 
507
782
  for assignment in get_assignments(offerings[0]):
508
783
  if assignment.title.replace("-late", "") not in assignments:
@@ -531,7 +806,7 @@ def submissions2canvas(offering, canvas_course, dryrun):
531
806
  for user, best in best_submissions.items():
532
807
  for kattis_submission in best.values():
533
808
  if user not in submissions_by_user:
534
- warn(f"i don't see a canvas submission for {user}")
809
+ warn(f"i don't see a canvas user for {user}")
535
810
  elif user not in kattis_user2canvas_id:
536
811
  warn(f'skipping submission for unknown user {user}')
537
812
  elif kattis_submission.date > submissions_by_user[user].last_comment:
@@ -549,9 +824,13 @@ def submissions2canvas(offering, canvas_course, dryrun):
549
824
 
550
825
  def get_best_submissions(offering: str, assignment_id: str) -> {str: {str: Submission}}:
551
826
  best_submissions = collections.defaultdict(dict)
552
- rsp = web_get(f"https://{config.kattis_hostname}{offering}/assignments/{assignment_id}/submissions")
827
+ url = f"https://{config.kattis_hostname}{offering}/assignments/{assignment_id}/submissions"
828
+ rsp = web_get(url)
553
829
  bs = BeautifulSoup(rsp.content, "html.parser")
554
830
  judge_table = bs.find("table", id="judge_table")
831
+ if not judge_table:
832
+ info(f"no submissions yet for {assignment_id}")
833
+ return best_submissions
555
834
  headers = [x.get_text().strip() for x in judge_table.find_all("th")]
556
835
  tbody = judge_table.find("tbody")
557
836
  for submissions in tbody.find_all("tr", recursive=False):
@@ -595,6 +874,7 @@ def sendemail(canvas_course):
595
874
  Email students if they don't have a kattis link in their profile.
596
875
  It takes one input argument canvas course name.
597
876
  """
877
+ load_config()
598
878
  canvas = Canvas(config.canvas_url, config.canvas_token)
599
879
  course = get_course(canvas, canvas_course)
600
880
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kattis2canvas
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: CLI tool to integrate Kattis offerings with Canvas LMS courses
5
5
  Author: bcr33d
6
6
  License-Expression: MIT
File without changes
File without changes