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