Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
33bb108
feat(web): add get_config_page_size and get_page_from_request utilities
kaapstorm May 13, 2026
3697f94
test: move export_config fixture to apps/exports/tests/fixtures.py
kaapstorm Jun 1, 2026
0756739
feat(exports): add edit_url and last_run_log_url to ExportConfigBase
kaapstorm Jun 1, 2026
11930b7
feat(exports): add config_table HTMX endpoint with pagination and ETag
kaapstorm Jun 2, 2026
65466f2
refactor: move last_run() into ScheduleMixin
kaapstorm Jun 1, 2026
c97be5f
perf: use prefetched _all_runs in ScheduleMixin.last_run()
kaapstorm Jun 1, 2026
ba0af89
fix(exports): eliminate N+1 queries in config_table via prefetch_related
kaapstorm Jun 2, 2026
d343843
feat(exports): replace two-table layout with merged paginated config_…
kaapstorm May 13, 2026
a70577e
chore(exports): remove dead legacy exports context
kaapstorm May 13, 2026
7a40433
feat(refreshes): add config_table partial with pagination, ETag, and …
kaapstorm May 13, 2026
fed1eec
feat(forwarding): add config_table partial with pagination, ETag, and…
kaapstorm May 13, 2026
6e9b3a5
test: add smoke tests for list pages
kaapstorm May 13, 2026
930dc31
feat: add status filter constant and pagination to run history views
kaapstorm May 13, 2026
7af9250
feat: run history component — inline status filter dropdown, remove o…
kaapstorm May 13, 2026
cbb336f
feat: redesign detail page headers — inline buttons, Schedule field
kaapstorm May 13, 2026
ada5817
feat(exports): run history table — new columns, pagination, HTMX log …
kaapstorm May 13, 2026
792f01f
feat(refreshes): run history table — new columns, pagination, HTMX lo…
kaapstorm May 13, 2026
4c94eca
feat(forwarding): run history table — new columns, pagination, HTMX l…
kaapstorm May 13, 2026
4ce4c77
test: smoke tests for detail pages and run history table endpoints
kaapstorm May 13, 2026
2a47fa2
fix: Lint
kaapstorm Jun 2, 2026
72c6d83
Move `edit_url` and `last_run_log_url` to subclasses
kaapstorm Jun 3, 2026
60e30dc
Add tests with run instances
kaapstorm Jun 3, 2026
bbe1cb7
Reuse `VALID_CONFIG_PAGE_SIZES` const
kaapstorm Jun 3, 2026
8a9233f
Rename "configs" to "page_obj"
kaapstorm Jun 3, 2026
8719d9f
Suppress false-positive mypy error
kaapstorm Jun 3, 2026
74322b0
Test the function's contract, not the constant's value
kaapstorm Jun 5, 2026
3cfaab5
Drop unnecessary variable
kaapstorm Jun 5, 2026
fa3adbb
Nit: Variable names
kaapstorm Jun 5, 2026
c777228
Nit: Linebreaks
kaapstorm Jun 5, 2026
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
32 changes: 24 additions & 8 deletions apps/exports/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ class ExportConfigBase(ScheduleMixin, BaseModel):
class Meta:
abstract = True

@property
def last_run(self):
return (
self.runs.exclude(status=ExportRun.Status.QUEUED)
.order_by('-created_at')
.first()
)

@property
def latest_version(self):
return Version.objects.get_for_object(self).first()
Expand Down Expand Up @@ -67,6 +59,18 @@ def details_url(self):
def __str__(self):
return f'{self.name} - {self.project}'

@property
def edit_url(self):
return reverse('exports:edit_export_config', args=[self.id])

@property
def last_run_log_url(self):
run = self.last_run
return None if run is None else reverse(
'exports:run_log',
args=[run.id],
)


@reversion.register()
class MultiProjectExportConfig(ExportConfigBase):
Expand Down Expand Up @@ -102,6 +106,18 @@ def get_projects_display_short(self):
'<br>'.join(p.domain for p in self.projects.all())
)

@property
def edit_url(self):
return reverse('exports:edit_multi_export_config', args=[self.id])

@property
def last_run_log_url(self):
run = self.last_run
return None if run is None else reverse(
'exports:multi_run_log',
args=[run.id],
)


class ExportRunBase(RunBaseModel):
class Status(models.TextChoices):
Expand Down
46 changes: 45 additions & 1 deletion apps/exports/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

from apps.commcare.models import CommCareAccount, CommCareProject, CommCareServer
from apps.db.models import Database
from apps.exports.models import ExportConfig
from apps.exports.models import (
ExportConfig,
ExportRun,
MultiProjectExportConfig,
MultiProjectExportRun,
)
from tests.fixtures import (
commcare_account,
commcare_project,
Expand Down Expand Up @@ -131,3 +136,42 @@ def export_config_db_fixture():
)
config_file.close()
yield export_config


