Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create course-based groups. #3509

Merged
merged 17 commits into from
Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions deployments/prob140/config/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jupyterhub:
hub:
config:
Authenticator:
# https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html#authenticator-managed-group-membership
manage_groups: True
admin_users:
# infrastructure
- rylo
Expand Down
2 changes: 1 addition & 1 deletion hub/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ apiVersion: v1
appVersion: '1.0'
description: Deployment Chart for JupyterHub
name: hub
version: 0.0.1-n5181.haad09135
version: 0.0.1-n5192.h48b2636e
11 changes: 8 additions & 3 deletions hub/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ jupyterhub:
authenticate_prometheus: false
Authenticator:
enable_auth_state: true
# When we turn this on for all hubs
#manage_groups: true
KubeSpawner:
# Make sure working directory is ${HOME}
# hubploy has a bug where it unconditionally puts workingdir to be /srv/repo
Expand All @@ -211,7 +213,7 @@ jupyterhub:
# Generated by chartpress
image:
name: gcr.io/ucb-datahub-2018/jupyterhub-hub
tag: '0.0.1-n5181.haad09135'
tag: '0.0.1-n5191.hc61e89e4'
networkPolicy:
enabled: true
ingress:
Expand Down Expand Up @@ -319,6 +321,9 @@ jupyterhub:
mem_cmp(profile_data['mem_guarantee'], self.mem_guarantee) == 1:
self.mem_guarantee = profile_data['mem_guarantee']

# groups
self.log.info(f'self.user.groups: {self.user.groups}')

# custom.canvas_courses
auth_state = await self.user.get_auth_state()

Expand Down Expand Up @@ -354,8 +359,8 @@ jupyterhub:
if end_at_p > time.localtime():
course_codes.append(user_canvas_course.get('course_code', ''))
sis_course_ids.append(sis_course_id)
self.log.info(f'SIS course IDs: {sis_course_ids}')
self.log.info(f'SIS course codes: {course_codes}')
self.log.info(f'sis_course_ids: {sis_course_ids}')
self.log.info(f'course_codes: {course_codes}')
## end: log sis course IDS

# the key we use to uniquely identify courses. different
Expand Down
66 changes: 62 additions & 4 deletions images/hub/canvasauthenticator/canvasauthenticator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from traitlets import List, Unicode
from traitlets import List, Unicode, default
from oauthenticator.generic import GenericOAuthenticator
import aiohttp

Expand Down Expand Up @@ -36,6 +36,28 @@ class CanvasOAuthenticator(GenericOAuthenticator):
"""
)

canvas_course_key = Unicode(
'',
config=True,
help="""
Key to lookup course identifier from Canvas course data.
See https://canvas.instructure.com/doc/api/courses.html.

This might be 'sis_course_id', 'course_code', 'id', etc.

sis_course_id examples: CRS:MATH-98-2021-C, CRS:CHEM-1A-2021-D, CRS:PHYSICS-77-2022-C
course_code examples: "Math 98", "Chem 1A Fall 2021", "PHYSICS 77-LEC-001"
"""
)

@default('canvas_course_key')
def _default_canvas_course_key(self):
"""
The default is 'course_code' because it should contain human-readable
information, it cannot be overridden by nicknames, and the user won't be
excluded from reading it, unlike the possibility of some sis_ attributes.
"""
return 'course_code'

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -80,21 +102,57 @@ async def get_canvas_items(self, token, url):
async def get_courses(self, token):
"""
Get list of courses enrolled by the current user

See https://canvas.instructure.com/doc/api/courses.html#method.courses.index
"""
url = f"{self.canvas_url}/api/v1/courses"

data = await self.get_canvas_items(token, url)

return data

def format_group(self, course_identifier, enrollment_type):
if enrollment_type is None:
return f'canvas::{course_identifier}'
else:
return f'canvas::{course_identifier}::{enrollment_type}'

def extract_course_groups(self, courses):
'''
Extract course identifiers for each course the user is enrolled in
and format them as group names.
'''
groups = []

for course in courses:
course_id = course.get(self.canvas_course_key, None)
if course_id is None:
continue

# examples: [{'enrollment_state': 'active', 'role': 'TeacherEnrollment', 'role_id': 1773, 'type': 'teacher', 'user_id': 12345}],
# https://canvas.instructure.com/doc/api/courses.html#method.courses.index
# there may be multiple enrollments per course
enrollment_types = list(
map(
lambda x: x.get('type', None), course.get('enrollments', [])
)
)

for enrollment_type in enrollment_types:
groups.append(self.format_group(course_id, enrollment_type))

return groups

async def authenticate(self, handler, data=None):
"""
Augment base user auth info with course info
"""
user = await super().authenticate(handler, data)
user['auth_state']['courses'] = await self.get_courses(
user['auth_state']['access_token']
)
courses = await self.get_courses(user['auth_state']['access_token'])
user['auth_state']['courses'] = courses
user['groups'] = self.extract_course_groups(courses)

self.log.info(f'user groups: {user["groups"]}')
return user

def normalize_username(self, username):
Expand Down
2 changes: 1 addition & 1 deletion node-placeholder/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1-n5180.hc0a416c6
version: 0.0.1-n5192.h48b2636e

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
Expand Down