atsfill 0.0.8.dev227__tar.gz → 0.0.8.dev229__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.
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/PKG-INFO +1 -1
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/pyproject.toml +1 -1
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/pyproject.toml.orig +1 -1
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/join.py +4 -2
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/workday.py +42 -19
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/adapter.py +9 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/browser.py +5 -2
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/commands.py +14 -3
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/results.py +1 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/telemetry.py +1 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/LICENSE +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/README.md +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/adp.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/adpmyjobs.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/jobaffinity.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/jobstreet.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/recruiterflow.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/welcomekit.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/wellfound.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/werecruit.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/workatastartup.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/countries.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.8.dev227 → atsfill-0.0.8.dev229}/src/atsfill/registry.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atsfill
|
|
3
|
-
Version: 0.0.8.
|
|
3
|
+
Version: 0.0.8.dev229
|
|
4
4
|
Summary: Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle.
|
|
5
5
|
Keywords: job-applications,ats,ashby,greenhouse,teamtailor,ai-agents,cli
|
|
6
6
|
Author: shbernal
|
|
@@ -376,8 +376,9 @@ class Join:
|
|
|
376
376
|
return {k: v for k, v in values.items() if v}
|
|
377
377
|
|
|
378
378
|
def save(self, page: Page) -> None:
|
|
379
|
-
"""Save changes posts `UpdateTalentPersonalInformation
|
|
380
|
-
a
|
|
379
|
+
"""Save changes posts `UpdateTalentPersonalInformation`, then the page returns to the
|
|
380
|
+
account page: a navigation started before that would be aborted by it. The button stays
|
|
381
|
+
disabled while a required field is empty or nothing changed."""
|
|
381
382
|
button = page.locator("form button[type=submit]").last
|
|
382
383
|
try: # it is enabled a moment after the last change
|
|
383
384
|
page.wait_for_function(
|
|
@@ -397,6 +398,7 @@ class Join:
|
|
|
397
398
|
raise RuntimeError(f"UpdateTalentPersonalInformation answered {response.status}")
|
|
398
399
|
if errors := response.json().get("errors"):
|
|
399
400
|
raise Refused(f"step_refused: {GENERAL}: {errors[0].get('message')}")
|
|
401
|
+
page.wait_for_url(lambda url: url != PROFILE, timeout=SAVE_TIMEOUT_MS)
|
|
400
402
|
|
|
401
403
|
def _file(self, page: Page, field_id: str, answer: Answer | None) -> None:
|
|
402
404
|
if not isinstance(answer, str):
|
|
@@ -5,8 +5,8 @@ observes.
|
|
|
5
5
|
Applying needs an account, one per tenant: the account scope. The form is a wizard of pages,
|
|
6
6
|
each saved to a draft application as it is left; only the current page is in the DOM. The
|
|
7
7
|
read walks the pages that save with what they hold, the posting's questionnaires and apply
|
|
8
|
-
flow standing for the pages it does not reach;
|
|
9
|
-
before it is saved.
|
|
8
|
+
flow standing for the pages it does not reach; a page behind a consent's tick only a fill
|
|
9
|
+
reaches, on the user's answer; the read-back is each page's values, read before it is saved.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import re
|
|
@@ -193,7 +193,7 @@ def _profile_hosts(request: Request, scope: str | None) -> list[str]:
|
|
|
193
193
|
|
|
194
194
|
class Workday:
|
|
195
195
|
name = "workday"
|
|
196
|
-
|
|
196
|
+
behind_consent = "the pages after a consent's tick, which the draft does not keep"
|
|
197
197
|
per_employer = True
|
|
198
198
|
asks: set[str] = set()
|
|
199
199
|
known_gaps = {
|
|
@@ -216,6 +216,8 @@ class Workday:
|
|
|
216
216
|
self._listed: set[str] = set() # the ids the read listed, unfillable ones too
|
|
217
217
|
self._walked: set[str] = set() # the pages the read reached
|
|
218
218
|
self._walled = False # no read gets past the page this one stopped on
|
|
219
|
+
self._behind = False # the fill stopped on a page behind that one, for `revealed`
|
|
220
|
+
self._phone_codes = ""
|
|
219
221
|
self._values: dict[str, Answer] = {}
|
|
220
222
|
self._apply = ""
|
|
221
223
|
|
|
@@ -292,15 +294,16 @@ class Workday:
|
|
|
292
294
|
page.goto(self._apply)
|
|
293
295
|
_wait_for_page(page)
|
|
294
296
|
seen: dict[str, list[Field]] = {}
|
|
295
|
-
self._walked, self._walled = set(), False
|
|
297
|
+
self._walked, self._walled, self._behind = set(), False, False
|
|
298
|
+
self._phone_codes = f"{calypso}/jobapplication/{found.tenant}{PHONE_CODES}"
|
|
296
299
|
for _ in range(MAX_PAGES):
|
|
297
300
|
step = _step(page)
|
|
298
301
|
if step == REVIEW: # repeats the answers, an upload without its input
|
|
299
302
|
break
|
|
300
303
|
self._walked.add(step)
|
|
301
304
|
if not _questions_page(step):
|
|
302
|
-
|
|
303
|
-
seen[step] = _read(page,
|
|
305
|
+
listed = {f.id for f in from_api.get(step, [])}
|
|
306
|
+
seen[step] = _read(page, self._phone_codes, listed)
|
|
304
307
|
try:
|
|
305
308
|
_save_and_continue(page)
|
|
306
309
|
except NotTaken:
|
|
@@ -316,10 +319,13 @@ class Workday:
|
|
|
316
319
|
return fields
|
|
317
320
|
|
|
318
321
|
def fill(self, page: Page, answers: dict[str, Answer]) -> None:
|
|
319
|
-
"""Fills each page of the wizard from the first and saves it, up to Review
|
|
320
|
-
page.
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
"""Fills each page of the wizard from the first and saves it, up to Review, or up to a
|
|
323
|
+
page behind the consent: the next call goes on from there with what `revealed` listed."""
|
|
324
|
+
if not self._behind:
|
|
325
|
+
page.goto(self._apply)
|
|
326
|
+
_wait_for_page(page)
|
|
327
|
+
self._values = {}
|
|
328
|
+
self._behind = False
|
|
323
329
|
pending: set[PageRequest] = set()
|
|
324
330
|
page.on("request", lambda r: pending.add(r) if DEFINITION in r.url else None)
|
|
325
331
|
for done in ("requestfinished", "requestfailed"):
|
|
@@ -343,6 +349,8 @@ class Workday:
|
|
|
343
349
|
_until_rendered(page, pending)
|
|
344
350
|
page.wait_for_timeout(SETTLE_MS)
|
|
345
351
|
self._unread(page)
|
|
352
|
+
if self._behind:
|
|
353
|
+
return
|
|
346
354
|
self._values |= {r["id"]: r["value"] for r in _filled(page, answers)}
|
|
347
355
|
if self._values.get(PHONE_NUMBER) and (code := _phone_code(page)):
|
|
348
356
|
self._values[PHONE_NUMBER] = f"+{code} {self._values[PHONE_NUMBER]}"
|
|
@@ -351,21 +359,31 @@ class Workday:
|
|
|
351
359
|
|
|
352
360
|
def _unread(self, page: Page) -> None:
|
|
353
361
|
"""Stops on fields the read did not list, optional ones too. A page the read did not
|
|
354
|
-
reach is read once the pages before it are saved
|
|
355
|
-
it read, an answer opened the field (a country's address, a referral's
|
|
356
|
-
stops unsaved, so no read would list it."""
|
|
362
|
+
reach is read once the pages before it are saved; where no read can get there, `revealed`
|
|
363
|
+
lists it. On one it read, an answer opened the field (a country's address, a referral's
|
|
364
|
+
name): the page stops unsaved, so no read would list it."""
|
|
357
365
|
unread = _not_listed(_page_fields(page), self._listed)
|
|
358
366
|
if not unread:
|
|
359
367
|
return
|
|
360
368
|
step, labels = _step(page), ", ".join(r["label"] for r in unread)
|
|
361
369
|
if step not in self._walked and self._walled:
|
|
362
|
-
|
|
363
|
-
|
|
370
|
+
self._behind = True
|
|
371
|
+
elif step not in self._walked:
|
|
364
372
|
raise NotTaken(f"{step} shows fields the read did not: {labels}; read again")
|
|
365
|
-
|
|
373
|
+
elif step == MY_INFORMATION and any(r["id"].startswith(ADDRESS) for r in unread):
|
|
366
374
|
detail = f"the country answered shows fields the read did not: {labels}"
|
|
367
375
|
raise Unsupported(known_gap(self, "other_country", detail))
|
|
368
|
-
|
|
376
|
+
else:
|
|
377
|
+
raise Unsupported(f"unhandled_rendering: an answer showed {labels}, not listed")
|
|
378
|
+
|
|
379
|
+
def revealed(self, page: Page) -> list[Field]:
|
|
380
|
+
"""The fields of the page behind the consent the fill stopped on, none once it ended."""
|
|
381
|
+
if not self._behind:
|
|
382
|
+
return []
|
|
383
|
+
fields = _read(page, self._phone_codes, self._listed)
|
|
384
|
+
self._fields |= {f.id: f for f in fields if not f.unfillable}
|
|
385
|
+
self._listed |= {f.id for f in fields}
|
|
386
|
+
return fields
|
|
369
387
|
|
|
370
388
|
def read_back(self, page: Page) -> dict[str, Answer]:
|
|
371
389
|
"""Each page's values, read before it was saved; one file as its name."""
|
|
@@ -607,6 +625,7 @@ def _field(raw: dict[str, Any]) -> Field | None:
|
|
|
607
625
|
return Field(
|
|
608
626
|
raw["id"], raw["label"], kind, raw["required"], options,
|
|
609
627
|
prefilled=None if site else value, default=value if site else None,
|
|
628
|
+
notice=raw["notice"],
|
|
610
629
|
) # fmt: skip
|
|
611
630
|
|
|
612
631
|
|
|
@@ -978,7 +997,8 @@ def _errors(page: Page) -> str:
|
|
|
978
997
|
return re.sub(r"\s+", " ", " ".join(page.locator(ERRORS).all_inner_texts())).strip()
|
|
979
998
|
|
|
980
999
|
|
|
981
|
-
# Each field the page shows: its id, label, whether required (`*`), rendering and
|
|
1000
|
+
# Each field the page shows: its id, label, whether required (`*`), rendering, value, and the
|
|
1001
|
+
# text a consent's box stands under.
|
|
982
1002
|
_FIELDS = """() => {
|
|
983
1003
|
const text = e => (e?.innerText ?? "").replace(/\\s+/g, " ").trim();
|
|
984
1004
|
const labelOf = (f, e) => text(f.querySelector(`label[for="${CSS.escape(e.id)}"]`)
|
|
@@ -1029,8 +1049,11 @@ _FIELDS = """() => {
|
|
|
1029
1049
|
kind = "text";
|
|
1030
1050
|
value = typed.value;
|
|
1031
1051
|
}
|
|
1052
|
+
const notice = kind === "box" && f.closest("[role=group]")
|
|
1053
|
+
?.querySelector("[data-automation-id=instructionalText]");
|
|
1032
1054
|
return {id: f.dataset.fkitId, label: label.replace(/\\*$/, "").trim(),
|
|
1033
|
-
required: label.endsWith("*"), kind, value, options
|
|
1055
|
+
required: label.endsWith("*"), kind, value, options,
|
|
1056
|
+
notice: text(notice || null) || null};
|
|
1034
1057
|
});
|
|
1035
1058
|
}"""
|
|
1036
1059
|
|
|
@@ -317,6 +317,15 @@ class RevealsFields(Adapter, Protocol):
|
|
|
317
317
|
...
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
@runtime_checkable
|
|
321
|
+
class RevealsBehindConsent(RevealsFields, Protocol):
|
|
322
|
+
"""A `RevealsFields` whose fields stand on pages behind a consent's tick, which no draft
|
|
323
|
+
keeps: a read gives no answer, so it stops at the tick, and only a fill gets past it. A
|
|
324
|
+
refusal for them is `behind_consent`, counted apart from other answers (ADR 0005)."""
|
|
325
|
+
|
|
326
|
+
behind_consent: str # where they stand
|
|
327
|
+
|
|
328
|
+
|
|
320
329
|
@runtime_checkable
|
|
321
330
|
class ListsShownFields(Adapter, Protocol):
|
|
322
331
|
"""An adapter whose form can show fields after an answer: once filled, it says which the
|
|
@@ -66,7 +66,7 @@ FILTERS = "assets/atsfill/filters.txt"
|
|
|
66
66
|
INCLUDE = re.compile(r"^!#include (\S+)$", re.M)
|
|
67
67
|
# A page holding a banner the lists hide: until they do, the blocker is still starting.
|
|
68
68
|
PROBE = ("https://blocker.invalid/", '<div id="cookie-banner">probe</div>')
|
|
69
|
-
BLOCKER_TIMEOUT_MS =
|
|
69
|
+
BLOCKER_TIMEOUT_MS = 120_000 # its lists compile on each launch: 4s alone, 47s at load 150
|
|
70
70
|
OVERLAY_GRACE_MS = 1_000 # how long a cover may stay on a click target before it counts
|
|
71
71
|
OVERLAY_POLL_MS = 100
|
|
72
72
|
|
|
@@ -226,7 +226,10 @@ def _wait_for_blocker(browser: Browser) -> None:
|
|
|
226
226
|
try:
|
|
227
227
|
page.locator("#cookie-banner").wait_for(state="hidden", timeout=BLOCKER_TIMEOUT_MS)
|
|
228
228
|
except Timeout:
|
|
229
|
-
raise BrowserError(
|
|
229
|
+
raise BrowserError(
|
|
230
|
+
f"the blocker hid no banner within {BLOCKER_TIMEOUT_MS // 1000}s: try again if the"
|
|
231
|
+
" machine is busy; if it is idle, run `atsfill install-browser`"
|
|
232
|
+
) from None
|
|
230
233
|
finally:
|
|
231
234
|
context.close()
|
|
232
235
|
|
|
@@ -34,6 +34,7 @@ from atsfill.core.adapter import (
|
|
|
34
34
|
ProfileIncomplete,
|
|
35
35
|
ReadsByRequest,
|
|
36
36
|
Refused,
|
|
37
|
+
RevealsBehindConsent,
|
|
37
38
|
RevealsFields,
|
|
38
39
|
SendsToOpen,
|
|
39
40
|
SignedOut,
|
|
@@ -354,9 +355,14 @@ def _shown_as_listed(
|
|
|
354
355
|
result.reason = f"mismatch: {', '.join(wrong)} shows though its condition is unmet"
|
|
355
356
|
|
|
356
357
|
|
|
357
|
-
def _refuse(
|
|
358
|
+
def _refuse(
|
|
359
|
+
result: Result,
|
|
360
|
+
problems: list[matching.Problem],
|
|
361
|
+
*more: str,
|
|
362
|
+
code: str = "invalid_answers",
|
|
363
|
+
) -> None:
|
|
358
364
|
result.status = Status.REFUSED
|
|
359
|
-
result.reason = "
|
|
365
|
+
result.reason = f"{code}: " + "; ".join([*problems, *more])
|
|
360
366
|
result.refused = [f"{p.field_id}: {p.rule}" for p in problems]
|
|
361
367
|
|
|
362
368
|
|
|
@@ -386,9 +392,14 @@ def _revealed(
|
|
|
386
392
|
result,
|
|
387
393
|
problems,
|
|
388
394
|
*[f"{f.id} takes {f.options}" for f in new if f.options and f.id not in mine],
|
|
395
|
+
code=(
|
|
396
|
+
"behind_consent"
|
|
397
|
+
if isinstance(adapter, RevealsBehindConsent)
|
|
398
|
+
else "invalid_answers"
|
|
399
|
+
),
|
|
389
400
|
)
|
|
390
401
|
return mine
|
|
391
|
-
left = [f for f in new if f.id not in mine]
|
|
402
|
+
left = [f for f in new if f.id not in mine and f.prefilled is None]
|
|
392
403
|
result.warnings += [f"an answer showed {f.id} ({f.label}), left empty" for f in left]
|
|
393
404
|
adapter.fill(page, matching.to_fill(new, {f.id: mine[f.id] for f in new if f.id in mine}))
|
|
394
405
|
_no_field(result, answers)
|
|
@@ -76,6 +76,7 @@ class Field:
|
|
|
76
76
|
option_sets: list[OptionSet] | None = None # a dependent list's; `options` where none holds
|
|
77
77
|
min_length: int | None = None # a text the system refuses shorter
|
|
78
78
|
knockouts: list[str] | None = None # the options whose choice ends the application (ADR 0005)
|
|
79
|
+
notice: str | None = None # the text a consent stands under: what a Yes accepts (ADR 0005)
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
@dataclass(frozen=True)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|