1+ # learning_observer/integrations/schoology.py
12import learning_observer .constants as constants
2- import learning_observer .kvs
33import learning_observer .settings as settings
44
55from . import util
66
77API = 'schoology'
88
9+ # All endpoints use LTI service URLs, not direct Schoology REST API.
10+ # These are accessed using the LTI access token negotiated at launch.
11+ #
12+ # LTI AGS spec: https://www.imsglobal.org/spec/lti-ags/v2p0
13+ # LTI NRPS spec: https://www.imsglobal.org/spec/lti-nrps/v2p0
14+ LTI_SERVICE_BASE = 'https://lti-service.svc.schoology.com/lti-service/tool/{clientId}'
15+
916ENDPOINTS = list (map (lambda x : util .Endpoint (** x , api_name = API ), [
10- {'name' : 'course_list' , 'remote_url' : 'https://lti-service.svc.schoology.com/lti-service/tool/{clientId}/services/names-roles/v2p0/membership/{courseId}' , 'headers' : {'Accept' : 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json' }},
11- {'name' : 'course_roster' , 'remote_url' : 'https://lti-service.svc.schoology.com/lti-service/tool/{clientId}/services/names-roles/v2p0/membership/{courseId}' , 'headers' : {'Accept' : 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json' }},
12- {'name' : 'course_assignments' , 'remote_url' : 'https://api.schoology.com/v1/sections/{courseId}/assignments' , 'headers' : {'Accept' : 'application/vnd.ims.lis.v2.lineitemcontainer+json' }},
17+ {
18+ 'name' : 'course_list' ,
19+ 'remote_url' : f'{ LTI_SERVICE_BASE } /services/names-roles/v2p0/membership/{{courseId}}' ,
20+ 'headers' : {'Accept' : 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json' } # required Schoology header for LTI membership requests
21+ },
22+ {
23+ 'name' : 'course_roster' ,
24+ 'remote_url' : f'{ LTI_SERVICE_BASE } /services/names-roles/v2p0/membership/{{courseId}}' ,
25+ 'headers' : {'Accept' : 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json' } # required Schoology header for LTI membership requests
26+ },
27+ {
28+ # AGS line item container — lists all assignments for the course
29+ 'name' : 'course_assignments' ,
30+ 'remote_url' : f'{ LTI_SERVICE_BASE } /services/ags/v2p0/lineitems/{{courseId}}' ,
31+ 'headers' : {'Accept' : 'application/vnd.ims.lis.v2.lineitemcontainer+json' }
32+ },
33+ {
34+ # AGS results for a specific line item — lists per-student results/submissions
35+ # The lineItemId is typically the full line item URL or the trailing segment.
36+ 'name' : 'assignment_results' ,
37+ 'remote_url' : f'{ LTI_SERVICE_BASE } /services/ags/v2p0/lineitems/{{courseId}}/{{courseWorkId}}/results' ,
38+ 'headers' : {'Accept' : 'application/vnd.ims.lis.v2.resultcontainer+json' }
39+ },
1340]))
1441
1542register_cleaner = util .make_cleaner_registrar (ENDPOINTS )
1643
1744
1845def register_endpoints (app ):
19- '''
20- '''
46+ '''Register Schoology LTI endpoints with the application.'''
2147 if not settings .feature_flag ('schoology_routes' ):
2248 return
2349
@@ -29,6 +55,10 @@ def register_endpoints(app):
2955 )
3056
3157
58+ # ---------------------------------------------------------------------------
59+ # Course list
60+ # ---------------------------------------------------------------------------
61+
3262@register_cleaner ('course_list' , 'courses' )
3363def clean_course_list (schoology_json ):
3464 '''
@@ -45,21 +75,27 @@ def clean_course_list(schoology_json):
4575 return [course ]
4676
4777
78+ # ---------------------------------------------------------------------------
79+ # Roster
80+ # ---------------------------------------------------------------------------
81+
4882def _process_schoology_user_for_system (member , google_id ):
49- # Skip if no canvas id
50- canvas_id = member .get ('user_id' )
51- if not canvas_id : return None
83+ '''Convert an LTI NRPS member record into an internal user dict.'''
84+ lti_user_id = member .get ('user_id' )
85+ if not lti_user_id :
86+ return None
5287
53- # Skip non students
54- is_student = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' in member .get ('roles' , [])
88+ is_student = (
89+ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
90+ in member .get ('roles' , [])
91+ )
5592 if not is_student :
5693 return None
5794
58- # Create user for our system
5995 email = member .get ('email' )
6096 local_id = google_id
6197 if not local_id :
62- local_id = f'canvas- { canvas_id } '
98+ local_id = f'schoology- { lti_user_id } '
6399
64100 member [constants .USER_ID ] = local_id
65101 user = {
@@ -73,18 +109,16 @@ def _process_schoology_user_for_system(member, google_id):
73109 'photo_url' : member .get ('picture' )
74110 },
75111 constants .USER_ID : local_id ,
76- # TODO is this needed? Other roster functions in LO include it
77- # 'course_id': course_id
78112 }
79113 return user
80114
81115
82116@register_cleaner ('course_roster' , 'roster' )
83117async def clean_course_roster (schoology_json ):
84118 '''
85- Retrieve and clean the roster for a Canvas course, alphabetically sorted
119+ Retrieve and clean the roster for a Schoology course, alphabetically sorted.
86120
87- Conforms to LTI NRPS v2 response format
121+ Conforms to LTI NRPS v2 response format.
88122 https://www.imsglobal.org/spec/lti-nrps/v2p0
89123 '''
90124 members = schoology_json .get ('members' , [])
@@ -97,16 +131,37 @@ async def clean_course_roster(schoology_json):
97131 user = _process_schoology_user_for_system (member , google_id )
98132 if user is not None :
99133 users .append (user )
134+
135+ users .sort (
136+ key = lambda user : user .get ('profile' , {}).get ('name' , {}).get ('family_name' , '' )
137+ )
100138 return users
101139
102140
103- @register_cleaner ('course_assignments' , 'assignments' )
141+ # ---------------------------------------------------------------------------
142+ # Assignments (AGS line items)
143+ # ---------------------------------------------------------------------------
144+ @register_cleaner ('course_assignments' , 'assignments' )
104145def clean_course_assignments (schoology_json ):
105146 '''
106- Clean course line items (assignments) from Schoology
147+ TODO implemement this function
148+ When launching via LTI, Schoology only allows us to see assignments
149+ created by our tool. To see all assignments we require an Oauth workflow.
150+ Clean course line items (assignments) from Schoology via LTI AGS.
151+ '''
152+ raise NotImplemented ('Schoology assignments have not yet been implemented.' )
153+
107154
108- Conforms to LTI AGS response format
109- https://www.imsglobal.org/spec/lti-ags/v2p0
155+ # ---------------------------------------------------------------------------
156+ # Assignment results / assigned docs (AGS results)
157+ # ---------------------------------------------------------------------------
158+ @register_cleaner ('assignment_results' , 'assigned_docs' )
159+ async def clean_assigned_docs (schoology_json ):
160+ '''
161+ TODO implemement this function
162+ When launching via LTI, Schoology only allows us to see assignments
163+ created by our tool. To see all assignments we require an Oauth workflow.
164+ Extract per-student Google Doc attachments from LTI AGS results
165+ for a single assignment.
110166 '''
111- # print('Schoology assignments TODO', schoology_json)
112- return []
167+ raise NotImplemented ('Schoology documents from assignments have not yet been implemented.' )
0 commit comments