diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dec5acf9d4..6776f7052c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,9 +6,9 @@ name: Tests on: # Triggers the workflow on push or pull request events but only for the develop branch push: - branches: [ develop ] + branches: [develop] pull_request: - branches: [ develop ] + branches: [develop] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -22,18 +22,21 @@ jobs: strategy: matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ["3.10", "3.11", "3.12"] # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox tox-gh-actions - - name: Test with tox - run: tox + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Check with ruff + uses: jpetrucciani/ruff-check@main + with: + path: "ajax_select" + - name: Install dependencies + run: | + python -m pip install ruff tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/Makefile b/Makefile deleted file mode 100644 index 1a8fece8eb..0000000000 --- a/Makefile +++ /dev/null @@ -1,44 +0,0 @@ -.PHONY: clean-pyc clean-build - -help: - @echo "test - run tests quickly with the currently installed Django" - @echo "clean - remove build artifacts" - @echo "lint - check style with flake8" - @echo "release - package and upload a release" - @echo "sdist - package" - # @echo "docs - generate Sphinx HTML documentation, including API docs" - -clean: clean-build clean-pyc - -clean-build: - rm -fr build/ - rm -fr dist/ - rm -fr *.egg-info - -clean-pyc: - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - -lint: - flake8 ajax_select tests example - -test: - tox - -# docs: -# rm -f docs/django-ajax-selects.rst -# rm -f docs/modules.rst -# sphinx-apidoc -o docs/ django-ajax-selects -# $(MAKE) -C docs clean -# $(MAKE) -C docs html -# open docs/_build/html/index.html - -release: clean - python setup.py sdist upload - python setup.py bdist_wheel - twine upload dist/* - -sdist: clean - python setup.py sdist - ls -l dist diff --git a/README.md b/README.md index ca615bed73..f3841eb285 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ [![Build Status](https://travis-ci.org/crucialfelix/django-ajax-selects.svg?branch=master)](https://travis-ci.org/crucialfelix/django-ajax-selects) [![PyPI version](https://badge.fury.io/py/django-ajax-selects.svg)](https://badge.fury.io/py/django-ajax-selects) +This Django app glues Django Admin, jQuery UI together to enable searching and managing ForeignKey and ManyToMany relationships. + +At the time it was created Django did not have any way to do this, and this solution glued together some technologies of the day. + +If you are building a new project then you should not use this. + +Django has built in support now: +https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields + + --- ![selecting](/docs/source/_static/kiss.png?raw=true) @@ -31,7 +41,9 @@ Include the urls in your project: ```py # urls.py -from django.conf.urls import url, include +from django.urls import path +from django.conf.urls import include + from django.conf.urls.static import static from django.contrib import admin from django.conf import settings @@ -40,11 +52,10 @@ from ajax_select import urls as ajax_select_urls admin.autodiscover() urlpatterns = [ - - # place it at whatever base url you like - url(r'^ajax_select/', include(ajax_select_urls)), - - url(r'^admin/', include(admin.site.urls)), + # This is the api endpoint that django-ajax-selects will call + # to lookup your model ids by name + path("admin/lookups/", include(ajax_select_urls)), + path("admin/", admin.site.urls), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ``` @@ -107,14 +118,34 @@ Read the full documention here: [outside of the admin](http://django-ajax-select ## Assets included by default -* //ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js' -* //code.jquery.com/ui/1.12.1/jquery-ui.js -* //code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css +https://jquery.com/ 3.7.1 +https://jqueryui.com/ 1.13.2 + +## Customize jquery + +To use a custom jQuery UI theme you can set: + +```python +# settings.py +AJAX_SELECT_JQUERYUI_THEME = "/static/path-to-your-theme/jquery-ui-min.css" +``` + +https://jqueryui.com/themeroller/ + +If you need to use a different jQuery or jQuery UI then turn off the default assets: + +```python +# settings.py +AJAX_SELECT_BOOTSTRAP = False +``` + +and include jquery and jquery-ui yourself, making sure they are loaded before the Django admin loads. + ## Compatibility -* Django >=2.2 -* Python >=3.6 +* Django >=3.2 +* Python >=3.10 ## Contributors diff --git a/ajax_select/admin.py b/ajax_select/admin.py index 39bdcc4eed..22a63d2e01 100644 --- a/ajax_select/admin.py +++ b/ajax_select/admin.py @@ -4,7 +4,7 @@ class AjaxSelectAdmin(admin.ModelAdmin): - """ in order to get + popup functions subclass this or do the same hook inside of your get_form """ + """in order to get + popup functions subclass this or do the same hook inside of your get_form""" def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) @@ -14,16 +14,19 @@ def get_form(self, request, obj=None, **kwargs): class AjaxSelectAdminInlineFormsetMixin: - def get_formset(self, request, obj=None, **kwargs): fs = super().get_formset(request, obj, **kwargs) autoselect_fields_check_can_add(fs.form, self.model, request.user) return fs -class AjaxSelectAdminTabularInline(AjaxSelectAdminInlineFormsetMixin, admin.TabularInline): +class AjaxSelectAdminTabularInline( + AjaxSelectAdminInlineFormsetMixin, admin.TabularInline +): pass -class AjaxSelectAdminStackedInline(AjaxSelectAdminInlineFormsetMixin, admin.StackedInline): +class AjaxSelectAdminStackedInline( + AjaxSelectAdminInlineFormsetMixin, admin.StackedInline +): pass diff --git a/ajax_select/fields.py b/ajax_select/fields.py index ff6ca10517..fe9eb48c15 100644 --- a/ajax_select/fields.py +++ b/ajax_select/fields.py @@ -15,24 +15,46 @@ from ajax_select.registry import registry -as_default_help = 'Enter text to search.' +as_default_help = "Enter text to search." def _media(self): - js = ['admin/js/jquery.init.js'] - - # Unless AJAX_SELECT_BOOTSTRAP == False - # then load include bootstrap which will load jquery and jquery ui + default css as needed - if getattr(settings, "AJAX_SELECT_BOOTSTRAP", True): - js.append('ajax_select/js/bootstrap.js') - - js.append('ajax_select/js/ajax_select.js') + bootstrap = getattr(settings, "AJAX_SELECT_BOOTSTRAP", True) + jqueryui_theme = getattr( + settings, + "AJAX_SELECT_JQUERYUI_THEME", + "ajax_select/vendor/jquery-ui-1.13.2/jquery-ui.min.css", + ) + if bootstrap: + js = [ + "ajax_select/vendor/jquery-3.7.1.min.js", + "ajax_select/vendor/jquery-ui-1.13.2/jquery-ui.min.js", + "ajax_select/js/ajax_select.js", + ] + css = [ + jqueryui_theme, + "ajax_select/css/ajax_select.css", + ] + else: + # Bring your own jquery and jquery ui + js = [ + "ajax_select/js/ajax_select.js", + ] + css = ["ajax_select/css/ajax_select.css"] - return forms.Media(css={'all': ('ajax_select/css/ajax_select.css',)}, js=js) + return forms.Media( + css={"all": css}, + js=js, + ) -json_encoder = import_string(getattr(settings, 'AJAX_SELECT_JSON_ENCODER', - 'django.core.serializers.json.DjangoJSONEncoder')) +json_encoder = import_string( + getattr( + settings, + "AJAX_SELECT_JSON_ENCODER", + "django.core.serializers.json.DjangoJSONEncoder", + ) +) ############################################################################### @@ -46,14 +68,17 @@ class AutoCompleteSelectWidget(forms.widgets.TextInput): media = property(_media) add_link = None - - def __init__(self, - channel, - help_text='', - show_help_text=True, - plugin_options=None, - *args, - **kwargs): + html_id: str + + def __init__( + self, + channel, + help_text="", + show_help_text=True, + plugin_options=None, + *args, + **kwargs, + ): self.plugin_options = plugin_options or {} super().__init__(*args, **kwargs) self.channel = channel @@ -61,45 +86,44 @@ def __init__(self, self.show_help_text = show_help_text def render(self, name, value, attrs=None, renderer=None, **_kwargs): - value = value or '' + value = value or "" final_attrs = self.build_attrs(self.attrs) final_attrs.update(attrs or {}) - final_attrs.pop('required', None) - self.html_id = final_attrs.pop('id', name) + final_attrs.pop("required", None) + self.html_id = str(final_attrs.pop("id", name)) - current_repr = '' + current_repr = "" initial = None lookup = registry.get(self.channel) if value: objs = lookup.get_objects([value]) try: obj = objs[0] - except IndexError: - raise Exception(f"{lookup} cannot find object:{value}") + except IndexError as e: + raise Exception(f"{lookup} cannot find object:{value}") from e current_repr = lookup.format_item_display(obj) initial = [current_repr, obj.pk] - if self.show_help_text: - help_text = self.help_text - else: - help_text = '' + help_text = self.help_text if self.show_help_text else "" context = { - 'name': name, - 'html_id': self.html_id, - 'current_id': value, - 'current_repr': current_repr, - 'help_text': help_text, - 'extra_attrs': mark_safe(flatatt(final_attrs)), - 'func_slug': self.html_id.replace("-", ""), - 'add_link': self.add_link, + "name": name, + "html_id": self.html_id, + "current_id": value, + "current_repr": current_repr, + "help_text": help_text, + "extra_attrs": mark_safe(flatatt(final_attrs)), + "func_slug": self.html_id.replace("-", ""), + "add_link": self.add_link, } context.update( - make_plugin_options(lookup, self.channel, self.plugin_options, initial)) - templates = ( - f'ajax_select/autocompleteselect_{self.channel}.html', - 'ajax_select/autocompleteselect.html') + make_plugin_options(lookup, self.channel, self.plugin_options, initial) + ) + templates = [ + f"ajax_select/autocompleteselect_{self.channel}.html", + "ajax_select/autocompleteselect.html", + ] out = render_to_string(templates, context) return mark_safe(out) @@ -107,7 +131,7 @@ def value_from_datadict(self, data, files, name): return data.get(name, None) def id_for_label(self, id_): - return f'{id_}_text' + return f"{id_}_text" class AutoCompleteSelectField(forms.fields.CharField): @@ -119,14 +143,14 @@ def __init__(self, channel, *args, **kwargs): self.channel = channel widget_kwargs = dict( - channel=channel, - help_text=kwargs.get('help_text', _(as_default_help)), - show_help_text=kwargs.pop('show_help_text', True), - plugin_options=kwargs.pop('plugin_options', {}) + channel=channel, + help_text=kwargs.get("help_text", _(as_default_help)), + show_help_text=kwargs.pop("show_help_text", True), + plugin_options=kwargs.pop("plugin_options", {}), ) - widget_kwargs.update(kwargs.pop('widget_options', {})) + widget_kwargs.update(kwargs.pop("widget_options", {})) kwargs["widget"] = AutoCompleteSelectWidget(**widget_kwargs) - super().__init__(max_length=255, *args, **kwargs) + super().__init__(*args, **kwargs, max_length=255) def clean(self, value): if value: @@ -137,12 +161,11 @@ def clean(self, value): # or your channel is faulty # out of the scope of this field to do anything more than # tell you it doesn't exist - raise forms.ValidationError( - f"{lookup} cannot find object: {value}") + raise forms.ValidationError(f"{lookup} cannot find object: {value}") return objs[0] else: if self.required: - raise forms.ValidationError(self.error_messages['required']) + raise forms.ValidationError(self.error_messages["required"]) return None def check_can_add(self, user, model): @@ -150,8 +173,8 @@ def check_can_add(self, user, model): def has_changed(self, initial, data): # 1 vs u'1' - initial_value = initial if initial is not None else '' - data_value = data if data is not None else '' + initial_value = initial if initial is not None else "" + data_value = data if data is not None else "" return str(initial_value) != str(data_value) @@ -166,14 +189,17 @@ class AutoCompleteSelectMultipleWidget(forms.widgets.SelectMultiple): media = property(_media) add_link = None - - def __init__(self, - channel, - help_text='', - show_help_text=True, - plugin_options=None, - *args, - **kwargs): + html_id: str + + def __init__( + self, + channel, + help_text="", + show_help_text=True, + plugin_options=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.channel = channel @@ -182,62 +208,57 @@ def __init__(self, self.plugin_options = plugin_options or {} def render(self, name, value, attrs=None, renderer=None, **_kwargs): - if value is None: value = [] final_attrs = self.build_attrs(self.attrs) final_attrs.update(attrs or {}) - final_attrs.pop('required', None) - self.html_id = final_attrs.pop('id', name) + final_attrs.pop("required", None) + self.html_id = str(final_attrs.pop("id", name)) lookup = registry.get(self.channel) values = list(value) - if all([isinstance(v, Model) for v in values]): - objects = values - else: - objects = lookup.get_objects(values) + objects = ( + values + if all([isinstance(v, Model) for v in values]) + else lookup.get_objects(values) + ) current_ids = pack_ids([obj.pk for obj in objects]) # text repr of currently selected items - initial = [ - [lookup.format_item_display(obj), obj.pk] - for obj in objects - ] + initial = [[lookup.format_item_display(obj), obj.pk] for obj in objects] - if self.show_help_text: - help_text = self.help_text - else: - help_text = '' + help_text = self.help_text if self.show_help_text else "" context = { - 'name': name, - 'html_id': self.html_id, - 'current': value, - 'current_ids': current_ids, - 'current_reprs': mark_safe( - json.dumps(initial, cls=json_encoder) - ), - 'help_text': help_text, - 'extra_attrs': mark_safe(flatatt(final_attrs)), - 'func_slug': self.html_id.replace("-", ""), - 'add_link': self.add_link, + "name": name, + "html_id": self.html_id, + "current": value, + "current_ids": current_ids, + "current_reprs": mark_safe(json.dumps(initial, cls=json_encoder)), + "help_text": help_text, + "extra_attrs": mark_safe(flatatt(final_attrs)), + "func_slug": self.html_id.replace("-", ""), + "add_link": self.add_link, } context.update( - make_plugin_options(lookup, self.channel, self.plugin_options, initial)) - templates = (f'ajax_select/autocompleteselectmultiple_{self.channel}.html', - 'ajax_select/autocompleteselectmultiple.html') + make_plugin_options(lookup, self.channel, self.plugin_options, initial) + ) + templates = [ + f"ajax_select/autocompleteselectmultiple_{self.channel}.html", + "ajax_select/autocompleteselectmultiple.html", + ] out = render_to_string(templates, context) return mark_safe(out) def value_from_datadict(self, data, files, name): # eg. 'members': ['|229|4688|190|'] - return [val for val in data.get(name, '').split('|') if val] + return [val for val in data.get(name, "").split("|") if val] - def id_for_label(self, id_, index='0'): - return f'{id_}_text' + def id_for_label(self, id_, index="0"): + return f"{id_}_text" class AutoCompleteSelectMultipleField(forms.fields.CharField): @@ -250,10 +271,10 @@ class AutoCompleteSelectMultipleField(forms.fields.CharField): def __init__(self, channel, *args, **kwargs): self.channel = channel - help_text = kwargs.get('help_text') - show_help_text = kwargs.pop('show_help_text', False) + help_text = kwargs.get("help_text") + show_help_text = kwargs.pop("show_help_text", False) - if not (help_text is None): + if help_text is not None: # '' will cause translation to fail # should be '' if isinstance(help_text, str): @@ -262,7 +283,7 @@ def __init__(self, channel, *args, **kwargs): # regardless of which widget is used. so even when you specify an # explicit help text it appends this other default text onto the end. # This monkey patches the help text to remove that - if help_text != '': + if help_text != "": if not isinstance(help_text, str): # ideally this could check request.LANGUAGE_CODE translated = help_text.translate(settings.LANGUAGE_CODE) @@ -271,8 +292,7 @@ def __init__(self, channel, *args, **kwargs): dh = 'Hold down "Control", or "Command" on a Mac, to select more than one.' django_default_help = _(dh).translate(settings.LANGUAGE_CODE) if django_default_help in translated: - cleaned_help = translated.replace( - django_default_help, '').strip() + cleaned_help = translated.replace(django_default_help, "").strip() # probably will not show up in translations if cleaned_help: help_text = cleaned_help @@ -288,20 +308,20 @@ def __init__(self, channel, *args, **kwargs): # so by default do not show it in widget # if using in a normal form then set to True when creating the field widget_kwargs = { - 'channel': channel, - 'help_text': help_text, - 'show_help_text': show_help_text, - 'plugin_options': kwargs.pop('plugin_options', {}) + "channel": channel, + "help_text": help_text, + "show_help_text": show_help_text, + "plugin_options": kwargs.pop("plugin_options", {}), } - widget_kwargs.update(kwargs.pop('widget_options', {})) - kwargs['widget'] = AutoCompleteSelectMultipleWidget(**widget_kwargs) - kwargs['help_text'] = help_text + widget_kwargs.update(kwargs.pop("widget_options", {})) + kwargs["widget"] = AutoCompleteSelectMultipleWidget(**widget_kwargs) + kwargs["help_text"] = help_text super().__init__(*args, **kwargs) def clean(self, value): if not value and self.required: - raise forms.ValidationError(self.error_messages['required']) + raise forms.ValidationError(self.error_messages["required"]) return value # a list of primary keys from widget value_from_datadict def check_can_add(self, user, model): @@ -327,44 +347,43 @@ class AutoCompleteWidget(forms.TextInput): media = property(_media) channel = None - help_text = '' - html_id = '' + help_text = "" + html_id: str = "" def __init__(self, channel, *args, **kwargs): self.channel = channel - self.help_text = kwargs.pop('help_text', '') - self.show_help_text = kwargs.pop('show_help_text', True) - self.plugin_options = kwargs.pop('plugin_options', {}) + self.help_text = kwargs.pop("help_text", "") + self.show_help_text = kwargs.pop("show_help_text", True) + self.plugin_options = kwargs.pop("plugin_options", {}) super().__init__(*args, **kwargs) def render(self, name, value, attrs=None, renderer=None, **_kwargs): - - initial = value or '' + initial = value or "" final_attrs = self.build_attrs(self.attrs) final_attrs.update(attrs or {}) - self.html_id = final_attrs.pop('id', name) - final_attrs.pop('required', None) + self.html_id = str(final_attrs.pop("id", name)) + final_attrs.pop("required", None) lookup = registry.get(self.channel) - if self.show_help_text: - help_text = self.help_text - else: - help_text = '' + help_text = self.help_text if self.show_help_text else "" context = { - 'current_repr': initial, - 'current_id': initial, - 'help_text': help_text, - 'html_id': self.html_id, - 'name': name, - 'extra_attrs': mark_safe(flatatt(final_attrs)), - 'func_slug': self.html_id.replace("-", ""), + "current_repr": initial, + "current_id": initial, + "help_text": help_text, + "html_id": self.html_id, + "name": name, + "extra_attrs": mark_safe(flatatt(final_attrs)), + "func_slug": self.html_id.replace("-", ""), } context.update( - make_plugin_options(lookup, self.channel, self.plugin_options, initial)) - templates = (f'ajax_select/autocomplete_{self.channel}.html', - 'ajax_select/autocomplete.html') + make_plugin_options(lookup, self.channel, self.plugin_options, initial) + ) + templates = [ + f"ajax_select/autocomplete_{self.channel}.html", + "ajax_select/autocomplete.html", + ] return mark_safe(render_to_string(templates, context)) @@ -373,22 +392,23 @@ class AutoCompleteField(forms.CharField): A CharField that uses an AutoCompleteWidget to lookup matching and stores the result as plain text. """ + channel = None def __init__(self, channel, *args, **kwargs): self.channel = channel widget_kwargs = dict( - help_text=kwargs.get('help_text', _(as_default_help)), - show_help_text=kwargs.pop('show_help_text', True), - plugin_options=kwargs.pop('plugin_options', {}) + help_text=kwargs.get("help_text", _(as_default_help)), + show_help_text=kwargs.pop("show_help_text", True), + plugin_options=kwargs.pop("plugin_options", {}), ) - widget_kwargs.update(kwargs.pop('widget_options', {})) - if 'attrs' in kwargs: - widget_kwargs['attrs'] = kwargs.pop('attrs') + widget_kwargs.update(kwargs.pop("widget_options", {})) + if "attrs" in kwargs: + widget_kwargs["attrs"] = kwargs.pop("attrs") widget = AutoCompleteWidget(channel, **widget_kwargs) - defaults = {'max_length': 255, 'widget': widget} + defaults = {"max_length": 255, "widget": widget} defaults.update(kwargs) super().__init__(*args, **defaults) @@ -396,6 +416,7 @@ def __init__(self, channel, *args, **kwargs): ############################################################################### + def _check_can_add(self, user, related_model): """ Check if the User can create a related_model. @@ -407,7 +428,7 @@ def _check_can_add(self, user, related_model): If it can add, then enable the widget to show the green + link """ lookup = registry.get(self.channel) - if hasattr(lookup, 'can_add'): + if hasattr(lookup, "can_add"): can_add = lookup.can_add(user, related_model) else: ctype = ContentType.objects.get_for_model(related_model) @@ -415,8 +436,7 @@ def _check_can_add(self, user, related_model): if can_add: app_label = related_model._meta.app_label model = related_model._meta.object_name.lower() - self.widget.add_link = reverse( - f'admin:{app_label}_{model}_add') + '?_popup=1' + self.widget.add_link = reverse(f"admin:{app_label}_{model}_add") + "?_popup=1" def autoselect_fields_check_can_add(form, model, user): @@ -426,7 +446,9 @@ def autoselect_fields_check_can_add(form, model, user): related_model. """ for name, form_field in form.declared_fields.items(): - if isinstance(form_field, (AutoCompleteSelectMultipleField, AutoCompleteSelectField)): + if isinstance( + form_field, (AutoCompleteSelectMultipleField, AutoCompleteSelectField) + ): db_field = model._meta.get_field(name) if hasattr(db_field, "remote_field"): form_field.check_can_add(user, db_field.remote_field.model) @@ -435,24 +457,22 @@ def autoselect_fields_check_can_add(form, model, user): def make_plugin_options(lookup, channel_name, widget_plugin_options, initial): - """ Make a JSON dumped dict of all options for the jQuery ui plugin.""" + """Make a JSON dumped dict of all options for the jQuery ui plugin.""" po = {} if initial: - po['initial'] = initial - po.update(getattr(lookup, 'plugin_options', {})) + po["initial"] = initial + po.update(getattr(lookup, "plugin_options", {})) po.update(widget_plugin_options) - if not po.get('source'): - po['source'] = reverse('ajax_lookup', kwargs={'channel': channel_name}) + if not po.get("source"): + po["source"] = reverse("ajax_lookup", kwargs={"channel": channel_name}) # allow html unless explicitly set - if po.get('html') is None: - po['html'] = True + if po.get("html") is None: + po["html"] = True return { - 'plugin_options': mark_safe(json.dumps(po, cls=json_encoder)), - 'data_plugin_options': force_escape( - json.dumps(po, cls=json_encoder) - ) + "plugin_options": mark_safe(json.dumps(po, cls=json_encoder)), + "data_plugin_options": force_escape(json.dumps(po, cls=json_encoder)), } diff --git a/ajax_select/helpers.py b/ajax_select/helpers.py index 08bc94013a..07f33873fa 100644 --- a/ajax_select/helpers.py +++ b/ajax_select/helpers.py @@ -45,14 +45,14 @@ class TheForm(superclass): class Meta: exclude = [] - setattr(Meta, 'model', model) + Meta.model = model if hasattr(superclass, 'Meta'): if hasattr(superclass.Meta, 'fields'): - setattr(Meta, 'fields', superclass.Meta.fields) + Meta.fields = superclass.Meta.fields if hasattr(superclass.Meta, 'exclude'): - setattr(Meta, 'exclude', superclass.Meta.exclude) + Meta.exclude = superclass.Meta.exclude if hasattr(superclass.Meta, 'widgets'): - setattr(Meta, 'widgets', superclass.Meta.widgets) + Meta.widgets = superclass.Meta.widgets for model_fieldname, channel in fieldlist.items(): f = make_ajax_field(model, model_fieldname, channel, show_help_text) @@ -83,9 +83,7 @@ def make_ajax_field(related_model, fieldname_on_model, channel, show_help_text=F Returns: (AutoCompleteField, AutoCompleteSelectField, AutoCompleteSelectMultipleField): field """ - from ajax_select.fields import AutoCompleteField, \ - AutoCompleteSelectMultipleField, \ - AutoCompleteSelectField + from ajax_select.fields import AutoCompleteField, AutoCompleteSelectField, AutoCompleteSelectMultipleField field = related_model._meta.get_field(fieldname_on_model) if 'label' not in kwargs: diff --git a/ajax_select/registry.py b/ajax_select/registry.py index 4774be5501..c95bcd7430 100644 --- a/ajax_select/registry.py +++ b/ajax_select/registry.py @@ -11,6 +11,7 @@ class LookupChannelRegistry: This includes any installed apps that contain lookup.py modules (django 1.7+) and any lookups that are explicitly declared in `settings.AJAX_LOOKUP_CHANNELS` """ + _registry = {} def load_channels(self): @@ -18,9 +19,9 @@ def load_channels(self): Called when loading the application. Cannot be called a second time, (eg. for testing) as Django will not re-import and re-register anything. """ - autodiscover_modules('lookups') + autodiscover_modules("lookups") - if hasattr(settings, 'AJAX_LOOKUP_CHANNELS'): + if hasattr(settings, "AJAX_LOOKUP_CHANNELS"): self.register(settings.AJAX_LOOKUP_CHANNELS) def register(self, lookup_specs): @@ -54,9 +55,10 @@ def get(self, channel): try: lookup_spec = self._registry[channel] - except KeyError: + except KeyError as e: raise ImproperlyConfigured( - f"No ajax_select LookupChannel named {channel!r} is registered.") + f"No ajax_select LookupChannel named {channel!r} is registered." + ) from e if (type(lookup_spec) is type) and issubclass(lookup_spec, LookupChannel): return lookup_spec() @@ -68,12 +70,12 @@ def get(self, channel): elif isinstance(lookup_spec, dict): # 'channel' : dict(model='app.model', search_field='title' ) # generate a simple channel dynamically - return self.make_channel(lookup_spec['model'], lookup_spec['search_field']) + return self.make_channel(lookup_spec["model"], lookup_spec["search_field"]) elif isinstance(lookup_spec, tuple): # a tuple # 'channel' : ('app.module','LookupClass') # from app.module load LookupClass and instantiate - lookup_module = __import__(lookup_spec[0], {}, {}, ['']) + lookup_module = __import__(lookup_spec[0], {}, {}, [""]) lookup_class = getattr(lookup_module, lookup_spec[1]) return lookup_class() else: @@ -92,6 +94,7 @@ def make_channel(self, app_model, arg_search_field): LookupChannel """ from ajax_select import LookupChannel + app_label, model_name = app_model.split(".") class MadeLookupChannel(LookupChannel): @@ -127,7 +130,7 @@ def format_item(self): def _wrapper(lookup_class): if not channel: - raise ValueError('Lookup Channel must have a channel name') + raise ValueError("Lookup Channel must have a channel name") registry.register({channel: lookup_class}) diff --git a/ajax_select/static/ajax_select/js/ajax_select.js b/ajax_select/static/ajax_select/js/ajax_select.js index 2ec3474c0f..4750ff68d6 100644 --- a/ajax_select/static/ajax_select/js/ajax_select.js +++ b/ajax_select/static/ajax_select/js/ajax_select.js @@ -1,42 +1,42 @@ -(function($) { - - $.fn.autocompleteselect = function(options) { - return this.each(function() { +(function ($) { + $.fn.autocompleteselect = function (options) { + return this.each(function () { var id = this.id, - $this = $(this), - $text = $('#' + id + '_text'), - $deck = $('#' + id + '_on_deck'); + $this = $(this), + $text = $("#" + id + "_text"), + $deck = $("#" + id + "_on_deck"); function receiveResult(event, ui) { if ($this.val()) { kill(); } $this.val(ui.item.pk); - $text.val(''); + $text.val(""); addKiller(ui.item.repr, ui.item.pk); - $deck.trigger('added', [ui.item.pk, ui.item]); - $this.trigger('change'); + $deck.trigger("added", [ui.item.pk, ui.item]); + $this.trigger("change"); return false; } function addKiller(repr, pk) { - var killId = 'kill_' + pk + id, - killButton = '
'; + var killId = "kill_" + pk + id, + killButton = + ' '; if (repr) { $deck.empty(); - $deck.append('