From c4a34bd7b4733be925e4f50fcf750fdba47e029b Mon Sep 17 00:00:00 2001
From: Matthew Hague <Matthew.Hague@rhul.ac.uk>
Date: Thu, 20 Mar 2025 10:37:50 +0000
Subject: [PATCH] add(moodle): extend select users to cope with new email
 format

---
 moodle/select-users.py | 84 ++++++++++++++++++++++++------------------
 1 file changed, 49 insertions(+), 35 deletions(-)

diff --git a/moodle/select-users.py b/moodle/select-users.py
index 595cbee..18456c4 100644
--- a/moodle/select-users.py
+++ b/moodle/select-users.py
@@ -36,6 +36,8 @@ import os
 import re
 import sys
 
+from dataclasses import dataclass
+
 from typing import Dict, List, Optional, Set
 
 from selenium import webdriver
@@ -100,7 +102,15 @@ class Browser:
             for c in cookie_jar
         ]
 
-class LDAPEmailGetter:
+@dataclass(frozen=True, eq=True)
+class StudentInfo:
+    email : str
+    login : Optional[str]
+
+    def login_email(self):
+        return f"{self.login}@live.rhul.ac.uk"
+
+class LDAPStudentInfoGetter:
     def __init__(self, mod_codes : List[str]):
         self._mod_codes = mod_codes
 
@@ -109,30 +119,26 @@ class LDAPEmailGetter:
             PASS_CMD, stderr=subprocess.DEVNULL
         ).decode("ascii").strip()
 
-        self._numbers_to_email = None
+        self._numbers_to_info = None
 
-    def lookup(self, number : str) -> Optional[str]:
-        numbers_to_email = self._get_numbers_to_email()
+    def lookup(self, number : str) -> Optional[StudentInfo]:
+        numbers_to_info = self._get_numbers_to_info()
+        return numbers_to_info.get(number)
 
-        if number in numbers_to_email:
-            return numbers_to_email[number]
-        else:
-            return None
-
-    def _get_numbers_to_email(self) -> Dict[str, str]:
+    def _get_numbers_to_info(self) -> Dict[str, StudentInfo]:
         """Get student ID num to email in lower case from LDAP"""
 
-        if self._numbers_to_email is not None:
-            return self._numbers_to_email
+        if self._numbers_to_info is not None:
+            return self._numbers_to_info
 
-        self._numbers_to_email = dict()
+        self._numbers_to_info = dict()
 
         if len(self._mod_codes) == 0:
-            return self._numbers_to_email
+            return self._numbers_to_info
         if self._username is None:
-            return self._numbers_to_email
+            return self._numbers_to_info
         if self._password is None:
-            return self._numbers_to_email
+            return self._numbers_to_info
 
         try:
             with ldap3.Connection(
@@ -143,19 +149,22 @@ class LDAPEmailGetter:
                     mod_filter = f"(memberOf=CN=MG_STU_RC_{mod_code}," \
                         "OU=prog-section,OU=student,OU=Distribution Lists," \
                         "OU=MIIS Managed,DC=cc,DC=rhul,DC=local)"
-                    student_attrs = ["mail", "extensionAttribute3"]
+                    student_attrs = ["mail", "extensionAttribute3", "extensionAttribute2"]
 
                     conn.search(LDAP_BASE, mod_filter, attributes=student_attrs)
                     for student in conn.entries:
-                        self._numbers_to_email[student.extensionAttribute3.value] \
-                            = student.mail.value.lower()
+                        self._numbers_to_info[student.extensionAttribute3.value] \
+                            = StudentInfo(
+                                student.mail.value.lower(),
+                                student.extensionAttribute2.value.lower()
+                            )
         except Exception as e:
             print("WARNING: LDAP connection failed", e)
 
-        return self._numbers_to_email
+        return self._numbers_to_info
 
 def select_users(
-    browser : Browser, emails : Set[str], page_url : str
+    browser : Browser, info : Set[StudentInfo], page_url : str
 ):
     """Select the users on the given page
     :param browser: a logged in Moodle browser object
@@ -180,41 +189,46 @@ def select_users(
 
     email_nodes = browser.driver.find_elements(By.CSS_SELECTOR, email_css)
 
+    to_check_emails = set(i.email for i in info) \
+        | set(i.login_email() for i in info if i.login)
     checked_emails = set()
 
     for email_node in email_nodes:
         email = email_node.text.lower()
-        if email in emails:
+        if email in to_check_emails:
             email_node.find_element(
                 By.XPATH, "parent::*//input[@type='checkbox']"
             ).click()
             checked_emails.add(email)
 
     print(len(checked_emails), "emails selected")
-    for email in emails - checked_emails:
-        print("WARNING: did not select", email)
-
-def get_emails(mod_codes : List[str], id_file_csv : str):
+    for i in info:
+        if (
+            i.email not in checked_emails
+            and i.login_email() not in checked_emails
+        ):
+            print("WARNING: did not select", i.email)
+
+def get_info(mod_codes : List[str], id_file_csv : str) -> Set[StudentInfo]:
     """Returns emails in lower case"""
-    emails = set()
-    email_getter = LDAPEmailGetter(mod_codes)
+    info = set()
+    info_getter = LDAPStudentInfoGetter(mod_codes)
 
     with open(id_file_csv) as f:
         for row in csv.DictReader(f):
             id = row["ID"].lower()
             if "@" in id:
-                emails.add(id)
+                info.add(StudentInfo(id, None))
             elif re.match(r"\d+", id):
-                email = email_getter.lookup(id)
-                if email is not None:
-                    emails.add(email)
+                student_info = info_getter.lookup(id)
+                if student_info is not None:
+                    info.add(student_info)
                 else:
                     print("WARNING: could not look up", id)
             else:
                 print("WARNING: unrecognised ID format", id)
 
-    return emails
-
+    return info
 
 def main(argv):
     """Go, go, go!"""
@@ -228,7 +242,7 @@ def main(argv):
     id_file_csv = argv[1]
     assignment_page_url = argv[2]
 
-    emails = get_emails(mod_codes, id_file_csv)
+    emails = get_info(mod_codes, id_file_csv)
 
     with Browser() as browser:
         browser.login()
-- 
GitLab