././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7678764 django-select2-7.10.0/0000755000175100001710000000000000000000000013740 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/CONTRIBUTING.rst0000644000175100001710000000135200000000000016402 0ustar00runnerdockerContributing ============ This package uses the pyTest test runner. To run the tests locally simply run:: python setup.py test If you need to the development dependencies installed of you local IDE, you can run:: python setup.py develop Documentation pull requests welcome. The Sphinx documentation can be compiled via:: python setup.py build_sphinx Bug reports welcome, even more so if they include a correct patch. Much more so if you start your patch by adding a failing unit test, and correct the code until zero unit tests fail. The list of supported Django and Python version can be found in the CI suite setup. Please make sure to verify that none of the linters or tests failed, before you submit a patch for review. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/LICENSE0000644000175100001710000000205700000000000014751 0ustar00runnerdockerMIT License Copyright (c) 2017 Johannes Hoppe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/MANIFEST.in0000644000175100001710000000023100000000000015472 0ustar00runnerdockerinclude django_select2/static/django_select2/django_select2.js include django_select2/static/django_select2/django_select2.css prune .github exclude .* ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7678764 django-select2-7.10.0/PKG-INFO0000644000175100001710000000405200000000000015036 0ustar00runnerdockerMetadata-Version: 2.1 Name: django-select2 Version: 7.10.0 Summary: Select2 option fields for Django Home-page: https://github.com/codingjoe/django-select2 Author: Johannes Hoppe Author-email: info@johanneshoppe.com License: MIT Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Framework :: Django Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.1 Classifier: Framework :: Django :: 4.0 Provides-Extra: test License-File: LICENSE ============== Django-Select2 ============== |version| |coverage| |license| This is a `Django`_ integration of `Select2`_. The app includes Select2 driven Django Widgets. Documentation ------------- Documentation available at https://django-select2.readthedocs.io/. .. note:: Django's admin comes with builtin support for Select2 via the `autocomplete_fields`_ feature. .. _Django: https://www.djangoproject.com/ .. _Select2: https://select2.org/ .. _autocomplete_fields: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields .. |version| image:: https://img.shields.io/pypi/v/Django-Select2.svg :target: https://pypi.python.org/pypi/Django-Select2/ .. |coverage| image:: https://codecov.io/gh/codingjoe/django-select2/branch/master/graph/badge.svg :target: https://codecov.io/gh/codingjoe/django-select2 .. |license| image:: https://img.shields.io/badge/license-APL2-blue.svg :target: https://raw.githubusercontent.com/codingjoe/django-select2/master/LICENSE.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/README.rst0000644000175100001710000000203000000000000015422 0ustar00runnerdocker============== Django-Select2 ============== |version| |coverage| |license| This is a `Django`_ integration of `Select2`_. The app includes Select2 driven Django Widgets. Documentation ------------- Documentation available at https://django-select2.readthedocs.io/. .. note:: Django's admin comes with builtin support for Select2 via the `autocomplete_fields`_ feature. .. _Django: https://www.djangoproject.com/ .. _Select2: https://select2.org/ .. _autocomplete_fields: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields .. |version| image:: https://img.shields.io/pypi/v/Django-Select2.svg :target: https://pypi.python.org/pypi/Django-Select2/ .. |coverage| image:: https://codecov.io/gh/codingjoe/django-select2/branch/master/graph/badge.svg :target: https://codecov.io/gh/codingjoe/django-select2 .. |license| image:: https://img.shields.io/badge/license-APL2-blue.svg :target: https://raw.githubusercontent.com/codingjoe/django-select2/master/LICENSE.txt ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/django_select2/0000755000175100001710000000000000000000000016623 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/__init__.py0000644000175100001710000000050500000000000020734 0ustar00runnerdocker""" This is a Django_ integration of Select2_. The application includes Select2 driven Django Widgets and Form Fields. .. _Django: https://www.djangoproject.com/ .. _Select2: https://select2.org/ """ from django import get_version if get_version() < "3.2": default_app_config = "django_select2.apps.Select2AppConfig" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/apps.py0000644000175100001710000000032500000000000020140 0ustar00runnerdocker"""Django application configuration.""" from django.apps import AppConfig class Select2AppConfig(AppConfig): """Django application configuration.""" name = "django_select2" verbose_name = "Select2" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/cache.py0000644000175100001710000000104500000000000020240 0ustar00runnerdocker""" Shared memory across multiple machines to the heavy AJAX lookups. Select2 uses django.core.cache_ to share fields across multiple threads and even machines. Select2 uses the cache backend defined in the setting ``SELECT2_CACHE_BACKEND`` [default=``default``]. It is advised to always setup a separate cache server for Select2. .. _django.core.cache: https://docs.djangoproject.com/en/dev/topics/cache/ """ from django.core.cache import caches from .conf import settings __all__ = ("cache",) cache = caches[settings.SELECT2_CACHE_BACKEND] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/conf.py0000644000175100001710000001346700000000000020135 0ustar00runnerdocker"""Settings for Django-Select2.""" from appconf import AppConf from django.conf import settings # NOQA __all__ = ("settings", "Select2Conf") class Select2Conf(AppConf): """Settings for Django-Select2.""" LIB_VERSION = "4.0.12" """Version of the Select2 library.""" CACHE_BACKEND = "default" """ Django-Select2 uses Django's cache to sure a consistent state across multiple machines. Example of settings.py:: CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } }, 'select2': { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/2", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } } # Set the cache backend to select2 SELECT2_CACHE_BACKEND = 'select2' .. tip:: To ensure a consistent state across all you machines you need to user a consistent external cache backend like Memcached, Redis or a database. .. note:: Should you have copied the example configuration please make sure you have Redis setup. It's recommended to run a separate Redis server in a production environment. .. note:: The timeout of select2's caching backend determines how long a browser session can last. Once widget is dropped from the cache the json response view will return a 404. """ CACHE_PREFIX = "select2_" """ If you caching backend does not support multiple databases you can isolate select2 using the cache prefix setting. It has set `select2_` as a default value, which you can change if needed. """ JS = "https://cdnjs.cloudflare.com/ajax/libs/select2/{version}/js/select2.min.js".format( version=LIB_VERSION ) """ The URI for the Select2 JS file. By default this points to the Cloudflare CDN. If you want to select the version of the JS library used, or want to serve it from the local 'static' resources, add a line to your settings.py like so:: SELECT2_JS = 'assets/js/select2.min.js' If you provide your own JS and would not like Django-Select2 to load any, change this setting to a blank string like so:: SELECT2_JS = '' .. tip:: Change this setting to a local asset in your development environment to develop without an Internet connection. """ CSS = "https://cdnjs.cloudflare.com/ajax/libs/select2/{version}/css/select2.min.css".format( version=LIB_VERSION ) """ The URI for the Select2 CSS file. By default this points to the Cloudflare CDN. If you want to select the version of the library used, or want to serve it from the local 'static' resources, add a line to your settings.py like so:: SELECT2_CSS = 'assets/css/select2.css' If you want to add more css (usually used in select2 themes), add a line in settings.py like this:: SELECT2_CSS = [ 'assets/css/select2.css', 'assets/css/select2-theme.css', ] If you provide your own CSS and would not like Django-Select2 to load any, change this setting to a blank string like so:: SELECT2_CSS = '' .. tip:: Change this setting to a local asset in your development environment to develop without an Internet connection. """ THEME = "default" """ Select2 supports custom themes using the theme option so you can style Select2 to match the rest of your application. .. tip:: When using other themes, you may need use select2 css and theme css. """ I18N_PATH = ( "https://cdnjs.cloudflare.com/ajax/libs/select2/{version}/js/i18n".format( version=LIB_VERSION ) ) """ The base URI for the Select2 i18n files. By default this points to the Cloudflare CDN. If you want to select the version of the I18N library used, or want to serve it from the local 'static' resources, add a line to your settings.py like so:: SELECT2_I18N_PATH = 'assets/js/i18n' .. tip:: Change this setting to a local asset in your development environment to develop without an Internet connection. """ I18N_AVAILABLE_LANGUAGES = [ "ar", "az", "bg", "ca", "cs", "da", "de", "el", "en", "es", "et", "eu", "fa", "fi", "fr", "gl", "he", "hi", "hr", "hu", "id", "is", "it", "ja", "km", "ko", "lt", "lv", "mk", "ms", "nb", "nl", "pl", "pt-BR", "pt", "ro", "ru", "sk", "sr-Cyrl", "sr", "sv", "th", "tr", "uk", "vi", "zh-CN", "zh-TW", ] """ List of available translations. List of ISO 639-1 language codes that are supported by Select2. If currently set language code (e.g. using the HTTP ``Accept-Language`` header) is in this list, Django-Select2 will use the language code to create load the proper translation. The full path for the language file consists of:: from django.utils import translations full_path = "{i18n_path}/{language_code}.js".format( i18n_path=settings.DJANGO_SELECT2_I18N, language_code=translations.get_language(), ) ``settings.DJANGO_SELECT2_I18N`` refers to :attr:`.I18N_PATH`. """ class Meta: """Prefix for all Django-Select2 settings.""" prefix = "SELECT2" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/forms.py0000644000175100001710000004741100000000000020332 0ustar00runnerdocker""" Django-Select2 Widgets. These components are responsible for rendering the necessary HTML data markups. Since this whole package is to render choices using Select2 JavaScript library, hence these components are meant to be used with choice fields. Widgets are generally of two types: 1. **Light** -- They are not meant to be used when there are too many options, say, in thousands. This is because all those options would have to be pre-rendered onto the page and JavaScript would be used to search through them. Said that, they are also one the most easiest to use. They are a drop-in-replacement for Django's default select widgets. 2(a). **Heavy** -- They are suited for scenarios when the number of options are large and need complex queries (from maybe different sources) to get the options. This dynamic fetching of options undoubtedly requires Ajax communication with the server. Django-Select2 includes a helper JS file which is included automatically, so you need not worry about writing any Ajax related JS code. Although on the server side you do need to create a view specifically to respond to the queries. 2(b). **Model** -- Model-widgets are a further specialized versions of Heavies. These do not require views to serve Ajax requests. When they are instantiated, they register themselves with one central view which handles Ajax requests for them. Heavy and Model widgets have respectively the word 'Heavy' and 'Model' in their name. Light widgets are normally named, i.e. there is no 'Light' word in their names. .. inheritance-diagram:: django_select2.forms :parts: 1 """ import operator import uuid from functools import reduce from itertools import chain from pickle import PicklingError # nosec import django from django import forms from django.contrib.admin.widgets import SELECT2_TRANSLATIONS, AutocompleteMixin from django.core import signing from django.db.models import Q from django.forms.models import ModelChoiceIterator from django.urls import reverse from django.utils.translation import get_language from .cache import cache from .conf import settings if django.VERSION < (4, 0): from django.contrib.admin.utils import ( lookup_needs_distinct as lookup_spawns_duplicates, ) else: from django.contrib.admin.utils import lookup_spawns_duplicates class Select2Mixin: """ The base mixin of all Select2 widgets. This mixin is responsible for rendering the necessary data attributes for select2 as well as adding the static form media. """ css_class_name = "django-select2" theme = None empty_label = "" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.i18n_name = SELECT2_TRANSLATIONS.get(get_language()) def build_attrs(self, base_attrs, extra_attrs=None): """Add select2 data attributes.""" default_attrs = { "lang": self.i18n_name, "data-minimum-input-length": 0, "data-theme": self.theme or settings.SELECT2_THEME, } if self.is_required: default_attrs["data-allow-clear"] = "false" else: default_attrs["data-allow-clear"] = "true" default_attrs["data-placeholder"] = self.empty_label or "" default_attrs.update(base_attrs) attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs) if "class" in attrs: attrs["class"] += " " + self.css_class_name else: attrs["class"] = self.css_class_name return attrs def optgroups(self, name, value, attrs=None): """Add empty option for clearable selects.""" if not self.is_required and not self.allow_multiple_selected: self.choices = list(chain([("", "")], self.choices)) return super().optgroups(name, value, attrs=attrs) @property def media(self): """ Construct Media as a dynamic property. .. Note:: For more information visit https://docs.djangoproject.com/en/stable/topics/forms/media/#media-as-a-dynamic-property """ select2_js = [settings.SELECT2_JS] if settings.SELECT2_JS else [] select2_css = settings.SELECT2_CSS if settings.SELECT2_CSS else [] if isinstance(select2_css, str): select2_css = [select2_css] i18n_file = [] if self.i18n_name in settings.SELECT2_I18N_AVAILABLE_LANGUAGES: i18n_file = [f"{settings.SELECT2_I18N_PATH}/{self.i18n_name}.js"] return forms.Media( js=select2_js + i18n_file + ["django_select2/django_select2.js"], css={"screen": select2_css + ["django_select2/django_select2.css"]}, ) class Select2AdminMixin: """Select2 mixin that uses Django's own select template.""" css_class_name = "admin-autocomplete" theme = "admin-autocomplete" @property def media(self): return forms.Media( js=Select2Mixin().media._js, css=AutocompleteMixin(None, None).media._css, ) class Select2TagMixin: """Mixin to add select2 tag functionality.""" def build_attrs(self, base_attrs, extra_attrs=None): """Add select2's tag attributes.""" default_attrs = { "data-minimum-input-length": 1, "data-tags": "true", "data-token-separators": '[",", " "]', } default_attrs.update(base_attrs) return super().build_attrs(default_attrs, extra_attrs=extra_attrs) class Select2Widget(Select2Mixin, forms.Select): """ Select2 drop in widget. Example usage:: class MyModelForm(forms.ModelForm): class Meta: model = MyModel fields = ('my_field', ) widgets = { 'my_field': Select2Widget } or:: class MyForm(forms.Form): my_choice = forms.ChoiceField(widget=Select2Widget) """ class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple): """ Select2 drop in widget for multiple select. Works just like :class:`.Select2Widget` but for multi select. """ class Select2TagWidget(Select2TagMixin, Select2Mixin, forms.SelectMultiple): """ Select2 drop in widget for for tagging. Example for :class:`.django.contrib.postgres.fields.ArrayField`:: class MyWidget(Select2TagWidget): def value_from_datadict(self, data, files, name): values = super().value_from_datadict(data, files, name) return ",".join(values) def optgroups(self, name, value, attrs=None): values = value[0].split(',') if value[0] else [] selected = set(values) subgroup = [self.create_option(name, v, v, selected, i) for i, v in enumerate(values)] return [(None, subgroup, 0)] """ class HeavySelect2Mixin: """Mixin that adds select2's AJAX options and registers itself on Django's cache.""" dependent_fields = {} def __init__(self, attrs=None, choices=(), **kwargs): """ Return HeavySelect2Mixin. Args: data_view (str): URL pattern name data_url (str): URL dependent_fields (dict): Dictionary of dependent parent fields. The value of the dependent field will be passed as to :func:`.filter_queryset`. It can be used to further restrict the search results. For example, a city widget could be dependent on a country. Key is a name of a field in a form. Value is a name of a field in a model (used in `queryset`). """ super().__init__(attrs, choices) self.uuid = str(uuid.uuid4()) self.field_id = signing.dumps(self.uuid) self.data_view = kwargs.pop("data_view", None) self.data_url = kwargs.pop("data_url", None) dependent_fields = kwargs.pop("dependent_fields", None) if dependent_fields is not None: self.dependent_fields = dict(dependent_fields) if not (self.data_view or self.data_url): raise ValueError('You must ether specify "data_view" or "data_url".') self.userGetValTextFuncName = kwargs.pop("userGetValTextFuncName", "null") def get_url(self): """Return URL from instance or by reversing :attr:`.data_view`.""" if self.data_url: return self.data_url return reverse(self.data_view) def build_attrs(self, base_attrs, extra_attrs=None): """Set select2's AJAX attributes.""" default_attrs = { "data-ajax--url": self.get_url(), "data-ajax--cache": "true", "data-ajax--type": "GET", "data-minimum-input-length": 2, } if self.dependent_fields: default_attrs["data-select2-dependent-fields"] = " ".join( self.dependent_fields ) default_attrs.update(base_attrs) attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs) attrs["data-field_id"] = self.field_id attrs["class"] += " django-select2-heavy" return attrs def render(self, *args, **kwargs): """Render widget and register it in Django's cache.""" output = super().render(*args, **kwargs) self.set_to_cache() return output def _get_cache_key(self): return "%s%s" % (settings.SELECT2_CACHE_PREFIX, self.uuid) def set_to_cache(self): """ Add widget object to Django's cache. You may need to overwrite this method, to pickle all information that is required to serve your JSON response view. """ try: cache.set(self._get_cache_key(), {"widget": self, "url": self.get_url()}) except (PicklingError, AttributeError): msg = 'You need to overwrite "set_to_cache" or ensure that %s is serialisable.' raise NotImplementedError(msg % self.__class__.__name__) class HeavySelect2Widget(HeavySelect2Mixin, Select2Widget): """ Select2 widget with AJAX support that registers itself to Django's Cache. Usage example:: class MyWidget(HeavySelect2Widget): data_view = 'my_view_name' or:: class MyForm(forms.Form): my_field = forms.ChoiceField( widget=HeavySelect2Widget( data_url='/url/to/json/response' ) ) """ class HeavySelect2MultipleWidget(HeavySelect2Mixin, Select2MultipleWidget): """Select2 multi select widget similar to :class:`.HeavySelect2Widget`.""" class HeavySelect2TagWidget(HeavySelect2Mixin, Select2TagWidget): """Select2 tag widget.""" # Auto Heavy widgets class ModelSelect2Mixin: """Widget mixin that provides attributes and methods for :class:`.AutoResponseView`.""" model = None queryset = None search_fields = [] """ Model lookups that are used to filter the QuerySet. Example:: search_fields = [ 'title__icontains', ] """ max_results = 25 """Maximal results returned by :class:`.AutoResponseView`.""" @property def empty_label(self): if isinstance(self.choices, ModelChoiceIterator): return self.choices.field.empty_label return "" def __init__(self, *args, **kwargs): """ Overwrite class parameters if passed as keyword arguments. Args: model (django.db.models.Model): Model to select choices from. queryset (django.db.models.query.QuerySet): QuerySet to select choices from. search_fields (list): List of model lookup strings. max_results (int): Max. JsonResponse view page size. """ self.model = kwargs.pop("model", self.model) self.queryset = kwargs.pop("queryset", self.queryset) self.search_fields = kwargs.pop("search_fields", self.search_fields) self.max_results = kwargs.pop("max_results", self.max_results) defaults = {"data_view": "django_select2:auto-json"} defaults.update(kwargs) super().__init__(*args, **defaults) def set_to_cache(self): """ Add widget's attributes to Django's cache. Split the QuerySet, to not pickle the result set. """ queryset = self.get_queryset() cache.set( self._get_cache_key(), { "queryset": [queryset.none(), queryset.query], "cls": self.__class__, "search_fields": tuple(self.search_fields), "max_results": int(self.max_results), "url": str(self.get_url()), "dependent_fields": dict(self.dependent_fields), }, ) def filter_queryset(self, request, term, queryset=None, **dependent_fields): """ Return QuerySet filtered by search_fields matching the passed term. Args: request (django.http.request.HttpRequest): The request is being passed from the JSON view and can be used to dynamically alter the response queryset. term (str): Search term queryset (django.db.models.query.QuerySet): QuerySet to select choices from. **dependent_fields: Dependent fields and their values. If you want to inherit from ModelSelect2Mixin and later call to this method, be sure to pop everything from keyword arguments that is not a dependent field. Returns: QuerySet: Filtered QuerySet """ if queryset is None: queryset = self.get_queryset() search_fields = self.get_search_fields() select = Q() use_distinct = False if search_fields and term: for bit in term.split(): or_queries = [Q(**{orm_lookup: bit}) for orm_lookup in search_fields] select &= reduce(operator.or_, or_queries) or_queries = [Q(**{orm_lookup: term}) for orm_lookup in search_fields] select |= reduce(operator.or_, or_queries) use_distinct |= any( lookup_spawns_duplicates(queryset.model._meta, search_spec) for search_spec in search_fields ) if dependent_fields: select &= Q(**dependent_fields) use_distinct |= any( lookup_spawns_duplicates(queryset.model._meta, search_spec) for search_spec in dependent_fields.keys() ) if use_distinct: return queryset.filter(select).distinct() return queryset.filter(select) def get_queryset(self): """ Return QuerySet based on :attr:`.queryset` or :attr:`.model`. Returns: QuerySet: QuerySet of available choices. """ if self.queryset is not None: queryset = self.queryset elif hasattr(self.choices, "queryset"): queryset = self.choices.queryset elif self.model is not None: queryset = self.model._default_manager.all() else: raise NotImplementedError( "%(cls)s is missing a QuerySet. Define " "%(cls)s.model, %(cls)s.queryset, or override " "%(cls)s.get_queryset()." % {"cls": self.__class__.__name__} ) return queryset def get_search_fields(self): """Return list of lookup names.""" if self.search_fields: return self.search_fields raise NotImplementedError( '%s, must implement "search_fields".' % self.__class__.__name__ ) def optgroups(self, name, value, attrs=None): """Return only selected options and set QuerySet from `ModelChoicesIterator`.""" default = (None, [], 0) groups = [default] has_selected = False selected_choices = {str(v) for v in value} if not self.is_required and not self.allow_multiple_selected: default[1].append(self.create_option(name, "", "", False, 0)) if not isinstance(self.choices, ModelChoiceIterator): return super().optgroups(name, value, attrs=attrs) selected_choices = { c for c in selected_choices if c not in self.choices.field.empty_values } field_name = self.choices.field.to_field_name or "pk" query = Q(**{"%s__in" % field_name: selected_choices}) for obj in self.choices.queryset.filter(query): option_value = self.choices.choice(obj)[0] option_label = self.label_from_instance(obj) selected = str(option_value) in value and ( has_selected is False or self.allow_multiple_selected ) if selected is True and has_selected is False: has_selected = True index = len(default[1]) subgroup = default[1] subgroup.append( self.create_option( name, option_value, option_label, selected_choices, index ) ) return groups def label_from_instance(self, obj): """ Return option label representation from instance. Can be overridden to change the representation of each choice. Example usage:: class MyWidget(ModelSelect2Widget): def label_from_instance(obj): return str(obj.title).upper() Args: obj (django.db.models.Model): Instance of Django Model. Returns: str: Option label. """ return str(obj) class ModelSelect2Widget(ModelSelect2Mixin, HeavySelect2Widget): """ Select2 drop in model select widget. Example usage:: class MyWidget(ModelSelect2Widget): search_fields = [ 'title__icontains', ] class MyModelForm(forms.ModelForm): class Meta: model = MyModel fields = ('my_field', ) widgets = { 'my_field': MyWidget, } or:: class MyForm(forms.Form): my_choice = forms.ChoiceField( widget=ModelSelect2Widget( model=MyOtherModel, search_fields=['title__icontains'] ) ) .. tip:: The ModelSelect2(Multiple)Widget will try to get the QuerySet from the fields choices. Therefore you don't need to define a QuerySet, if you just drop in the widget for a ForeignKey field. """ class ModelSelect2MultipleWidget(ModelSelect2Mixin, HeavySelect2MultipleWidget): """ Select2 drop in model multiple select widget. Works just like :class:`.ModelSelect2Widget` but for multi select. """ class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget): """ Select2 model widget with tag support. This it not a simple drop in widget. It requires to implement you own :func:`.value_from_datadict` that adds missing tags to you QuerySet. Example:: class MyModelSelect2TagWidget(ModelSelect2TagWidget): queryset = MyModel.objects.all() def value_from_datadict(self, data, files, name): '''Create objects for given non-pimary-key values. Return list of all primary keys.''' values = set(super().value_from_datadict(data, files, name)) # This may only work for MyModel, if MyModel has title field. # You need to implement this method yourself, to ensure proper object creation. pks = self.queryset.filter(**{'pk__in': list(values)}).values_list('pk', flat=True) pks = set(map(str, pks)) cleaned_values = list(values) for val in values - pks: cleaned_values.append(self.queryset.create(title=val).pk) return cleaned_values """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/django_select2/static/0000755000175100001710000000000000000000000020112 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/django_select2/static/django_select2/0000755000175100001710000000000000000000000022775 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/static/django_select2/django_select2.css0000644000175100001710000000006600000000000026374 0ustar00runnerdocker.change-form select.django-select2 { width: 20em; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/static/django_select2/django_select2.js0000644000175100001710000000375200000000000026225 0ustar00runnerdocker/* global define, jQuery */ (function (factory) { if (typeof define === 'function' && define.amd) { define(['jquery'], factory) } else if (typeof module === 'object' && module.exports) { module.exports = factory(require('jquery')) } else { // Browser globals factory(jQuery) } }(function ($) { 'use strict' var init = function ($element, options) { $element.select2(options) } var initHeavy = function ($element, options) { var settings = $.extend({ ajax: { data: function (params) { var result = { term: params.term, page: params.page, field_id: $element.data('field_id') } var dependentFields = $element.data('select2-dependent-fields') if (dependentFields) { dependentFields = dependentFields.trim().split(/\s+/) $.each(dependentFields, function (i, dependentField) { result[dependentField] = $('[name=' + dependentField + ']', $element.closest('form')).val() }) } return result }, processResults: function (data, page) { return { results: data.results, pagination: { more: data.more } } } } }, options) $element.select2(settings) } $.fn.djangoSelect2 = function (options) { var settings = $.extend({}, options) $.each(this, function (i, element) { var $element = $(element) if ($element.hasClass('django-select2-heavy')) { initHeavy($element, settings) } else { init($element, settings) } $element.on('select2:select', function (e) { var name = $(e.currentTarget).attr('name') $('[data-select2-dependent-fields=' + name + ']').each(function () { $(this).val('').trigger('change') }) }) }) return this } $(function () { $('.django-select2').djangoSelect2() }) return $.fn.djangoSelect2 })) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/urls.py0000644000175100001710000000062000000000000020160 0ustar00runnerdocker""" Django-Select2 URL configuration. Add `django_select` to your ``urlconf`` **if** you use any 'Model' fields:: from django.urls import path path('select2/', include('django_select2.urls')), """ from django.urls import path from .views import AutoResponseView app_name = "django_select2" urlpatterns = [ path("fields/auto.json", AutoResponseView.as_view(), name="auto-json"), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/django_select2/views.py0000644000175100001710000000637500000000000020345 0ustar00runnerdocker"""JSONResponse views for model widgets.""" from django.core import signing from django.core.signing import BadSignature from django.http import Http404, JsonResponse from django.views.generic.list import BaseListView from .cache import cache from .conf import settings class AutoResponseView(BaseListView): """ View that handles requests from heavy model widgets. The view only supports HTTP's GET method. """ def get(self, request, *args, **kwargs): """ Return a :class:`.django.http.JsonResponse`. Example:: { 'results': [ { 'text': "foo", 'id': 123 } ], 'more': true } """ self.widget = self.get_widget_or_404() self.term = kwargs.get("term", request.GET.get("term", "")) self.object_list = self.get_queryset() context = self.get_context_data() return JsonResponse( { "results": [ {"text": self.widget.label_from_instance(obj), "id": obj.pk} for obj in context["object_list"] ], "more": context["page_obj"].has_next(), } ) def get_queryset(self): """Get QuerySet from cached widget.""" kwargs = { model_field_name: self.request.GET.get(form_field_name) for form_field_name, model_field_name in self.widget.dependent_fields.items() } kwargs.update( { f"{model_field_name}__in": self.request.GET.getlist( f"{form_field_name}[]", [] ) for form_field_name, model_field_name in self.widget.dependent_fields.items() } ) return self.widget.filter_queryset( self.request, self.term, self.queryset, **{k: v for k, v in kwargs.items() if v}, ) def get_paginate_by(self, queryset): """Paginate response by size of widget's `max_results` parameter.""" return self.widget.max_results def get_widget_or_404(self): """ Get and return widget from cache. Raises: Http404: If if the widget can not be found or no id is provided. Returns: ModelSelect2Mixin: Widget from cache. """ field_id = self.kwargs.get("field_id", self.request.GET.get("field_id", None)) if not field_id: raise Http404('No "field_id" provided.') try: key = signing.loads(field_id) except BadSignature: raise Http404('Invalid "field_id".') else: cache_key = "%s%s" % (settings.SELECT2_CACHE_PREFIX, key) widget_dict = cache.get(cache_key) if widget_dict is None: raise Http404("field_id not found") if widget_dict.pop("url") != self.request.path: raise Http404("field_id was issued for the view.") qs, qs.query = widget_dict.pop("queryset") self.queryset = qs.all() widget_dict["queryset"] = self.queryset widget_cls = widget_dict.pop("cls") return widget_cls(**widget_dict) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/django_select2.egg-info/0000755000175100001710000000000000000000000020315 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917535.0 django-select2-7.10.0/django_select2.egg-info/PKG-INFO0000644000175100001710000000405200000000000021413 0ustar00runnerdockerMetadata-Version: 2.1 Name: django-select2 Version: 7.10.0 Summary: Select2 option fields for Django Home-page: https://github.com/codingjoe/django-select2 Author: Johannes Hoppe Author-email: info@johanneshoppe.com License: MIT Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Framework :: Django Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.1 Classifier: Framework :: Django :: 4.0 Provides-Extra: test License-File: LICENSE ============== Django-Select2 ============== |version| |coverage| |license| This is a `Django`_ integration of `Select2`_. The app includes Select2 driven Django Widgets. Documentation ------------- Documentation available at https://django-select2.readthedocs.io/. .. note:: Django's admin comes with builtin support for Select2 via the `autocomplete_fields`_ feature. .. _Django: https://www.djangoproject.com/ .. _Select2: https://select2.org/ .. _autocomplete_fields: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields .. |version| image:: https://img.shields.io/pypi/v/Django-Select2.svg :target: https://pypi.python.org/pypi/Django-Select2/ .. |coverage| image:: https://codecov.io/gh/codingjoe/django-select2/branch/master/graph/badge.svg :target: https://codecov.io/gh/codingjoe/django-select2 .. |license| image:: https://img.shields.io/badge/license-APL2-blue.svg :target: https://raw.githubusercontent.com/codingjoe/django-select2/master/LICENSE.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917535.0 django-select2-7.10.0/django_select2.egg-info/SOURCES.txt0000644000175100001710000000250100000000000022177 0ustar00runnerdockerCONTRIBUTING.rst LICENSE MANIFEST.in README.rst package.json set_version.py setup.cfg setup.py django_select2/__init__.py django_select2/apps.py django_select2/cache.py django_select2/conf.py django_select2/forms.py django_select2/urls.py django_select2/views.py django_select2.egg-info/PKG-INFO django_select2.egg-info/SOURCES.txt django_select2.egg-info/dependency_links.txt django_select2.egg-info/requires.txt django_select2.egg-info/top_level.txt django_select2/static/django_select2/django_select2.css django_select2/static/django_select2/django_select2.js docs/CONTRIBUTING.rst docs/conf.py docs/django_select2.rst docs/extra.rst docs/index.rst example/README.md example/manage.py example/requirements.txt example/example/__init__.py example/example/asgi.py example/example/forms.py example/example/models.py example/example/settings.py example/example/urls.py example/example/views.py example/example/wsgi.py example/example/migrations/0001_initial.py example/example/migrations/__init__.py example/example/templates/example/book_form.html tests/__init__.py tests/conftest.py tests/test_cache.py tests/test_forms.py tests/test_views.py tests/testapp/__init__.py tests/testapp/forms.py tests/testapp/manage.py tests/testapp/models.py tests/testapp/settings.py tests/testapp/urls.py tests/testapp/views.py tests/testapp/templates/form.html././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917535.0 django-select2-7.10.0/django_select2.egg-info/dependency_links.txt0000644000175100001710000000000100000000000024363 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917535.0 django-select2-7.10.0/django_select2.egg-info/requires.txt0000644000175100001710000000012300000000000022711 0ustar00runnerdockerdjango>=2.2 django-appconf>=0.6.0 [test] pytest pytest-cov pytest-django selenium ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917535.0 django-select2-7.10.0/django_select2.egg-info/top_level.txt0000644000175100001710000000001700000000000023045 0ustar00runnerdockerdjango_select2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/docs/0000755000175100001710000000000000000000000014670 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/docs/CONTRIBUTING.rst0000644000175100001710000000004100000000000017324 0ustar00runnerdocker.. include:: ../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/docs/conf.py0000644000175100001710000000330500000000000016170 0ustar00runnerdockerimport os import pathlib import sys from pkg_resources import get_distribution BASE_DIR = pathlib.Path(__file__).resolve(strict=True).parent.parent # This is needed since django_select2 requires django model modules # and those modules assume that django settings is configured and # have proper DB settings. # Using this we give a proper environment with working django settings. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testapp.settings") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, str(BASE_DIR / "tests" / "testapp")) sys.path.insert(0, str(BASE_DIR)) project = "Django-Select2" author = "Johannes Hoppe" copyright = "2017-2020, Johannes Hoppe" release = get_distribution("django_select2").version version = ".".join(release.split(".")[:2]) master_doc = "index" # default in Sphinx v2 extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.inheritance_diagram", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx.ext.doctest", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "django": ( "https://docs.djangoproject.com/en/stable/", "https://docs.djangoproject.com/en/stable/_objects/", ), } autodoc_default_flags = ["members", "show-inheritance"] autodoc_member_order = "bysource" inheritance_graph_attrs = dict(rankdir="TB") inheritance_node_attrs = dict( shape="rect", fontsize=14, fillcolor="gray90", color="gray30", style="filled" ) inheritance_edge_attrs = dict(penwidth=0.75) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/docs/django_select2.rst0000644000175100001710000000457200000000000020315 0ustar00runnerdockerAPI Documentation ================= Configuration ------------- .. automodule:: django_select2.conf :members: :undoc-members: :show-inheritance: Widgets ------- .. automodule:: django_select2.forms :members: :undoc-members: :show-inheritance: URLs ---- .. automodule:: django_select2.urls :members: :undoc-members: :show-inheritance: Views ----- .. automodule:: django_select2.views :members: :undoc-members: :show-inheritance: Cache ----- .. automodule:: django_select2.cache :members: :undoc-members: :show-inheritance: JavaScript ---------- DjangoSelect2 handles the initialization of select2 fields automatically. Just include ``{{ form.media.js }}`` in your template before the closing ``body`` tag. That's it! If you insert forms after page load or if you want to handle the initialization yourself, DjangoSelect2 provides a jQuery plugin, replacing and enhancing the Select2 plugin. It will handle both normal and heavy fields. Simply call ``djangoSelect2(options)`` on your select fields.:: $('.django-select2').djangoSelect2(); You can pass see `Select2 options `_ if needed:: $('.django-select2').djangoSelect2({placeholder: 'Select an option'}); Please replace all your ``.select2`` invocations with the here provided ``.djangoSelect2``. Security & Authentication ------------------------- Security is important. Therefore make sure to read and understand what the security measures in place and their limitations. Set up a separate cache. If you have a public form that uses a model widget make sure to setup a separate cache database for Select2. An attacker could constantly reload your site and fill up the select2 cache. Having a separate cache allows you to limit the effect to select2 only. You might want to add a secure select2 JSON endpoint for data you don't want to be accessible to the general public. Doing so is easy:: class UserSelect2View(LoginRequiredMixin, AutoResponseView): pass class UserSelect2WidgetMixin(object): def __init__(self, *args, **kwargs): kwargs['data_view'] = 'user-select2-view' super(UserSelect2WidgetMixin, self).__init__(*args, **kwargs) class MySecretWidget(UserSelect2WidgetMixin, ModelSelect2Widget): model = MySecretModel search_fields = ['title__icontains'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/docs/extra.rst0000644000175100001710000000667300000000000016561 0ustar00runnerdockerExtra ===== Chained select2 --------------- Suppose you have an address form where a user should choose a Country and a City. When the user selects a country we want to show only cities belonging to that country. So the one selector depends on another one. Models `````` Here are our two models: .. code-block:: python class Country(models.Model): name = models.CharField(max_length=255) class City(models.Model): name = models.CharField(max_length=255) country = models.ForeignKey('Country', related_name="cities") Customizing a Form `````````````````` Lets link two widgets via a *dependent_fields* dictionary. The key represents the name of the field in the form. The value represents the name of the field in the model (used in `queryset`). .. code-block:: python :emphasize-lines: 17 class AddressForm(forms.Form): country = forms.ModelChoiceField( queryset=Country.objects.all(), label=u"Country", widget=ModelSelect2Widget( model=Country, search_fields=['name__icontains'], ) ) city = forms.ModelChoiceField( queryset=City.objects.all(), label=u"City", widget=ModelSelect2Widget( model=City, search_fields=['name__icontains'], dependent_fields={'country': 'country'}, max_results=500, ) ) Interdependent select2 ---------------------- Also you may want not to restrict the user to which field should be selected first. Instead you want to suggest to the user options for any select2 depending of his selection in another one. Customize the form in a manner: .. code-block:: python :emphasize-lines: 7 class AddressForm(forms.Form): country = forms.ModelChoiceField( queryset=Country.objects.all(), label=u"Country", widget=ModelSelect2Widget( search_fields=['name__icontains'], dependent_fields={'city': 'cities'}, ) ) city = forms.ModelChoiceField( queryset=City.objects.all(), label=u"City", widget=ModelSelect2Widget( search_fields=['name__icontains'], dependent_fields={'country': 'country'}, max_results=500, ) ) Take attention to country's dependent_fields. The value of 'city' is 'cities' because of related name used in a filter condition `cities` which differs from widget field name `city`. .. caution:: Be aware of using interdependent select2 in parent-child relation. When a child is selected, you are restricted to change parent (only one value is available). Probably you should let the user reset the child first to release parent select2. Multi-dependent select2 ----------------------- Furthermore you may want to filter options on two or more select2 selections (some code is dropped for clarity): .. code-block:: python :emphasize-lines: 14 class SomeForm(forms.Form): field1 = forms.ModelChoiceField( widget=ModelSelect2Widget( ) ) field2 = forms.ModelChoiceField( widget=ModelSelect2Widget( ) ) field3 = forms.ModelChoiceField( widget=ModelSelect2Widget( dependent_fields={'field1': 'field1', 'field2': 'field2'}, ) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/docs/index.rst0000644000175100001710000001210500000000000016530 0ustar00runnerdocker.. include:: ../README.rst Installation ------------ Install ``django-select2``:: python3 -m pip install django-select2 Add ``django_select2`` to your ``INSTALLED_APPS`` in your project settings. Add ``django_select`` to your URL root configuration: .. code-block:: python from django.urls import include, path urlpatterns = [ # … other patterns path("select2/", include("django_select2.urls")), # … other patterns ] ``django-select2`` requires a cache backend which is **persistent** across all application servers.. **This means that the** :class:`.DummyCache` **backend will not work!** The default cache backend is :class:`.LocMemCache`, which is persistent across a single node. For projects with a single application server this will work fine, however you will run into issues when you scale up into multiple servers. Below is an example setup using Redis, which is a solution that works for multi-server setups: Make sure you have a Redis server up and running:: # Debian sudo apt-get install redis-server # macOS brew install redis # install Redis python client python3 -m pip install django-redis Next, add the cache configuration to your ``settings.py`` as follows: .. code-block:: python CACHES = { # … default cache config and others "select2": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/2", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } } # Tell select2 which cache configuration to use: SELECT2_CACHE_BACKEND = "select2" .. note:: A custom timeout for your cache backend, will serve as an indirect session limit. Auto select fields will stop working after, once the cache has expired. It's recommended to use a dedicated cache database with an adequate cache replacement policy such as LRU, FILO, etc. External Dependencies --------------------- - jQuery is not included in the package since it is expected that in most scenarios this would already be available. Quick Start ----------- Here is a quick example to get you started: First make sure you followed the installation instructions above. Once everything is setup, let's start with a simple example. We have the following model: .. code-block:: python # models.py from django.conf import settings from django.db import models class Book(models.Model): author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) co_authors = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='co_authored_by') Next, we create a model form with custom Select2 widgets. .. code-block:: python # forms.py from django import forms from django_select2 import forms as s2forms from . import models class AuthorWidget(s2forms.ModelSelect2Widget): search_fields = [ "username__icontains", "email__icontains", ] class CoAuthorsWidget(s2forms.ModelSelect2MultipleWidget): search_fields = [ "username__icontains", "email__icontains", ] class BookForm(forms.ModelForm): class Meta: model = models.Book fields = "__all__" widgets = { "author": AuthorWidget, "co_authors": CoAuthorsWidget, } A simple class based view will do, to render your form: .. code-block:: python # views.py from django.views import generic from . import forms, models class BookCreateView(generic.CreateView): model = models.Book form_class = forms.BookForm success_url = "/" Make sure to add the view to your ``urls.py``: .. code-block:: python # urls.py from django.urls import include, path from . import views urlpatterns = [ # … other patterns path("select2/", include("django_select2.urls")), # … other patterns path("book/create", views.BookCreateView.as_view(), name="book-create"), ] Finally, we need a little template, ``myapp/templates/myapp/book_form.html`` .. code-block:: HTML Create Book {{ form.media.css }}

