kattis2canvas 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
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/__init__.py +1 -1
- kattis2canvas/cli.py +344 -64
- {kattis2canvas-0.1.2.dist-info → kattis2canvas-0.1.4.dist-info}/METADATA +1 -1
- kattis2canvas-0.1.4.dist-info/RECORD +8 -0
- kattis2canvas-0.1.2.dist-info/RECORD +0 -8
- {kattis2canvas-0.1.2.dist-info → kattis2canvas-0.1.4.dist-info}/WHEEL +0 -0
- {kattis2canvas-0.1.2.dist-info → kattis2canvas-0.1.4.dist-info}/entry_points.txt +0 -0
- {kattis2canvas-0.1.2.dist-info → kattis2canvas-0.1.4.dist-info}/top_level.txt +0 -0
kattis2canvas/__init__.py
CHANGED
kattis2canvas/cli.py
CHANGED
|
@@ -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")
|
|
@@ -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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
330
|
-
# create assignment group if not present on canvas
|
|
574
|
+
if not canvas_group:
|
|
331
575
|
if dryrun:
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
info(f"created assignment 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
|
|
354
|
-
if
|
|
355
|
-
canvas_assignments = {a.name: a for a in course.get_assignments(assignment_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
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
770
|
+
canvas_group = None
|
|
496
771
|
for ag in course.get_assignment_groups():
|
|
497
|
-
if ag.name ==
|
|
498
|
-
|
|
772
|
+
if ag.name == assignment_group:
|
|
773
|
+
canvas_group = ag
|
|
499
774
|
break
|
|
500
775
|
|
|
501
|
-
if not
|
|
502
|
-
error(f"no
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
kattis2canvas/__init__.py,sha256=iZxFVdxd7EFidzpHII8RrgHeMtVwrwESqgy4HZKBxPo,109
|
|
2
|
+
kattis2canvas/__main__.py,sha256=GGdT4J5WJQ5MvnJ7m-VX_noR8DGaYXclhjDUIFjW5SY,120
|
|
3
|
+
kattis2canvas/cli.py,sha256=SN_Yyh6M5NRB7XHii1zLlrfFfNz7zIjh4ssR0rQI680,34600
|
|
4
|
+
kattis2canvas-0.1.4.dist-info/METADATA,sha256=UPhRSxzdaHrPl1JSjMIO2P24vKkw-dvVYXjA8wHGaB0,7081
|
|
5
|
+
kattis2canvas-0.1.4.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
6
|
+
kattis2canvas-0.1.4.dist-info/entry_points.txt,sha256=V7GPrZNe7aIcu6f_dNo_pCjkuqBQxRzKAQZCNxl9DYg,56
|
|
7
|
+
kattis2canvas-0.1.4.dist-info/top_level.txt,sha256=ZoThmon7y1CR0sTAZndaF2rloBK8xz10mGyz5PUbtCo,14
|
|
8
|
+
kattis2canvas-0.1.4.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
kattis2canvas/__init__.py,sha256=eQhFufU8FowVsAgP6GPL4jY3u_OYsDabZPIN-qefj4k,109
|
|
2
|
-
kattis2canvas/__main__.py,sha256=GGdT4J5WJQ5MvnJ7m-VX_noR8DGaYXclhjDUIFjW5SY,120
|
|
3
|
-
kattis2canvas/cli.py,sha256=Ch0Mr-p6_-H0108k5T_FdvsuGsKvh7BERH3EjxEcpDk,23984
|
|
4
|
-
kattis2canvas-0.1.2.dist-info/METADATA,sha256=8D5iPAMRI6bCvUnwidUQphTfUPUf3VwFSV28_1SWpvU,7081
|
|
5
|
-
kattis2canvas-0.1.2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
6
|
-
kattis2canvas-0.1.2.dist-info/entry_points.txt,sha256=V7GPrZNe7aIcu6f_dNo_pCjkuqBQxRzKAQZCNxl9DYg,56
|
|
7
|
-
kattis2canvas-0.1.2.dist-info/top_level.txt,sha256=ZoThmon7y1CR0sTAZndaF2rloBK8xz10mGyz5PUbtCo,14
|
|
8
|
-
kattis2canvas-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|