jazzband / django-smart-selects

chained and grouped selects for django forms
https://django-smart-selects.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
1.1k stars 348 forks source link

Fix render() error in widgets.py #283

Closed Murakam1 closed 4 years ago

Murakam1 commented 4 years ago

Fix render() error

import json

import django

from django.apps import apps from django.conf import settings try: from django.core.urlresolvers import reverse except ImportError:

TODO: swap this over when Django 2+ becomes more prevalent

from django.urls import reverse

from django.forms.widgets import Select, SelectMultiple, Media from django.utils.safestring import mark_safe from django.utils.encoding import force_text from django.utils.html import escape

from smart_selects.utils import unicode_sorter, sort_results

get_model = apps.get_model

USE_DJANGO_JQUERY = getattr(settings, 'USE_DJANGO_JQUERY', False) JQUERY_URL = getattr(settings, 'JQUERY_URL', 'https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js')

URL_PREFIX = getattr(settings, "SMART_SELECTS_URL_PREFIX", "")

class JqueryMediaMixin(object): @property def media(self): """Media defined as a dynamic property instead of an inner class.""" media = super(JqueryMediaMixin, self).media

    js = []

    if JQUERY_URL:
        js.append(JQUERY_URL)
    elif JQUERY_URL is not False:
        vendor = '' if django.VERSION < (1, 9, 0) else 'vendor/jquery/'
        extra = '' if settings.DEBUG else '.min'

        jquery_paths = [
            '{}jquery{}.js'.format(vendor, extra),
            'jquery.init.js',
        ]

        if USE_DJANGO_JQUERY:
            jquery_paths = ['admin/js/{}'.format(path) for path in jquery_paths]

        js.extend(jquery_paths)

    media += Media(js=js)
    return media

class ChainedSelect(JqueryMediaMixin, Select): def init(self, to_app_name, to_model_name, chained_field, chained_model_field, foreign_key_app_name, foreign_key_model_name, foreign_key_field_name, show_all, auto_choose, sort=True, manager=None, view_name=None, *args, *kwargs): self.to_app_name = to_app_name self.to_model_name = to_model_name self.chained_field = chained_field self.chained_model_field = chained_model_field self.show_all = show_all self.auto_choose = auto_choose self.sort = sort self.manager = manager self.view_name = view_name self.foreign_key_app_name = foreign_key_app_name self.foreign_key_model_name = foreign_key_model_name self.foreign_key_field_name = foreign_key_field_name super(Select, self).init(args, **kwargs)

@property
def media(self):
    """Media defined as a dynamic property instead of an inner class."""
    media = super(ChainedSelect, self).media
    js = ['smart-selects/admin/js/chainedfk.js',
          'smart-selects/admin/js/bindfields.js']
    media += Media(js=js)
    return media

# TODO: Simplify this and remove the noqa tag
def render(self, name, value, attrs=None, choices=(), renderer=None):  # noqa: C901
    if len(name.split('-')) > 1:  # formset
        chained_field = '-'.join(name.split('-')[:-1] + [self.chained_field])
    else:
        chained_field = self.chained_field

    if not self.view_name:
        if self.show_all:
            view_name = "chained_filter_all"
        else:
            view_name = "chained_filter"
    else:
        view_name = self.view_name
    kwargs = {
        'app': self.to_app_name,
        'model': self.to_model_name,
        'field': self.chained_model_field,
        'foreign_key_app_name': self.foreign_key_app_name,
        'foreign_key_model_name': self.foreign_key_model_name,
        'foreign_key_field_name': self.foreign_key_field_name,
        'value': '1'
        }
    if self.manager is not None:
        kwargs.update({'manager': self.manager})
    url = URL_PREFIX + ("/".join(reverse(view_name, kwargs=kwargs).split("/")[:-2]))
    if self.auto_choose:
        auto_choose = 'true'
    else:
        auto_choose = 'false'
    if choices:
        iterator = iter(self.choices)
        if hasattr(iterator, '__next__'):
            empty_label = iterator.__next__()[1]
        else:
            # Hacky way to getting the correct empty_label from the field instead of a hardcoded '--------'
            empty_label = iterator.next()[1]
    else:
        empty_label = "--------"

    final_choices = []

    if value:
        available_choices = self._get_available_choices(self.queryset, value)
        for choice in available_choices:
            final_choices.append((choice.pk, force_text(choice)))
    if len(final_choices) > 1:
        final_choices = [("", (empty_label))] + final_choices
    if self.show_all:
        final_choices.append(("", (empty_label)))
        self.choices = list(self.choices)
        if self.sort:
            self.choices.sort(key=lambda x: unicode_sorter(x[1]))
        for ch in self.choices:
            if ch not in final_choices:
                final_choices.append(ch)
    self.choices = final_choices

    attrs.update(self.attrs)
    attrs["data-chainfield"] = chained_field
    attrs["data-url"] = url
    attrs["data-value"] = "null" if value is None or value == "" else value
    attrs["data-auto_choose"] = auto_choose
    attrs["data-empty_label"] = escape(empty_label)
    attrs["name"] = name
    final_attrs = self.build_attrs(attrs)
    if 'class' in final_attrs:
        final_attrs['class'] += ' chained-fk'
    else:
        final_attrs['class'] = 'chained-fk'

    output = super(ChainedSelect, self).render(name, value, final_attrs, renderer)

    return mark_safe(output)

