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
class BlackboardBackend(lms.model.backend.APIBackend):
 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_user: str

The user to authenticate with. This is set in config and compied for type checking.

auth_password: str

The (cleartext) password to authenticate with. This is set in config and compied for type checking.

def reset_connection(self) -> None:
46    def reset_connection(self) -> None:
47        self._session_headers = 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.

def courses_list(self, **kwargs: Any) -> List[lms.model.courses.Course]:
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.

def courses_users_list(self, course_id: str, **kwargs: Any) -> List[lms.model.users.CourseUser]:
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