You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
80 lines
3.2 KiB
Python
80 lines
3.2 KiB
Python
"""AAO Career Center adapter — click-based AJAX pagination."""
|
|
from __future__ import annotations
|
|
|
|
from loguru import logger
|
|
|
|
from gimme_job.adapters.base import ManifestDrivenAdapter
|
|
from gimme_job.adapters.registry import register
|
|
from gimme_job.models.dto import JobPostingCandidate, RawJobCard
|
|
|
|
|
|
@register("aaoinfo")
|
|
class AAOInfoAdapter(ManifestDrivenAdapter):
|
|
_BASE_URL = "https://careers.aaoinfo.org"
|
|
|
|
def collect_cards(self, page, manifest):
|
|
try:
|
|
page.wait_for_selector("div.job-tile", timeout=15000, state="attached")
|
|
except Exception:
|
|
logger.debug("[aaoinfo] Results wait timed out — may be zero results")
|
|
return super().collect_cards(page, manifest)
|
|
|
|
def paginate(self, page, page_index: int, config) -> bool:
|
|
"""Click the next page number button in the AJAX pagination."""
|
|
if page_index >= config.pagination.max_pages - 1:
|
|
return False
|
|
|
|
next_page = page_index + 2
|
|
|
|
# Capture current first job URL to detect when content actually changes
|
|
try:
|
|
first_tile = page.query_selector("div.job-tile .job-title a")
|
|
current_first_url = first_tile.get_attribute("href") if first_tile else None
|
|
except Exception:
|
|
current_first_url = None
|
|
|
|
# Determine which selector to use for the next page button
|
|
primary_sel = f"#page-item-{next_page} a"
|
|
fallback_sel = "ul.pagination li.page-item:not(.active):not(.disabled):last-child a"
|
|
|
|
if page.query_selector(primary_sel):
|
|
btn_sel = primary_sel
|
|
elif page.query_selector(fallback_sel):
|
|
btn_sel = fallback_sel
|
|
else:
|
|
logger.debug(f"[aaoinfo] No next page button found — stopping at page {page_index + 1}")
|
|
return False
|
|
|
|
logger.debug(f"[aaoinfo] Paginating to page {next_page}")
|
|
# Use page.click() (locator-based) so Playwright re-queries the element
|
|
# at click time, avoiding "not attached to DOM" errors after AJAX refresh.
|
|
page.click(btn_sel)
|
|
|
|
# Wait for page indicator to become active
|
|
try:
|
|
page.wait_for_selector(f"#page-item-{next_page}.active", timeout=12000)
|
|
except Exception:
|
|
logger.debug(f"[aaoinfo] Page {next_page} did not become active — end of pagination")
|
|
return False
|
|
|
|
# Wait until the content actually changes (first job URL differs from before)
|
|
if current_first_url:
|
|
try:
|
|
page.wait_for_function(
|
|
f"""() => {{
|
|
const a = document.querySelector('div.job-tile .job-title a');
|
|
return a && a.getAttribute('href') !== {repr(current_first_url)};
|
|
}}""",
|
|
timeout=5000,
|
|
)
|
|
except Exception:
|
|
logger.debug(f"[aaoinfo] Content did not change after pagination to page {next_page}")
|
|
|
|
return True
|
|
|
|
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
|
candidate = super().normalize(raw)
|
|
if candidate.job_url and candidate.job_url.startswith("/"):
|
|
candidate.job_url = self._BASE_URL + candidate.job_url
|
|
return candidate
|