Repository Public Research preview
1#!/usr/bin/env python32"""Lit le carnet de notes Moodle@UQO (rapport de l'évaluateur) en pilotant Safari — réutilise la recette de moodle_publish.py.3Usage : moodle_grades.py <course_id> > notes.json"""4import json, subprocess, sys56MOODLE = "https://moodle.uqo.ca"78def osa(script):9 r = subprocess.run(["osascript", "-"], input=script, capture_output=True, text=True, encoding="utf-8")10 if r.returncode != 0:11 raise SystemExit("osascript: " + r.stderr.strip())12 return r.stdout.strip()1314def as_quote(s):15 return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'1617def step(url, js, delay=6, poll=15):18 nav = f'set URL of current tab of front window to "{url}"\n delay 8' if url else ""19 script = f'''tell application "Safari"20 {nav}21 do JavaScript {as_quote("window.__res=null;" + js)} in current tab of front window22 delay {delay}23 set out to ""24 repeat {poll} times25 set out to do JavaScript "window.__res===null?'':String(window.__res)" in current tab of front window26 if out is not "" then exit repeat27 delay 228 end repeat29 return out30end tell'''31 return osa(script)3233SCRAPE = r"""34(function(){35 try {36 const t = document.querySelector('table.gradereport-grader-table, table#user-grades');37 if(!t){ window.__res = JSON.stringify({error:'no table', title: document.title, url: location.href}); return; }38 const clean = s => (s||'').replace(/\s+/g,' ').trim();39 const hrow = t.querySelector('tr.heading');40 const heads = [];41 hrow.querySelectorAll('th.item, th.courseitem').forEach(th => {42 const a = th.querySelector('a.gradeitemheader');43 let txt = a ? clean(a.textContent) : clean(th.textContent);44 txt = txt.replace(/^Déplier la colonne\s*/,'').replace(/\s*Actions de cellule.*$/,'').replace(/\s*Actions de.*$/,'');45 const id = (th.className.match(/\bi(\d+)\b/)||[])[1] || '';46 heads.push({text: txt, id, course: th.classList.contains('courseitem')});47 });48 const rows = [];49 t.querySelectorAll('tr.userrow').forEach(tr => {50 const uth = tr.querySelector('th.user');51 const a = uth.querySelector('a');52 const name = clean(a ? a.textContent : uth.textContent).replace(/\s*Actions de.*$/,'');53 const email = clean((tr.querySelector('td.useremail')||{}).textContent);54 const cells = [];55 tr.querySelectorAll('td.gradecell').forEach(td => {56 const id = (td.className.match(/\bi(\d+)\b/)||[])[1] || '';57 const gv = td.querySelector('.gradevalue');58 const inp = td.querySelector('input[type=text]');59 let v = inp ? inp.value : (gv ? gv.textContent : td.textContent);60 v = clean(v).replace(/\s*Actions de cellule.*$/,'');61 cells.push({id, v, overridden: td.classList.contains('overridden'), excluded: td.classList.contains('excluded')});62 });63 rows.push({name, email, cells});64 });65 window.__res = JSON.stringify({title: document.title, heads, rows, n: rows.length});66 } catch(e) { window.__res = JSON.stringify({error: e.message}); }67})();68"""6970if __name__ == "__main__":71 cid = sys.argv[1]72 url = f"{MOODLE}/grade/report/grader/index.php?id={cid}&perpage=200"73 out = step(url, SCRAPE)74 print(out)75