Skip to content

Commit

Permalink
update from carltongibson/neapolitan upstream (#15)
Browse files Browse the repository at this point in the history
Co-authored-by: Kasun Herath <[email protected]>
Co-authored-by: Carlton Gibson <[email protected]>
Co-authored-by: Andrew Miller <[email protected]>
  • Loading branch information
4 people authored Jul 22, 2024
1 parent 3effe4a commit 3e69cf2
Show file tree
Hide file tree
Showing 11 changed files with 645 additions and 212 deletions.
83 changes: 83 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,89 @@ 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.4
====

* ``CRUDView`` subclasses may now pass a set of ``roles`` to ``get_urls()`` in
order to route only a subset of all the available CRUD roles.

As an example, to route only the list and detail views, the README/Quickstart example
would become::

from neapolitan.views import CRUDView, Role
from .models import Bookmark

class BookmarkView(CRUDView):
model = Bookmark
fields = ["url", "title", "note"]
filterset_fields = [
"favourite",
]

urlpatterns = [
*BookmarkView.get_urls(roles={Role.LIST, Role.DETAIL}),
]

In order to keep this logic within the view here, you would likely override
``get_urls()`` in this case, rather than calling it from the outside in your
URL configuration.

* As well as setting the existing ``lookup_field`` (which defaults to ``"pk"``)
and ``lookup_url_kwarg`` (which defaults to ``lookup_field`` if not set) you
may now set ``path_converter`` and ``url_base`` attributes on your
``CRUDView`` subclass in order to customise URL generation.

For example, for this model and ``CRUDView``::

class NamedCollection(models.Model):
name = models.CharField(max_length=25, unique=True)
code = models.UUIDField(unique=True, default=uuid.uuid4)

class NamedCollectionView(CRUDView):
model = NamedCollection
fields = ["name", "code"]

lookup_field = "code"
path_converter = "uuid"
url_base = "named-collections"

``CRUDView`` will generate URLs such as ``/named-collections/``,
``/named-collections/<uuid:code>/``, and so on. URL patterns will be named
using ``url_base``: "named-collections-list", "named-collections-detail", and
so on.

Thanks to Kasun Herath for preliminary discussion and exploration here.

* BREAKING CHANGE. In order to facilitate the above the ``object_list``
template tag now takes the whole ``view`` from the context, rather than just
the ``view.fields``.

If you've overridden the ``object_list.html`` template and are still using
the ``object_list`` template tag, you will need to update your usage to be
like this:

.. code-block:: html+django

{% object_list object_list view %}

* Improved app folder discovery in mktemplate command.

Thanks to Andrew Miller.

24.3
====

* Added the used ``filterset`` to list-view context.

* Added CI testing for supported Python and Django versions. (Python 3.10
onwards; Django 4.2 onwards, including the development branch.)

Thanks to Josh Thomas.

* Added CI build for the documentation.

Thanks to Eduardo Enriquez

24.2
====

Expand Down
5 changes: 4 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ I want easy CRUD views for it, without it taking all day:
# urls.py
from neapolitan.views import CRUDView
from .models import Bookmark
class BookmarkView(CRUDView):
model = Bookmark
Expand All @@ -32,7 +33,9 @@ I want easy CRUD views for it, without it taking all day:
"favourite",
]
urlpatterns = [ ... ] + BookmarkView.get_urls()
urlpatterns = [
*BookmarkView.get_urls(),
]
Neapolitan's ``CRUDView`` provides the standard list, detail,
create, edit, and delete views for a model, as well as the hooks you need to
Expand Down
179 changes: 177 additions & 2 deletions docs/source/crud-view.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,182 @@
CRUDView Reference
==================

.. autoclass:: neapolitan.views.CRUDView
.. py:currentmodule:: neapolitan.views
.. autoclass:: CRUDView

.. automethod:: neapolitan.views.CRUDView.get_context_data
Request Handlers
================

The core of a class-based view are the request handlers — methods that convert
an HTTP request into an HTTP response. The request handlers are the essence of
the **Django view**.

Neapolitan's ``CRUDView`` provides handlers the standard list, detail, create,
edit, and delete views for a model.

List and Detail Views
----------------------

.. automethod:: CRUDView.list

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.list

.. automethod:: CRUDView.detail

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.detail

Create and Update Views
-----------------------

.. automethod:: CRUDView.show_form

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.show_form

.. automethod:: CRUDView.process_form

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.process_form

Delete View
-----------

.. automethod:: CRUDView.confirm_delete

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.confirm_delete

.. automethod:: CRUDView.process_deletion

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.process_deletion


QuerySet and object lookup
==========================

.. automethod:: CRUDView.get_queryset

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_queryset

.. automethod:: CRUDView.get_object

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_object


Form handling
=============