Create a new Book

{% csrf_token %} {{ form.as_p }}
{{ form.media.js }} Done - enjoy the wonders of Select2! Changelog --------- See `Github releases`_. .. _Github releases: https://github.com/codingjoe/django-select2/releases All Contents ============ Contents: .. toctree:: :maxdepth: 2 :glob: django_select2 extra CONTRIBUTING Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/example/0000755000175100001710000000000000000000000015373 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/README.md0000644000175100001710000000105200000000000016650 0ustar00runnerdocker# Sample App Before you start, make sure you have Redis installed, since we need if for caching purposes. ``` # Debian sudo apt-get install redis-server -y # macOS brew install redis ``` Now, to run the sample app, please execute: ``` git clone https://github.com/codingjoe/django-select2.git cd django-select2/example python3 -m pip install -r requirements.txt python3 manage.py migrate python3 manage.py createsuperuser # follow the instructions to create a superuser python3 manage.py runserver # follow the instructions and open your browser ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/example/example/0000755000175100001710000000000000000000000017026 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/__init__.py0000644000175100001710000000000000000000000021125 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/asgi.py0000644000175100001710000000060700000000000020326 0ustar00runnerdocker""" ASGI config for example project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") application = get_asgi_application() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/forms.py0000644000175100001710000000104600000000000020527 0ustar00runnerdockerfrom django import forms from django_select2 import forms as s2forms from . import models class AuthorWidget(s2forms.ModelSelect2Widget): search_fields = ["username__istartswith", "email__icontains"] class CoAuthorsWidget(s2forms.ModelSelect2MultipleWidget): search_fields = ["username__istartswith", "email__icontains"] class BookForm(forms.ModelForm): class Meta: model = models.Book fields = "__all__" widgets = { "author": AuthorWidget, "co_authors": CoAuthorsWidget, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/example/example/migrations/0000755000175100001710000000000000000000000021202 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/migrations/0001_initial.py0000644000175100001710000000226700000000000023654 0ustar00runnerdocker# Generated by Django 3.1a1 on 2020-05-23 17:06 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name="Book", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "author", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, ), ), ( "co_authors", models.ManyToManyField( related_name="co_authored_by", to=settings.AUTH_USER_MODEL ), ), ], ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/migrations/__init__.py0000644000175100001710000000000000000000000023301 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/models.py0000644000175100001710000000043400000000000020664 0ustar00runnerdockerfrom django.conf import settings from django.db import models class Book(models.Model): author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) co_authors = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name="co_authored_by" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/settings.py0000644000175100001710000000673000000000000021246 0ustar00runnerdocker""" Django settings for example project. Generated by 'django-admin startproject' using Django 3.1a1. For more information on this file, see https://docs.djangoproject.com/en/dev/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/dev/ref/settings/ """ from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve(strict=True).parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "kstexlapcf3lucx@47mmxsu9-9eixia+6n97aw)4$qo&!laxad" # nosec # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django_select2", "example", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "example.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] WSGI_APPLICATION = "example.wsgi.application" # Database # https://docs.djangoproject.com/en/dev/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } # Password validation # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/dev/howto/static-files/ STATIC_URL = "/static/" CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, }, "select2": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/2", "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, }, } SELECT2_CACHE_BACKEND = "select2" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7638762 django-select2-7.10.0/example/example/templates/0000755000175100001710000000000000000000000021024 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7678764 django-select2-7.10.0/example/example/templates/example/0000755000175100001710000000000000000000000022457 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/templates/example/book_form.html0000644000175100001710000000070500000000000025324 0ustar00runnerdocker Create Book {{ form.media.css }}

