Skip to content

[FIX] bedelia_api - Corrige semántica de modalidades, filtro de posprevias y paginación - #2

Merged
moura-code merged 1 commit into
mainfrom
fix/mejoras-pendientes
Aug 4, 2026
Merged

[FIX] bedelia_api - Corrige semántica de modalidades, filtro de posprevias y paginación#2
moura-code merged 1 commit into
mainfrom
fix/mejoras-pendientes

Conversation

@moura-code

@moura-code moura-code commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Resumen

Resuelve los cuatro hallazgos que habían quedado documentados como pendientes en el PR anterior: la lógica curso-vs-examen de eligible_courses.py, el filtro unidad_tipo del endpoint de posprevias que no filtraba, la fecha de importación reportada como fecha del dato, y la paginación del scraper que no alcanzaba páginas fuera de la ventana visible.

Cambios realizados

  • eligible_courses.py: un requisito de modalidad exam ahora solo se satisface con el examen aprobado (antes bastaba el curso, incorrecto en Udelar); un examen aprobado sigue satisfaciendo requisitos de curso (implica la materia aprobada). Las modalidades course_enrollment/exam_enrollment ahora se satisfacen con la aprobación correspondiente (antes no matcheaban nunca y excluían el curso permanentemente).
  • API (views/materias.py): unidad_tipo filtra también las unidades emitidas en el list() (antes solo acotaba los PosPreviaItem y devolvía unidades de todos los tipos) y agrega distinct() al join M2M que duplicaba filas.
  • Modelo + loader + serializer: PosPreviaItem.fecha (nuevo campo, migración incluida) guarda la fecha que reporta Bedelías; el serializer la expone en el mismo formato dd/mm/yyyy (antes devolvía fecha_creacion, o sea la fecha de importación a la base).
  • Scraper (usetable.py): go_to_page avanza/retrocede por bloques del paginador con las flechas hasta que el link de la página objetivo aparece, con tope de intentos (antes, saltar a una página fuera de la ventana visible desde una página distinta de la 1 hacía timeout de 60s garantizado).

Validación

  • compileall, flake8 (selectores del CI) y manage.py check: sin errores.
  • Schema OpenAPI genera con 0 errores.
  • migrate + carga REAL completa de los JSON del repo en SQLite: 22 planes, 2407 materias, 106.744 relaciones de posprevias.
  • Test del endpoint con datos reales: ?materia_code=TI09&unidad_tipo=EXAMEN devuelve solo filas EXAMEN (antes mezclaba CURSO), 0 duplicados, y fecha = 01/01/2007 (la fecha real de Bedelías, no la de hoy).
  • eligible_courses.py ejecutado antes y después con el approved.csv del repo: salida idéntica (con estos datos ambas semánticas coinciden), confirmando que el cambio no rompe el caso actual.
  • El scraper no se ejecutó contra el sitio real (requiere credenciales); el cambio de paginación está validado por compilación e imports.

Notas

  • La carga de datos existente en producción necesita re-ejecutar load_bedelia_data para poblar el campo fecha (queda null y el serializer lo tolera mientras tanto).
  • Repo personal sin sistema de tickets; el título no lleva referencia.

🤖 Generated with Claude Code

Summary by Sourcery

Adjust Bedelías modality semantics, posprevia filtering, date handling, and scraper pagination to match real-world behaviour and data.

New Features:

  • Expose Bedelías-reported posprevia date in the API via a new fecha field on PosPreviaItem.

Bug Fixes:

  • Ensure exam modality requirements are only satisfied by approved exams while still allowing approved exams to satisfy course requirements.
  • Make course_enrollment and exam_enrollment modalities satisfiable by corresponding approvals instead of never matching.
  • Apply unidad_tipo filtering consistently to both PosPreviaItem rows and emitted unidades in the posprevias endpoint, removing duplicated rows with a distinct query.
  • Fix posprevias fecha to represent the source Bedelías date instead of the local import timestamp in serializer responses.
  • Update scraper pagination to navigate through paginator windows using next/previous arrows so that non-visible pages can be reached without timing out.

Enhancements:

  • Add robust parsing of Bedelías date strings when loading posprevia data, tolerating invalid or missing values.

@sourcery-ai

sourcery-ai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes modality semantics for course vs exam requirements, corrects unidad_tipo filtering and duplication in the posprevias endpoint, adds a proper Bedelías-reported date field to PosPreviaItem and its loader/serializer, and makes the scraper paginator robust for pages outside the visible window.

