diff --git a/.gitignore b/.gitignore index eb1630c..6edf137 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ test-results/ Thumbs.db .idea/ .vscode/ +uv.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df2f1d..ca749a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [0.1.1] — 2026-04-28 + +### Fixed +- **Consumer compatibility regression**: `CfUiConfig.ready()` no longer + overrides `settings.COTTON_DIR`. The previous fix in 0.1.0 set + `COTTON_DIR="cotton/bulma"` globally, which broke any consumer project + whose own cotton templates lived at `templates/cotton//...` + (every `` lookup got rewritten to + `cotton/bulma/foo/bar/index.html` and raised `TemplateDoesNotExist`). + +### Changed +- cf-ui's cotton templates moved from `cotton//cf/*.html` to + `cotton/cf/*.html`. Theme variation will now happen inside the templates + (or via `_themes/` partials) instead of at the directory level. With the + default `COTTON_DIR="cotton"`, `` continues to resolve and + consumer cotton trees are no longer affected. + +### Migration +- Most consumers: no change required. Removing any explicit + `COTTON_DIR="cotton/bulma"` from `settings.py` (added as a workaround + while 0.1.0 was broken) is recommended. +- If you imported `COTTON_TEMPLATES_DIR / "bulma"` directly via the escape + hatch, switch to `COTTON_TEMPLATES_DIR / "cf"`. + ## [0.1.0] — 2026-04-25 ### Added diff --git a/README.md b/README.md index bc033a6..933bb39 100644 --- a/README.md +++ b/README.md @@ -201,10 +201,18 @@ If you need direct access to template directories for custom configuration: ```python from cf_ui import JINJA_TEMPLATES_DIR, COTTON_TEMPLATES_DIR -# JINJA_TEMPLATES_DIR / "bulma" → Path to Jinja2 templates -# COTTON_TEMPLATES_DIR / "bulma" → Path to Cotton templates +# JINJA_TEMPLATES_DIR / "bulma" → Path to Jinja2 templates (theme-prefixed) +# COTTON_TEMPLATES_DIR / "cf" → Path to Cotton component templates ``` +> **Cotton templates are theme-agnostic on disk.** They live at +> `cotton/cf/*.html` (not `cotton//cf/*.html`) so cf-ui can sit +> alongside any consumer project's own `templates/cotton//...` tree +> without colliding on `COTTON_DIR`. The active CSS framework is selected +> via `CF_UI_THEME` (used by the asset tags) — switching themes in a +> future release will happen inside the templates, not via the directory +> layout. + --- ## Planned Themes diff --git a/pyproject.toml b/pyproject.toml index 9575f0b..0e5e41c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cf-ui" -version = "0.1.0" +version = "0.1.1" description = "CSS framework UI kit for component-framework — Bulma, Bootstrap, Foundation, Fomantic, DaisyUI" readme = "README.md" requires-python = ">=3.11" diff --git a/src/cf_ui/_version.py b/src/cf_ui/_version.py index 3dc1f76..485f44a 100644 --- a/src/cf_ui/_version.py +++ b/src/cf_ui/_version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/src/cf_ui/django.py b/src/cf_ui/django.py index 0bff0c9..1b9f5ce 100644 --- a/src/cf_ui/django.py +++ b/src/cf_ui/django.py @@ -1,7 +1,4 @@ -from pathlib import Path - from django.apps import AppConfig -from django.core.exceptions import ImproperlyConfigured class CfUiConfig(AppConfig): @@ -9,22 +6,9 @@ class CfUiConfig(AppConfig): label = "cf_ui" verbose_name = "Component Framework UI" - def ready(self) -> None: - from django.conf import settings - - theme = getattr(settings, "CF_UI_THEME", "bulma") - cotton_dir = Path(__file__).parent / "templates" / "cotton" / theme - - if not cotton_dir.is_dir(): - raise ImproperlyConfigured( - f"cf-ui: no templates found for theme {theme!r} at {cotton_dir}. " - f"Check CF_UI_THEME in settings." - ) - - # django-cotton reads COTTON_DIR (singular). Setting it to - # "cotton/" makes resolve to - # cotton//cf/foo.html, which the cotton loader finds via - # the app-templates walk (cf_ui/templates/cotton//cf/foo.html). - # Don't overwrite a value the consumer has already set. - if not getattr(settings, "COTTON_DIR", None): - settings.COTTON_DIR = f"cotton/{theme}" + # cf-ui Cotton templates live at src/cf_ui/templates/cotton/cf/*.html. + # Django's app-templates loader (APP_DIRS=True) picks them up directly + # and django-cotton's default COTTON_DIR ("cotton") resolves + # -> cotton/cf/foo.html. We deliberately do not touch + # COTTON_DIR here: doing so would break consumer projects whose own + # cotton templates live at cotton//*.html. diff --git a/src/cf_ui/templates/cotton/bulma/cf/breadcrumb.html b/src/cf_ui/templates/cotton/cf/breadcrumb.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/breadcrumb.html rename to src/cf_ui/templates/cotton/cf/breadcrumb.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/card.html b/src/cf_ui/templates/cotton/cf/card.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/card.html rename to src/cf_ui/templates/cotton/cf/card.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/checkbox-group.html b/src/cf_ui/templates/cotton/cf/checkbox-group.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/checkbox-group.html rename to src/cf_ui/templates/cotton/cf/checkbox-group.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/form-field.html b/src/cf_ui/templates/cotton/cf/form-field.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/form-field.html rename to src/cf_ui/templates/cotton/cf/form-field.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/modal.html b/src/cf_ui/templates/cotton/cf/modal.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/modal.html rename to src/cf_ui/templates/cotton/cf/modal.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/navbar.html b/src/cf_ui/templates/cotton/cf/navbar.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/navbar.html rename to src/cf_ui/templates/cotton/cf/navbar.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/notification.html b/src/cf_ui/templates/cotton/cf/notification.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/notification.html rename to src/cf_ui/templates/cotton/cf/notification.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/pagination.html b/src/cf_ui/templates/cotton/cf/pagination.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/pagination.html rename to src/cf_ui/templates/cotton/cf/pagination.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/panel.html b/src/cf_ui/templates/cotton/cf/panel.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/panel.html rename to src/cf_ui/templates/cotton/cf/panel.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/progress.html b/src/cf_ui/templates/cotton/cf/progress.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/progress.html rename to src/cf_ui/templates/cotton/cf/progress.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/select.html b/src/cf_ui/templates/cotton/cf/select.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/select.html rename to src/cf_ui/templates/cotton/cf/select.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/table.html b/src/cf_ui/templates/cotton/cf/table.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/table.html rename to src/cf_ui/templates/cotton/cf/table.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/tabs.html b/src/cf_ui/templates/cotton/cf/tabs.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/tabs.html rename to src/cf_ui/templates/cotton/cf/tabs.html diff --git a/src/cf_ui/templates/cotton/bulma/cf/textarea.html b/src/cf_ui/templates/cotton/cf/textarea.html similarity index 100% rename from src/cf_ui/templates/cotton/bulma/cf/textarea.html rename to src/cf_ui/templates/cotton/cf/textarea.html diff --git a/tests/e2e/_e2e_django_settings.py b/tests/e2e/_e2e_django_settings.py index 32b9058..8a692b0 100644 --- a/tests/e2e/_e2e_django_settings.py +++ b/tests/e2e/_e2e_django_settings.py @@ -2,8 +2,7 @@ Django settings for E2E tests only. This module is used by the E2E server subprocess. It explicitly includes -django_cotton in INSTALLED_APPS and configures COTTON_DIR and -COTTON_SNAKE_CASED_NAMES so that cf-ui cotton components render fully. +django_cotton in INSTALLED_APPS so that cf-ui cotton components render fully. It MUST NOT be imported by the main pytest process to avoid polluting the shared Django settings used by unit and integration tests. @@ -15,7 +14,7 @@ BASE_DIR = Path(__file__).parent.parent / "integration" / "cotton_app" -# cf-ui templates root — contains cotton/{theme}/cf/*.html +# cf-ui templates root — contains cotton/cf/*.html (theme-agnostic path) CF_UI_TEMPLATES_ROOT = JINJA_TEMPLATES_DIR.parent.parent SECRET_KEY = "e2e-test-secret-key-not-for-production" @@ -32,7 +31,7 @@ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [ - CF_UI_TEMPLATES_ROOT, # provides cotton/bulma/cf/*.html + CF_UI_TEMPLATES_ROOT, # provides cotton/cf/*.html BASE_DIR / "templates", # provides cotton_gallery/*.html ], "APP_DIRS": True, @@ -43,9 +42,8 @@ } ] CF_UI_THEME = "bulma" -# COTTON_DIR: django-cotton resolves → cotton/bulma/cf/card.html -# With CF_UI_TEMPLATES_ROOT in DIRS, it finds src/cf_ui/templates/cotton/bulma/cf/card.html -COTTON_DIR = "cotton/bulma" +# django-cotton default COTTON_DIR="cotton" resolves -> +# cotton/cf/card.html, picked up via APP_DIRS from cf_ui's package templates. # Allow hyphenated filenames (form-field.html, checkbox-group.html) COTTON_SNAKE_CASED_NAMES = False ROOT_URLCONF = "tests.integration.cotton_app.urls" diff --git a/tests/unit/cotton/conftest.py b/tests/unit/cotton/conftest.py index 064d63f..319ed8e 100644 --- a/tests/unit/cotton/conftest.py +++ b/tests/unit/cotton/conftest.py @@ -10,6 +10,6 @@ def cotton_render(): from django.template.loader import render_to_string def _render(template_name: str, **props: object) -> str: - return render_to_string(f"cotton/bulma/{template_name}", props) + return render_to_string(f"cotton/{template_name}", props) return _render diff --git a/tests/unit/test_consumer_compatibility.py b/tests/unit/test_consumer_compatibility.py new file mode 100644 index 0000000..a7aded2 --- /dev/null +++ b/tests/unit/test_consumer_compatibility.py @@ -0,0 +1,95 @@ +""" +Regression tests guarding against COTTON_DIR hijacking. + +Background: an earlier fix (commit 5814476c) set ``COTTON_DIR="cotton/bulma"`` +in ``CfUiConfig.ready()``. Because django-cotton uses ``COTTON_DIR`` as a +single global prefix for *every* ```` lookup, any consumer with +its own templates at ``templates/cotton//...`` immediately broke +once it added ``cf_ui.django.CfUiConfig`` to ``INSTALLED_APPS``. + +cf-ui's templates now live at ``cotton/cf/*.html`` (no theme prefix), and +``CfUiConfig`` no longer touches ``COTTON_DIR``. These tests pin both +behaviors so the regression cannot return. +""" + +from pathlib import Path + + +def test_cf_ui_does_not_set_cotton_dir(): + """CfUiConfig.ready() must not write to settings.COTTON_DIR.""" + from django.conf import settings + + cf_ui_managed_value = "cotton/bulma" + assert getattr(settings, "COTTON_DIR", None) != cf_ui_managed_value + + +def test_consumer_and_cf_ui_cotton_templates_resolve_together(tmp_path: Path): + """A consumer's ``cotton//foo.html`` and cf-ui's ``cotton/cf/*.html`` + must both resolve through the same Django template engine when + ``cf_ui.django.CfUiConfig`` is installed. + """ + from django.apps import apps + from django.template.engine import Engine + + # Django is configured + populated by tests/unit/conftest.py + # (which calls django.setup()). Verify cf_ui is registered as an app + # before testing template resolution. + assert apps.get_app_config("cf_ui") is not None + + # Simulate a consumer's template tree at templates/cotton/myapp/foo.html + consumer_root = tmp_path / "templates" + consumer_template = consumer_root / "cotton" / "myapp" / "foo.html" + consumer_template.parent.mkdir(parents=True) + consumer_template.write_text("consumer-foo-content") + + # Engine with APP_DIRS=True walks INSTALLED_APPS for templates, + # exactly like Django's default get_template() flow. + engine = Engine( + dirs=[str(consumer_root)], + app_dirs=True, + libraries={"cf_ui": "cf_ui.templatetags.cf_ui"}, + ) + + from django.template import Context + + # Consumer template at cotton/myapp/foo.html resolves cleanly — + # the regression made this raise TemplateDoesNotExist because + # the Cotton lookup got rewritten to cotton/bulma/myapp/foo.html. + consumer_t = engine.get_template("cotton/myapp/foo.html") + assert "consumer-foo-content" in consumer_t.render(Context({})) + assert str(consumer_template) == consumer_t.origin.name + + # cf-ui's templates also resolve under the default Cotton path. + cf_ui_t = engine.get_template("cotton/cf/notification.html") + assert cf_ui_t is not None + # Sanity-check we found cf-ui's actual file, not a same-named consumer one. + assert "cf_ui" in (cf_ui_t.origin.name or "") + + +def test_all_cf_ui_cotton_components_resolve_at_cotton_cf_path(): + """Every cf-ui cotton component must be reachable at cotton/cf/.html.""" + from django.template.engine import Engine + + engine = Engine( + app_dirs=True, + libraries={"cf_ui": "cf_ui.templatetags.cf_ui"}, + ) + expected = [ + "breadcrumb", + "card", + "checkbox-group", + "form-field", + "modal", + "navbar", + "notification", + "pagination", + "panel", + "progress", + "select", + "table", + "tabs", + "textarea", + ] + for name in expected: + t = engine.get_template(f"cotton/cf/{name}.html") + assert t is not None diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 6c7f191..dff9c41 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -14,7 +14,8 @@ def test_cotton_templates_dir_exported(): assert isinstance(COTTON_TEMPLATES_DIR, Path) assert COTTON_TEMPLATES_DIR.exists() - assert (COTTON_TEMPLATES_DIR / "bulma").exists() + assert (COTTON_TEMPLATES_DIR / "cf").exists() + assert (COTTON_TEMPLATES_DIR / "cf" / "card.html").exists() def test_jinja_templates_dir_points_inside_package(): @@ -32,10 +33,17 @@ def test_django_appconfig_name(): assert app is not None -def test_django_appconfig_ready_sets_cotton_dir(): +def test_django_appconfig_does_not_override_cotton_dir(): + """cf-ui must not set COTTON_DIR; doing so breaks consumer cotton trees. + + cf-ui templates live at cotton/cf/*.html so the django-cotton default + (COTTON_DIR="cotton") resolves . Consumers keep whatever value + they configured (or the default). + """ from django.conf import settings - assert getattr(settings, "COTTON_DIR", None) == "cotton/bulma" + cf_ui_managed_value = "cotton/bulma" + assert getattr(settings, "COTTON_DIR", None) != cf_ui_managed_value def test_fastapi_install_cf_ui_adds_template_dir(): @@ -80,4 +88,4 @@ def test_version_exported(): from cf_ui import __version__ assert isinstance(__version__, str) - assert __version__ == "0.1.0" + assert __version__ == "0.1.1"