moodlesolver 2.1.1__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/PKG-INFO +1 -1
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/client.py +9 -3
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/jsonapi.py +18 -3
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/quizapi.py +9 -8
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/PKG-INFO +1 -1
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/pyproject.toml +1 -1
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/LICENSE +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/README.md +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/__init__.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/aipreview.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/airunner.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/cdn.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/ctx.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/errors.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/mcp_server.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/parse.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/schema/quiz_protocol.json +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver/vision.py +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/SOURCES.txt +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/dependency_links.txt +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/entry_points.txt +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/requires.txt +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/moodlesolver.egg-info/top_level.txt +0 -0
- {moodlesolver-2.1.1 → moodlesolver-2.2.0}/setup.cfg +0 -0
|
@@ -101,12 +101,18 @@ class AsyncTab:
|
|
|
101
101
|
|
|
102
102
|
async def next_page(self):
|
|
103
103
|
u0 = await self.url()
|
|
104
|
+
h0 = await self.evaluate("(() => { const q = document.querySelector('.qtext'); return q ? q.innerText.trim().slice(0, 80) : ''; })()")
|
|
105
|
+
h0 = (h0.get('result') or '')
|
|
104
106
|
await self.evaluate("(() => { const n = document.querySelector('input[name=next]') || document.querySelector('input[name=finishattempt]'); if (n) n.click(); return n ? 'ok' : 'nonext'; })()")
|
|
105
|
-
for _ in range(
|
|
107
|
+
for _ in range(12):
|
|
106
108
|
await asyncio.sleep(1)
|
|
107
109
|
u = await self.url()
|
|
110
|
+
h = await self.evaluate("(() => { const q = document.querySelector('.qtext'); return q ? q.innerText.trim().slice(0, 80) : ''; })()")
|
|
111
|
+
h = (h.get('result') or '')
|
|
108
112
|
if u != u0 and '[object' not in u:
|
|
109
113
|
return u
|
|
114
|
+
if h and h != h0 and u == u0 and '[object' not in u:
|
|
115
|
+
return u
|
|
110
116
|
return u0
|
|
111
117
|
|
|
112
118
|
async def qtext(self):
|
|
@@ -297,10 +303,10 @@ class AsyncMoodle:
|
|
|
297
303
|
|
|
298
304
|
async def dump_question(self, q, quiz_id):
|
|
299
305
|
rec = {'n': q['number'], 'text': q['text'], 'options': [o['label'] for o in q['options']], 'multi': q['multi'], 'timer': q['timer']}
|
|
300
|
-
p = await self.download_image(
|
|
306
|
+
p = await self.download_image('%s/q%s_n%s.jpg' % (self.dumps_dir, quiz_id, q['number']))
|
|
301
307
|
if p:
|
|
302
308
|
rec['image'] = p
|
|
303
|
-
self.dump(
|
|
309
|
+
self.dump("q%s_n%s.json" % (quiz_id, q['number']), rec)
|
|
304
310
|
return rec
|
|
305
311
|
|
|
306
312
|
async def answer_step(self, labels, tab=None):
|
|
@@ -174,9 +174,24 @@ class JsonAPI:
|
|
|
174
174
|
st = await self.q.get_state() or {}
|
|
175
175
|
if not (st.get('checked') or st.get('text_inputs') or st.get('selects')):
|
|
176
176
|
return err('E_NOTHING_CHECKED', page=self._page(st), question=st.get('question'))
|
|
177
|
-
t = self.q.
|
|
178
|
-
|
|
179
|
-
await
|
|
177
|
+
t = await self.q._tab()
|
|
178
|
+
u0 = st.get('url') or ''
|
|
179
|
+
fin = await t.evaluate("(async () => { const n = document.querySelector('input[name=finishattempt], input[name=next]'); if (n) { n.click(); return n.name; } return 'nobtn'; })()")
|
|
180
|
+
for _ in range(15):
|
|
181
|
+
await asyncio.sleep(1)
|
|
182
|
+
u = await t.url()
|
|
183
|
+
if u != u0 and '[object' not in u and 'summary.php' in (u or ''):
|
|
184
|
+
break
|
|
185
|
+
if u != u0 and '[object' not in u:
|
|
186
|
+
break
|
|
187
|
+
u = await t.url()
|
|
188
|
+
if u == u0 or '[object' in (u or ''):
|
|
189
|
+
from .ctx import CtxAPI
|
|
190
|
+
mm = re.search('cmid=(\d+)', u0)
|
|
191
|
+
if mm:
|
|
192
|
+
r = await CtxAPI(self.q.m).resume_attempt(int(mm.group(1)))
|
|
193
|
+
if r.get('ok'):
|
|
194
|
+
t = self.q.m.tab
|
|
180
195
|
st2 = await self.q.get_state() or {}
|
|
181
196
|
if not st2.get('question'):
|
|
182
197
|
st2 = await self._resume() or {}
|
|
@@ -73,17 +73,18 @@ class QuizAPI:
|
|
|
73
73
|
st['option_images_b64'] = arr2
|
|
74
74
|
return st
|
|
75
75
|
|
|
76
|
-
async def set_answer(self, label=None, index=None, labels=None):
|
|
76
|
+
async def set_answer(self, label=None, index=None, labels=None, checked=None):
|
|
77
77
|
t = await self._tab()
|
|
78
|
-
if index is not None:
|
|
79
|
-
r2 = await t.click_answer(index)
|
|
80
|
-
return {'clicked': 1 if r2 == 'ok' else 0}
|
|
81
78
|
wants = labels if labels else [label] if label else []
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
if wants:
|
|
80
|
+
set_mark = 'true' if checked is not False else 'false'
|
|
81
|
+
r = await t.evaluate("(async () => { const ins = [...document.querySelectorAll('.answer input[type=radio], .answer input[type=checkbox]')]; const wants = " + json.dumps([w.lower() for w in wants]) + "; const setTo = " + set_mark + "; let n = 0; const norm = s => (s || '').replace(/[\\s\\xa0\\u200b\\u200c]+/g, ' ') .trim().toLowerCase(); for (const w of wants) { const wn = norm(w); let e = ins.find(x => norm(x.parentElement.textContent) === wn); if (!e) e = ins.find(x => norm(x.parentElement.textContent) .startsWith(wn.slice(0, 25))); if (e) { if (e.type === 'checkbox' && e.checked !== setTo) { e.click(); } else if (e.type === 'radio' && !e.checked) { e.click(); } if (e.type === 'radio' || e.checked === setTo) n++; } } return JSON.stringify({clicked: n}); })()")
|
|
84
82
|
return json.loads(r.get('result') or '{}')
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
if index is not None:
|
|
84
|
+
set_mark = 'true' if checked is not False else 'false'
|
|
85
|
+
r2 = await t.evaluate(f"(() => {{ const ins = document.querySelectorAll('.answer input[type=radio], .answer input[type=checkbox]'); if (!ins[{index}]) return 'nobtn'; const e = ins[{index}]; if (e.type === 'checkbox') {{ const want = {set_mark}; if (e.checked !== want) e.click(); return e.checked === want ? 'ok' : 'fail'; }} if (e.checked) return 'ok'; e.click(); return e.checked ? 'ok' : 'fail'; }})()")
|
|
86
|
+
return {'clicked': 1 if r2 == 'ok' else 0}
|
|
87
|
+
return {'clicked': 0}
|
|
87
88
|
|
|
88
89
|
async def set_text(self, text, index=0):
|
|
89
90
|
t = await self._tab()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|