Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions bedelia/api/management/commands/load_bedelia_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1442,19 +1442,27 @@ def _validate_or_create_posprevia_relationship(self, plan_materia_fuente: PlanMa
# 4. Crear o obtener PospreviaItem
materia_fuente = plan_materia_fuente.materia
descripcion = posprevia.get('descripcion', '')

fecha = None
fecha_raw = (posprevia.get('fecha') or '').strip()
if fecha_raw:
try:
fecha = datetime.strptime(fecha_raw, '%d/%m/%Y').date()
except ValueError:
fecha = None

try:
if self.dry_run:
posprevia_item = PosPreviaItem(
materia=materia_fuente,
plan_estudio=plan_estudio,
descripcion=descripcion
descripcion=descripcion,
fecha=fecha
)
else:
posprevia_item, created = PosPreviaItem.objects.get_or_create(
materia=materia_fuente,
plan_estudio=plan_estudio,
defaults={'descripcion': descripcion}
defaults={'descripcion': descripcion, 'fecha': fecha}
)

# Agregar unidad dependiente a la relación ManyToMany
Expand Down
18 changes: 18 additions & 0 deletions bedelia/api/migrations/0002_pospreviaitem_fecha.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2026-08-04 00:32

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='pospreviaitem',
name='fecha',
field=models.DateField(blank=True, null=True, verbose_name='Fecha reportada por Bedelías'),
),
]
5 changes: 5 additions & 0 deletions bedelia/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ class PosPreviaItem(models.Model):
blank=True,
verbose_name="Descripción / texto auxiliar",
)
fecha = models.DateField(
null=True,
blank=True,
verbose_name="Fecha reportada por Bedelías",
)
materia = models.ForeignKey(
Materia,
on_delete=models.CASCADE,
Expand Down
2 changes: 1 addition & 1 deletion bedelia/api/serializers/materias.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class PosPreviaSerializer(serializers.Serializer):

anio_plan = serializers.CharField(source='plan_estudio.anio')
carrera = serializers.CharField(source='plan_estudio.nombre_carrera')
fecha = serializers.DateTimeField(source='posprevia_item.fecha_creacion', format='%d/%m/%Y')
fecha = serializers.DateField(source='posprevia_item.fecha', format='%d/%m/%Y', allow_null=True)
descripcion = serializers.CharField(source='posprevia_item.descripcion')
tipo = serializers.CharField(source='unidad_dependiente.tipo')
materia_codigo = serializers.CharField(source='materia_dependiente.codigo')
Expand Down
14 changes: 10 additions & 4 deletions bedelia/api/views/materias.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,10 +542,10 @@ def get_queryset(self):
# Find all PosPreviaItem records for this materia
queryset = PosPreviaItem.objects.filter(materia=materia).select_related('plan_estudio').prefetch_related('unidades_dependientes__materia')

# Optional: Filter by unidad_tipo
# Optional: Filter by unidad_tipo (distinct: the M2M join duplicates rows)
unidad_tipo = self.request.query_params.get('unidad_tipo')
if unidad_tipo:
queryset = queryset.filter(unidades_dependientes__tipo=unidad_tipo)
queryset = queryset.filter(unidades_dependientes__tipo=unidad_tipo).distinct()

# Optional: Filter by active status
activo = self.request.query_params.get('activo')
Expand All @@ -559,10 +559,16 @@ def list(self, request, *args, **kwargs):
"""Override list to flatten unidades_dependientes into separate entries."""
queryset = self.get_queryset()

# Flatten the data - each unidad_dependiente becomes a separate entry
# Flatten the data - each unidad_dependiente becomes a separate entry.
# The unidad_tipo filter must also apply here: filtering the queryset only
# narrows which PosPreviaItem rows come back, not which unidades they emit.
unidad_tipo = request.query_params.get('unidad_tipo')
flattened_data = []
for posprevia_item in queryset:
for unidad in posprevia_item.unidades_dependientes.all():
unidades = posprevia_item.unidades_dependientes.all()
if unidad_tipo:
unidades = [u for u in unidades if u.tipo == unidad_tipo]
for unidad in unidades:
entry = {
'posprevia_item': posprevia_item,
'unidad_dependiente': unidad,
Expand Down
10 changes: 8 additions & 2 deletions eligible_courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,17 @@ def requirement_satisfied(node: Dict[str, Any], ctx: Dict[str, Any]) -> bool:
for item in items:
modality = item.get("modality")
code = item.get("code", "")
if modality in {"course", "ucb_module"}:
if modality in {"course", "ucb_module", "course_enrollment", "exam_enrollment"}:
# An approved exam implies the materia is fully passed, so it also
# satisfies course requirements. Approval of the course covers the
# enrollment modalities (you can only have passed it if you enrolled,
# and a passed course grants access to the exam).
if code in ctx["approved_courses"] or code in ctx["approved_exams"]:
matched += 1
elif modality == "exam":
if code in ctx["approved_exams"] or code in ctx["approved_courses"]:
# Only a passed exam satisfies an exam requirement; a passed course
# alone does not.
if code in ctx["approved_exams"]:
matched += 1
elif modality == "credits_in_plan":
needed = item.get("credits_required", 0)
Expand Down
22 changes: 18 additions & 4 deletions scraper/common/usetable.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,25 @@ def go_to_page(self, page: int):
page_text == f"Page {page}"
):
return
if page > 10 and (page_text == "Page 1" or page_text == "1"):
self.scroll_to_element_and_click(self.wait_for_element_to_be_clickable((By.XPATH, f'//a[contains(@class,"ui-paginator-last")]')))
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.

target_xpath = f'//a[@aria-label="Page {page}"]'
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()
Comment on lines +73 to +78

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.

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()
self.logger.info("Waiting for loading to finish")
self.wait.until(EC.invisibility_of_element_located((By.XPATH, "//img[@src='/jakarta.faces.resource/img/cargando.gif.xhtml?ln=default']")))
Expand Down
Loading