lms.backend.moodle.backend
1# pylint: disable=abstract-method 2 3import logging 4import typing 5import urllib.parse 6 7import bs4 8import edq.net.request 9import requests 10 11import lms.model.backend 12import lms.model.constants 13import lms.util.net 14 15_logger = logging.getLogger(__name__) 16 17class MoodleBackend(lms.model.backend.APIBackend): 18 """ An API backend for the Moodle LMS. """ 19 20 def __init__(self, 21 server: str, 22 auth_user: typing.Union[str, None] = None, 23 auth_password: typing.Union[str, None] = None, 24 **kwargs: typing.Any) -> None: 25 super().__init__(server, lms.model.constants.BACKEND_TYPE_MOODLE, **kwargs) 26 27 if (auth_user is None): 28 raise ValueError("Moodle backends require a username.") 29 30 if (auth_password is None): 31 raise ValueError("Moodle backends require a password.") 32 33 self._username = auth_user 34 """ The username to authenticate with. """ 35 36 self._password = auth_password 37 """ The password to authenticate with. """ 38 39 self._session_headers: typing.Union[typing.Dict[str, typing.Any], None] = None 40 """ The headers (e.g., cookies) for our logged in Moodle session. """ 41 42 def _parse_cookies(self, response: requests.Response) -> typing.Dict[str, typing.Any]: 43 """ 44 Parse Moodle cookies. 45 Return fake cookies when testing. 46 """ 47 48 if (self.is_testing()): 49 return { 50 'moodlesession': 'testing-moodle-session', 51 'moodleid1_': 'testing-moodle-id', 52 } 53 54 return lms.util.net.parse_cookies(response.headers.get('set-cookie', None)) 55 56 def _login(self, update_server: bool = True) -> None: 57 """ 58 Try to login to the Moodle server. 59 If `update_server` is true, then this may try to update the backend's server location if redirected by the Moodle server. 60 """ 61 62 # Check if we are already logged in. 63 if (self._session_headers is not None): 64 return 65 66 response, body = edq.net.request.make_get(self.server + '/login/index.php') 67 cookies = self._parse_cookies(response) 68 69 new_cookies = { 70 'MoodleSession': cookies['moodlesession'], 71 } 72 text_cookies = '; '.join(['='.join(items) for items in new_cookies.items()]) 73 74 # Parse the login token from the page HTML. 75 document = bs4.BeautifulSoup(body, 'html.parser') 76 token = document.select('input[name="logintoken"]')[0]['value'] 77 78 headers = { 79 'cookie': text_cookies, 80 'host': urllib.parse.urlparse(self.server).netloc, 81 } 82 83 data = { 84 'logintoken': token, 85 'username': self._username, 86 'password': self._password, 87 } 88 89 response, _ = edq.net.request.make_post(self.server + '/login/index.php', 90 headers = headers, data = data, 91 allow_redirects = False) 92 93 # Check for a successful login. 94 cookies = self._parse_cookies(response) 95 if ('moodleid1_' in cookies): 96 self._session_headers = { 97 'cookie': response.headers.get('set-cookie', None), 98 # Insert a header to identify the user. 99 'edq-lms-moodle-user': self._username, 100 } 101 return 102 103 # Login Failed 104 105 # The specified server/host needs to match exactly what the Moodle server wants it to be, 106 # e.g., `127.0.0.1` does not work when the server wants the host to be `localhost`. 107 # If these do not match, we will get a redirect here. 108 # Use this redirect to discover the correct server. 109 location = response.headers.get('location', None) 110 if (update_server and (location is not None) and (not location.startswith(self.server))): 111 parts = urllib.parse.urlparse(location) 112 host = f"{parts.scheme}://{parts.netloc}" 113 114 _logger.debug(("Mismatch in the client-specified server ('%s') and server-requested host ('%s')." 115 + " To avoid extra requests, update the server (e.g., `--server`) to match the host."), 116 self.server, host) 117 118 # Update the server and try to login again (without updating the server again (to avoid loops)). 119 self.server = host 120 self._login(update_server = False) 121 return 122 123 raise ValueError(f"Could not log into Moodle server ({self.server}) with user '{self._username}'. Is username/password correct?") 124 125 def courses_list(self, 126 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 127 self._login() 128 129 url = self.server + "/user/profile.php" 130 response, _ = edq.net.request.make_get(url, headers = self._session_headers) 131 132 document = bs4.BeautifulSoup(response.text, 'html.parser') 133 cards = document.select('div.card-body') 134 135 node = None 136 for card in cards: 137 text = card.get_text() 138 if (text.startswith("Course details")): 139 node = card 140 break 141 142 if (node is None): 143 return [] 144 145 links = node.select('a') 146 147 courses = [] 148 for link in links: 149 name = link.get_text() 150 151 href = link.get('href', None) 152 if (href is None): 153 continue 154 155 id = str(href).rsplit("=", maxsplit = 1)[-1] 156 157 courses.append(lms.model.courses.Course( 158 id = id, 159 name = name, 160 )) 161 162 return sorted(courses)
18class MoodleBackend(lms.model.backend.APIBackend): 19 """ An API backend for the Moodle LMS. """ 20 21 def __init__(self, 22 server: str, 23 auth_user: typing.Union[str, None] = None, 24 auth_password: typing.Union[str, None] = None, 25 **kwargs: typing.Any) -> None: 26 super().__init__(server, lms.model.constants.BACKEND_TYPE_MOODLE, **kwargs) 27 28 if (auth_user is None): 29 raise ValueError("Moodle backends require a username.") 30 31 if (auth_password is None): 32 raise ValueError("Moodle backends require a password.") 33 34 self._username = auth_user 35 """ The username to authenticate with. """ 36 37 self._password = auth_password 38 """ The password to authenticate with. """ 39 40 self._session_headers: typing.Union[typing.Dict[str, typing.Any], None] = None 41 """ The headers (e.g., cookies) for our logged in Moodle session. """ 42 43 def _parse_cookies(self, response: requests.Response) -> typing.Dict[str, typing.Any]: 44 """ 45 Parse Moodle cookies. 46 Return fake cookies when testing. 47 """ 48 49 if (self.is_testing()): 50 return { 51 'moodlesession': 'testing-moodle-session', 52 'moodleid1_': 'testing-moodle-id', 53 } 54 55 return lms.util.net.parse_cookies(response.headers.get('set-cookie', None)) 56 57 def _login(self, update_server: bool = True) -> None: 58 """ 59 Try to login to the Moodle server. 60 If `update_server` is true, then this may try to update the backend's server location if redirected by the Moodle server. 61 """ 62 63 # Check if we are already logged in. 64 if (self._session_headers is not None): 65 return 66 67 response, body = edq.net.request.make_get(self.server + '/login/index.php') 68 cookies = self._parse_cookies(response) 69 70 new_cookies = { 71 'MoodleSession': cookies['moodlesession'], 72 } 73 text_cookies = '; '.join(['='.join(items) for items in new_cookies.items()]) 74 75 # Parse the login token from the page HTML. 76 document = bs4.BeautifulSoup(body, 'html.parser') 77 token = document.select('input[name="logintoken"]')[0]['value'] 78 79 headers = { 80 'cookie': text_cookies, 81 'host': urllib.parse.urlparse(self.server).netloc, 82 } 83 84 data = { 85 'logintoken': token, 86 'username': self._username, 87 'password': self._password, 88 } 89 90 response, _ = edq.net.request.make_post(self.server + '/login/index.php', 91 headers = headers, data = data, 92 allow_redirects = False) 93 94 # Check for a successful login. 95 cookies = self._parse_cookies(response) 96 if ('moodleid1_' in cookies): 97 self._session_headers = { 98 'cookie': response.headers.get('set-cookie', None), 99 # Insert a header to identify the user. 100 'edq-lms-moodle-user': self._username, 101 } 102 return 103 104 # Login Failed 105 106 # The specified server/host needs to match exactly what the Moodle server wants it to be, 107 # e.g., `127.0.0.1` does not work when the server wants the host to be `localhost`. 108 # If these do not match, we will get a redirect here. 109 # Use this redirect to discover the correct server. 110 location = response.headers.get('location', None) 111 if (update_server and (location is not None) and (not location.startswith(self.server))): 112 parts = urllib.parse.urlparse(location) 113 host = f"{parts.scheme}://{parts.netloc}" 114 115 _logger.debug(("Mismatch in the client-specified server ('%s') and server-requested host ('%s')." 116 + " To avoid extra requests, update the server (e.g., `--server`) to match the host."), 117 self.server, host) 118 119 # Update the server and try to login again (without updating the server again (to avoid loops)). 120 self.server = host 121 self._login(update_server = False) 122 return 123 124 raise ValueError(f"Could not log into Moodle server ({self.server}) with user '{self._username}'. Is username/password correct?") 125 126 def courses_list(self, 127 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 128 self._login() 129 130 url = self.server + "/user/profile.php" 131 response, _ = edq.net.request.make_get(url, headers = self._session_headers) 132 133 document = bs4.BeautifulSoup(response.text, 'html.parser') 134 cards = document.select('div.card-body') 135 136 node = None 137 for card in cards: 138 text = card.get_text() 139 if (text.startswith("Course details")): 140 node = card 141 break 142 143 if (node is None): 144 return [] 145 146 links = node.select('a') 147 148 courses = [] 149 for link in links: 150 name = link.get_text() 151 152 href = link.get('href', None) 153 if (href is None): 154 continue 155 156 id = str(href).rsplit("=", maxsplit = 1)[-1] 157 158 courses.append(lms.model.courses.Course( 159 id = id, 160 name = name, 161 )) 162 163 return sorted(courses)
An API backend for the Moodle LMS.
MoodleBackend( server: str, auth_user: Optional[str] = None, auth_password: Optional[str] = None, **kwargs: Any)
21 def __init__(self, 22 server: str, 23 auth_user: typing.Union[str, None] = None, 24 auth_password: typing.Union[str, None] = None, 25 **kwargs: typing.Any) -> None: 26 super().__init__(server, lms.model.constants.BACKEND_TYPE_MOODLE, **kwargs) 27 28 if (auth_user is None): 29 raise ValueError("Moodle backends require a username.") 30 31 if (auth_password is None): 32 raise ValueError("Moodle backends require a password.") 33 34 self._username = auth_user 35 """ The username to authenticate with. """ 36 37 self._password = auth_password 38 """ The password to authenticate with. """ 39 40 self._session_headers: typing.Union[typing.Dict[str, typing.Any], None] = None 41 """ The headers (e.g., cookies) for our logged in Moodle session. """
126 def courses_list(self, 127 **kwargs: typing.Any) -> typing.List[lms.model.courses.Course]: 128 self._login() 129 130 url = self.server + "/user/profile.php" 131 response, _ = edq.net.request.make_get(url, headers = self._session_headers) 132 133 document = bs4.BeautifulSoup(response.text, 'html.parser') 134 cards = document.select('div.card-body') 135 136 node = None 137 for card in cards: 138 text = card.get_text() 139 if (text.startswith("Course details")): 140 node = card 141 break 142 143 if (node is None): 144 return [] 145 146 links = node.select('a') 147 148 courses = [] 149 for link in links: 150 name = link.get_text() 151 152 href = link.get('href', None) 153 if (href is None): 154 continue 155 156 id = str(href).rsplit("=", maxsplit = 1)[-1] 157 158 courses.append(lms.model.courses.Course( 159 id = id, 160 name = name, 161 )) 162 163 return sorted(courses)
List the courses associated with the context user.
Inherited Members
- lms.model.backend.APIBackend
- server
- backend_type
- testing
- is_testing
- get_standard_headers
- 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_syllabus_fetch
- courses_syllabus_get
- courses_users_get
- courses_users_fetch
- courses_users_list
- 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_user_queries