Sequence diagram for updated scraper paginator navigation in go_to_page

sequenceDiagram
    participant Scraper
    participant BrowserPaginator

    Scraper->>BrowserPaginator: go_to_page(page)
    BrowserPaginator->>BrowserPaginator: wait.until(invisibility_of_element_located)
    loop until target_xpath visible or steps == 50
        BrowserPaginator->>BrowserPaginator: try_find_element(target_xpath)
        alt [target not found]
            BrowserPaginator->>BrowserPaginator: wait_for_element_to_be_visible(ui-state-active)
            BrowserPaginator->>BrowserPaginator: scroll_to_element_and_click(ui-paginator-next/ui-paginator-prev)
            BrowserPaginator->>BrowserPaginator: wait_for_page_to_load()
        end
    end
    BrowserPaginator->>BrowserPaginator: scroll_to_element_and_click(target_xpath)
    BrowserPaginator->>BrowserPaginator: wait_for_page_to_load()
Loading

Flow diagram for unidad_tipo filtering and flattening in posprevias endpoint

flowchart LR
    A[Request with unidad_tipo] --> B[get_queryset]
    B --> C[filter PosPreviaItem by unidades_dependientes.tipo]
    C --> D[apply distinct on queryset]
    D --> E[list]
    E --> F[iterate posprevia_item.unidades_dependientes]
    F --> G[filter unidades by unidad_tipo]
    G --> H[emit flattened entries in response]
Loading

File-Level Changes

Change Details Files
Adjust requirement satisfaction logic to distinguish exam vs course and treat enrollment modalities as satisfied by approvals.
  • Extend course-like modalities to include course_enrollment and exam_enrollment and satisfy them when either the course or exam is approved.
  • Restrict exam modality requirements to only be satisfied by approved exams instead of approved courses.
  • Keep the implication that approved exams also satisfy course requirements.
eligible_courses.py
Fix unidad_tipo filtering and eliminate duplicate rows in the posprevias API list endpoint.
  • Apply unidad_tipo filter with distinct() at the queryset level for PosPreviaItem to avoid duplicates from the M2M join.
  • Propagate unidad_tipo filtering to the flattened unidades_dependientes emitted in list(), so only matching unit types are returned.
bedelia/api/views/materias.py
Capture and expose the Bedelías-reported date for posprevias instead of the import timestamp.
  • Add a nullable fecha DateField to PosPreviaItem via a Django migration.
  • Parse dd/mm/yyyy fechas from JSON in the loader and persist them on PosPreviaItem, handling invalid or missing dates as None.
  • Update the PosPreviaSerializer to read posprevia_item.fecha as a DateField, formatted dd/mm/yyyy and allowing nulls.
bedelia/api/models.py
bedelia/api/management/commands/load_bedelia_data.py
bedelia/api/serializers/materias.py
bedelia/api/migrations/0002_pospreviaitem_fecha.py
Make the scraper paginator navigate via next/prev arrows until the target page link is visible, with bounded attempts.
  • Replace the special-case jump to the last page with a loop that clicks next or previous paginator arrows depending on the target vs current page.
  • Use aria-label-based XPaths to detect the active page and target page links.
  • Bound the stepping loop to a maximum number of iterations and keep existing modal invisibility and page-load waits around pagination actions.
scraper/common/usetable.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

- eligible_courses: un requisito de examen ya no se satisface con solo el
  curso aprobado; las modalidades de inscripción (course/exam_enrollment)
  ahora son satisfacibles en vez de excluir el curso permanentemente
- API posprevias: el filtro unidad_tipo ahora filtra también las unidades
  emitidas (antes devolvía todos los tipos) y deduplica con distinct()
- PosPreviaItem: nuevo campo fecha con la fecha real de Bedelías; el
  serializer ya no reporta la fecha de importación como "fecha"
- scraper go_to_page: navega por bloques del paginador cuando la página
  objetivo no está en la ventana visible (antes timeout garantizado)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@moura-code
