|
1 |
| -import datetime |
2 |
| -import os |
3 |
| -import pprint |
4 |
| -import json |
| 1 | +from flask import Flask, render_template |
5 | 2 |
|
6 |
| -from tempfile import mkdtemp |
7 |
| -from flask import Flask, jsonify, request, render_template, url_for, session, abort, send_from_directory |
8 |
| - |
9 |
| -from flask_caching import Cache |
10 |
| -from werkzeug.exceptions import Forbidden |
11 |
| -from pylti1p3.contrib.flask import FlaskOIDCLogin, FlaskMessageLaunch, FlaskRequest, FlaskCacheDataStorage |
12 |
| -from pylti1p3.deep_link_resource import DeepLinkResource |
13 |
| -from pylti1p3.grade import Grade |
14 |
| -from pylti1p3.lineitem import LineItem |
15 |
| -from pylti1p3.tool_config import ToolConfJsonFile |
16 |
| -from pylti1p3.registration import Registration |
17 | 3 | from flask_session import Session
|
| 4 | +from utils import ReverseProxied, initialize_cache |
| 5 | +from modules.lti import register as register_lti |
| 6 | +from modules.home import register as register_home |
| 7 | +from modules.nrps import register as register_nrps |
| 8 | +from modules.dl import register as register_dl |
| 9 | +from modules.ags import register as register_ags |
18 | 10 |
|
19 |
| -class ReverseProxied: |
20 |
| - def __init__(self, app): |
21 |
| - self.app = app |
| 11 | +# from modules.deeplink.routes import deeplink_response |
22 | 12 |
|
23 |
| - def __call__(self, environ, start_response): |
24 |
| - scheme = environ.get('HTTP_X_FORWARDED_PROTO') |
25 |
| - if scheme: |
26 |
| - environ['wsgi.url_scheme'] = scheme |
27 |
| - return self.app(environ, start_response) |
28 | 13 |
|
29 | 14 | app = Flask(__name__)
|
30 | 15 | app.wsgi_app = ReverseProxied(app.wsgi_app)
|
31 | 16 |
|
| 17 | +app.config.from_object('config.Config') |
| 18 | +initialize_cache(app) |
32 | 19 |
|
33 |
| - |
34 |
| -config = { |
35 |
| - "DEBUG": True, |
36 |
| - "ENV": "development", |
37 |
| - "CACHE_TYPE": "simple", |
38 |
| - "CACHE_DEFAULT_TIMEOUT": 600, |
39 |
| - "SECRET_KEY": "replace-me", |
40 |
| - "SESSION_TYPE": "filesystem", |
41 |
| - "SESSION_FILE_DIR": mkdtemp(), |
42 |
| - "SESSION_COOKIE_NAME": "lti1p3session-id", |
43 |
| - "SESSION_COOKIE_HTTPONLY": True, |
44 |
| - "SESSION_COOKIE_SECURE": True, # should be True in case of HTTPS usage (production) |
45 |
| - "SESSION_COOKIE_SAMESITE": "None", # should be 'None' in case of HTTPS usage (production) |
46 |
| - "SESSION_COOKIE_PARTITIONED ": True, |
47 |
| - "SESSION_PERMANENT": True, |
48 |
| - "PERMANENT_SESSION_LIFETIME": datetime.timedelta(minutes=60), |
49 |
| - "DEBUG_TB_INTERCEPT_REDIRECTS": False |
50 |
| -} |
51 |
| -app.config.from_mapping(config) |
52 |
| -cache = Cache(app) |
53 |
| -# Initialize session extension |
54 | 20 | Session(app)
|
55 | 21 |
|
56 |
| -def get_lti_config_path(): |
57 |
| - return os.path.join(app.root_path, '..', 'configs', 'registrations.json') |
58 |
| - |
59 |
| - |
60 |
| -def get_launch_data_storage(): |
61 |
| - return FlaskCacheDataStorage(cache) |
62 |
| - |
63 |
| - |
64 |
| -def get_jwk_from_public_key(key_name): |
65 |
| - key_path = os.path.join(app.root_path, '..', 'configs', key_name) |
66 |
| - f = open(key_path, 'r') |
67 |
| - key_content = f.read() |
68 |
| - jwk = Registration.get_jwk(key_content) |
69 |
| - f.close() |
70 |
| - return jwk |
71 |
| - |
72 |
| -@app.route("/home") |
73 |
| -def home(): |
74 |
| - # Get launch data from session |
75 |
| - launch_data = session.get('launch_data', {}) |
76 |
| - launch_id = session.get('launch_id', '') |
77 |
| - |
78 |
| - |
79 |
| - tpl_kwargs = { |
80 |
| - 'page_title': "Home", |
81 |
| - 'launch_id': launch_id, |
82 |
| - 'target_link_uri': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/target_link_uri', ''), |
83 |
| - 'user_data': { |
84 |
| - 'sub': launch_data.get('sub', ''), |
85 |
| - 'name': launch_data.get('name', ''), |
86 |
| - 'family_name': launch_data.get('family_name', ''), |
87 |
| - 'given_name': launch_data.get('given_name', ''), |
88 |
| - 'email': launch_data.get('email', ''), |
89 |
| - 'sourced_id': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/lis', '{}').get('person_sourcedid', ''), |
90 |
| - }, |
91 |
| - 'context_data': { |
92 |
| - **launch_data.get('https://purl.imsglobal.org/spec/lti/claim/context', {}), |
93 |
| - 'sourced_id': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/lis', {}).get('course_section_sourcedid', '') |
94 |
| - }, |
95 |
| - 'user_roles': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/roles', []), |
96 |
| - 'resource_link': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}) |
97 |
| - } |
98 |
| - return render_template("home.html", **tpl_kwargs) |
99 |
| - |
100 |
| -# Load the courses from the JSON file |
101 |
| -def load_courses(): |
102 |
| - courses_path = os.path.join(app.root_path, '..', 'configs', 'resources.json') |
103 |
| - with open(courses_path) as f: |
104 |
| - courses = json.load(f) |
105 |
| - return courses |
106 |
| - |
107 |
| - |
108 |
| -def deeplink(): |
109 |
| - # Get launch data from session |
110 |
| - launch_data = session.get('launch_data', {}) |
111 |
| - launch_id = session.get('launch_id', '') |
112 |
| - |
113 |
| - courses = load_courses() |
114 |
| - |
115 |
| - tpl_kwargs = { |
116 |
| - 'page_title': "Deeplinking", |
117 |
| - "courses": courses, |
118 |
| - 'launch_id': launch_id, |
119 |
| - 'target_link_uri': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/target_link_uri', ''), |
120 |
| - 'user_data': { |
121 |
| - 'sub': launch_data.get('sub', ''), |
122 |
| - 'name': launch_data.get('name', ''), |
123 |
| - 'family_name': launch_data.get('family_name', ''), |
124 |
| - 'given_name': launch_data.get('given_name', ''), |
125 |
| - 'email': launch_data.get('email', ''), |
126 |
| - 'sourced_id': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/lis', '{}').get('person_sourcedid', ''), |
127 |
| - }, |
128 |
| - 'context_data': { |
129 |
| - **launch_data.get('https://purl.imsglobal.org/spec/lti/claim/context', {}), |
130 |
| - 'sourced_id': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/lis', {}).get('course_section_sourcedid', '') |
131 |
| - }, |
132 |
| - 'user_roles': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/roles', []), |
133 |
| - 'resource_link': launch_data.get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}) |
134 |
| - } |
135 |
| - return render_template("deeplink.html", **tpl_kwargs) |
136 |
| - |
137 |
| -@app.route("/") |
138 |
| -def basic(): |
139 |
| - return render_template("basic.html") |
140 |
| - |
141 |
| -@app.route('/jwks/', methods=['GET']) |
142 |
| -def get_jwks(): |
143 |
| - tool_conf = ToolConfJsonFile(get_lti_config_path()) |
144 |
| - return jsonify(tool_conf.get_jwks()) |
145 |
| - |
146 |
| - |
147 |
| -@app.route("/nrps") |
148 |
| -def nrps(): |
149 |
| - return render_template("nrps.html") |
150 |
| - |
151 |
| -@app.route("/ags") |
152 |
| -def ags(): |
153 |
| - launch_id = session.get('launch_id', '') |
154 |
| - |
155 |
| - tool_conf = ToolConfJsonFile(get_lti_config_path()) |
156 |
| - flask_request = FlaskRequest() |
157 |
| - launch_data_storage = get_launch_data_storage() |
158 |
| - message_launch = FlaskMessageLaunch.from_cache(launch_id, flask_request, tool_conf, |
159 |
| - launch_data_storage=launch_data_storage) |
160 |
| - |
161 |
| - if not message_launch.has_ags(): |
162 |
| - raise Forbidden('AGS not enabled!') |
163 |
| - |
164 |
| - ags_service = message_launch.get_ags() |
165 |
| - |
166 |
| - # Fetch members and line items |
167 |
| - members = get_nrps_members() |
168 |
| - lineitems = ags_service.get_lineitems() |
169 |
| - |
170 |
| - # Create a dictionary to store grades by user and lineitem id for fast lookup |
171 |
| - grade_lookup = {} |
172 |
| - for item in lineitems: |
173 |
| - lineitem = ags_service.find_lineitem_by_id(item['id']) |
174 |
| - grades = ags_service.get_grades(lineitem) |
175 |
| - if grades: |
176 |
| - for grade in grades: |
177 |
| - grade_lookup[(grade['userId'], item['id'])] = grade['resultScore'] |
178 |
| - |
179 |
| - # Create gradebook data |
180 |
| - gradebook = [] |
181 |
| - for member in members: |
182 |
| - row = [member['user_id'], member['name']] |
183 |
| - for item in lineitems: |
184 |
| - # Use the grade_lookup dictionary for fast access to grades |
185 |
| - grade = grade_lookup.get((member['user_id'], item['id']), '') |
186 |
| - row.append(grade) |
187 |
| - gradebook.append(row) |
188 |
| - |
189 |
| - # Pretty print the gradebook for debugging |
190 |
| - pprint.pprint(gradebook) |
191 |
| - |
192 |
| - # Render the template with gradebook data |
193 |
| - tpl_kwargs = { |
194 |
| - 'page_title': "Assignments and Grades", |
195 |
| - 'lineitems': lineitems, |
196 |
| - 'gradebook': gradebook |
197 |
| - } |
198 |
| - |
199 |
| - return render_template("ags.html", **tpl_kwargs) |
200 |
| - |
201 |
| - |
202 |
| -# @app.route("/assignments_grades") |
203 |
| -# def assignments_grades(): |
204 |
| -# return render_template("assignments_grades.html") |
205 |
| - |
206 |
| -@app.route("/id_token") |
207 |
| -def id_token(): |
208 |
| - # Get launch data from session |
209 |
| - launch_data = session.get('launch_data', {}) |
210 |
| - launch_id = session.get('launch_id', '') |
211 |
| - |
212 |
| - tpl_kwargs = { |
213 |
| - 'page_title': "ID Token", |
214 |
| - 'launch_id': launch_id, |
215 |
| - 'launch_data': launch_data |
216 |
| - } |
217 |
| - |
218 |
| - return render_template("id_token.html", **tpl_kwargs) |
219 |
| - |
220 |
| - |
221 |
| -@app.route('/login/', methods=['GET', 'POST']) |
222 |
| -def login(): |
223 |
| - tool_conf = ToolConfJsonFile(get_lti_config_path()) |
224 |
| - launch_data_storage = get_launch_data_storage() |
225 |
| - |
226 |
| - flask_request = FlaskRequest() |
227 |
| - launch_url = url_for('launch', _external=True) |
228 |
| - target_link_uri = flask_request.get_param('target_link_uri') |
229 |
| - if not target_link_uri: |
230 |
| - raise Exception('Missing "target_link_uri" param') |
231 |
| - |
232 |
| - oidc_login = FlaskOIDCLogin(flask_request, tool_conf, launch_data_storage=launch_data_storage) |
233 |
| - return oidc_login\ |
234 |
| - .enable_check_cookies()\ |
235 |
| - .redirect(launch_url) |
236 |
| - |
237 |
| - |
238 |
| -@app.route('/launch/', methods=['POST']) |
239 |
| -def launch(): |
240 |
| - tool_conf = ToolConfJsonFile(get_lti_config_path()) |
241 |
| - flask_request = FlaskRequest() |
242 |
| - launch_data_storage = get_launch_data_storage() |
243 |
| - message_launch = FlaskMessageLaunch(flask_request, tool_conf, launch_data_storage=launch_data_storage) |
244 |
| - message_launch_data = message_launch.get_launch_data() |
245 |
| - # pprint.pprint(message_launch_data) |
246 |
| - |
247 |
| - # Store the launch data in the session |
248 |
| - session['launch_data'] = message_launch_data |
249 |
| - session['launch_id'] = message_launch.get_launch_id() |
250 |
| - |
251 |
| - if message_launch.is_deep_link_launch(): |
252 |
| - return deeplink() |
253 |
| - return home() |
254 |
| - |
255 |
| - |
256 |
| - |
257 |
| -@app.route('/api/nrps/members', methods=['GET']) |
258 |
| -def get_nrps_members(): |
259 |
| - launch_id = session.get('launch_id', '') |
260 |
| - |
261 |
| - tool_conf = ToolConfJsonFile(get_lti_config_path()) |
262 |
| - flask_request = FlaskRequest() |
263 |
| - launch_data_storage = get_launch_data_storage() |
264 |
| - message_launch = FlaskMessageLaunch.from_cache(launch_id, flask_request, tool_conf, |
265 |
| - launch_data_storage=launch_data_storage) |
266 |
| - if not message_launch.has_nrps(): |
267 |
| - raise Forbidden('NRPS not enabled!') |
268 |
| - |
269 |
| - members = message_launch.get_nrps().get_members() |
270 |
| - return members |
271 |
| - |
272 |
| -@app.route('/dl/<resource_id>/', methods=['GET', 'POST']) |
273 |
| -def deeplink_response(resource_id): |
274 |
| - |
275 |
| - |
276 |
| - launch_id = session.get('launch_id', '') |
277 |
| - tool_conf = ToolConfJsonFile(get_lti_config_path()) |
278 |
| - flask_request = FlaskRequest() |
279 |
| - launch_data_storage = get_launch_data_storage() |
280 |
| - message_launch = FlaskMessageLaunch.from_cache(launch_id, flask_request, tool_conf, |
281 |
| - launch_data_storage=launch_data_storage) |
282 |
| - |
283 |
| - if not message_launch.is_deep_link_launch(): |
284 |
| - raise Forbidden('Must be a deep link!') |
285 |
| - |
286 |
| - courses = load_courses() |
287 |
| - course = next((course for course in courses if course['id'] == int(resource_id)), None) |
288 |
| - |
289 |
| - if not course: |
290 |
| - abort(404, description=f"Course with ID {resource_id} not found") |
291 |
| - |
292 |
| - |
293 |
| - launch_url = url_for('home', _external=True) + '/resource/' + resource_id + '/' |
294 |
| - |
295 |
| - resource = DeepLinkResource() |
296 |
| - resource.set_url(launch_url) \ |
297 |
| - .set_title(course.get("title", "Resource " + resource_id)) |
298 |
| - |
299 |
| - # Get the optional 'model_id' from query parameters |
300 |
| - model_id = request.args.get('model_id', 'default') |
301 |
| - |
302 |
| - if model_id: |
303 |
| - resource.set_custom_params({'model_id': model_id}) |
304 |
| - |
305 |
| - |
306 |
| - html = message_launch.get_deep_link().output_response_form([resource]) |
307 |
| - return html |
308 |
| - |
| 22 | +register_home(app) |
| 23 | +register_lti(app) |
| 24 | +register_nrps(app) |
| 25 | +register_dl(app) |
| 26 | +register_ags(app) |
309 | 27 |
|
310 |
| -# @app.route('/api/ags/gradebook', methods=['GET']) |
311 |
| -# def get_gradebook(): |
312 |
| -# launch_id = session.get('launch_id', '') |
| 28 | +# @app.route("/home") |
| 29 | +# def home(): |
| 30 | +# return home_route() |
313 | 31 |
|
314 |
| -# tool_conf = ToolConfJsonFile(get_lti_config_path()) |
315 |
| -# flask_request = FlaskRequest() |
316 |
| -# launch_data_storage = get_launch_data_storage() |
317 |
| -# message_launch = FlaskMessageLaunch.from_cache(launch_id, flask_request, tool_conf, |
318 |
| -# launch_data_storage=launch_data_storage) |
319 |
| -# if not message_launch.has_ags(): |
320 |
| -# raise Forbidden('AGS not enabled!') |
321 |
| -# ags_service = message_launch.get_ags() |
322 | 32 |
|
323 |
| -# lineitems = ags_service.get_lineitems() |
324 |
| - |
325 |
| -# return lineitems |
326 | 33 |
|
327 | 34 |
|
| 35 | +# @app.route('/dl/<resource_id>/', methods=['GET', 'POST']) |
| 36 | +# def deeplink(resource_id): |
| 37 | +# return deeplink_response(resource_id) |
328 | 38 |
|
329 |
| -if __name__ == '__main__': |
| 39 | +if __name__ == "__main__": |
330 | 40 | app.run(host='0.0.0.0', port=3000, debug=True)
|
0 commit comments