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
774 stars 180 forks source link

Unexpected Keyword Argument 'default_order_direction' #343

Closed selected-pixel-jameson closed 1 year ago

selected-pixel-jameson commented 1 year ago

Hello, I'm receiving the following error when I load a Admin Model form with a Inline that is inheriting from SortableInlineAdminMixin with a custom formset.

Traceback (most recent call last):
  File ".../lib/python3.8/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File ".../lib/python3.8/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File ".../lib/python3.8/site-packages/django/contrib/admin/options.py", line 686, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File ".../lib/python3.8/site-packages/django/utils/decorators.py", line 133, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File ".../lib/python3.8/site-packages/django/views/decorators/cache.py", line 62, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File ".../lib/python3.8/site-packages/django/contrib/admin/sites.py", line 242, in inner
    return view(request, *args, **kwargs)
  File ".../lib/python3.8/site-packages/reversion/admin.py", line 162, in change_view
    return super().change_view(request, object_id, form_url, extra_context)
  File ".../lib/python3.8/site-packages/django/contrib/admin/options.py", line 1893, in change_view
    return self.changeform_view(request, object_id, form_url, extra_context)
  File ".../lib/python3.8/site-packages/django/utils/decorators.py", line 46, in _wrapper
    return bound_method(*args, **kwargs)
  File ".../lib/python3.8/site-packages/django/utils/decorators.py", line 133, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File ".../lib/python3.8/site-packages/django/contrib/admin/options.py", line 1750, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File ".../lib/python3.8/site-packages/django/contrib/admin/options.py", line 1824, in _changeform_view
    formsets, inline_instances = self._create_formsets(
  File ".../lib/python3.8/site-packages/django/contrib/admin/options.py", line 2268, in _create_formsets
    formset = FormSet(**formset_params)
  File ".../lib/python3.8/site-packages/django/forms/models.py", line 1102, in __init__
    super().__init__(data, files, prefix=prefix, queryset=qs, **kwargs)
  File ".../lib/python3.8/site-packages/django/forms/models.py", line 678, in __init__
    super().__init__(
TypeError: __init__() got an unexpected keyword argument 'default_order_direction'

TypeError: __init__() got an unexpected keyword argument 'default_order_direction'

I have the following packages related to Django installed.

Django==4.1.5
django-admin-autocomplete-filter==0.7.1
django-admin-sortable2==2.1.4
django-admin-tools==0.9.2
django-admin-tools-stats==0.9.0
django-appconf==1.0.5
django-autocomplete-light==3.9.4
django-autoslug==1.9.8
django-bower==5.2.0
django-colorfield==0.8.0
django-common-helpers==0.9.2
django-cors-headers==3.13.0
django-crispy-forms==1.14.0
django-cron==0.6.0
django-discover-runner==1.0
django-elasticache==1.0.3
django-extensions==3.2.1
django-filter==22.1
django-formtools==2.4
django-group-by==0.3.1
django-guardian==2.4.0
django-horizontal-list-filter==0.2.7
django-import-export==3.0.2
django-jquery==3.1.0
django-jsonfield==1.4.1
django-lb-health-check==1.0.1
django-maintenance-mode==0.18.0
django-meta==2.1.0
django-middleware-global-request==0.3.1
django-money==3.0.0
django-nested-admin==4.0.2
django-nvd3==0.9.7
django-object-actions==4.1.0
django-password-reset==2.0
django-polymorphic==3.1.0
django-qsstats-magic==1.1.0
django-redis-cache==3.0.1
django-rest-swagger==2.2.0
django-reversion==5.0.4
django-silk==5.0.3
django-simple-tags==0.6.0
django-sslserver==0.22
django-storages==1.13.2
django-suit==0.2.28
django-taggit==3.1.0
django-tinymce==3.5.0
django-tsvector-field==0.9.5
django-user-accounts==3.2.0
django-user-agents==0.4.0
django-widget-tweaks==1.4.12
djangorestframework==3.14.0
djangorestframework-bulk==0.2.1

I've traced this error back to an inline that I have which is inheriting from theSortableInlineAdminMixin and utilizing a custom formset.

If I remove the mixin and/or the formset the error is resolved.

Here is the inline class and the custom formset. *Note that I do have custom code in theRequireOneFormSet but I removed it for this example to show that it doesn't matter what I have in that class. This error occurs regardless.

class RequireOneFormSet(forms.models.BaseInlineFormSet):
    pass

class CoverIssueInline(SortableInlineAdminMixin, admin.TabularInline):
    model = CoverUpload
    fields = [
        "image_display",
        "image",
        "description",
        "cover_labels",
        "ignore_image_match",
    ]
    form = CoverUploadForm
    formset = RequireOneFormSet
    verbose_name = "Cover"
    verbose_name_plural = "Covers"
    extra = 0
    readonly_fields = ("image_display",)

    def image_display(self, obj):
        if obj.s3_key is not None and len(obj.s3_key) != 0:
            return mark_safe(
                '<a target="_blank" href="{thumb}"><img style="height:120px" src="{thumb}" /></a>'.format(
                    thumb="https://"
                    + settings.AWS_S3_CUSTOM_DOMAIN
                    + settings.MEDIA_URL
                    + obj.s3_key,
                )
            )
        else:
            return mark_safe(
                '<a target="_blank" href="{thumb}"><img style="height:120px" src="{thumb}" /></a>'.format(
                    thumb="https://via.placeholder.com/90x120",
                )
            )

    image_display.allow_tags = True

Here is the custom form being used.

class CoverUploadForm(forms.ModelForm):

    direct_to_boxes = []

    class Meta:
        model = CoverUpload
        exclude = ()

    def __init__(self, *args, **kwargs):
        super(CoverUploadForm, self).__init__(*args, **kwargs)

    image = forms.ImageField(
        label="Upload New Cover",
        widget=forms.ClearableFileInput(attrs={"multiple": False}),
        required=False,
    )

    ignore_image_match = forms.BooleanField(
        label="Ignore Cover Validation", widget=forms.CheckboxInput(), required=False
    )

    cover_labels = forms.ModelMultipleChoiceField(
        queryset=CoverLabel.objects.all(),
        required=False,
        widget=autocomplete.ModelSelect2Multiple(
            url="autocomplete-cover-labels",
            attrs={"data-placeholder": "", "class": "content-id"},
        ),
    )

    def handle_uploaded_file(self, file):
        cache_buster = str(round(time.time() * 1000))
        file_name = cache_buster + file.name
        s3_key = helpers.clean_file_name(file_name)
        destination = default_storage.open(s3_key, "w")
        for chunk in file.chunks():
            destination.write(chunk)
        destination.close()
        return s3_key

    def clean(self):
        validation_errors = []
        cleaned_data = super(CoverUploadForm, self).clean()
        file = cleaned_data.get("image")
        if file is not None:
            s3_key = self.handle_uploaded_file(file)
            if cleaned_data.get("ignore_image_match") == False:
                response = IndexService.search_image_match(s3_key)
                if response:
                    default_storage.delete(s3_key)
                    metadata = response.get("metadata")
                    id = metadata.get("id")
                    newIssue = metadata.get("newIssue", False)
                    ignored = response.get("image.metadata.ignored", False)
                    if ignored:
                        message = mark_safe(
                            "This cover already exists and is associated with an issue which has been ignored."
                            + ' <a href="/admin_x951pu/api/ignoredissue/?q='
                            + str(id)
                            + '">Click here to view the new issue</a>'
                        )
                        validation_errors.append(message)
                    elif newIssue:
                        message = mark_safe(
                            "This cover already exists and is associated with A NEW issue. If this is actually a duplicate the new issue should be ignored. "
                            + ' <a href="/admin_x951pu/api/newissue/?q='
                            + str(id)
                            + '">Click here to view the new issue</a>'
                        )
                        validation_errors.append(message)
                    else:
                        message = mark_safe(
                            "This cover already exists. If this is actually a duplicate cover the issues should be merged. "
                            + ' <a href="/admin_x951pu/api/issue/'
                            + str(id)
                            + '/change/"'
                            + '">Click here to view the issue</a>'
                        )
                        validation_errors.append(message)

        if not self.is_valid():
            return
        if not self.forms or not self.forms[0].cleaned_data:
            validation_errors.append(
                "At least one {} required".format(self.model._meta.verbose_name)
            )

        if len(validation_errors) > 0:
            raise forms.ValidationError(validation_errors)

    def save(self, commit=True):

        instance = super(CoverUploadForm, self).save(commit=False)
        file = self.cleaned_data.get("image")
        flagToStoreInMatch = False
        if file is not None:
            s3_key = self.handle_uploaded_file(file)
            instance.file = s3_key
            instance.s3_key = s3_key
            flagToStoreInMatch = True

        if commit:
            instance.save()

        if flagToStoreInMatch:
            IndexService.store_image_match(instance.id, instance.issues_id)
        return instance

This was not an issue until I updated to the latest version of django-admin-sortable and Django 4.1.

selected-pixel-jameson commented 1 year ago

Ah, I figured this out. My custom formset needed to inherit from this packages CustomInlineFormSet.

class RequireOneFormSet(CustomInlineFormSet, forms.models.BaseInlineFormSet):