.. automethod:: CRUDView.get_form_class

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_form_class

.. automethod:: CRUDView.get_form

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_form

.. automethod:: CRUDView.form_valid

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.form_valid

.. automethod:: CRUDView.form_invalid

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.form_invalid

.. automethod:: CRUDView.get_success_url

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_success_url

Pagination and filtering
========================

.. automethod:: CRUDView.get_paginate_by

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_paginate_by

.. automethod:: CRUDView.get_paginator

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_paginator

.. automethod:: CRUDView.paginate_queryset

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.paginate_queryset

.. automethod:: CRUDView.get_filterset

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_filterset

Response rendering
==================

.. automethod:: CRUDView.get_context_object_name

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_context_object_name

.. automethod:: CRUDView.get_context_data

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_context_data

.. automethod:: CRUDView.get_template_names

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_template_names

.. automethod:: CRUDView.render_to_response

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.render_to_response

URLs and view callables
=======================

.. automethod:: CRUDView.get_urls

This is the usual entry-point for routing all CRUD URLs in a single pass::

urlpatterns = [
*BookmarkView.get_urls(),
]

Optionally, you may provide an appropriate set of roles in order to limit
the handlers exposed::

urlpatterns = [
*BookmarkView.get_urls(roles={Role.LIST, Role.DETAIL}),
]

Subclasses may wish to override ``get_urls()`` in order to encapsulate such
logic.

.. literalinclude:: ../../src/neapolitan/views.py
:pyobject: CRUDView.get_urls


.. automethod:: CRUDView.as_view

This is the lower-level method used to manually route individual URLs.

It's extends the Django `View.as_view()` method, and should be passed a an
appropriate ``Role`` giving the handlers to be exposed::

path(
"bookmarks/",
BookmarkCRUDView.as_view(role=Role.LIST),
name="bookmark-list",
)
2 changes: 1 addition & 1 deletion src/neapolitan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ class BookmarkView(CRUDView):
Let's go! 🚀
"""

__version__ = "24.2"
__version__ = "24.4"
4 changes: 3 additions & 1 deletion src/neapolitan/management/commands/mktemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.core.management.base import BaseCommand, CommandError
from django.template.loader import TemplateDoesNotExist, get_template
from django.template.engine import Engine
from django.apps import apps

class Command(BaseCommand):
help = "Bootstrap a CRUD template for a model, copying from the active neapolitan default templates."
Expand Down Expand Up @@ -92,7 +93,8 @@ def handle(self, *args, **options):
# 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"
app_config = apps.get_app_config(app_name)
target_dir = f"{app_config.path}/templates"
if not Path(target_dir).exists():
try:
target_dir = Engine.get_default().template_dirs[0]
Expand Down
2 changes: 1 addition & 1 deletion src/neapolitan/templates/neapolitan/object_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h1 class="sm:flex-auto text-base font-semibold leading-6 text-gray-900">{{ obje
</div>

{% if object_list %}
{% object_list object_list view.get_list_fields %}
{% object_list object_list view %}
{% else %}
<p class="mt-8">There are no {{ object_verbose_name_plural }}. Create one now?</p>
{% endif %}
Expand Down
19 changes: 10 additions & 9 deletions src/neapolitan/templatetags/neapolitan.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from django import template
from django.urls import reverse

from neapolitan.views import Role

register = template.Library()


def action_links(object):
model_name = object._meta.model_name
def action_links(view, object):
actions = {
"detail": {
"url": reverse(f"{model_name}-detail", kwargs={"pk": object.pk}),
"url": Role.DETAIL.reverse(view, object),
"text": "View",
},
"update": {
"url": reverse(f"{model_name}-update", kwargs={"pk": object.pk}),
"url": Role.UPDATE.reverse(view, object),
"text": "Edit",
},
"delete": {
"url": reverse(f"{model_name}-delete", kwargs={"pk": object.pk}),
"url": Role.DELETE.reverse(view, object),
"text": "Delete",
},
}
Expand Down Expand Up @@ -45,24 +45,25 @@ def iter():


@register.inclusion_tag("neapolitan/partial/list.html")
def object_list(objects, fields):
def object_list(objects, view):
"""
Renders a list of objects with the given fields.
Inclusion tag usage::
{% object_list objects fields %}
{% object_list objects view %}
Template: ``neapolitan/partial/list.html`` — Will render a table of objects
with links to view, edit, and delete views.
"""

fields = view.get_list_fields()
headers = [objects[0]._meta.get_field(f).verbose_name for f in fields]
object_list = [
{
"object": object,
"fields": [{"name": f, "value": str(getattr(object, f))} for f in fields],
"actions": action_links(object),
"actions": action_links(view, object),
}
for object in objects
]
Expand Down
Loading

0 comments on commit 3e69cf2

Please sign in to comment.