@fixture
@use('db')
def export_config():
yield ExportConfig.objects.create(
name='Test Export Config',
project=commcare_project(),
account=commcare_account(),
database=database(),
)


@fixture
@use('db')
def multi_export_config():
yield MultiProjectExportConfig.objects.create(
name='Multi Export Config',
account=commcare_account(),
database=database(),
)


@fixture
@use('db')
def export_run():
yield ExportRun.objects.create(
base_export_config=export_config(),
status=ExportRun.Status.COMPLETED,
)


@fixture
@use('db')
def multi_export_run():
yield MultiProjectExportRun.objects.create(
base_export_config=multi_export_config(),
status=MultiProjectExportRun.Status.COMPLETED,
)
132 changes: 132 additions & 0 deletions apps/exports/tests/test_details_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from django.urls import reverse
from unmagic import use

from apps.exports.models import ExportRun
from apps.exports.tests.fixtures import export_config_db_fixture
from tests.fixtures import authed_client


class TestExportDetailsSmoke:
@use(authed_client, export_config_db_fixture)
def test_returns_200(self):
response = authed_client().get(
reverse(
'exports:export_details', args=[export_config_db_fixture().id]
)
)
assert response.status_code == 200

@use(authed_client, export_config_db_fixture)
def test_no_details_suffix_in_heading(self):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm not sure I understand what this test is for as written, we're testing the heading is no longer what it used to be? Could be more useful to actually see what the heading is now.

response = authed_client().get(
reverse(
'exports:export_details', args=[export_config_db_fixture().id]
)
)
assert '- Details' not in response.content.decode()

@use(authed_client, export_config_db_fixture)
def test_run_history_section_present(self):
response = authed_client().get(
reverse(
'exports:export_details', args=[export_config_db_fixture().id]
)
)
content = response.content.decode()
assert 'Run History' in content
assert 'id="run-table"' in content

@use(authed_client, export_config_db_fixture)
def test_schedule_column_present(self):
response = authed_client().get(
reverse(
'exports:export_details', args=[export_config_db_fixture().id]
)
)
assert 'Schedule' in response.content.decode()

@use(authed_client, export_config_db_fixture)
def test_status_filter_dropdown_present(self):
response = authed_client().get(
reverse(
'exports:export_details', args=[export_config_db_fixture().id]
)
)
content = response.content.decode()
assert 'status-filter-form' in content
assert 'has_status_filter' in content


class TestExportRunHistoryTableEndpoint:
@use(authed_client, export_config_db_fixture)
def test_returns_200(self):
url = reverse(
'exports:run_history_table', args=[export_config_db_fixture().id]
)
assert authed_client().get(url).status_code == 200

@use(authed_client, export_config_db_fixture)
def test_no_filter_shows_all_statuses(self):
config = export_config_db_fixture()
completed_run = ExportRun.objects.create(
base_export_config=config, status=ExportRun.Status.COMPLETED
)
failed_run = ExportRun.objects.create(
base_export_config=config, status=ExportRun.Status.FAILED
)
url = reverse('exports:run_history_table', args=[config.id])
content = authed_client().get(url).content.decode()
# Use log-{id} marker which only appears in rendered run rows
assert f'log-{completed_run.id}' in content
assert f'log-{failed_run.id}' in content

@use(authed_client, export_config_db_fixture)
def test_status_filter_excludes_unchecked(self):
config = export_config_db_fixture()
completed_run = ExportRun.objects.create(
base_export_config=config, status=ExportRun.Status.COMPLETED
)
failed_run = ExportRun.objects.create(
base_export_config=config, status=ExportRun.Status.FAILED
)
url = reverse('exports:run_history_table', args=[config.id])
content = (
authed_client()
.get(
url,
QUERY_STRING='has_status_filter=1&status_filter=completed',
)
.content.decode()
)
# Use log-{id} marker which only appears in rendered run rows
assert f'log-{completed_run.id}' in content
assert f'log-{failed_run.id}' not in content

@use(authed_client, export_config_db_fixture)
def test_empty_filter_shows_nothing(self):
config = export_config_db_fixture()
run = ExportRun.objects.create(
base_export_config=config, status=ExportRun.Status.COMPLETED
)
url = reverse('exports:run_history_table', args=[config.id])
content = (
authed_client()
.get(url, QUERY_STRING='has_status_filter=1')
.content.decode()
)
# No status values sent → no runs visible; use log-{id} marker which only
# appears when a run row is rendered, not in URL paths.
assert f'log-{run.id}' not in content

@use(authed_client, export_config_db_fixture)
def test_pagination_default_10(self):
config = export_config_db_fixture()
for _ in range(15):
ExportRun.objects.create(
base_export_config=config, status=ExportRun.Status.COMPLETED
)
url = reverse('exports:run_history_table', args=[config.id])
response = authed_client().get(url)
assert response.status_code == 200
# Pagination controls should appear when there are > 10 runs
assert 'pagination' in response.content.decode()
Loading
Loading