kattis2canvas 0.1.3__tar.gz → 0.1.5__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.3
3
+ Version: 0.1.5
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.3"
7
+ version = "0.1.5"
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.3"
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")
@@ -307,10 +543,12 @@ def get_courses(canvas: Canvas, name: str, is_active=True, is_finished=False) ->
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
545
  @click.option("--assignment-group", default="kattis", help="the canvas assignment group to use (default: kattis).")
310
- def course2canvas(offering, canvas_course, dryrun, force, add_to_module, assignment_group):
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):
311
548
  """
312
549
  create assignments in canvas for all the assignments in kattis.
313
550
  """
551
+ load_config()
314
552
  offerings = list(get_offerings(offering))
315
553
  if len(offerings) == 0:
316
554
  error(f"no offerings found for {offering}")
@@ -322,18 +560,27 @@ def course2canvas(offering, canvas_course, dryrun, force, add_to_module, assignm
322
560
  canvas = Canvas(config.canvas_url, config.canvas_token)
323
561
  course = get_course(canvas, canvas_course)
324
562
 
563
+ # Get section if specified
564
+ canvas_section = None
565
+ if section:
566
+ canvas_section = get_section(course, section)
567
+
325
568
  canvas_group = None
326
- for ag in course.get_assignment_groups():
569
+ available_groups = list(course.get_assignment_groups())
570
+ for ag in available_groups:
327
571
  if ag.name == assignment_group:
328
572
  canvas_group = ag
329
573
  break
330
574
  if not canvas_group:
331
- # create assignment group if not present on canvas
332
575
  if dryrun:
333
- info(f"would create assignment group '{assignment_group}'.")
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)
334
581
  else:
335
582
  canvas_group = course.create_assignment_group(name=assignment_group)
336
- info(f"created assignment group {canvas_group}.")
583
+ info(f"created assignment group '{assignment_group}'.")
337
584
 
338
585
  if add_to_module:
339
586
  modules = {m.name: m for m in course.get_modules()}
@@ -362,41 +609,49 @@ def course2canvas(offering, canvas_course, dryrun, force, add_to_module, assignm
362
609
  sorted_assignments.sort(key=lambda a: a.start)
363
610
  for assignment in sorted_assignments:
364
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}" target="kattis-details">{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
+
365
636
  if assignment.title in canvas_assignments:
366
637
  info(f"{assignment.title} already exists.")
367
638
  if force:
368
639
  if dryrun:
369
640
  info(f"would update {assignment.title}.")
370
641
  else:
371
- canvas_assignments[assignment.title].edit(assignment={
372
- 'assignment_group_id': canvas_group.id,
373
- 'name': assignment.title,
374
- 'description': f'Solve the problems found at <a href="{assignment.url}">{assignment.url}</a>. {description}',
375
- 'points_possible': 100,
376
- 'due_at': assignment.end,
377
- 'lock_at': assignment.end,
378
- 'unlock_at': assignment.start,
379
- 'published': True,
380
- })
642
+ canvas_assignments[assignment.title].edit(assignment=assignment_data)
381
643
  info(f"updated {assignment.title}.")
382
644
  else:
383
645
  if dryrun:
384
- info(f"would create {assignment}")
646
+ section_info = f" (section: {section})" if canvas_section else ""
647
+ info(f"would create {assignment}{section_info}")
385
648
  elif 'late' in assignment.title and assignment.title.replace("-late", "") in canvas_assignments:
386
649
  info(f"no new assignment created as --late assignment for {assignment.title.replace('-late', '')}.")
387
650
  continue
388
651
  else:
389
- canvas_assignments[assignment.title] = course.create_assignment({
390
- 'assignment_group_id': canvas_group.id,
391
- 'name': assignment.title,
392
- 'description': f'Solve the problems found at <a href="{assignment.url}">{assignment.url}</a>. {description}',
393
- 'points_possible': 100,
394
- 'due_at': assignment.end,
395
- 'lock_at': assignment.end,
396
- 'unlock_at': assignment.start,
397
- 'published': True,
398
- })
399
- 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}.")
400
655
  if add_to_module:
401
656
  if assignment.title not in [i.title for i in add_to_module.get_module_items()]:
