From 8f0de52b754b21478003de6824685a69c71444b3 Mon Sep 17 00:00:00 2001 From: Matthew Hague <Matthew.Hague@rhul.ac.uk> Date: Tue, 6 Aug 2024 18:37:03 +0100 Subject: [PATCH] add(moodle): add script for bulk enrolling students to a Moodle module --- moodle/README.md | 1 + moodle/bulk-enrol.py | 163 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 moodle/bulk-enrol.py diff --git a/moodle/README.md b/moodle/README.md index 9ac3254..77880f5 100644 --- a/moodle/README.md +++ b/moodle/README.md @@ -6,6 +6,7 @@ useful. * `assignment-select-users.py` -- select a list of users on the assignments page (useful for e.g. granting bulk extensions) +* `bulk-enrol.py` -- bulk enrol a bunch of students on a module * `create-groups.rb` -- ruby script that takes a CSV file of students/groups and creates the groups on Moodle. * `quiz-get-urls.py` -- make a CSV of all quizzes on a module, for use diff --git a/moodle/bulk-enrol.py b/moodle/bulk-enrol.py new file mode 100644 index 0000000..23afacb --- /dev/null +++ b/moodle/bulk-enrol.py @@ -0,0 +1,163 @@ +# Script for bulk enrolling students on a module +# +# Uses browser_cookie3 to steal your Moodle login cookies from Firefox. +# Can be adjusted to work for whichever browser you use. Replace +# "browser_cookie3.firefox" with "browser_cookie3.chrome" or whatever. + +import browser_cookie3 +import csv +import re +import requests +import sys + +from dataclasses import dataclass + +from typing import Dict, Optional + +if len(sys.argv) < 3: + print("Usage: python bulk-enrol.py <url> <students file>") + print("where") + print(" <url> is the url of the participants page of the Moodle module") + print( + " <students file> a text file listing students " + "-- each line is searched on Moodle for a matching student." + "If a single student is found, they are enrolled, else you are asked." + ) + exit() + +url = sys.argv[1] +students_file = sys.argv[2] + +MOODLE_DOMAIN = "moodle.royalholloway.ac.uk" +MOODLE_SESSION_COOKIE = "MoodleSession" + +SESSKEY_RE = re.compile(r'.*"sesskey":"([^"]*)".*', re.MULTILINE) +SESSKEY_GRP = 1 + +ENROLID_RE = re.compile(r'.*name="enrolid"\s*value="([^"]*)"', re.MULTILINE) +ENROLID_GRP = 1 + +COURSEID_RE = re.compile(r".*?id=(\d{4})") +COURSEID_GRP = 1 + +# e.g userlist = "&userlist[]=160749&userlist[]=161078" +ENROL_URL = "https://moodle.royalholloway.ac.uk/enrol/manual/ajax.php?" \ + "mform_showmore_main=0&id={courseid}&action=enrol&enrolid={enrolid}&" \ + "sesskey={sesskey}&_qf__enrol_manual_enrol_users_form=1&" \ + "mform_showmore_id_main=0&userlist[]={userid}&roletoassign=5&startdate=3" \ + "&duration=" + +GET_USER_URL = "https://moodle.royalholloway.ac.uk/lib/ajax/service.php?" \ + "sesskey={sesskey}&info=core_enrol_get_potential_users" + +@dataclass +class EnrolInfo: + courseid : Optional[str] = None + sesskey : Optional[str] = None + enrolid : Optional[str] = None + +@dataclass +class Student: + userid : str + email : str + +def get_moodle_cookies() -> Dict[str, Optional[str]]: + """Gets current moodle cookies from firefox""" + cookie_jar = browser_cookie3.firefox(domain_name=MOODLE_DOMAIN) + cookies = { + c.name : c.value + for c in cookie_jar + if c.name == MOODLE_SESSION_COOKIE + } + + if len(cookies) == 0: + raise Exception( + "No", + MOODLE_SESSION_COOKIE, + "found, please make sure you are logged in." + ) + + return cookies + +def get_enrol_info( + cookies : Dict[str, Optional[str]], participants_url : str +) -> EnrolInfo: + info = EnrolInfo() + + if m := COURSEID_RE.match(participants_url): + info.courseid = m[COURSEID_GRP] + + response = requests.get(participants_url, cookies=cookies) + for line in response.text.split("\n"): + if m := SESSKEY_RE.match(line): + info.sesskey = m[SESSKEY_GRP] + elif m := ENROLID_RE.match(line): + info.enrolid = m[ENROLID_GRP] + + return info + +def get_user( + cookies : Dict[str, Optional[str]], info : EnrolInfo, search : str +) -> Optional[Student]: + url = GET_USER_URL.format(sesskey=info.sesskey, cookies=cookies) + r = requests.post(url, cookies=cookies, json=[{ + "index" : 0, + "methodname" : "core_enrol_get_potential_users", + "args" : { + "courseid" : info.courseid, + "enrolid": info.enrolid, + "search": search, + "searchanywhere" : True, + "page" : 0, + "perpage" : 101 + } + }]) + + students = r.json()[0]["data"] + + if len(students) == 0: + return None + elif len(students) == 1: + return Student(students[0]["id"], students[0]["email"]) + else: + while True: + for idx, student in enumerate(students): + print(f"{idx} : {student['fullname']} [{student['email']}]") + print("q : none") + choice = input("Enter choice: ").strip() + if choice.lower() == "q": + return None + try: + i = int(choice) + if 0 <= i and i < len(students): + return Student(students[i]["id"], students[i]["email"]) + except ValueError: + pass + +def enrol_user( + cookies : Dict[str, Optional[str]], info : EnrolInfo, userid : str +): + requests.get(ENROL_URL.format( + courseid=info.courseid, + enrolid=info.enrolid, + sesskey=info.sesskey, + userid=userid + ), cookies=cookies) + +cookies = get_moodle_cookies() +info = get_enrol_info(cookies, url) + +with open(students_file) as f: + for line in f: + search = line.strip() + if len(search) == 0: + continue + + student = get_user(cookies, info, search) + if student is None: + print(f"WARNING: no hit for {search}, ignoring.") + else: + # can probably do them in bulk, but 1 by 1 will avoid limits + print(f"Enroling {search} ({student.email} [{student.userid}])") + enrol_user(cookies, info, student.userid) + -- GitLab