ff-ltitoolkit 0.1.0__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.
- ff_ltitoolkit-0.1.0.dist-info/METADATA +98 -0
- ff_ltitoolkit-0.1.0.dist-info/RECORD +94 -0
- ff_ltitoolkit-0.1.0.dist-info/WHEEL +4 -0
- ff_ltitoolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- ltitoolkit/__init__.py +20 -0
- ltitoolkit/adapters/__init__.py +11 -0
- ltitoolkit/adapters/brightspace/__init__.py +35 -0
- ltitoolkit/adapters/brightspace/client.py +176 -0
- ltitoolkit/adapters/canvas/__init__.py +27 -0
- ltitoolkit/adapters/canvas/client.py +142 -0
- ltitoolkit/advantage/__init__.py +9 -0
- ltitoolkit/advantage/service.py +96 -0
- ltitoolkit/core/__init__.py +19 -0
- ltitoolkit/core/actions.py +6 -0
- ltitoolkit/core/assignments_grades.py +300 -0
- ltitoolkit/core/contrib/__init__.py +0 -0
- ltitoolkit/core/contrib/django/__init__.py +5 -0
- ltitoolkit/core/contrib/django/cookie.py +56 -0
- ltitoolkit/core/contrib/django/launch_data_storage/__init__.py +0 -0
- ltitoolkit/core/contrib/django/launch_data_storage/cache.py +10 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/__init__.py +139 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/admin.py +48 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/apps.py +6 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/0001_initial.py +168 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/__init__.py +0 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/models.py +185 -0
- ltitoolkit/core/contrib/django/message_launch.py +39 -0
- ltitoolkit/core/contrib/django/oidc_login.py +41 -0
- ltitoolkit/core/contrib/django/redirect.py +34 -0
- ltitoolkit/core/contrib/django/request.py +32 -0
- ltitoolkit/core/contrib/django/session.py +5 -0
- ltitoolkit/core/contrib/flask/__init__.py +7 -0
- ltitoolkit/core/contrib/flask/cookie.py +34 -0
- ltitoolkit/core/contrib/flask/launch_data_storage/__init__.py +0 -0
- ltitoolkit/core/contrib/flask/launch_data_storage/cache.py +9 -0
- ltitoolkit/core/contrib/flask/message_launch.py +32 -0
- ltitoolkit/core/contrib/flask/oidc_login.py +31 -0
- ltitoolkit/core/contrib/flask/redirect.py +34 -0
- ltitoolkit/core/contrib/flask/request.py +40 -0
- ltitoolkit/core/contrib/flask/session.py +5 -0
- ltitoolkit/core/contrib/py.typed +0 -0
- ltitoolkit/core/cookie.py +17 -0
- ltitoolkit/core/cookies_allowed_check.py +151 -0
- ltitoolkit/core/course_groups.py +115 -0
- ltitoolkit/core/deep_link.py +100 -0
- ltitoolkit/core/deep_link_resource.py +96 -0
- ltitoolkit/core/deployment.py +13 -0
- ltitoolkit/core/exception.py +16 -0
- ltitoolkit/core/grade.py +143 -0
- ltitoolkit/core/launch_data_storage/__init__.py +0 -0
- ltitoolkit/core/launch_data_storage/base.py +75 -0
- ltitoolkit/core/launch_data_storage/cache.py +43 -0
- ltitoolkit/core/launch_data_storage/session.py +29 -0
- ltitoolkit/core/lineitem.py +205 -0
- ltitoolkit/core/message_launch.py +828 -0
- ltitoolkit/core/message_validators/__init__.py +13 -0
- ltitoolkit/core/message_validators/abstract.py +25 -0
- ltitoolkit/core/message_validators/deep_link.py +34 -0
- ltitoolkit/core/message_validators/privacy_launch.py +40 -0
- ltitoolkit/core/message_validators/resource_message.py +21 -0
- ltitoolkit/core/message_validators/submission_review.py +45 -0
- ltitoolkit/core/names_roles.py +97 -0
- ltitoolkit/core/oidc_login.py +275 -0
- ltitoolkit/core/py.typed +0 -0
- ltitoolkit/core/redirect.py +24 -0
- ltitoolkit/core/registration.py +119 -0
- ltitoolkit/core/request.py +17 -0
- ltitoolkit/core/roles.py +109 -0
- ltitoolkit/core/service_connector.py +144 -0
- ltitoolkit/core/session.py +70 -0
- ltitoolkit/core/tool_config/__init__.py +4 -0
- ltitoolkit/core/tool_config/abstract.py +117 -0
- ltitoolkit/core/tool_config/dict.py +253 -0
- ltitoolkit/core/tool_config/json_file.py +100 -0
- ltitoolkit/core/tool_config/py.typed +0 -0
- ltitoolkit/core/utils.py +10 -0
- ltitoolkit/dynamic_registration/__init__.py +39 -0
- ltitoolkit/dynamic_registration/models.py +192 -0
- ltitoolkit/dynamic_registration/service.py +156 -0
- ltitoolkit/dynamic_registration/store.py +40 -0
- ltitoolkit/dynamic_registration/tool_conf.py +102 -0
- ltitoolkit/exceptions.py +42 -0
- ltitoolkit/fastapi/__init__.py +30 -0
- ltitoolkit/fastapi/cookie.py +53 -0
- ltitoolkit/fastapi/dynamic_registration.py +40 -0
- ltitoolkit/fastapi/message_launch.py +60 -0
- ltitoolkit/fastapi/oidc_login.py +47 -0
- ltitoolkit/fastapi/redirect.py +54 -0
- ltitoolkit/fastapi/request.py +77 -0
- ltitoolkit/fastapi/session.py +13 -0
- ltitoolkit/http.py +80 -0
- ltitoolkit/token/__init__.py +20 -0
- ltitoolkit/token/cache.py +47 -0
- ltitoolkit/token/service.py +165 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from flask import request # type: ignore
|
|
2
|
+
from flask import session as flask_session
|
|
3
|
+
from ltitoolkit.core.request import Request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FlaskRequest(Request):
|
|
7
|
+
_cookies = None
|
|
8
|
+
_request_data = None
|
|
9
|
+
_request_is_secure = None
|
|
10
|
+
_session = None
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self, cookies=None, session=None, request_data=None, request_is_secure=None
|
|
14
|
+
):
|
|
15
|
+
super().__init__()
|
|
16
|
+
self._cookies = request.cookies if cookies is None else cookies
|
|
17
|
+
self._session = flask_session if session is None else session
|
|
18
|
+
self._request_is_secure = (
|
|
19
|
+
request.is_secure if request_is_secure is None else request_is_secure
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if request_data:
|
|
23
|
+
self._request_data = request_data
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def session(self):
|
|
27
|
+
return self._session
|
|
28
|
+
|
|
29
|
+
def get_param(self, key):
|
|
30
|
+
if self._request_data:
|
|
31
|
+
return self._request_data.get(key)
|
|
32
|
+
if request.method == "GET":
|
|
33
|
+
return request.args.get(key, None)
|
|
34
|
+
return request.form.get(key, None)
|
|
35
|
+
|
|
36
|
+
def get_cookie(self, key):
|
|
37
|
+
return self._cookies.get(key)
|
|
38
|
+
|
|
39
|
+
def is_secure(self):
|
|
40
|
+
return self._request_is_secure
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from abc import ABCMeta, abstractmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CookieService:
|
|
6
|
+
__metaclass__ = ABCMeta
|
|
7
|
+
_cookie_prefix: str = "lti1p3"
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def get_cookie(self, name: str) -> t.Optional[str]:
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def set_cookie(
|
|
15
|
+
self, name: str, value: t.Union[str, int], exp: t.Optional[int] = 3600
|
|
16
|
+
):
|
|
17
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from html import escape # type: ignore
|
|
2
|
+
import json
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CookiesAllowedCheckPage:
|
|
7
|
+
_params: t.Mapping[str, str] = {}
|
|
8
|
+
_protocol: str = "http"
|
|
9
|
+
_main_text: str = ""
|
|
10
|
+
_click_text: str = ""
|
|
11
|
+
_loading_text: str = ""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
params: t.Mapping[str, str],
|
|
16
|
+
protocol: str,
|
|
17
|
+
main_text: str,
|
|
18
|
+
click_text: str,
|
|
19
|
+
loading_text: str,
|
|
20
|
+
*args,
|
|
21
|
+
**kwargs
|
|
22
|
+
):
|
|
23
|
+
# pylint: disable=unused-argument
|
|
24
|
+
self._params = params
|
|
25
|
+
self._protocol = protocol
|
|
26
|
+
self._main_text = main_text
|
|
27
|
+
self._click_text = click_text
|
|
28
|
+
self._loading_text = loading_text
|
|
29
|
+
|
|
30
|
+
def get_css_block(self) -> str:
|
|
31
|
+
css_block = """\
|
|
32
|
+
body {
|
|
33
|
+
font-family: Geneva, Arial, Helvetica, sans-serif;
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
return css_block
|
|
37
|
+
|
|
38
|
+
def get_js_block(self) -> str:
|
|
39
|
+
js_block = """\
|
|
40
|
+
var siteProtocol = '%s';
|
|
41
|
+
var urlParams = %s;
|
|
42
|
+
var htmlEntities = {
|
|
43
|
+
"<": "<",
|
|
44
|
+
">": ">",
|
|
45
|
+
"&": "&",
|
|
46
|
+
""": '"',
|
|
47
|
+
"'": "'"
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function unescapeHtmlEntities(str) {
|
|
51
|
+
for (var htmlCode in htmlEntities) {
|
|
52
|
+
str = str.replace(new RegExp(htmlCode, "g"), htmlEntities[htmlCode]);
|
|
53
|
+
}
|
|
54
|
+
return str;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getUpdatedUrl() {
|
|
58
|
+
var newSearchParams = [];
|
|
59
|
+
for (var key in urlParams) {
|
|
60
|
+
if (window.location.search.indexOf(key + '=') === -1) {
|
|
61
|
+
newSearchParams.push(key + '=' + encodeURIComponent(unescapeHtmlEntities(urlParams[key])));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var searchParamsStr = newSearchParams.join('&');
|
|
65
|
+
if (window.location.search !== '') {
|
|
66
|
+
searchParamsStr = window.location.search + '&' + searchParamsStr;
|
|
67
|
+
} else {
|
|
68
|
+
searchParamsStr = '?' + searchParamsStr;
|
|
69
|
+
}
|
|
70
|
+
return window.location.protocol + '//' + window.location.hostname +
|
|
71
|
+
(window.location.port ? (":" + window.location.port) : "") +
|
|
72
|
+
window.location.pathname + searchParamsStr;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function displayLoadingBlock() {
|
|
76
|
+
document.getElementById("lti1p3-loading-msg").style.display = "block";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function displayWarningBlock() {
|
|
80
|
+
document.getElementById("lti1p3-warning-msg").style.display = "block";
|
|
81
|
+
var newTabLink = document.getElementById("lti1p3-new-tab-link");
|
|
82
|
+
var contentUrl = getUpdatedUrl();
|
|
83
|
+
newTabLink.onclick = function() {
|
|
84
|
+
window.open(contentUrl , '_blank');
|
|
85
|
+
newTabLink.parentNode.removeChild(newTabLink);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkCookiesAllowed() {
|
|
90
|
+
var cookie = "lti1p3_test_cookie=1; path=/";
|
|
91
|
+
if (siteProtocol === 'https') {
|
|
92
|
+
cookie = cookie + '; SameSite=None; secure';
|
|
93
|
+
}
|
|
94
|
+
document.cookie = cookie;
|
|
95
|
+
var res = document.cookie.indexOf("lti1p3_test_cookie") !== -1;
|
|
96
|
+
if (res) {
|
|
97
|
+
// remove test cookie and reload page
|
|
98
|
+
document.cookie = "lti1p3_test_cookie=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
|
|
99
|
+
displayLoadingBlock();
|
|
100
|
+
window.location.href = getUpdatedUrl();
|
|
101
|
+
} else {
|
|
102
|
+
displayWarningBlock();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
document.addEventListener("DOMContentLoaded", checkCookiesAllowed);
|
|
107
|
+
"""
|
|
108
|
+
# pylint: disable=deprecated-method
|
|
109
|
+
js_block = js_block % (
|
|
110
|
+
self._protocol,
|
|
111
|
+
json.dumps({k: escape(v, True) for k, v in self._params.items()}),
|
|
112
|
+
)
|
|
113
|
+
return js_block
|
|
114
|
+
|
|
115
|
+
def get_header_block(self) -> str:
|
|
116
|
+
return ""
|
|
117
|
+
|
|
118
|
+
def get_html(self) -> str:
|
|
119
|
+
html = """\
|
|
120
|
+
<!DOCTYPE html>
|
|
121
|
+
<html lang="en">
|
|
122
|
+
<head>
|
|
123
|
+
<title></title>
|
|
124
|
+
<meta charset="UTF-8">
|
|
125
|
+
<style type="text/css">
|
|
126
|
+
{css_block}
|
|
127
|
+
</style>
|
|
128
|
+
<script type="text/javascript">
|
|
129
|
+
{js_block}
|
|
130
|
+
</script>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<div id="lti1p3-loading-msg" style="display: none;">
|
|
134
|
+
{loading_text}
|
|
135
|
+
</div>
|
|
136
|
+
<div id="lti1p3-warning-msg" style="display: none;">
|
|
137
|
+
{header_block}
|
|
138
|
+
<p><strong>{main_text}</strong> <a href="javascript: void(0);" id="lti1p3-new-tab-link">{click_text}</a></p>
|
|
139
|
+
</div>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
|
142
|
+
"""
|
|
143
|
+
html = html.format(
|
|
144
|
+
css_block=self.get_css_block(),
|
|
145
|
+
js_block=self.get_js_block(),
|
|
146
|
+
loading_text=self._loading_text,
|
|
147
|
+
header_block=self.get_header_block(),
|
|
148
|
+
main_text=self._main_text,
|
|
149
|
+
click_text=self._click_text,
|
|
150
|
+
)
|
|
151
|
+
return html
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
import typing_extensions as te
|
|
3
|
+
from .utils import add_param_to_url
|
|
4
|
+
from .service_connector import ServiceConnector
|
|
5
|
+
|
|
6
|
+
TGroupsServiceData = te.TypedDict(
|
|
7
|
+
"TGroupsServiceData",
|
|
8
|
+
{
|
|
9
|
+
# Required data
|
|
10
|
+
"context_groups_url": str,
|
|
11
|
+
"scope": t.List[
|
|
12
|
+
te.Literal[
|
|
13
|
+
"https://purl.imsglobal.org/spec/lti-gs/scope/contextgroup.readonly"
|
|
14
|
+
]
|
|
15
|
+
],
|
|
16
|
+
"service_versions": t.List[str],
|
|
17
|
+
# Optional data
|
|
18
|
+
"context_group_sets_url": str,
|
|
19
|
+
},
|
|
20
|
+
total=False,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
TGroup = te.TypedDict(
|
|
24
|
+
"TGroup",
|
|
25
|
+
{
|
|
26
|
+
# Required data
|
|
27
|
+
"id": t.Union[str, int],
|
|
28
|
+
"name": str,
|
|
29
|
+
# Optional data
|
|
30
|
+
"tag": str,
|
|
31
|
+
"set_id": t.Union[str, int],
|
|
32
|
+
},
|
|
33
|
+
total=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
TSet = te.TypedDict(
|
|
37
|
+
"TSet",
|
|
38
|
+
{
|
|
39
|
+
# Required data
|
|
40
|
+
"id": t.Union[str, int],
|
|
41
|
+
"name": str,
|
|
42
|
+
# Optional data
|
|
43
|
+
"groups": t.List[TGroup],
|
|
44
|
+
},
|
|
45
|
+
total=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CourseGroupsService:
|
|
50
|
+
_service_connector: ServiceConnector
|
|
51
|
+
_service_data: TGroupsServiceData
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
service_connector: ServiceConnector,
|
|
56
|
+
groups_service_data: TGroupsServiceData,
|
|
57
|
+
):
|
|
58
|
+
self._service_connector = service_connector
|
|
59
|
+
self._service_data = groups_service_data
|
|
60
|
+
|
|
61
|
+
def get_page(
|
|
62
|
+
self, data_url: str, data_key: str = "groups"
|
|
63
|
+
) -> t.Tuple[list, t.Optional[str]]:
|
|
64
|
+
"""
|
|
65
|
+
Get one page with the groups/sets.
|
|
66
|
+
|
|
67
|
+
:param data_url
|
|
68
|
+
:param data_key
|
|
69
|
+
:return: tuple in format: (list with data items, next page url)
|
|
70
|
+
"""
|
|
71
|
+
data = self._service_connector.make_service_request(
|
|
72
|
+
self._service_data["scope"],
|
|
73
|
+
data_url,
|
|
74
|
+
accept="application/vnd.ims.lti-gs.v1.contextgroupcontainer+json",
|
|
75
|
+
)
|
|
76
|
+
data_body = t.cast(t.Any, data.get("body", {}))
|
|
77
|
+
return data_body.get(data_key, []), data["next_page_url"]
|
|
78
|
+
|
|
79
|
+
def get_groups(self, user_id=None):
|
|
80
|
+
groups_res_lst = []
|
|
81
|
+
groups_url = self._service_data.get("context_groups_url")
|
|
82
|
+
if user_id:
|
|
83
|
+
groups_url = add_param_to_url(groups_url, "user_id", user_id)
|
|
84
|
+
|
|
85
|
+
while groups_url:
|
|
86
|
+
groups, groups_url = self.get_page(groups_url, data_key="groups")
|
|
87
|
+
groups_res_lst.extend(groups)
|
|
88
|
+
|
|
89
|
+
return groups_res_lst
|
|
90
|
+
|
|
91
|
+
def has_sets(self):
|
|
92
|
+
return "context_group_sets_url" in self._service_data
|
|
93
|
+
|
|
94
|
+
def get_sets(self, include_groups=False):
|
|
95
|
+
sets_res_lst = []
|
|
96
|
+
sets_url = self._service_data.get("context_group_sets_url")
|
|
97
|
+
|
|
98
|
+
while sets_url:
|
|
99
|
+
sets, sets_url = self.get_page(sets_url, data_key="sets")
|
|
100
|
+
sets_res_lst.extend(sets)
|
|
101
|
+
|
|
102
|
+
if include_groups and sets_res_lst:
|
|
103
|
+
set_id_to_index = {}
|
|
104
|
+
for i, s in enumerate(sets_res_lst):
|
|
105
|
+
set_id_to_index[s["id"]] = i
|
|
106
|
+
sets_res_lst[i]["groups"] = []
|
|
107
|
+
|
|
108
|
+
groups = self.get_groups()
|
|
109
|
+
for group in groups:
|
|
110
|
+
set_id = group.get("set_id")
|
|
111
|
+
if set_id and set_id in set_id_to_index:
|
|
112
|
+
index = set_id_to_index[set_id]
|
|
113
|
+
sets_res_lst[index]["groups"].append(group)
|
|
114
|
+
|
|
115
|
+
return sets_res_lst
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import typing as t
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import jwt # type: ignore
|
|
6
|
+
import typing_extensions as te
|
|
7
|
+
from .deep_link_resource import DeepLinkResource
|
|
8
|
+
from .registration import Registration
|
|
9
|
+
|
|
10
|
+
TDeepLinkData = te.TypedDict(
|
|
11
|
+
"TDeepLinkData",
|
|
12
|
+
{
|
|
13
|
+
# Required data:
|
|
14
|
+
"deep_link_return_url": str,
|
|
15
|
+
"accept_types": t.List[te.Literal["link", "ltiResourceLink"]],
|
|
16
|
+
"accept_presentation_document_targets": t.List[
|
|
17
|
+
te.Literal["iframe", "window", "embed"]
|
|
18
|
+
],
|
|
19
|
+
# Optional data
|
|
20
|
+
"accept_multiple": t.Union[bool, te.Literal["true", "false"]],
|
|
21
|
+
"auto_create": t.Union[bool, te.Literal["true", "false"]],
|
|
22
|
+
"title": str,
|
|
23
|
+
"text": str,
|
|
24
|
+
"data": object,
|
|
25
|
+
},
|
|
26
|
+
total=False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DeepLink:
|
|
31
|
+
_registration: Registration
|
|
32
|
+
_deployment_id: str
|
|
33
|
+
_deep_link_settings: TDeepLinkData
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
registration: Registration,
|
|
38
|
+
deployment_id: str,
|
|
39
|
+
deep_link_settings: TDeepLinkData,
|
|
40
|
+
):
|
|
41
|
+
self._registration = registration
|
|
42
|
+
self._deployment_id = deployment_id
|
|
43
|
+
self._deep_link_settings = deep_link_settings
|
|
44
|
+
|
|
45
|
+
def _generate_nonce(self):
|
|
46
|
+
return uuid.uuid4().hex + uuid.uuid1().hex
|
|
47
|
+
|
|
48
|
+
def get_message_jwt(
|
|
49
|
+
self, resources: t.Sequence[DeepLinkResource]
|
|
50
|
+
) -> t.Dict[str, object]:
|
|
51
|
+
message_jwt = {
|
|
52
|
+
"iss": self._registration.get_client_id(),
|
|
53
|
+
"aud": [self._registration.get_issuer()],
|
|
54
|
+
"exp": int(time.time()) + 600,
|
|
55
|
+
"iat": int(time.time()),
|
|
56
|
+
"nonce": "nonce-" + self._generate_nonce(),
|
|
57
|
+
"https://purl.imsglobal.org/spec/lti/claim/deployment_id": self._deployment_id,
|
|
58
|
+
"https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiDeepLinkingResponse",
|
|
59
|
+
"https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
|
|
60
|
+
"https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
|
|
61
|
+
r.to_dict() for r in resources
|
|
62
|
+
],
|
|
63
|
+
"https://purl.imsglobal.org/spec/lti-dl/claim/data": self._deep_link_settings.get(
|
|
64
|
+
"data"
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
return message_jwt
|
|
68
|
+
|
|
69
|
+
def encode_jwt(self, message):
|
|
70
|
+
headers = None
|
|
71
|
+
kid = self._registration.get_kid()
|
|
72
|
+
if kid:
|
|
73
|
+
headers = {"kid": kid}
|
|
74
|
+
encoded_jwt = jwt.encode(
|
|
75
|
+
message,
|
|
76
|
+
self._registration.get_tool_private_key(),
|
|
77
|
+
algorithm="RS256",
|
|
78
|
+
headers=headers,
|
|
79
|
+
)
|
|
80
|
+
if isinstance(encoded_jwt, bytes):
|
|
81
|
+
return encoded_jwt.decode("utf-8")
|
|
82
|
+
return encoded_jwt
|
|
83
|
+
|
|
84
|
+
def get_response_jwt(self, resources: t.Sequence[DeepLinkResource]) -> str:
|
|
85
|
+
message_jwt = self.get_message_jwt(resources)
|
|
86
|
+
return self.encode_jwt(message_jwt)
|
|
87
|
+
|
|
88
|
+
def get_response_form_html(self, jwt_val: str) -> str:
|
|
89
|
+
deep_link_return_url = self._deep_link_settings["deep_link_return_url"]
|
|
90
|
+
html = (
|
|
91
|
+
f'<form id="lti13_deep_link_auto_submit" action="{deep_link_return_url}" method="POST">'
|
|
92
|
+
f'<input type="hidden" name="JWT" value="{jwt_val}" /></form>'
|
|
93
|
+
f"<script type=\"text/javascript\">document.getElementById('lti13_deep_link_auto_submit').submit();"
|
|
94
|
+
f"</script>"
|
|
95
|
+
)
|
|
96
|
+
return html
|
|
97
|
+
|
|
98
|
+
def output_response_form(self, resources: t.List[DeepLinkResource]) -> str:
|
|
99
|
+
jwt_val = self.get_response_jwt(resources)
|
|
100
|
+
return self.get_response_form_html(jwt_val)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from .lineitem import LineItem
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DeepLinkResource:
|
|
6
|
+
_type: str = "ltiResourceLink"
|
|
7
|
+
_title: t.Optional[str] = None
|
|
8
|
+
_url: t.Optional[str] = None
|
|
9
|
+
_lineitem: t.Optional[LineItem] = None
|
|
10
|
+
_custom_params: t.Mapping[str, str] = {}
|
|
11
|
+
_target: str = "iframe"
|
|
12
|
+
_icon_url: t.Optional[str] = None
|
|
13
|
+
|
|
14
|
+
def get_type(self):
|
|
15
|
+
return self._type
|
|
16
|
+
|
|
17
|
+
def set_type(self, value: str) -> "DeepLinkResource":
|
|
18
|
+
self._type = value
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def get_title(self) -> t.Optional[str]:
|
|
22
|
+
return self._title
|
|
23
|
+
|
|
24
|
+
def set_title(self, value: str) -> "DeepLinkResource":
|
|
25
|
+
self._title = value
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def get_url(self) -> t.Optional[str]:
|
|
29
|
+
return self._url
|
|
30
|
+
|
|
31
|
+
def set_url(self, value: str) -> "DeepLinkResource":
|
|
32
|
+
self._url = value
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def get_lineitem(self) -> t.Optional[LineItem]:
|
|
36
|
+
return self._lineitem
|
|
37
|
+
|
|
38
|
+
def set_lineitem(self, value: LineItem) -> "DeepLinkResource":
|
|
39
|
+
self._lineitem = value
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def get_custom_params(self) -> t.Mapping[str, str]:
|
|
43
|
+
return self._custom_params
|
|
44
|
+
|
|
45
|
+
def set_custom_params(self, value: t.Mapping[str, str]) -> "DeepLinkResource":
|
|
46
|
+
self._custom_params = value
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def get_target(self) -> str:
|
|
50
|
+
return self._target
|
|
51
|
+
|
|
52
|
+
def set_target(self, value: str) -> "DeepLinkResource":
|
|
53
|
+
self._target = value
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def get_icon_url(self) -> t.Optional[str]:
|
|
57
|
+
return self._icon_url
|
|
58
|
+
|
|
59
|
+
def set_icon_url(self, value: str) -> "DeepLinkResource":
|
|
60
|
+
self._icon_url = value
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> t.Dict[str, object]:
|
|
64
|
+
res: t.Dict[str, object] = {
|
|
65
|
+
"type": self._type,
|
|
66
|
+
"title": self._title,
|
|
67
|
+
"url": self._url,
|
|
68
|
+
"custom": self._custom_params,
|
|
69
|
+
}
|
|
70
|
+
if self._lineitem:
|
|
71
|
+
line_item: t.Dict[str, object] = {
|
|
72
|
+
"scoreMaximum": self._lineitem.get_score_maximum(),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
label = self._lineitem.get_label()
|
|
76
|
+
if label:
|
|
77
|
+
line_item["label"] = label
|
|
78
|
+
|
|
79
|
+
resource_id = self._lineitem.get_resource_id()
|
|
80
|
+
if resource_id:
|
|
81
|
+
line_item["resourceId"] = resource_id
|
|
82
|
+
|
|
83
|
+
tag = self._lineitem.get_tag()
|
|
84
|
+
if tag:
|
|
85
|
+
line_item["tag"] = tag
|
|
86
|
+
|
|
87
|
+
submission_review = self._lineitem.get_submission_review()
|
|
88
|
+
if submission_review:
|
|
89
|
+
line_item["submissionReview"] = submission_review
|
|
90
|
+
|
|
91
|
+
res["lineItem"] = line_item
|
|
92
|
+
|
|
93
|
+
if self._icon_url:
|
|
94
|
+
res["icon"] = {"url": self._icon_url}
|
|
95
|
+
|
|
96
|
+
return res
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Deployment:
|
|
5
|
+
|
|
6
|
+
_deployment_id: t.Optional[str] = None
|
|
7
|
+
|
|
8
|
+
def get_deployment_id(self) -> t.Optional[str]:
|
|
9
|
+
return self._deployment_id
|
|
10
|
+
|
|
11
|
+
def set_deployment_id(self, deployment_id: str) -> "Deployment":
|
|
12
|
+
self._deployment_id = deployment_id
|
|
13
|
+
return self
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LtiException(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OIDCException(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LtiServiceException(LtiException):
|
|
13
|
+
def __init__(self, response: requests.Response):
|
|
14
|
+
msg = f"HTTP response [{response.url}]: {str(response.status_code)} - {response.text}"
|
|
15
|
+
super().__init__(msg)
|
|
16
|
+
self.response = response
|