lms.backend.blackboard.backend
1# pylint: disable=abstract-method 2 3import typing 4 5import edq.net.request 6 7import lms.backend.blackboard.model 8import lms.model.backend 9import lms.model.constants 10import lms.model.courses 11import lms.model.users 12import lms.util.net 13import lms.util.parse 14 15class BlackboardBackend(lms.model.backend.APIBackend): 16 """ An API backend for the Blackboard Learn LMS. """ 17 18 def __init__(self, 19 **kwargs: typing.Any) -> None: 20 super().__init__(**kwargs) 21 22 assert(self.config.backend_type == lms.model.constants.BackendType.BLACKBOARD) 23 24 if (self.config.auth_user is None): 25 raise ValueError("Blackboard backends require a username.") 26 27 self.auth_user: str = self.config.auth_user 28 """ 29 The user to authenticate with. 30 This is set in config and compied for type checking. 31 """ 32 33 if (self.config.auth_password is None): 34 raise ValueError("Blackboard backends require a password.") 35 36 self.auth_password: str = self.config.auth_password.cleartext 37 """ 38 The (cleartext) password to authenticate with. 39 This is set in config and compied for type checking. 40 """ 41 42 self._session_headers: typing.Union[typing.Dict[str, typing.Any], None] = None 43 """ The headers (e.g., cookies) for our logged in Blackboard session. """ 44 45 def reset_connection(self) -> None: 46 self._session_headers = None 47 48 def get_standard_headers(self, write: bool = False) -> typing.Dict[str, str]: 49 headers = super().get_standard_headers(write) 50 51 if (self._session_headers is not None): 52 headers.update(self._session_headers) 53 54 return headers 55 56 def _login(self) -> None: 57 """ Try to login to the Blackboard server. """ 58 59 # Check if we are already logged in. 60 if (self._session_headers is not None): 61 return 62 63 response, _ = edq.net.request.make_get(self.server) 64 cookies, router_params = self._parse_bb_cookies(response.headers.get('set-cookie', None)) 65 66 new_cookies = { 67 'BbRouter': cookies['bbrouter'] 68 } 69 70 text_cookies = '; '.join(['='.join(items) for items in new_cookies.items()]) 71 72 headers = { 73 'cookie': text_cookies, 74 } 75 data = { 76 'user_id': self.auth_user, 77 'password': self.auth_password, 78 'blackboard.platform.security.NonceUtil.nonce.ajax': router_params['xsrf'], 79 } 80 81 response, _ = edq.net.request.make_post(self.server + '/webapps/login/', 82 headers = headers, data = data, 83 # Don't store the nonce in exchanges. 84 params_to_skip = ['blackboard.platform.security.NonceUtil.nonce.ajax'], 85 allow_redirects = False) 86 87 cookies, router_params = self._parse_bb_cookies(response.headers.get('set-cookie', None)) 88 if ('sessionId' not in router_params): 89 raise ValueError(f"Could not log into Blackboard server ({self.server}) with user '{self.auth_user}'. Is username/password correct?") 90 91 self._session_headers = { 92 'cookie': response.headers.get('set-cookie', None), 93 'x-blackboard-xsrf': router_params['xsrf'], 94 # Insert a header to identify the user. 95 'edq-lms-blackboard-user': self.auth_user, 96 } 97 98 def _parse_bb_cookies(self, text_cookies: typing.Union[str, None]) -> typing.Tuple[typing.Dict[str, typing.Any], typing.Dict[str, str]]: 99 """ 100 Parse out the Blackboard cookies and return two dicts: 101 - All cookies (with lower case keys). 102 - The router params/cookies (specific to Blackboard). 103 """ 104 105 cookies: typing.Dict[str, typing.Any] = {} 106 router_params: typing.Dict[str, str] = {} 107 108 # If we are testing, return fake cookies. 109 if (self.is_testing()): 110 cookies['bbrouter'] = 'xsrf:abc,sessionId:9999999' 111 router_params['xsrf'] = 'abc' 112 router_params['sessionId'] = '9999999' 113 114 return cookies, router_params 115 116 cookies = lms.util.net.parse_cookies(text_cookies) 117 118 router_text = cookies.get('bbrouter', None) 119 if (isinstance(router_text, str)): 120 for text in router_text.split(','): 121 key, value = text.split(':', maxsplit = 1) 122 router_params[key] = value 123 124 return cookies, router_params 125 126 def courses_list(self, 127 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 128 self._login() 129 130 url = self.server + '/learn/api/public/v3/courses' 131 data = { 132 'availability.available': 'Yes', 133 } 134 response, _ = edq.net.request.make_get(url, headers = self.get_standard_headers(), data = data) 135 136 courses = [] 137 for raw_course in response.json().get('results', []): 138 courses.append(lms.backend.blackboard.model.course(raw_course)) 139 140 courses.sort() 141 142 return courses 143 144 def courses_users_list(self, 145 course_id: str, 146 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 147 parsed_course_id = lms.util.parse.required_int(course_id, 'course_id') 148 149 self._login() 150 151 url = self.server + '/learn/api/public/v1/courses/{course_id}/users' 152 url = url.format(course_id = lms.backend.blackboard.model.format_id(parsed_course_id)) 153 154 data = { 155 'expand': 'user', 156 } 157 158 response, _ = edq.net.request.make_get(url, headers = self.get_standard_headers(), data = data) 159 160 users = [] 161 for raw_user in response.json().get('results', []): 162 users.append(lms.backend.blackboard.model.course_user(raw_user)) 163 164 users.sort() 165 166 return users
16class BlackboardBackend(lms.model.backend.APIBackend): 17 """ An API backend for the Blackboard Learn LMS. """ 18 19 def __init__(self, 20 **kwargs: typing.Any) -> None: 21 super().__init__(**kwargs) 22 23 assert(self.config.backend_type == lms.model.constants.BackendType.BLACKBOARD) 24 25 if (self.config.auth_user is None): 26 raise ValueError("Blackboard backends require a username.") 27 28 self.auth_user: str = self.config.auth_user 29 """ 30 The user to authenticate with. 31 This is set in config and compied for type checking. 32 """ 33 34 if (self.config.auth_password is None): 35 raise ValueError("Blackboard backends require a password.") 36 37 self.auth_password: str = self.config.auth_password.cleartext 38 """ 39 The (cleartext) password to authenticate with. 40 This is set in config and compied for type checking. 41 """ 42 43 self._session_headers: typing.Union[typing.Dict[str, typing.Any], None] = None 44 """ The headers (e.g., cookies) for our logged in Blackboard session. """ 45 46 def reset_connection(self) -> None: 47 self._session_headers = None 48 49 def get_standard_headers(self, write: bool = False) -> typing.Dict[str, str]: 50 headers = super().get_standard_headers(write) 51 52 if (self._session_headers is not None): 53 headers.update(self._session_headers) 54 55 return headers 56 57 def _login(self) -> None: 58 """ Try to login to the Blackboard server. """ 59 60 # Check if we are already logged in. 61 if (self._session_headers is not None): 62 return 63 64 response, _ = edq.net.request.make_get(self.server) 65 cookies, router_params = self._parse_bb_cookies(response.headers.get('set-cookie', None)) 66 67 new_cookies = { 68 'BbRouter': cookies['bbrouter'] 69 } 70 71 text_cookies = '; '.join(['='.join(items) for items in new_cookies.items()]) 72 73 headers = { 74 'cookie': text_cookies, 75 } 76 data = { 77 'user_id': self.auth_user, 78 'password': self.auth_password, 79 'blackboard.platform.security.NonceUtil.nonce.ajax': router_params['xsrf'], 80 } 81 82 response, _ = edq.net.request.make_post(self.server + '/webapps/login/', 83 headers = headers, data = data, 84 # Don't store the nonce in exchanges. 85 params_to_skip = ['blackboard.platform.security.NonceUtil.nonce.ajax'], 86 allow_redirects = False) 87 88 cookies, router_params = self._parse_bb_cookies(response.headers.get('set-cookie', None)) 89 if ('sessionId' not in router_params): 90 raise ValueError(f"Could not log into Blackboard server ({self.server}) with user '{self.auth_user}'. Is username/password correct?") 91 92 self._session_headers = { 93 'cookie': response.headers.get('set-cookie', None), 94 'x-blackboard-xsrf': router_params['xsrf'], 95 # Insert a header to identify the user. 96 'edq-lms-blackboard-user': self.auth_user, 97 } 98 99 def _parse_bb_cookies(self, text_cookies: typing.Union[str, None]) -> typing.Tuple[typing.Dict[str, typing.Any], typing.Dict[str, str]]: 100 """ 101 Parse out the Blackboard cookies and return two dicts: 102 - All cookies (with lower case keys). 103 - The router params/cookies (specific to Blackboard). 104 """ 105 106 cookies: typing.Dict[str, typing.Any] = {} 107 router_params: typing.Dict[str, str] = {} 108 109 # If we are testing, return fake cookies. 110 if (self.is_testing()): 111 cookies['bbrouter'] = 'xsrf:abc,sessionId:9999999' 112 router_params['xsrf'] = 'abc' 113 router_params['sessionId'] = '9999999' 114 115 return cookies, router_params 116 117 cookies = lms.util.net.parse_cookies(text_cookies) 118 119 router_text = cookies.get('bbrouter', None) 120 if (isinstance(router_text, str)): 121 for text in router_text.split(','): 122 key, value = text.split(':', maxsplit = 1) 123 router_params[key] = value 124 125 return cookies, router_params 126 127 def courses_list(self, 128 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 129 self._login() 130 131 url = self.server + '/learn/api/public/v3/courses' 132 data = { 133 'availability.available': 'Yes', 134 } 135 response, _ = edq.net.request.make_get(url, headers = self.get_standard_headers(), data = data) 136 137 courses = [] 138 for raw_course in response.json().get('results', []): 139 courses.append(lms.backend.blackboard.model.course(raw_course)) 140 141 courses.sort() 142 143 return courses 144 145 def courses_users_list(self, 146 course_id: str, 147 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 148 parsed_course_id = lms.util.parse.required_int(course_id, 'course_id') 149 150 self._login() 151 152 url = self.server + '/learn/api/public/v1/courses/{course_id}/users' 153 url = url.format(course_id = lms.backend.blackboard.model.format_id(parsed_course_id)) 154 155 data = { 156 'expand': 'user', 157 } 158 159 response, _ = edq.net.request.make_get(url, headers = self.get_standard_headers(), data = data) 160 161 users = [] 162 for raw_user in response.json().get('results', []): 163 users.append(lms.backend.blackboard.model.course_user(raw_user)) 164 165 users.sort() 166 167 return users
An API backend for the Blackboard Learn LMS.
BlackboardBackend(**kwargs: Any)
19 def __init__(self, 20 **kwargs: typing.Any) -> None: 21 super().__init__(**kwargs) 22 23 assert(self.config.backend_type == lms.model.constants.BackendType.BLACKBOARD) 24 25 if (self.config.auth_user is None): 26 raise ValueError("Blackboard backends require a username.") 27 28 self.auth_user: str = self.config.auth_user 29 """ 30 The user to authenticate with. 31 This is set in config and compied for type checking. 32 """ 33 34 if (self.config.auth_password is None): 35 raise ValueError("Blackboard backends require a password.") 36 37 self.auth_password: str = self.config.auth_password.cleartext 38 """ 39 The (cleartext) password to authenticate with. 40 This is set in config and compied for type checking. 41 """ 42 43 self._session_headers: typing.Union[typing.Dict[str, typing.Any], None] = None 44 """ The headers (e.g., cookies) for our logged in Blackboard session. """
auth_password: str
The (cleartext) password to authenticate with. This is set in config and compied for type checking.
def
reset_connection(self) -> None:
Inform the backend that their connection has been reset. Note that this is not on the individual HTTP connection level, but instead on the server level. For example, this is called when a testing server is reset (e.g., in a server runner).
def
get_standard_headers(self, write: bool = False) -> Dict[str, str]:
49 def get_standard_headers(self, write: bool = False) -> typing.Dict[str, str]: 50 headers = super().get_standard_headers(write) 51 52 if (self._session_headers is not None): 53 headers.update(self._session_headers) 54 55 return headers
Get standard headers for this backend. Children should take care to set the write header when performing a write operation.
127 def courses_list(self, 128 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 129 self._login() 130 131 url = self.server + '/learn/api/public/v3/courses' 132 data = { 133 'availability.available': 'Yes', 134 } 135 response, _ = edq.net.request.make_get(url, headers = self.get_standard_headers(), data = data) 136 137 courses = [] 138 for raw_course in response.json().get('results', []): 139 courses.append(lms.backend.blackboard.model.course(raw_course)) 140 141 courses.sort() 142 143 return courses
List the courses associated with the context user.
145 def courses_users_list(self, 146 course_id: str, 147 **kwargs: typing.Any) -> typing.List[lms.model.users.CourseUser]: 148 parsed_course_id = lms.util.parse.required_int(course_id, 'course_id') 149 150 self._login() 151 152 url = self.server + '/learn/api/public/v1/courses/{course_id}/users' 153 url = url.format(course_id = lms.backend.blackboard.model.format_id(parsed_course_id)) 154 155 data = { 156 'expand': 'user', 157 } 158 159 response, _ = edq.net.request.make_get(url, headers = self.get_standard_headers(), data = data) 160 161 users = [] 162 for raw_user in response.json().get('results', []): 163 users.append(lms.backend.blackboard.model.course_user(raw_user)) 164 165 users.sort() 166 167 return users
List the users associated with the given course.
Inherited Members
- lms.model.backend.APIBackend
- config
- backend_type
- server
- testing
- is_testing
- not_found
- courses_get
- courses_fetch
- courses_assignments_get
- courses_assignments_fetch
- courses_assignments_list
- courses_assignments_resolve_and_list
- courses_assignments_scores_get
- courses_assignments_scores_fetch
- courses_assignments_scores_list
- courses_assignments_scores_resolve_and_list
- courses_assignments_scores_resolve_and_upload
- courses_assignments_scores_upload
- courses_gradebook_get
- courses_gradebook_fetch
- courses_gradebook_list
- courses_gradebook_resolve_and_list
- courses_gradebook_resolve_and_upload
- courses_gradebook_upload
- courses_groupsets_create
- courses_groupsets_resolve_and_create
- courses_groupsets_delete
- courses_groupsets_resolve_and_delete
- courses_groupsets_get
- courses_groupsets_fetch
- courses_groupsets_list
- courses_groupsets_resolve_and_list
- courses_groupsets_memberships_resolve_and_add
- courses_groupsets_memberships_resolve_and_set
- courses_groupsets_memberships_resolve_and_subtract
- courses_groupsets_memberships_list
- courses_groupsets_memberships_resolve_and_list
- courses_groups_create
- courses_groups_resolve_and_create
- courses_groups_delete
- courses_groups_resolve_and_delete
- courses_groups_get
- courses_groups_fetch
- courses_groups_list
- courses_groups_resolve_and_list
- courses_groups_memberships_add
- courses_groups_memberships_resolve_and_add
- courses_groups_memberships_list
- courses_groups_memberships_resolve_and_list
- courses_groups_memberships_resolve_and_set
- courses_groups_memberships_subtract
- courses_groups_memberships_resolve_and_subtract
- courses_quizzes_download
- courses_quizzes_resolve_and_download
- courses_quizzes_get
- courses_quizzes_fetch
- courses_quizzes_list
- courses_quizzes_resolve_and_list
- courses_quizzes_resolve_and_remove
- courses_quizzes_remove
- courses_quizzes_resolve_and_upload
- courses_quizzes_upload
- courses_syllabus_fetch
- courses_syllabus_get
- courses_users_get
- courses_users_fetch
- courses_users_resolve_and_list
- courses_users_scores_get
- courses_users_scores_fetch
- courses_users_scores_list
- courses_users_scores_resolve_and_list
- parse_assignment_query
- parse_assignment_queries
- parse_course_query
- parse_course_queries
- parse_groupset_query
- parse_groupset_queries
- parse_group_query
- parse_group_queries
- parse_user_query
- parse_user_queries
- resolve_assignment_query
- resolve_assignment_queries
- resolve_course_query
- resolve_course_queries
- resolve_group_queries
- resolve_group_query
- resolve_groupset_queries
- resolve_groupset_query
- resolve_quiz_query
- resolve_quiz_queries
- resolve_user_queries