402
657
  add_to_module.create_module_item(module_item={
@@ -426,14 +681,21 @@ class KattisLink(NamedTuple):
426
681
  kattis_user: str
427
682
 
428
683
 
429
- def get_kattis_links(course: Course) -> [KattisLink]:
684
+ def get_kattis_links(course: Course, section_id: int = None) -> [KattisLink]:
430
685
  # this is so terribly slow because of all the requests, we need threads
431
686
  with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
432
687
  futures = []
433
688
  for u in course.get_users(include=["enrollments"]):
434
- 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:
435
692
  continue
436
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
+
437
699
  def get_profile(user: User) -> Optional[KattisLink]:
438
700
  profile = user.get_profile(include=["links"])
439
701
  kattis_url = find_kattis_link(profile)
@@ -453,6 +715,7 @@ def kattislinks(canvas_course):
453
715
  """
454
716
  list the students in the class with their email and kattis links.
455
717
  """
718
+ load_config()
456
719
  canvas = Canvas(config.canvas_url, config.canvas_token)
457
720
  course = get_course(canvas, canvas_course)
458
721
 
@@ -470,10 +733,12 @@ def kattislinks(canvas_course):
470
733
  @click.argument("canvas_course")
471
734
  @click.option("--dryrun/--no-dryrun", default=True, help="show planned actions, do not make them happen.")
472
735
  @click.option("--assignment-group", default="kattis", help="the canvas assignment group to use (default: kattis).")
473
- def submissions2canvas(offering, canvas_course, dryrun, assignment_group):
736
+ @click.option("--section", help="only process submissions for students in this specific section.")
737
+ def submissions2canvas(offering, canvas_course, dryrun, assignment_group, section):
474
738
  """
475
739
  mirror summary of submission from kattis into canvas as a submission comment.
476
740
  """
741
+ load_config()
477
742
  offerings = list(get_offerings(offering))
478
743
  if len(offerings) == 0:
479
744
  error(f"no offerings found for {offering}")
@@ -485,9 +750,17 @@ def submissions2canvas(offering, canvas_course, dryrun, assignment_group):
485
750
  canvas = Canvas(config.canvas_url, config.canvas_token)
486
751
  course = get_course(canvas, canvas_course)
487
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
+
488
761
  kattis_user2canvas_id = {}
489
762
  canvas_id2kattis_user = {}
490
- for link in get_kattis_links(course):
763
+ for link in get_kattis_links(course, section_id=section_id):
491
764
  if link.kattis_user:
492
765
  kattis_user2canvas_id[link.kattis_user] = link.canvas_user
493
766
  canvas_id2kattis_user[link.canvas_user.id] = link.kattis_user
@@ -533,7 +806,7 @@ def submissions2canvas(offering, canvas_course, dryrun, assignment_group):
533
806
  for user, best in best_submissions.items():
534
807
  for kattis_submission in best.values():
535
808
  if user not in submissions_by_user:
536
- warn(f"i don't see a canvas submission for {user}")
809
+ warn(f"i don't see a canvas user for {user}")
537
810
  elif user not in kattis_user2canvas_id:
538
811
  warn(f'skipping submission for unknown user {user}')
539
812
  elif kattis_submission.date > submissions_by_user[user].last_comment:
@@ -541,8 +814,9 @@ def submissions2canvas(offering, canvas_course, dryrun, assignment_group):
541
814
  warn(
542
815
  f"would update {kattis_user2canvas_id[kattis_submission.user]} on problem {kattis_submission.problem} scored {kattis_submission.score}")
543
816
  else:
817
+ href_url = f"https://{config.kattis_hostname}{kattis_submission.url}"
544
818
  submissions_by_user[user].edit(comment={
545
- 'text_comment': f"{prefix}Submission https://{config.kattis_hostname}{kattis_submission.url} scored {kattis_submission.score} on {kattis_submission.problem}."})
819
+ 'text_comment': f"{prefix}Submission <a href={href_url}>{href_url}</a> scored {kattis_submission.score} on {kattis_submission.problem}."})
546
820
  info(
547
821
  f"updated {submissions_by_user[user]} {kattis_user2canvas_id[kattis_submission.user]} for {assignment.title}")
548
822
  else:
@@ -551,9 +825,13 @@ def submissions2canvas(offering, canvas_course, dryrun, assignment_group):
551
825
 
552
826
  def get_best_submissions(offering: str, assignment_id: str) -> {str: {str: Submission}}:
553
827
  best_submissions = collections.defaultdict(dict)
554
- rsp = web_get(f"https://{config.kattis_hostname}{offering}/assignments/{assignment_id}/submissions")
828
+ url = f"https://{config.kattis_hostname}{offering}/assignments/{assignment_id}/submissions"
829
+ rsp = web_get(url)
555
830
  bs = BeautifulSoup(rsp.content, "html.parser")
556
831
  judge_table = bs.find("table", id="judge_table")
832
+ if not judge_table:
833
+ info(f"no submissions yet for {assignment_id}")
834
+ return best_submissions
557
835
  headers = [x.get_text().strip() for x in judge_table.find_all("th")]
558
836
  tbody = judge_table.find("tbody")
559
837
  for submissions in tbody.find_all("tr", recursive=False):
@@ -597,6 +875,7 @@ def sendemail(canvas_course):
597
875
  Email students if they don't have a kattis link in their profile.
598
876
  It takes one input argument canvas course name.
599
877
  """
878
+ load_config()
600
879
  canvas = Canvas(config.canvas_url, config.canvas_token)
601
880
  course = get_course(canvas, canvas_course)
602
881
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kattis2canvas
3
- Version: 0.1.3
3
+ Version: 0.1.5
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