spb/uqo-imm1033
Public
1#!/usr/bin/env python32"""Publication de fichiers sur Moodle@UQO en pilotant Safari (session Microsoft déjà ouverte) — recette du 2026-09-24,3généralisée le 2026-10-10. Chaque étape = un seul osascript (injection JS asynchrone + delay + lecture de window.__res).45Usage :6 moodle_publish.py add <course_id> <section> <url_pdf> "<nom>" "<description html>" [--before <cmid>]7 moodle_publish.py replace <cmid> <url_pdf> ["<nom>"] ["<description html>"]8 moodle_publish.py list <course_id>9Le fichier source est récupéré par fetch() depuis le site du cours (en-tête CORS Access-Control-Allow-Origin: * sur /files/).10"""11import json, subprocess, sys, time1213MOODLE = "https://moodle.uqo.ca"141516def osa(script):17 r = subprocess.run(["osascript", "-"], input=script, capture_output=True, text=True)18 if r.returncode != 0:19 raise SystemExit("osascript: " + r.stderr.strip())20 return r.stdout.strip()212223def js_quote(s):24 return json.dumps(s)252627def step(url, js, delay=8, poll=12):28 """Navigue vers url (si non vide), injecte js (doit poser window.__res), attend, retourne window.__res."""29 nav = f'set URL of current tab of front window to "{url}"\n delay 7' if url else ""30 script = f'''tell application "Safari"31 {nav}32 do JavaScript {js_quote("window.__res=null;" + js)} in current tab of front window33 delay {delay}34 set out to ""35 repeat {poll} times36 set out to do JavaScript "window.__res===null?'':String(window.__res)" in current tab of front window37 if out is not "" then exit repeat38 delay 239 end repeat40 return out41end tell'''42 return osa(script)434445def upload_js(pdf_url, name, desc, submit=True):46 return f'''(async function(){{47 try {{48 var it=document.querySelector('input[name="files"]').value;49 var cid=document.querySelector('.filemanager').id.replace('filemanager-','');50 var ctx=M.cfg.contextid, sk=M.cfg.sesskey;51 // 1. vider le brouillon (remplacement)52 var fdl=new FormData(); fdl.append('sesskey',sk); fdl.append('client_id',cid); fdl.append('itemid',it); fdl.append('filepath','/');53 var lst=await (await fetch('/repository/draftfiles_ajax.php?action=list',{{method:'POST',body:fdl,credentials:'same-origin'}})).json();54 for (var f of (lst.list||[])) {{ var fdd=new FormData(); fdd.append('sesskey',sk); fdd.append('client_id',cid); fdd.append('itemid',it); fdd.append('filepath',f.filepath||'/'); fdd.append('filename',f.filename); await fetch('/repository/draftfiles_ajax.php?action=delete',{{method:'POST',body:fdd,credentials:'same-origin'}}); }}55 // 2. téléverser56 var src={js_quote(pdf_url)}; var fname=decodeURIComponent(src.split('/').pop());57 var blob=await (await fetch(src,{{mode:'cors'}})).blob();58 var fd=new FormData(); fd.append('repo_upload_file',blob,fname); fd.append('sesskey',sk); fd.append('repo_id','3'); fd.append('itemid',it); fd.append('savepath','/'); fd.append('title',''); fd.append('author',''); fd.append('license','allrightsreserved'); fd.append('ctx_id',ctx); fd.append('client_id',cid); fd.append('overwrite','1');59 var up=await (await fetch('/repository/repository_ajax.php?action=upload',{{method:'POST',body:fd,credentials:'same-origin'}})).json();60 if (up.error) {{ window.__res='UPLOAD ERROR '+JSON.stringify(up); return; }}61 // 3. nom + description62 var nm={js_quote(name)}; if (nm) document.querySelector('#id_name').value=nm;63 var ds={js_quote(desc)}; if (ds && window.tinyMCE && tinyMCE.get('id_introeditor')) tinyMCE.get('id_introeditor').setContent(ds);64 var sd=document.querySelector('#id_showdescription'); if (sd && ds) sd.checked=true;65 var ss=document.querySelector('#id_showsize'); if (ss) ss.checked=true;66 var st=document.querySelector('#id_showtype'); if (st) st.checked=true;67 window.__res='UPLOADED '+JSON.stringify({{file:up.file||up.title||fname,size:blob.size}});68 if ({str(submit).lower()}) setTimeout(function(){{document.querySelector('#id_submitbutton2').click();}},500);69 }} catch(e) {{ window.__res='ERROR '+e; }}70}})();'''717273def list_course(course_id):74 js = '''(function(){var out=[];document.querySelectorAll('li.section').forEach(function(s){var name=(s.querySelector('.sectionname, h3')||{}).textContent||'';s.querySelectorAll('li.activity').forEach(function(m){out.push((s.id||'')+'|'+(m.id||'')+'|'+((m.querySelector('.activityname')||m.querySelector('.instancename')||{textContent:''}).textContent||'').trim().slice(0,90));});});window.__res=out.join('\\n');})();'''75 return step(f"{MOODLE}/course/view.php?id={course_id}", js, delay=2, poll=3)767778def find_cmid(course_id, name):79 for line in list_course(course_id).splitlines():80 parts = line.split('|')81 if len(parts) >= 3 and parts[2].startswith(name[:60]):82 return int(parts[1].replace('module-', ''))83 return None848586def move_before(course_id, cmid, target_cmid):87 js = f'''(async function(){{try{{var r=await (await fetch('/lib/ajax/service.php?sesskey='+M.cfg.sesskey+'&info=core_courseformat_update_course',{{method:'POST',credentials:'same-origin',headers:{{'Content-Type':'application/json'}},body:JSON.stringify([{{index:0,methodname:'core_courseformat_update_course',args:{{action:'cm_move',courseid:{course_id},ids:[{cmid}],targetcmid:{target_cmid}}}}}])}})).text();window.__res='MOVED '+r.slice(0,120);}}catch(e){{window.__res='ERROR '+e}}}})();'''88 return step(f"{MOODLE}/course/view.php?id={course_id}", js, delay=4, poll=4)899091def add(course_id, section, pdf_url, name, desc, before=None):92 url = f"{MOODLE}/course/modedit.php?add=resource&type=&course={course_id}§ion={section}&return=0&sr=0"93 r = step(url, upload_js(pdf_url, name, desc), delay=12, poll=10)94 print("add:", r)95 if not r.startswith("UPLOADED"):96 raise SystemExit("échec du téléversement")97 time.sleep(8)98 cmid = find_cmid(course_id, name)99 print("cmid:", cmid)100 if before and cmid:101 print(move_before(course_id, cmid, before))102 return cmid103104105def replace(cmid, pdf_url, name="", desc=""):106 url = f"{MOODLE}/course/modedit.php?update={cmid}&return=1"107 r = step(url, upload_js(pdf_url, name, desc), delay=12, poll=10)108 print("replace:", r)109 return r110111112if __name__ == "__main__":113 a = sys.argv[1:]114 if not a:115 print(__doc__); sys.exit(0)116 if a[0] == "list":117 print(list_course(int(a[1])))118 elif a[0] == "add":119 before = int(a[a.index("--before") + 1]) if "--before" in a else None120 add(int(a[1]), int(a[2]), a[3], a[4], a[5] if len(a) > 5 and a[5] != "--before" else "", before)121 elif a[0] == "replace":122 replace(int(a[1]), a[2], a[3] if len(a) > 3 else "", a[4] if len(a) > 4 else "")123