moura-code merged commit 7a9339f into main Aug 4, 2026
2 checks passed
@moura-code
moura-code deleted the fix/mejoras-pendientes branch August 4, 2026 00:37

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In go_to_page, the hard-coded step limit of 50 and the reliance on parsing aria-label/text could be made more robust by extracting this logic into a helper with clearer bounds or retries and handling unexpected label formats explicitly.
  • When parsing fecha in load_bedelia_data, invalid date strings are silently dropped; consider logging or otherwise surfacing these cases so data issues from Bedelías can be traced rather than failing quietly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `go_to_page`, the hard-coded step limit of 50 and the reliance on parsing `aria-label`/text could be made more robust by extracting this logic into a helper with clearer bounds or retries and handling unexpected label formats explicitly.
- When parsing `fecha` in `load_bedelia_data`, invalid date strings are silently dropped; consider logging or otherwise surfacing these cases so data issues from Bedelías can be traced rather than failing quietly.

## Individual Comments

### Comment 1
<location path="scraper/common/usetable.py" line_range="73-78" />
<code_context>
         self.wait.until(EC.invisibility_of_element_located((By.XPATH, '//div[@id="j_idt22_modal"]')))
-        self.scroll_to_element_and_click(self.wait_for_element_to_be_clickable((By.XPATH, f'//a[@aria-label="Page {page}"]')))
+        steps = 0
+        while not self.try_find_element((By.XPATH, target_xpath)) and steps < 50:
+            steps += 1
+            current = self.wait_for_element_to_be_visible(
+                (By.XPATH, '//a[contains(@class, "ui-state-active")]')
+            )
+            current_label = current.get_attribute("aria-label") or current.text.strip()
+            current_page = int(current_label.replace("Page", "").strip() or 1)
+            arrow = "ui-paginator-next" if page > current_page else "ui-paginator-prev"
</code_context>
<issue_to_address>
**issue:** Current page parsing can raise ValueError if the label format changes or is non-numeric.

This relies on `current_label.replace("Page", "").strip()` always being numeric. If the aria-label/text changes (translation, extra text, or no number), `int()` will raise and break pagination. Please make the conversion more defensive (e.g., helper that safely defaults to page 1 or regex to extract the numeric portion) so pagination remains robust to label changes.
</issue_to_address>

### Comment 2
<location path="scraper/common/usetable.py" line_range="80-69" />
<code_context>
+            )
+            current_label = current.get_attribute("aria-label") or current.text.strip()
+            current_page = int(current_label.replace("Page", "").strip() or 1)
+            arrow = "ui-paginator-next" if page > current_page else "ui-paginator-prev"
+            self.scroll_to_element_and_click(
+                self.wait_for_element_to_be_clickable((By.XPATH, f'//a[contains(@class,"{arrow}")]'))
+            )
+            self.wait_for_page_to_load()
+        self.scroll_to_element_and_click(self.wait_for_element_to_be_clickable((By.XPATH, target_xpath)))
         self.wait_for_page_to_load()
</code_context>
<issue_to_address>
**issue:** Loop relies on next/prev arrows always being present, which can cause hangs at paginator boundaries.

At the first or last page, the chosen arrow link may be disabled or absent, causing `wait_for_element_to_be_clickable` to block until timeout and making the bounded loop fail slowly or incompletely. Consider detecting paginator boundaries (e.g., arrow not found or not enabled) and breaking early with a clear error instead of continuing to loop.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +73 to +78
while not self.try_find_element((By.XPATH, target_xpath)) and steps < 50:
steps += 1
current = self.wait_for_element_to_be_visible(
(By.XPATH, '//a[contains(@class, "ui-state-active")]')
)
current_label = current.get_attribute("aria-label") or current.text.strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Current page parsing can raise ValueError if the label format changes or is non-numeric.

This relies on current_label.replace("Page", "").strip() always being numeric. If the aria-label/text changes (translation, extra text, or no number), int() will raise and break pagination. Please make the conversion more defensive (e.g., helper that safely defaults to page 1 or regex to extract the numeric portion) so pagination remains robust to label changes.

self.wait_for_page_to_load()
# The paginator only renders a window of ~10 page links. If the target
# link is not visible from the current position, step through blocks
# with the next/prev arrows until it appears (bounded to avoid looping).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Loop relies on next/prev arrows always being present, which can cause hangs at paginator boundaries.

At the first or last page, the chosen arrow link may be disabled or absent, causing wait_for_element_to_be_clickable to block until timeout and making the bounded loop fail slowly or incompletely. Consider detecting paginator boundaries (e.g., arrow not found or not enabled) and breaking early with a clear error instead of continuing to loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant