diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e8a2f6f..32f53b7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,14 @@ Version numbers correspond to git tags. Please use the compare view on GitHub for full details. Until we're further along, I will just note the highlights here: +24.2 +==== + +* Added the ``mktemplate`` management command to create an override template from the + the active default templates for the specified model and CRUD action. + + See ``./manage.py mktemplate --help`` for full details. + 24.1 ==== diff --git a/docs/source/templates.rst b/docs/source/templates.rst index 11c1674..df38144 100644 --- a/docs/source/templates.rst +++ b/docs/source/templates.rst @@ -33,6 +33,25 @@ Templates You can override these templates by creating your own, either individually or as a whole. +If you want to override a single template for your model, you can run the ``mktemplate`` +management command: + +.. code-block:: shell + + python manage.py mktemplate myapp.MyModel --list + +You pass your model in the ``app_name.ModelName`` format, and then an option for the +CRUD template you want to override. The specified template will be copied to your app's +``templates``, using your active neapolitan default templates, and having the correct +name applied. + +For example, the above command will copy the active ``neapoltian/object_list.html`` template to your app's +``templates/myapp/mymodel_list.html``, where it will be picked up by a ``CRUDView`` for +``MyModel`` when serving the list view. + +See ``python manage.py mktemplate --help`` for full details. + + .. admonition:: Under construction 🚧 The templates are still being developed. If a change in a release affects diff --git a/src/neapolitan/__init__.py b/src/neapolitan/__init__.py index 83aa44e..59c9025 100644 --- a/src/neapolitan/__init__.py +++ b/src/neapolitan/__init__.py @@ -35,4 +35,4 @@ class BookmarkView(CRUDView): Let's go! 🚀 """ -__version__ = "24.1" +__version__ = "24.2" diff --git a/src/neapolitan/management/__init__.py b/src/neapolitan/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/neapolitan/management/commands/__init__.py b/src/neapolitan/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/neapolitan/management/commands/mktemplate.py b/src/neapolitan/management/commands/mktemplate.py new file mode 100644 index 0000000..a31a806 --- /dev/null +++ b/src/neapolitan/management/commands/mktemplate.py @@ -0,0 +1,109 @@ +import shutil +from pathlib import Path + +from django.core.exceptions import ImproperlyConfigured +from django.core.management.base import BaseCommand, CommandError +from django.template.loader import TemplateDoesNotExist, get_template +from django.template.engine import Engine + +class Command(BaseCommand): + help = "Bootstrap a CRUD template for a model, copying from the active neapolitan default templates." + + def add_arguments(self, parser): + parser.add_argument( + "model", + type=str, + help="The to bootstrap a template for.", + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "-l", + "--list", + action="store_const", + const="list", + dest="role", + help="List role", + ) + group.add_argument( + "-d", + "--detail", + action="store_const", + const="detail", + dest="role", + help="Detail role", + ) + group.add_argument( + "-c", + "--create", + action="store_const", + const="form", + dest="role", + help="Create role", + ) + group.add_argument( + "-u", + "--update", + action="store_const", + const="form", + dest="role", + help="Update role", + ) + group.add_argument( + "-f", + "--form", + action="store_const", + const="form", + dest="role", + help="Form role", + ) + group.add_argument( + "--delete", + action="store_const", + const="delete", + dest="role", + help="Delete role", + ) + + def handle(self, *args, **options): + model = options["model"] + role = options["role"] + + if role == "list": + suffix = "_list.html" + elif role == "detail": + suffix = "_detail.html" + elif role == "form": + suffix = "_form.html" + elif role == "delete": + suffix = "_confirm_delete.html" + + app_name, model_name = model.split(".") + template_name = f"{app_name}/{model_name.lower()}{suffix}" + neapolitan_template_name = f"neapolitan/object{suffix}" + + # Check if the template already exists. + try: + get_template(template_name) + except TemplateDoesNotExist: + # Get the filesystem path of neapolitan's object template. + neapolitan_template = get_template(neapolitan_template_name) + neapolitan_template_path = neapolitan_template.origin.name + + # Find target directory. + # 1. If f"{app_name}/templates" exists, use that. + # 2. Otherwise, use first project level template dir. + target_dir = f"{app_name}/templates" + if not Path(target_dir).exists(): + try: + target_dir = Engine.get_default().template_dirs[0] + except (ImproperlyConfigured, IndexError): + raise CommandError( + "No app or project level template dir found." + ) + # Copy the neapolitan template to the target directory with template_name. + shutil.copyfile(neapolitan_template_path, f"{target_dir}/{template_name}") + else: + self.stdout.write( + f"Template {template_name} already exists. Remove it manually if you want to regenerate it." + ) + raise CommandError("Template already exists.") diff --git a/tests/templates/tests/.gitignore b/tests/templates/tests/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/templates/tests/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/tests.py b/tests/tests.py index 8067ea0..3281062 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,7 +1,9 @@ +import os + +from django.core.management import call_command from django.test import TestCase from django.urls import reverse from django.utils.html import escape - from neapolitan.views import CRUDView from .models import Bookmark @@ -126,3 +128,16 @@ def test_filter(self): self.assertContains(response, self.homepage.title) self.assertNotContains(response, self.github.title) self.assertNotContains(response, self.fosstodon.title) + + +class MktemplateCommandTest(TestCase): + def test_mktemplate_command(self): + # Run the command + call_command('mktemplate', 'tests.Bookmark', '--list') + + # Check if the file was created + file_path = 'tests/templates/tests/bookmark_list.html' + self.assertTrue(os.path.isfile(file_path)) + + # Remove the created file + os.remove(file_path)