Create a new Book

{% csrf_token %} {{ form.as_p }}
{{ form.media.js }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/urls.py0000644000175100001710000000041400000000000020364 0ustar00runnerdockerfrom django.contrib import admin from django.urls import include, path from . import views urlpatterns = [ path("", views.BookCreateView.as_view(), name="book-create"), path("select2/", include("django_select2.urls")), path("admin/", admin.site.urls), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/views.py0000644000175100001710000000027000000000000020534 0ustar00runnerdockerfrom django.views import generic from . import forms, models class BookCreateView(generic.CreateView): model = models.Book form_class = forms.BookForm success_url = "/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/example/wsgi.py0000644000175100001710000000060700000000000020354 0ustar00runnerdocker""" WSGI config for example project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") application = get_wsgi_application() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/manage.py0000755000175100001710000000122700000000000017202 0ustar00runnerdocker#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/example/requirements.txt0000644000175100001710000000002300000000000020652 0ustar00runnerdocker-e .. django-redis ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/package.json0000644000175100001710000000145700000000000016235 0ustar00runnerdocker{ "name": "django-select2", "version": "0.0.0", "description": "This is a Django integration of Select2.", "files": [ "django_select2/static/**/*" ], "main": "django_select2/static/django_select2/django_select2.js", "directories": { "doc": "docs", "test": "tests" }, "scripts": { "test": "standard" }, "repository": { "type": "git", "url": "git://github.com/codingjoe/django-select2.git" }, "keywords": [ "django", "select2" ], "author": "Johannes Hoppe", "license": "Apache-2.0", "bugs": { "url": "https://github.com/codingjoe/django-select2/issues" }, "homepage": "https://github.com/codingjoe/django-select2#readme", "peerDependencies": { "select2": "*", "jquery": ">= 1.2" }, "devDependencies": { "standard": "*" } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/set_version.py0000755000175100001710000000052600000000000016660 0ustar00runnerdocker#!/usr/bin/env python3 """Set the version in NPM's package.json to match the git tag.""" import json import os if __name__ == "__main__": with open("package.json", "r+") as f: data = json.load(f) f.seek(0) data["version"] = os.environ["GITHUB_REF"].rsplit("/")[-1] json.dump(data, f) f.truncate() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7678764 django-select2-7.10.0/setup.cfg0000644000175100001710000000360200000000000015562 0ustar00runnerdocker[metadata] name = django-select2 author = Johannes Hoppe author_email = info@johanneshoppe.com description = Select2 option fields for Django long_description = file: README.rst url = https://github.com/codingjoe/django-select2 license = MIT license_file = LICENSE classifier = Development Status :: 5 - Production/Stable Environment :: Web Environment Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.1 Framework :: Django :: 4.0 [options] include_package_data = True packages = django_select2 install_requires = django>=2.2 django-appconf>=0.6.0 setup_requires = setuptools_scm sphinx pytest-runner tests_require = pytest pytest-cov pytest-django selenium [options.extras_require] test = pytest pytest-cov pytest-django selenium [bdist_wheel] universal = 1 [bdist_rpm] requires = python-django-appconf >= 2.0 python-django-appconf >= 0.6 [aliases] test = pytest [build_sphinx] source-dir = docs build-dir = docs/_build [tool:pytest] addopts = tests --doctest-glob='*.rst' --doctest-modules --cov=django_select2 DJANGO_SETTINGS_MODULE = tests.testapp.settings [flake8] max-line-length = 88 select = C,E,F,W,B,B950 ignore = E203, E501, W503 exclude = venv,.tox,.eggs [pydocstyle] add-ignore = D1 [isort] atomic = true line_length = 88 multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True known_first_party = django_select2, tests default_section = THIRDPARTY combine_as_imports = true [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/setup.py0000755000175100001710000000015000000000000015451 0ustar00runnerdocker#!/usr/bin/env python from setuptools import setup setup(name="django-select2", use_scm_version=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7678764 django-select2-7.10.0/tests/0000755000175100001710000000000000000000000015102 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/__init__.py0000644000175100001710000000000000000000000017201 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/conftest.py0000644000175100001710000000324500000000000017305 0ustar00runnerdockerimport random import string import pytest from selenium import webdriver from selenium.common.exceptions import WebDriverException def random_string(n): return "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(n) ) def random_name(n): words = ( "".join(random.choice(string.ascii_lowercase + " ") for _ in range(n)) .strip() .split(" ") ) return "-".join([x.capitalize() for x in words]) @pytest.fixture(scope="session") def driver(): chrome_options = webdriver.ChromeOptions() chrome_options.headless = True try: b = webdriver.Chrome(options=chrome_options) except WebDriverException as e: pytest.skip(str(e)) else: yield b b.quit() @pytest.fixture def genres(db): from .testapp.models import Genre return Genre.objects.bulk_create( [Genre(pk=pk, title=random_string(50)) for pk in range(100)] ) @pytest.fixture def artists(db): from .testapp.models import Artist return Artist.objects.bulk_create( [Artist(pk=pk, title=random_string(50)) for pk in range(100)] ) @pytest.fixture def countries(db): from .testapp.models import Country return Country.objects.bulk_create( [Country(pk=pk, name=random_name(random.randint(10, 20))) for pk in range(10)] ) @pytest.fixture def cities(db, countries): from .testapp.models import City return City.objects.bulk_create( [ City( pk=pk, name=random_name(random.randint(5, 15)), country=random.choice(countries), ) for pk in range(100) ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/test_cache.py0000644000175100001710000000021400000000000017553 0ustar00runnerdockerdef test_default_cache(): from django_select2.cache import cache cache.set("key", "value") assert cache.get("key") == "value" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/test_forms.py0000644000175100001710000007673400000000000017662 0ustar00runnerdockerimport json import os from collections.abc import Iterable import pytest from django.contrib.admin.widgets import SELECT2_TRANSLATIONS from django.db.models import QuerySet from django.urls import reverse from django.utils import translation from django.utils.encoding import force_str from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait from django_select2.cache import cache from django_select2.conf import settings from django_select2.forms import ( HeavySelect2MultipleWidget, HeavySelect2Widget, ModelSelect2TagWidget, ModelSelect2Widget, Select2Widget, Select2AdminMixin, ) from tests.testapp import forms from tests.testapp.forms import ( NUMBER_CHOICES, HeavySelect2MultipleWidgetForm, TitleModelSelect2Widget, ) from tests.testapp.models import Artist, City, Country, Genre, Groupie class TestSelect2Mixin: url = reverse("select2_widget") form = forms.AlbumSelect2WidgetForm() multiple_form = forms.AlbumSelect2MultipleWidgetForm() widget_cls = Select2Widget def test_initial_data(self, genres): genre = genres[0] form = self.form.__class__(initial={"primary_genre": genre.pk}) assert str(genre) in form.as_p() def test_initial_form_class(self): widget = self.widget_cls(attrs={"class": "my-class"}) assert "my-class" in widget.render("name", None) assert "django-select2" in widget.render("name", None) @pytest.mark.parametrize("code,name", SELECT2_TRANSLATIONS.items()) def test_lang_attr(self, code, name): translation.activate(code) widget = self.widget_cls() assert f'lang="{name}"' in widget.render("name", None) def test_allow_clear(self, db): required_field = self.form.fields["artist"] assert required_field.required is True assert 'data-allow-clear="true"' not in required_field.widget.render( "artist", None ) assert 'data-allow-clear="false"' in required_field.widget.render( "artist", None ) assert '' not in required_field.widget.render( "artist", None ) not_required_field = self.form.fields["primary_genre"] assert not_required_field.required is False assert 'data-allow-clear="true"' in not_required_field.widget.render( "primary_genre", None ) assert 'data-allow-clear="false"' not in not_required_field.widget.render( "primary_genre", None ) assert "data-placeholder" in not_required_field.widget.render( "primary_genre", None ) assert '' in not_required_field.widget.render( "primary_genre", None ) def test_no_js_error(self, db, live_server, driver): driver.get(live_server + self.url) with pytest.raises(NoSuchElementException): error = driver.find_element_by_xpath("//body[@JSError]") pytest.fail(error.get_attribute("JSError")) def test_selecting(self, db, live_server, driver): driver.get(live_server + self.url) with pytest.raises(NoSuchElementException): driver.find_element_by_css_selector(".select2-results") elem = driver.find_element_by_css_selector(".select2-selection") elem.click() results = driver.find_element_by_css_selector(".select2-results") assert results.is_displayed() is True elem = results.find_element_by_css_selector(".select2-results__option") elem.click() with pytest.raises(NoSuchElementException): error = driver.find_element_by_xpath("//body[@JSError]") pytest.fail(error.get_attribute("JSError")) def test_data_url(self): with pytest.raises(ValueError): HeavySelect2Widget() widget = HeavySelect2Widget(data_url="/foo/bar") assert widget.get_url() == "/foo/bar" def test_empty_option(self, db): # Empty options is only required for single selects # https://select2.github.io/options.html#allowClear single_select = self.form.fields["primary_genre"] assert single_select.required is False assert '' in single_select.widget.render( "primary_genre", None ) multiple_select = self.multiple_form.fields["featured_artists"] assert multiple_select.required is False assert multiple_select.widget.allow_multiple_selected output = multiple_select.widget.render("featured_artists", None) assert '' not in output assert 'data-placeholder=""' in output def test_i18n(self): translation.activate("de") assert tuple(Select2Widget().media._js) == ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js", f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/de.js", "django_select2/django_select2.js", ) translation.activate("en") assert tuple(Select2Widget().media._js) == ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js", f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/en.js", "django_select2/django_select2.js", ) translation.activate("00") assert tuple(Select2Widget().media._js) == ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js", "django_select2/django_select2.js", ) translation.activate("sr-cyrl") assert tuple(Select2Widget().media._js) == ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js", f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/sr-Cyrl.js", "django_select2/django_select2.js", ) pytest.importorskip("django", minversion="2.0.4") translation.activate("zh-hans") assert tuple(Select2Widget().media._js) == ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js", f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/zh-CN.js", "django_select2/django_select2.js", ) translation.activate("zh-hant") assert tuple(Select2Widget().media._js) == ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js", f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/zh-TW.js", "django_select2/django_select2.js", ) def test_theme_setting(self, settings): settings.SELECT2_THEME = "classic" widget = self.widget_cls() assert 'data-theme="classic"' in widget.render("name", None) class TestSelect2AdminMixin: def test_media(self): translation.activate("en") assert tuple(Select2AdminMixin().media._js) == ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js", f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/en.js", "django_select2/django_select2.js", ) assert dict(Select2AdminMixin().media._css) == { "screen": [ "admin/css/vendor/select2/select2.min.css", "admin/css/autocomplete.css", ] } class TestSelect2MixinSettings: def test_default_media(self): sut = Select2Widget() result = sut.media.render() assert ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js" in result ) assert ( f"https://cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/css/select2.min.css" in result ) assert "django_select2/django_select2.js" in result def test_js_setting(self, settings): settings.SELECT2_JS = "alternate.js" sut = Select2Widget() result = sut.media.render() assert "alternate.js" in result assert "django_select2/django_select2.js" in result def test_empty_js_setting(self, settings): settings.SELECT2_JS = "" sut = Select2Widget() result = sut.media.render() assert "django_select2/django_select2.js" in result def test_css_setting(self, settings): settings.SELECT2_CSS = "alternate.css" sut = Select2Widget() result = sut.media.render() assert "alternate.css" in result def test_empty_css_setting(self, settings): settings.SELECT2_CSS = "" sut = Select2Widget() result = sut.media.render() assert "/select2.css" not in result def test_multiple_css_setting(self, settings): settings.SELECT2_CSS = ["select2.css", "select2-theme.css"] sut = Select2Widget() result = sut.media.render() assert "select2.css" in result assert "select2-theme.css" in result class TestHeavySelect2Mixin(TestSelect2Mixin): url = reverse("heavy_select2_widget") form = forms.HeavySelect2WidgetForm(initial={"primary_genre": 1}) widget_cls = HeavySelect2Widget def test_initial_data(self): assert "One" in self.form.as_p() def test_initial_form_class(self): widget = self.widget_cls(data_view="heavy_data_1", attrs={"class": "my-class"}) assert "my-class" in widget.render("name", None) assert "django-select2" in widget.render("name", None) assert "django-select2-heavy" in widget.render("name", None), widget.render( "name", None ) @pytest.mark.parametrize("code,name", SELECT2_TRANSLATIONS.items()) def test_lang_attr(self, code, name): translation.activate(code) widget = self.widget_cls(data_view="heavy_data_1") assert f'lang="{name}"' in widget.render("name", None) def test_selected_option(self, db): not_required_field = self.form.fields["primary_genre"] assert not_required_field.required is False assert ( '' in not_required_field.widget.render("primary_genre", 1) or '' in not_required_field.widget.render("primary_genre", 1) ), (not_required_field.widget.render("primary_genre", 1)) def test_many_selected_option(self, db, genres): field = HeavySelect2MultipleWidgetForm().fields["genres"] field.widget.choices = NUMBER_CHOICES widget_output = field.widget.render("genres", [1, 2]) selected_option = ( ''.format( pk=1, value="One" ) ) selected_option_a = ''.format( pk=1, value="One" ) selected_option2 = ( ''.format( pk=2, value="Two" ) ) selected_option2a = ''.format( pk=2, value="Two" ) assert ( selected_option in widget_output or selected_option_a in widget_output ), widget_output assert selected_option2 in widget_output or selected_option2a in widget_output def test_multiple_widgets(self, db, live_server, driver): driver.get(live_server + self.url) with pytest.raises(NoSuchElementException): driver.find_element_by_css_selector(".select2-results") elem1, elem2 = driver.find_elements_by_css_selector(".select2-selection") elem1.click() search1 = driver.find_element_by_css_selector(".select2-search__field") search1.send_keys("fo") result1 = ( WebDriverWait(driver, 60) .until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li:first-child") ) ) .text ) elem2.click() search2 = driver.find_element_by_css_selector(".select2-search__field") search2.send_keys("fo") result2 = ( WebDriverWait(driver, 60) .until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li:first-child") ) ) .text ) assert result1 != result2 with pytest.raises(NoSuchElementException): error = driver.find_element_by_xpath("//body[@JSError]") pytest.fail(error.get_attribute("JSError")) def test_get_url(self): widget = self.widget_cls(data_view="heavy_data_1", attrs={"class": "my-class"}) assert isinstance(widget.get_url(), str) def test_can_not_pickle(self): widget = self.widget_cls(data_view="heavy_data_1", attrs={"class": "my-class"}) class NoPickle: pass widget.no_pickle = NoPickle() with pytest.raises(NotImplementedError): widget.set_to_cache() def test_theme_setting(self, settings): settings.SELECT2_THEME = "classic" widget = self.widget_cls(data_view="heavy_data_1") assert 'data-theme="classic"' in widget.render("name", None) class TestModelSelect2Mixin(TestHeavySelect2Mixin): form = forms.AlbumModelSelect2WidgetForm(initial={"primary_genre": 1}) multiple_form = forms.ArtistModelSelect2MultipleWidgetForm() def test_initial_data(self, genres): genre = genres[0] form = self.form.__class__(initial={"primary_genre": genre.pk}) assert str(genre) in form.as_p() def test_label_from_instance_initial(self, genres): genre = genres[0] genre.title = genre.title.lower() genre.save() form = self.form.__class__(initial={"primary_genre": genre.pk}) assert genre.title not in form.as_p(), form.as_p() assert genre.title.upper() in form.as_p() @pytest.fixture(autouse=True) def genres(self, genres): return genres def test_selected_option(self, db, genres): genre = genres[0] genre2 = genres[1] not_required_field = self.form.fields["primary_genre"] assert not_required_field.required is False widget_output = not_required_field.widget.render("primary_genre", genre.pk) selected_option = ( ''.format( pk=genre.pk, value=force_str(genre) ) ) selected_option_a = ''.format( pk=genre.pk, value=force_str(genre) ) unselected_option = ''.format( pk=genre2.pk, value=force_str(genre2) ) assert ( selected_option in widget_output or selected_option_a in widget_output ), widget_output assert unselected_option not in widget_output def test_selected_option_label_from_instance(self, db, genres): genre = genres[0] genre.title = genre.title.lower() genre.save() field = self.form.fields["primary_genre"] widget_output = field.widget.render("primary_genre", genre.pk) def get_selected_options(genre): return ( ''.format( pk=genre.pk, value=force_str(genre) ), ''.format( pk=genre.pk, value=force_str(genre) ), ) assert all(o not in widget_output for o in get_selected_options(genre)) genre.title = genre.title.upper() assert any(o in widget_output for o in get_selected_options(genre)) def test_get_queryset(self): widget = ModelSelect2Widget() with pytest.raises(NotImplementedError): widget.get_queryset() widget.model = Genre assert isinstance(widget.get_queryset(), QuerySet) widget.model = None widget.queryset = Genre.objects.all() assert isinstance(widget.get_queryset(), QuerySet) def test_tag_attrs_Select2Widget(self): widget = Select2Widget() output = widget.render("name", "value") assert 'data-minimum-input-length="0"' in output def test_custom_tag_attrs_Select2Widget(self): widget = Select2Widget(attrs={"data-minimum-input-length": "3"}) output = widget.render("name", "value") assert 'data-minimum-input-length="3"' in output def test_tag_attrs_ModelSelect2Widget(self): widget = ModelSelect2Widget( queryset=Genre.objects.all(), search_fields=["title__icontains"] ) output = widget.render("name", "value") assert 'data-minimum-input-length="2"' in output def test_tag_attrs_ModelSelect2TagWidget(self): widget = ModelSelect2TagWidget( queryset=Genre.objects.all(), search_fields=["title__icontains"] ) output = widget.render("name", "value") assert 'data-minimum-input-length="2"' in output def test_tag_attrs_HeavySelect2Widget(self): widget = HeavySelect2Widget(data_url="/foo/bar/") output = widget.render("name", "value") assert 'data-minimum-input-length="2"' in output def test_custom_tag_attrs_ModelSelect2Widget(self): widget = ModelSelect2Widget( queryset=Genre.objects.all(), search_fields=["title__icontains"], attrs={"data-minimum-input-length": "3"}, ) output = widget.render("name", "value") assert 'data-minimum-input-length="3"' in output def test_get_search_fields(self): widget = ModelSelect2Widget() with pytest.raises(NotImplementedError): widget.get_search_fields() widget.search_fields = ["title__icontains"] assert isinstance(widget.get_search_fields(), Iterable) assert all(isinstance(x, str) for x in widget.get_search_fields()) def test_filter_queryset(self, genres): widget = TitleModelSelect2Widget(queryset=Genre.objects.all()) assert widget.filter_queryset(None, genres[0].title[:3]).exists() widget = TitleModelSelect2Widget( search_fields=["title__icontains"], queryset=Genre.objects.all() ) qs = widget.filter_queryset( None, " ".join([genres[0].title[:3], genres[0].title[3:]]) ) assert qs.exists() def test_filter_queryset__empty(self, genres): widget = TitleModelSelect2Widget(queryset=Genre.objects.all()) assert widget.filter_queryset(None, genres[0].title[:3]).exists() widget = TitleModelSelect2Widget( search_fields=["title__icontains"], queryset=Genre.objects.all() ) qs = widget.filter_queryset(None, "") assert qs.exists() def test_filter_queryset__startswith(self, genres): genre = Genre.objects.create(title="Space Genre") widget = TitleModelSelect2Widget(queryset=Genre.objects.all()) assert widget.filter_queryset(None, genre.title).exists() widget = TitleModelSelect2Widget( search_fields=["title__istartswith"], queryset=Genre.objects.all() ) qs = widget.filter_queryset(None, "Space Gen") assert qs.exists() qs = widget.filter_queryset(None, "Gen") assert not qs.exists() def test_filter_queryset__contains(self, genres): genre = Genre.objects.create(title="Space Genre") widget = TitleModelSelect2Widget(queryset=Genre.objects.all()) assert widget.filter_queryset(None, genre.title).exists() widget = TitleModelSelect2Widget( search_fields=["title__contains"], queryset=Genre.objects.all() ) qs = widget.filter_queryset(None, "Space Gen") assert qs.exists() qs = widget.filter_queryset(None, "NOT Gen") assert not qs.exists(), "contains works even if all bits match" def test_filter_queryset__multiple_fields(self, genres): genre = Genre.objects.create(title="Space Genre") widget = TitleModelSelect2Widget(queryset=Genre.objects.all()) assert widget.filter_queryset(None, genre.title).exists() widget = TitleModelSelect2Widget( search_fields=[ "title__startswith", "title__endswith", ], queryset=Genre.objects.all(), ) qs = widget.filter_queryset(None, "Space") assert qs.exists() qs = widget.filter_queryset(None, "Genre") assert qs.exists() def test_model_kwarg(self): widget = ModelSelect2Widget(model=Genre, search_fields=["title__icontains"]) genre = Genre.objects.last() result = widget.filter_queryset(None, genre.title) assert result.exists() def test_queryset_kwarg(self): widget = ModelSelect2Widget( queryset=Genre.objects.all(), search_fields=["title__icontains"] ) genre = Genre.objects.last() result = widget.filter_queryset(None, genre.title) assert result.exists() def test_ajax_view_registration(self, client): widget = ModelSelect2Widget( queryset=Genre.objects.all(), search_fields=["title__icontains"] ) widget.render("name", "value") url = reverse("django_select2:auto-json") genre = Genre.objects.last() response = client.get( url, data=dict(field_id=widget.field_id, term=genre.title) ) assert response.status_code == 200, response.content data = json.loads(response.content.decode("utf-8")) assert data["results"] assert genre.pk in [result["id"] for result in data["results"]] def test_render(self): widget = ModelSelect2Widget(queryset=Genre.objects.all()) widget.render("name", "value") cached_widget = cache.get(widget._get_cache_key()) assert cached_widget["max_results"] == widget.max_results assert cached_widget["search_fields"] == tuple(widget.search_fields) qs = widget.get_queryset() assert isinstance(cached_widget["queryset"][0], qs.__class__) assert str(cached_widget["queryset"][1]) == str(qs.query) def test_get_url(self): widget = ModelSelect2Widget( queryset=Genre.objects.all(), search_fields=["title__icontains"] ) assert isinstance(widget.get_url(), str) def test_custom_to_field_name(self): the_best_band_in_the_world = Artist.objects.create(title="Take That") groupie = Groupie.objects.create(obsession=the_best_band_in_the_world) form = forms.GroupieForm(instance=groupie) assert '' in form.as_p() def test_empty_label(self, db): # Empty options is only required for single selects # https://select2.github.io/options.html#allowClear single_select = self.form.fields["primary_genre"] single_select.empty_label = "Hello World" assert single_select.required is False assert 'data-placeholder="Hello World"' in single_select.widget.render( "primary_genre", None ) class TestHeavySelect2TagWidget(TestHeavySelect2Mixin): def test_tag_attrs(self): widget = ModelSelect2TagWidget( queryset=Genre.objects.all(), search_fields=["title__icontains"] ) output = widget.render("name", "value") assert 'data-minimum-input-length="2"' in output assert 'data-tags="true"' in output assert "data-token-separators" in output def test_custom_tag_attrs(self): widget = ModelSelect2TagWidget( queryset=Genre.objects.all(), search_fields=["title__icontains"], attrs={"data-minimum-input-length": "3"}, ) output = widget.render("name", "value") assert 'data-minimum-input-length="3"' in output class TestHeavySelect2MultipleWidget: url = reverse("heavy_select2_multiple_widget") form = forms.HeavySelect2MultipleWidgetForm() widget_cls = HeavySelect2MultipleWidget @pytest.mark.xfail( bool(os.environ.get("CI", False)), reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=1772", ) def test_widgets_selected_after_validation_error(self, db, live_server, driver): driver.get(live_server + self.url) WebDriverWait(driver, 3).until( expected_conditions.presence_of_element_located((By.ID, "id_title")) ) title = driver.find_element_by_id("id_title") title.send_keys("fo") genres, fartists = driver.find_elements_by_css_selector( ".select2-selection--multiple" ) genres.click() genres.send_keys("o") # results are Zero One Two Four # select second element - One driver.find_element_by_css_selector(".select2-results li:nth-child(2)").click() genres.submit() # there is a ValidationError raised, check for it errstring = ( WebDriverWait(driver, 3) .until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, "ul.errorlist li") ) ) .text ) assert errstring == "Title must have more than 3 characters." # genres should still have One as selected option result_title = driver.find_element_by_css_selector( ".select2-selection--multiple li" ).get_attribute("title") assert result_title == "One" class TestAddressChainedSelect2Widget: url = reverse("model_chained_select2_widget") form = forms.AddressChainedSelect2WidgetForm() def test_widgets_selected_after_validation_error( self, db, live_server, driver, countries, cities ): driver.get(live_server + self.url) WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-selection--single") ) ) ( country_container, city_container, city2_container, ) = driver.find_elements_by_css_selector(".select2-selection--single") # clicking city select2 lists all available cities city_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li") ) ) city_options = driver.find_elements_by_css_selector(".select2-results li") city_names_from_browser = {option.text for option in city_options} city_names_from_db = set(City.objects.values_list("name", flat=True)) assert len(city_names_from_browser) == City.objects.count() assert city_names_from_browser == city_names_from_db # selecting a country really does it country_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li:nth-child(2)") ) ) country_option = driver.find_element_by_css_selector( ".select2-results li:nth-child(2)" ) country_name = country_option.text country_option.click() assert country_name == country_container.text # clicking city select2 lists reduced list of cities belonging to the country city_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li") ) ) city_options = driver.find_elements_by_css_selector(".select2-results li") city_names_from_browser = {option.text for option in city_options} city_names_from_db = set( Country.objects.get(name=country_name).cities.values_list("name", flat=True) ) assert len(city_names_from_browser) != City.objects.count() assert city_names_from_browser == city_names_from_db # selecting a city reaaly does it city_option = driver.find_element_by_css_selector( ".select2-results li:nth-child(2)" ) city_name = city_option.text city_option.click() assert city_name == city_container.text # clicking country select2 lists reduced list to the only country available to the city country_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li") ) ) country_options = driver.find_elements_by_css_selector(".select2-results li") country_names_from_browser = {option.text for option in country_options} country_names_from_db = {City.objects.get(name=city_name).country.name} assert len(country_names_from_browser) != Country.objects.count() assert country_names_from_browser == country_names_from_db def test_dependent_fields_clear_after_change_parent( self, db, live_server, driver, countries, cities ): driver.get(live_server + self.url) ( country_container, city_container, city2_container, ) = driver.find_elements_by_css_selector(".select2-selection--single") # selecting a country really does it country_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li:nth-child(2)") ) ) country_option = driver.find_element_by_css_selector( ".select2-results li:nth-child(2)" ) country_name = country_option.text country_option.click() assert country_name == country_container.text # selecting a city2 city2_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li") ) ) city2_option = driver.find_element_by_css_selector( ".select2-results li:nth-child(2)" ) city2_name = city2_option.text city2_option.click() assert city2_name == city2_container.text # change a country country_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li:nth-child(3)") ) ) country_option = driver.find_element_by_css_selector( ".select2-results li:nth-child(3)" ) country_name = country_option.text country_option.click() assert country_name == country_container.text # check the value in city2 city2_container.click() WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located( (By.CSS_SELECTOR, ".select2-results li") ) ) assert city2_container.text == "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/test_views.py0000644000175100001710000000736300000000000017661 0ustar00runnerdockerimport json from django.utils.encoding import smart_str from django_select2.cache import cache from django_select2.forms import ModelSelect2Widget from tests.testapp.forms import AlbumModelSelect2WidgetForm, ArtistCustomTitleWidget from tests.testapp.models import Genre try: from django.urls import reverse except ImportError: from django.core.urlresolvers import reverse class TestAutoResponseView: def test_get(self, client, artists): artist = artists[0] form = AlbumModelSelect2WidgetForm() assert form.as_p() field_id = form.fields["artist"].widget.field_id url = reverse("django_select2:auto-json") response = client.get(url, {"field_id": field_id, "term": artist.title}) assert response.status_code == 200 data = json.loads(response.content.decode("utf-8")) assert data["results"] assert {"id": artist.pk, "text": smart_str(artist)} in data["results"] def test_no_field_id(self, client, artists): artist = artists[0] url = reverse("django_select2:auto-json") response = client.get(url, {"term": artist.title}) assert response.status_code == 404 def test_wrong_field_id(self, client, artists): artist = artists[0] url = reverse("django_select2:auto-json") response = client.get(url, {"field_id": 123, "term": artist.title}) assert response.status_code == 404 def test_field_id_not_found(self, client, artists): artist = artists[0] field_id = "not-exists" url = reverse("django_select2:auto-json") response = client.get(url, {"field_id": field_id, "term": artist.title}) assert response.status_code == 404 def test_pagination(self, genres, client): url = reverse("django_select2:auto-json") widget = ModelSelect2Widget( max_results=10, model=Genre, search_fields=["title__icontains"] ) widget.render("name", None) field_id = widget.field_id response = client.get(url, {"field_id": field_id, "term": ""}) assert response.status_code == 200 data = json.loads(response.content.decode("utf-8")) assert data["more"] is True response = client.get(url, {"field_id": field_id, "term": "", "page": 1000}) assert response.status_code == 404 response = client.get(url, {"field_id": field_id, "term": "", "page": "last"}) assert response.status_code == 200 data = json.loads(response.content.decode("utf-8")) assert data["more"] is False def test_label_from_instance(self, artists, client): url = reverse("django_select2:auto-json") form = AlbumModelSelect2WidgetForm() form.fields["artist"].widget = ArtistCustomTitleWidget() assert form.as_p() field_id = form.fields["artist"].widget.field_id artist = artists[0] response = client.get(url, {"field_id": field_id, "term": artist.title}) assert response.status_code == 200 data = json.loads(response.content.decode("utf-8")) assert data["results"] assert {"id": artist.pk, "text": smart_str(artist.title.upper())} in data[ "results" ] def test_url_check(self, client, artists): artist = artists[0] form = AlbumModelSelect2WidgetForm() assert form.as_p() field_id = form.fields["artist"].widget.field_id cache_key = form.fields["artist"].widget._get_cache_key() widget_dict = cache.get(cache_key) widget_dict["url"] = "yet/another/url" cache.set(cache_key, widget_dict) url = reverse("django_select2:auto-json") response = client.get(url, {"field_id": field_id, "term": artist.title}) assert response.status_code == 404 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7678764 django-select2-7.10.0/tests/testapp/0000755000175100001710000000000000000000000016562 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/__init__.py0000644000175100001710000000000000000000000020661 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/forms.py0000644000175100001710000001373300000000000020271 0ustar00runnerdockerfrom django import forms from django.utils.encoding import force_str from django_select2.forms import ( HeavySelect2MultipleWidget, HeavySelect2Widget, ModelSelect2MultipleWidget, ModelSelect2TagWidget, ModelSelect2Widget, Select2MultipleWidget, Select2Widget, ) from tests.testapp import models from tests.testapp.models import Album, City, Country class TitleSearchFieldMixin: search_fields = ["title__icontains", "pk__startswith"] class TitleModelSelect2Widget(TitleSearchFieldMixin, ModelSelect2Widget): pass class TitleModelSelect2MultipleWidget( TitleSearchFieldMixin, ModelSelect2MultipleWidget ): pass class GenreSelect2TagWidget(TitleSearchFieldMixin, ModelSelect2TagWidget): model = models.Genre def create_value(self, value): self.get_queryset().create(title=value) class ArtistCustomTitleWidget(ModelSelect2Widget): model = models.Artist search_fields = ["title__icontains"] def label_from_instance(self, obj): return force_str(obj.title).upper() class GenreCustomTitleWidget(ModelSelect2Widget): model = models.Genre search_fields = ["title__icontains"] def label_from_instance(self, obj): return force_str(obj.title).upper() class AlbumSelect2WidgetForm(forms.ModelForm): class Meta: model = models.Album fields = ( "artist", "primary_genre", ) widgets = { "artist": Select2Widget, "primary_genre": Select2Widget, } class AlbumSelect2MultipleWidgetForm(forms.ModelForm): class Meta: model = models.Album fields = ( "genres", "featured_artists", ) widgets = { "genres": Select2MultipleWidget, "featured_artists": Select2MultipleWidget, } class AlbumModelSelect2WidgetForm(forms.ModelForm): class Meta: model = models.Album fields = ( "artist", "primary_genre", ) widgets = { "artist": ArtistCustomTitleWidget(), "primary_genre": GenreCustomTitleWidget(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["primary_genre"].initial = 2 class AlbumModelSelect2MultipleWidgetRequiredForm(forms.ModelForm): class Meta: model = Album fields = ( "genres", "featured_artists", ) widgets = { "genres": TitleModelSelect2MultipleWidget, "featured_artists": TitleModelSelect2MultipleWidget, } class ArtistModelSelect2MultipleWidgetForm(forms.Form): title = forms.CharField(max_length=50) genres = forms.ModelMultipleChoiceField( widget=ModelSelect2MultipleWidget( queryset=models.Genre.objects.all(), search_fields=["title__icontains"], ), queryset=models.Genre.objects.all(), required=True, ) featured_artists = forms.ModelMultipleChoiceField( widget=ModelSelect2MultipleWidget( queryset=models.Artist.objects.all(), search_fields=["title__icontains"], ), queryset=models.Artist.objects.all(), required=False, ) NUMBER_CHOICES = [ (1, "One"), (2, "Two"), (3, "Three"), (4, "Four"), ] class Select2WidgetForm(forms.Form): number = forms.ChoiceField( widget=Select2Widget, choices=NUMBER_CHOICES, required=False ) class HeavySelect2WidgetForm(forms.Form): artist = forms.ChoiceField( widget=HeavySelect2Widget(data_view="heavy_data_1"), choices=NUMBER_CHOICES ) primary_genre = forms.ChoiceField( widget=HeavySelect2Widget(data_view="heavy_data_2"), required=False, choices=NUMBER_CHOICES, ) class HeavySelect2MultipleWidgetForm(forms.Form): title = forms.CharField(max_length=50) genres = forms.MultipleChoiceField( widget=HeavySelect2MultipleWidget( data_view="heavy_data_1", choices=NUMBER_CHOICES, attrs={"data-minimum-input-length": 0}, ), choices=NUMBER_CHOICES, ) featured_artists = forms.MultipleChoiceField( widget=HeavySelect2MultipleWidget( data_view="heavy_data_2", choices=NUMBER_CHOICES, attrs={"data-minimum-input-length": 0}, ), choices=NUMBER_CHOICES, required=False, ) def clean_title(self): if len(self.cleaned_data["title"]) < 3: raise forms.ValidationError("Title must have more than 3 characters.") return self.cleaned_data["title"] class ModelSelect2TagWidgetForm(forms.ModelForm): class Meta: model = Album fields = ["genres"] widgets = {"genres": GenreSelect2TagWidget} class AddressChainedSelect2WidgetForm(forms.Form): country = forms.ModelChoiceField( queryset=Country.objects.all(), label="Country", widget=ModelSelect2Widget( search_fields=["name__icontains"], max_results=500, dependent_fields={"city": "cities"}, attrs={"data-minimum-input-length": 0}, ), ) city = forms.ModelChoiceField( queryset=City.objects.all(), label="City", widget=ModelSelect2Widget( search_fields=["name__icontains"], dependent_fields={"country": "country"}, max_results=500, attrs={"data-minimum-input-length": 0}, ), ) city2 = forms.ModelChoiceField( queryset=City.objects.all(), label="City not Interdependent", widget=ModelSelect2Widget( search_fields=["name__icontains"], dependent_fields={"country": "country"}, max_results=500, attrs={"data-minimum-input-length": 0}, ), ) class GroupieForm(forms.ModelForm): class Meta: model = models.Groupie fields = "__all__" widgets = {"obsession": ArtistCustomTitleWidget} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/manage.py0000755000175100001710000000036200000000000020370 0ustar00runnerdocker#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/models.py0000644000175100001710000000302300000000000020415 0ustar00runnerdockerfrom django.db import models class Genre(models.Model): title = models.CharField(max_length=50) class Meta: ordering = ("title",) def __str__(self): return self.title class Artist(models.Model): title = models.CharField(max_length=50, unique=True) genres = models.ManyToManyField(Genre) class Meta: ordering = ("title",) def __str__(self): return self.title class Album(models.Model): title = models.CharField(max_length=255) artist = models.ForeignKey(Artist, on_delete=models.CASCADE) featured_artists = models.ManyToManyField( Artist, blank=True, related_name="featured_album_set" ) primary_genre = models.ForeignKey( Genre, on_delete=models.CASCADE, blank=True, null=True, related_name="primary_album_set", ) genres = models.ManyToManyField(Genre) class Meta: ordering = ("title",) def __str__(self): return self.title class Country(models.Model): name = models.CharField(max_length=255) class Meta: ordering = ("name",) def __str__(self): return self.name class City(models.Model): name = models.CharField(max_length=255) country = models.ForeignKey( "Country", related_name="cities", on_delete=models.CASCADE ) class Meta: ordering = ("name",) def __str__(self): return self.name class Groupie(models.Model): obsession = models.ForeignKey(Artist, to_field="title", on_delete=models.CASCADE) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/settings.py0000644000175100001710000000141200000000000020772 0ustar00runnerdockerimport os.path BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DEBUG = True DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} INSTALLED_APPS = ( "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.staticfiles", "django_select2", "tests.testapp", ) STATIC_URL = "/static/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") SITE_ID = 1 ROOT_URLCONF = "tests.testapp.urls" LANGUAGES = [ ("de", "German"), ("en", "English"), ] LANGUAGE_CODE = "en" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "DIRS": ["templates"], }, ] SECRET_KEY = "123456" USE_L10N = True USE_I18N = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643917535.7678764 django-select2-7.10.0/tests/testapp/templates/0000755000175100001710000000000000000000000020560 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/templates/form.html0000644000175100001710000000075400000000000022417 0ustar00runnerdocker{% load static %} {{ form.media.css }}
{% csrf_token %} {{ form }}
{{ form.media.js }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/urls.py0000644000175100001710000000277000000000000020127 0ustar00runnerdockerfrom django.urls import include, path from .forms import ( AddressChainedSelect2WidgetForm, AlbumModelSelect2WidgetForm, HeavySelect2MultipleWidgetForm, HeavySelect2WidgetForm, ModelSelect2TagWidgetForm, Select2WidgetForm, ) from .views import TemplateFormView, heavy_data_1, heavy_data_2 urlpatterns = [ path( "select2_widget", TemplateFormView.as_view(form_class=Select2WidgetForm), name="select2_widget", ), path( "heavy_select2_widget", TemplateFormView.as_view(form_class=HeavySelect2WidgetForm), name="heavy_select2_widget", ), path( "heavy_select2_multiple_widget", TemplateFormView.as_view( form_class=HeavySelect2MultipleWidgetForm, success_url="/" ), name="heavy_select2_multiple_widget", ), path( "model_select2_widget", TemplateFormView.as_view(form_class=AlbumModelSelect2WidgetForm), name="model_select2_widget", ), path( "model_select2_tag_widget", TemplateFormView.as_view(form_class=ModelSelect2TagWidgetForm), name="model_select2_tag_widget", ), path( "model_chained_select2_widget", TemplateFormView.as_view(form_class=AddressChainedSelect2WidgetForm), name="model_chained_select2_widget", ), path("heavy_data_1", heavy_data_1, name="heavy_data_1"), path("heavy_data_2", heavy_data_2, name="heavy_data_2"), path("select2/", include("django_select2.urls")), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643917509.0 django-select2-7.10.0/tests/testapp/views.py0000644000175100001710000000172100000000000020272 0ustar00runnerdockerimport json from django.http import HttpResponse from django.views.generic import FormView class TemplateFormView(FormView): template_name = "form.html" def heavy_data_1(request): term = request.GET.get("term", "") numbers = ["Zero", "One", "Two", "Three", "Four", "Five"] numbers = filter(lambda num: term.lower() in num.lower(), numbers) results = [{"id": index, "text": value} for (index, value) in enumerate(numbers)] return HttpResponse( json.dumps({"err": "nil", "results": results}), content_type="application/json" ) def heavy_data_2(request): term = request.GET.get("term", "") numbers = ["Six", "Seven", "Eight", "Nine", "Ten", "Fortytwo"] numbers = filter(lambda num: term.lower() in num.lower(), numbers) results = [{"id": index, "text": value} for (index, value) in enumerate(numbers)] return HttpResponse( json.dumps({"err": "nil", "results": results}), content_type="application/json" )