neapolitan
neapolitan copied to clipboard
Adding parent / child functionality
I played around with neapolitan and it looks quite productive. My use case includes a lot of parent -> child relationships:
Author --> Book (book always is written by an author) House --> Room (room does not exist without a parent house)
I tried to add this functionality by creating a subclass of CrudView and it works. But there are a lot of changes and I cannot override the Role methods because it is an enum.
- Is there a planed featue for this use case?
- Is there a simpler way to achieve that than the subclass below?
- If there is no simpler way, I could actually see that this could be mergable with the CRUDView. Is that an option?
- Why are the url_pattern, get_url, reverse and maybe_reverse on the Role and not on the CrudView? It feels moving them to the CrudView makes it more understandable/easier to change.
class ParentCrudView(CRUDView):
parent_model = None
parent_lookup_field = None
parent_lookup_url_kwarg = "parent_pk"
parent_path_converter = "int"
@classproperty
def parent_url_base(cls):
return cls.parent_model._meta.model_name
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(
**{self.parent_lookup_field: self.kwargs[self.parent_lookup_url_kwarg]}
)
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["create_view_url"] = self.maybe_reverse(Role.CREATE)
return kwargs
@classmethod
def get_urls(cls, roles=None):
"""Classmethod to generate URL patterns for the view."""
if roles is None:
roles = iter(Role)
return [cls.get_url(role) for role in roles]
@classmethod
def url_pattern(view_cls, role: Role):
parent_url_base = view_cls.parent_url_base
parent_url_kwarg = (
view_cls.parent_lookup_url_kwarg or view_cls.parent_lookup_field
)
parent_path_converter = view_cls.parent_path_converter
parent_path = f"{parent_url_base}/<{parent_path_converter}:{parent_url_kwarg}>"
url_base = f"{parent_path}/{view_cls.url_base}"
url_kwarg = view_cls.lookup_url_kwarg or view_cls.lookup_field
path_converter = view_cls.path_converter
match role:
case Role.LIST:
return f"{url_base}/"
case Role.DETAIL:
return f"{url_base}/<{path_converter}:{url_kwarg}>/"
case Role.CREATE:
return f"{url_base}/new/"
case Role.UPDATE:
return f"{url_base}/<{path_converter}:{url_kwarg}>/edit/"
case Role.DELETE:
return f"{url_base}/<{path_converter}:{url_kwarg}>/delete/"
@classmethod
def get_url(view_cls, role: Role):
return path(
view_cls.url_pattern(role),
view_cls.as_view(role=role),
name=f"{view_cls.parent_url_base}-{view_cls.url_base}-{role.url_name_component}",
)
def reverse(self, role, object=None):
url_name = f"{self.parent_url_base}-{self.url_base}-{role.url_name_component}"
url_kwarg = self.lookup_url_kwarg or self.lookup_field
parent_url_kwarg = self.parent_lookup_url_kwarg or self.parent_lookup_field
match role:
case Role.LIST | Role.CREATE:
return reverse(
url_name,
kwargs={
parent_url_kwarg: self.kwargs[self.parent_lookup_url_kwarg]
},
)
case _:
return reverse(
url_name,
kwargs={
parent_url_kwarg: self.kwargs[self.parent_lookup_url_kwarg],
url_kwarg: getattr(object, self.lookup_field),
},
)
def maybe_reverse(self, role, object=None):
try:
return self.reverse(role, object)
except NoReverseMatch:
return None
def process_form(self, request, *args, **kwargs):
if self.role is Role.UPDATE:
self.object = self.get_object()
form = self.get_form(
data=request.POST,
files=request.FILES,
instance=self.object,
)
if form.is_valid():
parent_pk = self.kwargs[self.parent_lookup_url_kwarg]
setattr(form.instance, self.parent_lookup_field, parent_pk)
return self.form_valid(form)
return self.form_invalid(form)
def action_links(self, object):
actions = [
(url, name)
for url, name in [
(self.maybe_reverse(Role.DETAIL, object), "View"),
(self.maybe_reverse(Role.UPDATE, object), "Edit"),
(self.maybe_reverse(Role.DELETE, object), "Delete"),
]
if url is not None
]
return (a(href=url)[anchor_text] for url, anchor_text in actions)
def obj_list(self, objects):
fields = self.fields
headers = [objects[0]._meta.get_field(f).verbose_name for f in fields]
obj_list = [
{
"object": object,
"fields": [
object._meta.get_field(f).value_to_string(object) for f in fields
],
"actions": self.action_links(object),
}
for object in objects
]
return div[
table[
thead[tr[(th[capfirst(header)] for header in headers), th["Actions"]]],
tbody[
(
tr[(td[field] for field in obj["fields"]), td[obj["actions"]]]
for obj in obj_list
)
],
]
]
def get_success_url(self):
assert self.model is not None, (
"'%s' must define 'model' or override 'get_success_url()'"
% self.__class__.__name__
)
if self.role is Role.DELETE:
success_url = self.maybe_reverse(Role.LIST)
else:
success_url = self.maybe_reverse(Role.DETAIL, self.object)
return success_url
def render_to_response(self, context):
if self.role == Role.LIST:
return HttpResponse(
html[
body[
div[
h1[
capfirst(
context["object_verbose_name_plural"],
),
],
context["create_view_url"]
and div[
a(href=context["create_view_url"])[
f"Add a new { context['object_verbose_name'] }"
]
],
self.obj_list(context["object_list"])
if context["object_list"]
else p[
f"There are no { context['object_verbose_name_plural'] }. Create one now?"
],
]
]
]
)
return super().render_to_response(context)
The html stuff is provided by the htpy library.