kattis2canvas 0.1.3__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.
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/PKG-INFO +1 -1
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/pyproject.toml +1 -1
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas/__init__.py +1 -1
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas/cli.py +328 -50
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas.egg-info/PKG-INFO +1 -1
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/README.md +0 -0
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/setup.cfg +0 -0
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas/__main__.py +0 -0
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas.egg-info/SOURCES.txt +0 -0
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas.egg-info/dependency_links.txt +0 -0
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas.egg-info/entry_points.txt +0 -0
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas.egg-info/requires.txt +0 -0
- {kattis2canvas-0.1.3 → kattis2canvas-0.1.4}/src/kattis2canvas.egg-info/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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 =
|
|
91
|
+
rsp: requests.Response = kattis_session.get(url)
|
|
91
92
|
check_status(rsp)
|
|
92
93
|
return rsp
|
|
93
94
|
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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=
|
|
130
|
+
token=canvas_token
|
|
131
|
+
|
|
132
|
+
Run 'kattis2canvas setup' to configure.
|
|
121
133
|
""")
|
|
122
134
|
exit(2)
|
|
123
135
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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}">{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
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
@@ -551,9 +824,13 @@ def submissions2canvas(offering, canvas_course, dryrun, assignment_group):
|
|
|
551
824
|
|
|
552
825
|
def get_best_submissions(offering: str, assignment_id: str) -> {str: {str: Submission}}:
|
|
553
826
|
best_submissions = collections.defaultdict(dict)
|
|
554
|
-
|
|
827
|
+
url = f"https://{config.kattis_hostname}{offering}/assignments/{assignment_id}/submissions"
|
|
828
|
+
rsp = web_get(url)
|
|
555
829
|
bs = BeautifulSoup(rsp.content, "html.parser")
|
|
556
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
|
|
557
834
|
headers = [x.get_text().strip() for x in judge_table.find_all("th")]
|
|
558
835
|
tbody = judge_table.find("tbody")
|
|
559
836
|
for submissions in tbody.find_all("tr", recursive=False):
|
|
@@ -597,6 +874,7 @@ def sendemail(canvas_course):
|
|
|
597
874
|
Email students if they don't have a kattis link in their profile.
|
|
598
875
|
It takes one input argument canvas course name.
|
|
599
876
|
"""
|
|
877
|
+
load_config()
|
|
600
878
|
canvas = Canvas(config.canvas_url, config.canvas_token)
|
|
601
879
|
course = get_course(canvas, canvas_course)
|
|
602
880
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|