Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
19 changes: 19 additions & 0 deletions apps/exports/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ def last_run(self):
def latest_version(self):
return Version.objects.get_for_object(self).first()

@property
def edit_url(self):
if isinstance(self, ExportConfig):
return reverse('exports:edit_export_config', args=[self.id])
elif isinstance(self, MultiProjectExportConfig):
return reverse('exports:edit_multi_export_config', args=[self.id])
Comment thread
nospame marked this conversation as resolved.
Outdated
raise ValueError(f"Unknown config type: {type(self)}")

@property
def last_run_log_url(self):
run = self.last_run
if run is None:
return None
if isinstance(self, ExportConfig):
return reverse('exports:run_log', args=[run.id])
elif isinstance(self, MultiProjectExportConfig):
return reverse('exports:multi_run_log', args=[run.id])
raise ValueError(f"Unknown config type: {type(self)}")

@property
def details_url(self):
raise NotImplementedError
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,
)
46 changes: 26 additions & 20 deletions apps/exports/tests/test_list_view.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
from django.urls import reverse
from unmagic import fixture, use

from apps.exports.models import ExportConfig
from tests.fixtures import (
authed_client,
commcare_account,
commcare_project,
database,
)


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

from apps.exports.tests.fixtures import export_config, multi_export_config
from tests.fixtures import authed_client


class TestExportConfigBaseProperties:
@use(export_config)
def test_export_config_edit_url(self):
config = export_config()
expected = reverse('exports:edit_export_config', args=[config.id])
assert config.edit_url == expected

@use(multi_export_config)
def test_multi_export_config_edit_url(self):
config = multi_export_config()
expected = reverse('exports:edit_multi_export_config', args=[config.id])
assert config.edit_url == expected

@use(export_config)
def test_last_run_log_url_none_when_no_run(self):
Comment thread
nospame marked this conversation as resolved.
Outdated
assert export_config().last_run_log_url is None

@use(multi_export_config)
def test_last_run_log_url_none_when_no_run_multi(self):
assert multi_export_config().last_run_log_url is None


class TestExportsHomeView:
Expand Down
Empty file added commcare_sync/tests/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions commcare_sync/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pytest
from django.test import RequestFactory

from commcare_sync.views import get_config_page_size, get_page_from_request

VALID_PAGE_SIZES = [10, 20, 50]


class TestGetConfigPageSize:
def _req(self, params=''):
rf = RequestFactory()
return rf.get(f'/?{params}')

def test_default_is_10(self):
assert get_config_page_size(self._req()) == 10
Comment thread
jingcheng16 marked this conversation as resolved.
Outdated

def test_valid_sizes_accepted(self):
for size in VALID_PAGE_SIZES:
assert get_config_page_size(self._req(f'page_size={size}')) == size

def test_invalid_size_falls_back_to_default(self):
assert get_config_page_size(self._req('page_size=7')) == 10

def test_non_integer_falls_back_to_default(self):
assert get_config_page_size(self._req('page_size=abc')) == 10


class TestGetPageFromRequest:
def _req(self, params=''):
rf = RequestFactory()
return rf.get(f'/?{params}')

def test_default_is_1(self):
assert get_page_from_request(self._req()) == 1

def test_valid_page_returned(self):
assert get_page_from_request(self._req('page=3')) == 3

def test_zero_clamped_to_1(self):
assert get_page_from_request(self._req('page=0')) == 1

def test_negative_clamped_to_1(self):
assert get_page_from_request(self._req('page=-5')) == 1

def test_non_integer_returns_1(self):
assert get_page_from_request(self._req('page=abc')) == 1
20 changes: 20 additions & 0 deletions commcare_sync/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.conf import settings

_VALID_CONFIG_PAGE_SIZES = (10, 20, 50)


def get_ui_page_size(request):
limit = settings.COMMCARE_SYNC_UI_PAGE_SIZE
Expand All @@ -15,3 +17,21 @@ def get_hide_skipped_from_request(request):
if 'hide_skipped' in request.GET:
return request.GET['hide_skipped'] == 'y'
return False


def get_config_page_size(request):
if 'page_size' in request.GET:
try:
size = int(request.GET['page_size'])
if size in _VALID_CONFIG_PAGE_SIZES:
return size
except ValueError:
pass
return _VALID_CONFIG_PAGE_SIZES[0]


def get_page_from_request(request):
try:
return max(int(request.GET.get('page', 1)), 1)
except ValueError:
return 1