jrief / django-admin-sortable2

Generic drag-and-drop ordering for objects in the Django admin interface
https://django-admin-sortable2.readthedocs.io/en/latest/
Other
766 stars 179 forks source link

Does not work with polymorphic package #327

Open CelestialStreamer opened 2 years ago

CelestialStreamer commented 2 years ago

Brand new, minimal django project. On admin pages for parent polymorphic model with sortable admin mixin gives error:

Environment:

Request Method: GET
Request URL: http://localhost:8004/admin/django_gists/parent/

Django Version: 4.0
Python Version: 3.10.1
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.messages',
 'django.contrib.sessions',
 'django.contrib.staticfiles',
 'adminsortable2',
 'polymorphic',
 'django_gists']
Installed Middleware:
['django.contrib.sessions.middleware.SessionMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware']

Template error:
In template /usr/local/lib/python3.10/site-packages/adminsortable2/templates/adminsortable2/change_list.html, error at line 1
   join() argument must be str, bytes, or os.PathLike object, not 'list'
   1 :  {% extends base_change_list_template %} 
   2 : 
   3 : {% block extrahead %}
   4 :  {{ block.super }}
   5 :  <script type="application/json" id="admin_sortable2_config">
   6 :      {
   7 :          "update_url": "{{ sortable_update_url }}",
   8 :          "current_page": {{ cl.page_num }},
   9 :          "total_pages": {{ cl.paginator.num_pages }}
   10 :         }
   11 :     </script>

Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.10/site-packages/django/core/handlers/base.py", line 204, in _get_response
    response = response.render()
  File "/usr/local/lib/python3.10/site-packages/django/template/response.py", line 105, in render
    self.content = self.rendered_content
  File "/usr/local/lib/python3.10/site-packages/django/template/response.py", line 83, in rendered_content
    return template.render(context, self._request)
  File "/usr/local/lib/python3.10/site-packages/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
  File "/usr/local/lib/python3.10/site-packages/django/template/base.py", line 176, in render
    return self._render(context)
  File "/usr/local/lib/python3.10/site-packages/django/template/base.py", line 168, in _render
    return self.nodelist.render(context)
  File "/usr/local/lib/python3.10/site-packages/django/template/base.py", line 977, in render
    return SafeString(''.join([
  File "/usr/local/lib/python3.10/site-packages/django/template/base.py", line 978, in <listcomp>
    node.render_annotated(context) for node in self
  File "/usr/local/lib/python3.10/site-packages/django/template/base.py", line 938, in render_annotated
    return self.render(context)
  File "/usr/local/lib/python3.10/site-packages/django/template/loader_tags.py", line 130, in render
    compiled_parent = self.get_parent(context)
  File "/usr/local/lib/python3.10/site-packages/django/template/loader_tags.py", line 127, in get_parent
    return self.find_template(parent, context)
  File "/usr/local/lib/python3.10/site-packages/django/template/loader_tags.py", line 106, in find_template
    template, origin = context.template.engine.find_template(
  File "/usr/local/lib/python3.10/site-packages/django/template/engine.py", line 145, in find_template
    template = loader.get_template(name, skip=skip)
  File "/usr/local/lib/python3.10/site-packages/django/template/loaders/base.py", line 18, in get_template
    for origin in self.get_template_sources(template_name):
  File "/usr/local/lib/python3.10/site-packages/django/template/loaders/filesystem.py", line 36, in get_template_sources
    name = safe_join(template_dir, template_name)
  File "/usr/local/lib/python3.10/site-packages/django/utils/_os.py", line 17, in safe_join
    final_path = abspath(join(base, *paths))
  File "/usr/local/lib/python3.10/posixpath.py", line 90, in join
    genericpath._check_arg_types('join', a, *p)
  File "/usr/local/lib/python3.10/genericpath.py", line 152, in _check_arg_types
    raise TypeError(f'{funcname}() argument must be str, bytes, or '

Exception Type: TypeError at /admin/django_gists/parent/
Exception Value: join() argument must be str, bytes, or os.PathLike object, not 'list'

models.py

from django.db import models
from polymorphic.models import PolymorphicModel

class Parent(PolymorphicModel):
    class Meta:
        ordering = ("order_by",)
    order_by = models.PositiveIntegerField(default=0)

class ChildA(Parent):
    pass

class ChildB(Parent):
    pass

admin.py

from adminsortable2.admin import SortableAdminMixin
from django.contrib import admin
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin
from . import models

class ChildAdmin(PolymorphicChildModelAdmin):
    """Base admin class for all child models"""

@admin.register(models.ChildA)
class ChildAAdmin(ChildAdmin):
    base_model = models.ChildA

@admin.register(models.ChildB)
class ChildBAdmin(ChildAdmin):
    base_model = models.ChildB

@admin.register(models.Parent)
class ParentAdmin(SortableAdminMixin, PolymorphicParentModelAdmin):
    child_models = (models.ChildA, models.ChildB)

Versions

Stuck

I don't know if SortableAdminMixin is the culprit or PolymorphicParentModelAdmin. #196 does not work for me. I added SortableAdminMixin to ChildAAdmin and ChildBAdmin or ChildAdmin and I still get the same error.

jrief commented 2 years ago

Ok, will have a look for the next release.

alexkiro commented 1 year ago

TL;DR A quick workaround is using a custom PolymorphicParentModelAdmin class like so:

class CustomPolyParentAdmin(PolymorphicParentModelAdmin):
    @property
    def change_list_template(self):
        return "admin/change_list.html"

@admin.register(models.Parent)
class ParentAdmin(SortableAdminMixin, CustomPolyParentAdmin):
    child_models = (models.ChildA, models.ChildB)

This should work assuming you are not using any custom templates, in which case you would have to specify the path to your custom template instead of `"admin/change_list.html". Unsure what you need to do if you are using multiple mixins, as that gets confusing.


The issue stems from the fact that the property change_list_template from PolymorphicParentModelAdmin returns a list of templates instead of a single template. I believe this is done to allow custom templates based on the child object type. From django-polymorphic docs:

It extends the template lookup paths, to look for both the parent model and child model in the admin/app/model/change_form.html path.

Which is a supported feature of SimpleTemplateResponse. I believe the issue was introduced in #316