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
ltitoolkit/core/grade.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import typing as t
|
|
3
|
+
from .exception import LtiException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
TExtaClaims = t.Mapping[str, t.Any]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Grade:
|
|
10
|
+
_score_given: t.Optional[float] = None
|
|
11
|
+
_score_maximum: t.Optional[float] = None
|
|
12
|
+
_activity_progress: t.Optional[str] = None
|
|
13
|
+
_grading_progress: t.Optional[str] = None
|
|
14
|
+
_timestamp: t.Optional[str] = None
|
|
15
|
+
_user_id: t.Optional[str] = None
|
|
16
|
+
_comment: t.Optional[str] = None
|
|
17
|
+
_extra_claims: t.Optional[TExtaClaims] = None
|
|
18
|
+
|
|
19
|
+
def _validate_score(self, score_value) -> t.Optional[str]:
|
|
20
|
+
if not isinstance(score_value, (int, float)):
|
|
21
|
+
return "score must be integer or float"
|
|
22
|
+
if score_value < 0:
|
|
23
|
+
return "score must be positive number (including 0)"
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
def get_score_given(self) -> t.Optional[float]:
|
|
27
|
+
"""
|
|
28
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
|
|
29
|
+
"""
|
|
30
|
+
return self._score_given
|
|
31
|
+
|
|
32
|
+
def set_score_given(self, value: float) -> "Grade":
|
|
33
|
+
"""
|
|
34
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
|
|
35
|
+
"""
|
|
36
|
+
err_msg = self._validate_score(value)
|
|
37
|
+
if err_msg is not None:
|
|
38
|
+
raise LtiException("Invalid scoreGiven value: " + err_msg)
|
|
39
|
+
self._score_given = value
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def get_score_maximum(self) -> t.Optional[float]:
|
|
43
|
+
"""
|
|
44
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
|
|
45
|
+
"""
|
|
46
|
+
return self._score_maximum
|
|
47
|
+
|
|
48
|
+
def set_score_maximum(self, value: float) -> "Grade":
|
|
49
|
+
"""
|
|
50
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
|
|
51
|
+
"""
|
|
52
|
+
err_msg = self._validate_score(value)
|
|
53
|
+
if err_msg is not None:
|
|
54
|
+
raise LtiException("Invalid scoreMaximum value: " + err_msg)
|
|
55
|
+
self._score_maximum = value
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def get_activity_progress(self) -> t.Optional[str]:
|
|
59
|
+
"""
|
|
60
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#activityprogress
|
|
61
|
+
"""
|
|
62
|
+
return self._activity_progress
|
|
63
|
+
|
|
64
|
+
def set_activity_progress(self, value: str) -> "Grade":
|
|
65
|
+
"""
|
|
66
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#activityprogress
|
|
67
|
+
"""
|
|
68
|
+
self._activity_progress = value
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def get_grading_progress(self) -> t.Optional[str]:
|
|
72
|
+
"""
|
|
73
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#gradingprogress
|
|
74
|
+
"""
|
|
75
|
+
return self._grading_progress
|
|
76
|
+
|
|
77
|
+
def set_grading_progress(self, value: str) -> "Grade":
|
|
78
|
+
"""
|
|
79
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#gradingprogress
|
|
80
|
+
"""
|
|
81
|
+
self._grading_progress = value
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def get_timestamp(self) -> t.Optional[str]:
|
|
85
|
+
"""
|
|
86
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#timestamp
|
|
87
|
+
"""
|
|
88
|
+
return self._timestamp
|
|
89
|
+
|
|
90
|
+
def set_timestamp(self, value: str) -> "Grade":
|
|
91
|
+
"""
|
|
92
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#timestamp
|
|
93
|
+
"""
|
|
94
|
+
self._timestamp = value
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def get_user_id(self) -> t.Optional[str]:
|
|
98
|
+
"""
|
|
99
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#userid-0
|
|
100
|
+
"""
|
|
101
|
+
return self._user_id
|
|
102
|
+
|
|
103
|
+
def set_user_id(self, value: str) -> "Grade":
|
|
104
|
+
"""
|
|
105
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#userid-0
|
|
106
|
+
"""
|
|
107
|
+
self._user_id = value
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def get_comment(self) -> t.Optional[str]:
|
|
111
|
+
"""
|
|
112
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#comment-0
|
|
113
|
+
"""
|
|
114
|
+
return self._comment
|
|
115
|
+
|
|
116
|
+
def set_comment(self, value: str) -> "Grade":
|
|
117
|
+
"""
|
|
118
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#comment-0
|
|
119
|
+
"""
|
|
120
|
+
self._comment = value
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def set_extra_claims(self, value: TExtaClaims) -> "Grade":
|
|
124
|
+
self._extra_claims = value
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def get_extra_claims(self) -> t.Optional[TExtaClaims]:
|
|
128
|
+
return self._extra_claims
|
|
129
|
+
|
|
130
|
+
def get_value(self) -> str:
|
|
131
|
+
data = {
|
|
132
|
+
"scoreGiven": self._score_given,
|
|
133
|
+
"scoreMaximum": self._score_maximum,
|
|
134
|
+
"activityProgress": self._activity_progress,
|
|
135
|
+
"gradingProgress": self._grading_progress,
|
|
136
|
+
"timestamp": self._timestamp,
|
|
137
|
+
"userId": self._user_id,
|
|
138
|
+
"comment": self._comment,
|
|
139
|
+
}
|
|
140
|
+
if self._extra_claims is not None:
|
|
141
|
+
data.update(self._extra_claims)
|
|
142
|
+
|
|
143
|
+
return json.dumps({k: v for k, v in data.items() if v is not None})
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from abc import ABCMeta, abstractmethod
|
|
3
|
+
from ..request import Request
|
|
4
|
+
|
|
5
|
+
T = t.TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LaunchDataStorage(t.Generic[T]):
|
|
9
|
+
__metaclass__ = ABCMeta
|
|
10
|
+
_request: t.Optional[Request] = None
|
|
11
|
+
_session_id: t.Optional[str] = None
|
|
12
|
+
_session_cookie_name: str = "session-id"
|
|
13
|
+
_prefix: str = "lti1p3-"
|
|
14
|
+
|
|
15
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def set_request(self, request: Request) -> None:
|
|
19
|
+
self._request = request
|
|
20
|
+
|
|
21
|
+
def get_session_cookie_name(self) -> t.Optional[str]:
|
|
22
|
+
return self._session_cookie_name
|
|
23
|
+
|
|
24
|
+
def get_session_id(self) -> t.Optional[str]:
|
|
25
|
+
return self._session_id
|
|
26
|
+
|
|
27
|
+
def set_session_id(self, session_id: str) -> None:
|
|
28
|
+
self._session_id = session_id
|
|
29
|
+
|
|
30
|
+
def remove_session_id(self) -> None:
|
|
31
|
+
self._session_id = None
|
|
32
|
+
|
|
33
|
+
def _prepare_key(self, key: str) -> str:
|
|
34
|
+
if self._session_id:
|
|
35
|
+
if key.startswith(self._prefix):
|
|
36
|
+
key = key[len(self._prefix) :]
|
|
37
|
+
return self._prefix + self._session_id + "-" + key
|
|
38
|
+
if not key.startswith(self._prefix):
|
|
39
|
+
key = self._prefix + key
|
|
40
|
+
return key
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def can_set_keys_expiration(self) -> bool:
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def get_value(self, key: str) -> T:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def set_value(self, key: str, value: T, exp: t.Optional[int] = None) -> None:
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def check_value(self, key: str) -> bool:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DisableSessionId:
|
|
60
|
+
_session_id: t.Optional[str] = None
|
|
61
|
+
_launch_data_storage: t.Optional[LaunchDataStorage] = None
|
|
62
|
+
|
|
63
|
+
def __init__(self, launch_data_storage: t.Optional[LaunchDataStorage]) -> None:
|
|
64
|
+
self._launch_data_storage = launch_data_storage
|
|
65
|
+
if launch_data_storage:
|
|
66
|
+
self._session_id = launch_data_storage.get_session_id()
|
|
67
|
+
|
|
68
|
+
def __enter__(self) -> "DisableSessionId":
|
|
69
|
+
if self._launch_data_storage:
|
|
70
|
+
self._launch_data_storage.remove_session_id()
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(self, *args) -> None:
|
|
74
|
+
if self._launch_data_storage and self._session_id:
|
|
75
|
+
self._launch_data_storage.set_session_id(self._session_id)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from .base import LaunchDataStorage
|
|
4
|
+
|
|
5
|
+
T = t.TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheDataStorage(LaunchDataStorage[T], t.Generic[T]):
|
|
9
|
+
_cache = None
|
|
10
|
+
|
|
11
|
+
def get_session_cookie_name(self) -> t.Optional[str]:
|
|
12
|
+
"""
|
|
13
|
+
Workaround for the local non-HTTP usage.
|
|
14
|
+
There is odd situation that all cookies become unavailable from some time
|
|
15
|
+
on the launch step (even if they were set in the new window).
|
|
16
|
+
Looks like it is bug in Chrome >= 80 related to SameSite changes.
|
|
17
|
+
So because of this we have to set all cache values without session prefix.
|
|
18
|
+
It is less secure because if you know unique launch_id you may get access to launch data,
|
|
19
|
+
but unfortunately there is no other way. So please use HTTPS on production :-)
|
|
20
|
+
"""
|
|
21
|
+
assert self._request is not None, "Request should be set at this point"
|
|
22
|
+
if not self._request.is_secure():
|
|
23
|
+
return None
|
|
24
|
+
return super().get_session_cookie_name()
|
|
25
|
+
|
|
26
|
+
def _get_cache(self):
|
|
27
|
+
assert self._cache is not None, "Cache is not set"
|
|
28
|
+
return self._cache
|
|
29
|
+
|
|
30
|
+
def get_value(self, key) -> T:
|
|
31
|
+
key = self._prepare_key(key)
|
|
32
|
+
return self._get_cache().get(key)
|
|
33
|
+
|
|
34
|
+
def set_value(self, key: str, value: T, exp: t.Optional[int] = None) -> None:
|
|
35
|
+
key = self._prepare_key(key)
|
|
36
|
+
self._get_cache().set(key, value, exp)
|
|
37
|
+
|
|
38
|
+
def check_value(self, key: str) -> bool:
|
|
39
|
+
key = self._prepare_key(key)
|
|
40
|
+
return self._get_cache().get(key) is not None
|
|
41
|
+
|
|
42
|
+
def can_set_keys_expiration(self) -> bool:
|
|
43
|
+
return True
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from .base import LaunchDataStorage
|
|
4
|
+
|
|
5
|
+
T = t.TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SessionDataStorage(LaunchDataStorage[T], t.Generic[T]):
|
|
9
|
+
def get_session_cookie_name(self) -> None:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
def set_session_id(self, session_id: str) -> None:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def get_value(self, key: str) -> T:
|
|
16
|
+
assert self._request is not None, "Request should be set at this point"
|
|
17
|
+
return self._request.session.get(key, None)
|
|
18
|
+
|
|
19
|
+
def set_value(self, key: str, value: T, exp: t.Optional[int] = None) -> None:
|
|
20
|
+
# pylint: disable=unused-argument
|
|
21
|
+
assert self._request is not None, "Request should be set at this point"
|
|
22
|
+
self._request.session[key] = value
|
|
23
|
+
|
|
24
|
+
def check_value(self, key: str) -> bool:
|
|
25
|
+
assert self._request is not None, "Request should be set at this point"
|
|
26
|
+
return key in self._request.session
|
|
27
|
+
|
|
28
|
+
def can_set_keys_expiration(self) -> bool:
|
|
29
|
+
return False
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import typing as t
|
|
3
|
+
import typing_extensions as te
|
|
4
|
+
from .exception import LtiException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
TSubmissionReview = te.TypedDict(
|
|
8
|
+
"TSubmissionReview",
|
|
9
|
+
{
|
|
10
|
+
# Required data
|
|
11
|
+
"reviewableStatus": list,
|
|
12
|
+
# Optional data
|
|
13
|
+
"label": str,
|
|
14
|
+
"url": str,
|
|
15
|
+
"custom": t.Dict[str, str],
|
|
16
|
+
},
|
|
17
|
+
total=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
TLineItem = te.TypedDict(
|
|
21
|
+
"TLineItem",
|
|
22
|
+
{
|
|
23
|
+
"id": str,
|
|
24
|
+
"scoreMaximum": int,
|
|
25
|
+
"label": str,
|
|
26
|
+
"resourceId": str,
|
|
27
|
+
"tag": str,
|
|
28
|
+
"resourceLinkId": str,
|
|
29
|
+
"startDateTime": str,
|
|
30
|
+
"endDateTime": str,
|
|
31
|
+
"submissionReview": TSubmissionReview,
|
|
32
|
+
},
|
|
33
|
+
total=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LineItem:
|
|
38
|
+
_id: t.Optional[str] = None
|
|
39
|
+
_score_maximum: t.Optional[float] = None
|
|
40
|
+
_label: t.Optional[str] = None
|
|
41
|
+
_resource_id: t.Optional[str] = None
|
|
42
|
+
_resource_link_id: t.Optional[str] = None
|
|
43
|
+
_tag: t.Optional[str] = None
|
|
44
|
+
_start_date_time: t.Optional[str] = None
|
|
45
|
+
_end_date_time: t.Optional[str] = None
|
|
46
|
+
_submission_review: t.Optional[TSubmissionReview] = None
|
|
47
|
+
|
|
48
|
+
def __init__(self, lineitem: t.Optional[TLineItem] = None):
|
|
49
|
+
if not lineitem:
|
|
50
|
+
lineitem = {}
|
|
51
|
+
self._id = lineitem.get("id")
|
|
52
|
+
self._score_maximum = lineitem.get("scoreMaximum")
|
|
53
|
+
self._label = lineitem.get("label")
|
|
54
|
+
self._resource_id = lineitem.get("resourceId")
|
|
55
|
+
self._resource_link_id = lineitem.get("resourceLinkId")
|
|
56
|
+
self._tag = lineitem.get("tag")
|
|
57
|
+
self._start_date_time = lineitem.get("startDateTime")
|
|
58
|
+
self._end_date_time = lineitem.get("endDateTime")
|
|
59
|
+
self._submission_review = lineitem.get("submissionReview")
|
|
60
|
+
|
|
61
|
+
def get_id(self) -> t.Optional[str]:
|
|
62
|
+
return self._id
|
|
63
|
+
|
|
64
|
+
def set_id(self, value: str) -> "LineItem":
|
|
65
|
+
self._id = value
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def get_label(self) -> t.Optional[str]:
|
|
69
|
+
"""
|
|
70
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#label
|
|
71
|
+
"""
|
|
72
|
+
return self._label
|
|
73
|
+
|
|
74
|
+
def set_label(self, value: str) -> "LineItem":
|
|
75
|
+
"""
|
|
76
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#label
|
|
77
|
+
"""
|
|
78
|
+
self._label = value
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def get_score_maximum(self) -> t.Optional[float]:
|
|
82
|
+
"""
|
|
83
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#scoremaximum
|
|
84
|
+
"""
|
|
85
|
+
return self._score_maximum
|
|
86
|
+
|
|
87
|
+
def set_score_maximum(self, value: float) -> "LineItem":
|
|
88
|
+
"""
|
|
89
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#scoremaximum
|
|
90
|
+
"""
|
|
91
|
+
if not isinstance(value, (int, float)):
|
|
92
|
+
raise LtiException(
|
|
93
|
+
"Invalid scoreMaximum value: score must be integer or float"
|
|
94
|
+
)
|
|
95
|
+
if value <= 0:
|
|
96
|
+
raise LtiException(
|
|
97
|
+
"Invalid scoreMaximum value: score must be non null value, strictly greater than 0"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self._score_maximum = value
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def get_resource_id(self) -> t.Optional[str]:
|
|
104
|
+
"""
|
|
105
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#tool-resource-identifier-resourceid
|
|
106
|
+
"""
|
|
107
|
+
return self._resource_id
|
|
108
|
+
|
|
109
|
+
def set_resource_id(self, value: str) -> "LineItem":
|
|
110
|
+
"""
|
|
111
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#tool-resource-identifier-resourceid
|
|
112
|
+
"""
|
|
113
|
+
self._resource_id = value
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def get_resource_link_id(self) -> t.Optional[str]:
|
|
117
|
+
"""
|
|
118
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0#resourcelinkid-and-binding-a-line-item-to-a-resource-link
|
|
119
|
+
"""
|
|
120
|
+
return self._resource_link_id
|
|
121
|
+
|
|
122
|
+
def set_resource_link_id(self, value: str) -> "LineItem":
|
|
123
|
+
"""
|
|
124
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0#resourcelinkid-and-binding-a-line-item-to-a-resource-link
|
|
125
|
+
"""
|
|
126
|
+
self._resource_link_id = value
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def get_tag(self) -> t.Optional[str]:
|
|
130
|
+
"""
|
|
131
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#tag
|
|
132
|
+
"""
|
|
133
|
+
return self._tag
|
|
134
|
+
|
|
135
|
+
def set_tag(self, value: str) -> "LineItem":
|
|
136
|
+
"""
|
|
137
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#tag
|
|
138
|
+
"""
|
|
139
|
+
self._tag = value
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def get_start_date_time(self) -> t.Optional[str]:
|
|
143
|
+
"""
|
|
144
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#startdatetime
|
|
145
|
+
"""
|
|
146
|
+
return self._start_date_time
|
|
147
|
+
|
|
148
|
+
def set_start_date_time(self, value: str) -> "LineItem":
|
|
149
|
+
"""
|
|
150
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#startdatetime
|
|
151
|
+
"""
|
|
152
|
+
self._start_date_time = value
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
def get_end_date_time(self) -> t.Optional[str]:
|
|
156
|
+
"""
|
|
157
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#enddatetime
|
|
158
|
+
"""
|
|
159
|
+
return self._end_date_time
|
|
160
|
+
|
|
161
|
+
def set_end_date_time(self, value: str) -> "LineItem":
|
|
162
|
+
"""
|
|
163
|
+
https://www.imsglobal.org/spec/lti-ags/v2p0/#enddatetime
|
|
164
|
+
"""
|
|
165
|
+
self._end_date_time = value
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def get_submission_review(self) -> t.Optional[TSubmissionReview]:
|
|
169
|
+
return self._submission_review
|
|
170
|
+
|
|
171
|
+
def set_submission_review(
|
|
172
|
+
self,
|
|
173
|
+
reviewable_status: t.List,
|
|
174
|
+
label: t.Optional[str] = None,
|
|
175
|
+
url: t.Optional[str] = None,
|
|
176
|
+
custom: t.Optional[t.Dict[str, str]] = None,
|
|
177
|
+
) -> "LineItem":
|
|
178
|
+
if not isinstance(reviewable_status, list):
|
|
179
|
+
raise Exception('Invalid "reviewable_status" argument')
|
|
180
|
+
|
|
181
|
+
self._submission_review: TSubmissionReview = {
|
|
182
|
+
"reviewableStatus": reviewable_status
|
|
183
|
+
}
|
|
184
|
+
if label:
|
|
185
|
+
self._submission_review["label"] = label
|
|
186
|
+
if url:
|
|
187
|
+
self._submission_review["url"] = url
|
|
188
|
+
if custom:
|
|
189
|
+
self._submission_review["custom"] = custom
|
|
190
|
+
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def get_value(self) -> str:
|
|
194
|
+
data = {
|
|
195
|
+
"id": self._id if self._id else None,
|
|
196
|
+
"scoreMaximum": self._score_maximum,
|
|
197
|
+
"label": self._label,
|
|
198
|
+
"resourceId": self._resource_id,
|
|
199
|
+
"resourceLinkId": self._resource_link_id,
|
|
200
|
+
"tag": self._tag,
|
|
201
|
+
"startDateTime": self._start_date_time,
|
|
202
|
+
"endDateTime": self._end_date_time,
|
|
203
|
+
"submissionReview": self._submission_review,
|
|
204
|
+
}
|
|
205
|
+
return json.dumps({k: v for k, v in data.items() if v})
|