def _get_available_choices(self, queryset, value):
    """
    get possible choices for selection
    """
    item = queryset.filter(pk=value).first()
    if item:
        try:
            pk = getattr(item, self.chained_model_field + "_id")
            filter = {self.chained_model_field: pk}
        except AttributeError:
            try:  # maybe m2m?
                pks = getattr(item, self.chained_model_field).all().values_list('pk', flat=True)
                filter = {self.chained_model_field + "__in": pks}
            except AttributeError:
                try:  # maybe a set?
                    pks = getattr(item, self.chained_model_field + "_set").all().values_list('pk', flat=True)
                    filter = {self.chained_model_field + "__in": pks}
                except AttributeError:  # give up
                    filter = {}
        filtered = list(get_model(self.to_app_name, self.to_model_name).objects.filter(**filter).distinct())
        if self.sort:
            sort_results(filtered)
    else:
        # invalid value for queryset
        filtered = []

    return filtered

class ChainedSelectMultiple(JqueryMediaMixin, SelectMultiple): def init(self, to_app_name, to_model_name, chain_field, chained_model_field, foreign_key_app_name, foreign_key_model_name, foreign_key_field_name, auto_choose, horizontal, verbose_name='', manager=None, *args, *kwargs): self.to_app_name = to_app_name self.to_model_name = to_model_name self.chain_field = chain_field self.chained_model_field = chained_model_field self.auto_choose = auto_choose self.horizontal = horizontal self.verbose_name = verbose_name self.manager = manager self.foreign_key_app_name = foreign_key_app_name self.foreign_key_model_name = foreign_key_model_name self.foreign_key_field_name = foreign_key_field_name super(SelectMultiple, self).init(args, **kwargs)

@property
def media(self):
    """Media defined as a dynamic property instead of an inner class."""
    media = super(ChainedSelectMultiple, self).media
    js = ['smart-selects/admin/js/chainedm2m.js',
          'smart-selects/admin/js/bindfields.js']
    if self.horizontal:
        # For horizontal mode add django filter horizontal javascript code
        js.extend(["admin/js/core.js",
                   "admin/js/SelectBox.js",
                   "admin/js/SelectFilter2.js"])
    media += Media(js=js)
    return media

def render(self, name, value, attrs=None, choices=(), renderer=None):
    if len(name.split('-')) > 1:  # formset
        chain_field = '-'.join(name.split('-')[:-1] + [self.chain_field])
    else:
        chain_field = self.chain_field

    view_name = 'chained_filter'

    kwargs = {
        'app': self.to_app_name,
        'model': self.to_model_name,
        'field': self.chained_model_field,
        'foreign_key_app_name': self.foreign_key_app_name,
        'foreign_key_model_name': self.foreign_key_model_name,
        'foreign_key_field_name': self.foreign_key_field_name,
        'value': '1'
    }
    if self.manager is not None:
        kwargs.update({'manager': self.manager})
    url = URL_PREFIX + ("/".join(reverse(view_name, kwargs=kwargs).split("/")[:-2]))
    if self.auto_choose:
        auto_choose = 'true'
    else:
        auto_choose = 'false'

    # since we cannot deduce the value of the chained_field
    # so we just render empty choices here and let the js
    # fetch related choices later
    final_choices = []
    self.choices = final_choices

    attrs["data-chainfield"] = chain_field
    attrs["data-url"] = url
    attrs["data-value"] = "null" if value is None else json.dumps(value)
    attrs["data-auto_choose"] = auto_choose
    attrs["name"] = name
    final_attrs = self.build_attrs(attrs)
    if 'class' in final_attrs:
        final_attrs['class'] += ' chained'
    else:
        final_attrs['class'] = 'chained'
    if self.horizontal:
        # For hozontal mode add django filter horizontal javascript selector class
        final_attrs['class'] += ' selectfilter'
    final_attrs['data-field-name'] = self.verbose_name
    output = super(ChainedSelectMultiple, self).render(name, value, final_attrs, renderer)

    return mark_safe(output)
coveralls commented 4 years ago

Coverage Status

Coverage increased (+3.8%) to 89.809% when pulling 05dcc4a3de2874499ff3b9a3dfac5c623206e3e5 on master into 2ecfe8857d3ceee302d5045a6dd9c3c747b98c35 on issue_fix.

manelclos commented 4 years ago

Please reopen if needed.