Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding parent / child functionality #61

Open
awapf opened this issue Sep 24, 2024 · 2 comments
Open

Adding parent / child functionality #61

awapf opened this issue Sep 24, 2024 · 2 comments

Comments

@awapf
Copy link

awapf commented Sep 24, 2024

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.

  1. Is there a planed featue for this use case?
  2. Is there a simpler way to achieve that than the subclass below?
  3. If there is no simpler way, I could actually see that this could be mergable with the CRUDView. Is that an option?
  4. 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.

@ryanhiebert
Copy link

This is something that I wanted, and I ended up restructuring my app so that I could make the routes flat. I used to use drf-nested-routes to do something similar with Django Rest Framework, and tbh I was never totally happy with the results. It worked, though, and it's been long enough that I'm not precisely sure what felt off about it.

@carltongibson
Copy link
Owner

I have plans here but it will definitely be on a flat basis. Nested routes open up a whole load of weird edge cases that aren't worth the trouble, in my experience.

I have the patterns working well in the WORK project. Just a question of finding the time to extract them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants