Compare commits

..

12 commits

Author SHA1 Message Date
Benbb96
e7fa49a6e2
Prevent literal "None" placeholders (#591) 2020-02-25 11:50:21 +01:00
Johannes Hoppe
39ab326884 Add support for Django 3.0 2020-01-05 17:23:31 +01:00
Mario Frasca
d63f410bca Improve getting started documentation (#542) 2019-12-13 14:52:03 +01:00
Adrien Delhorme
5680f0daf9 Update Select2 library to version 4.0.12 2019-11-21 22:27:02 +07:00
Johannes Hoppe
8deb2b6a11
Create FUNDING.yml 2019-10-30 10:09:30 +09:00
Johannes Hoppe
37e2515be6
Add name to setup.py to enable GH dependency graph 2019-10-29 11:30:12 +09:00
Johannes Hoppe
cc8989a625 Fix typo 2019-10-10 13:09:03 -10:00
Johannes Hoppe
1ae52d7436 Fix #574 -- Update jQuery documentation
Update jQuery documentation to reflect jQuery versino support.
2019-10-10 13:09:03 -10:00
Johannes Hoppe
dca7dbc5d1 Fix #565 -- Support empty_label on ModelSelect fields. 2019-08-26 17:13:27 +02:00
predatell
8bd7f7209f Try to get queryset form choices, if possible (#509)
Get queryset for model widgets from choices. This omits the need to explicitly supply a queryset or model to the widget.
2019-07-09 18:38:59 +02:00
Johannes Hoppe
6b1ca10b06 Fix typo 2019-07-08 18:57:23 +02:00
Johannes Hoppe
4f96e21333 Resolve #557 -- Improve documentation for ModelSelect2TagWidget 2019-07-08 18:57:23 +02:00
14 changed files with 108 additions and 48 deletions

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
github: codingjoe
custom: https://paypal.me/codingjoe

View file

@ -4,10 +4,10 @@ dist: xenial
python:
- "3.6"
- "3.7"
- "3.8"
env:
- DJANGO=20
- DJANGO=21
- DJANGO=22
- DJANGO=30
- DJANGO=master
matrix:
@ -41,7 +41,7 @@ stages:
jobs:
include:
- python: "3.5"
- python: "3.7"
env: TOXENV=docs
addons:
apt:
@ -56,7 +56,7 @@ jobs:
node_js: lts/*
cache: npm
- stage: deploy
python: "3.7"
python: "3.8"
install: skip
script: skip
after_success: true
@ -71,7 +71,7 @@ jobs:
- stage: deploy
language: node_js
node_js: lts/*
python: "3.7"
python: "3.8"
install: skip
script: skip
after_success: true

22
CONTRIBUTING.rst Normal file
View file

@ -0,0 +1,22 @@
Contributing
============
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.

View file

@ -8,7 +8,7 @@ __all__ = ('settings', 'Select2Conf')
class Select2Conf(AppConf):
"""Settings for Django-Select2."""
LIB_VERSION = '4.0.5'
LIB_VERSION = '4.0.12'
"""Version of the Select2 library."""
CACHE_BACKEND = 'default'

View file

@ -20,10 +20,11 @@ Widgets are generally of two types:
drop-in-replacement for Django's default
select widgets.
2. **Heavy** --
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,
@ -31,15 +32,15 @@ Widgets are generally of two types:
Although on the server side you do need to create a view
specifically to respond to the queries.
3. **Model** --
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 widgets have the word 'Heavy' in their name.
Light widgets are normally named, i.e. there is no
'Light' word in their names.
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
@ -70,6 +71,8 @@ class Select2Mixin:
form media.
"""
empty_label = ''
def build_attrs(self, base_attrs, extra_attrs=None):
"""Add select2 data attributes."""
default_attrs = {'data-minimum-input-length': 0}
@ -77,7 +80,7 @@ class Select2Mixin:
default_attrs['data-allow-clear'] = 'false'
else:
default_attrs['data-allow-clear'] = 'true'
default_attrs['data-placeholder'] = ''
default_attrs['data-placeholder'] = self.empty_label or ""
default_attrs.update(base_attrs)
attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs)
@ -208,6 +211,7 @@ class HeavySelect2Mixin:
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`).
"""
self.choices = choices
if attrs is not None:
@ -339,6 +343,12 @@ class ModelSelect2Mixin:
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.
@ -419,6 +429,8 @@ class ModelSelect2Mixin:
"""
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:
@ -552,14 +564,15 @@ class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget):
queryset = MyModel.objects.all()
def value_from_datadict(self, data, files, name):
values = super().value_from_datadict(self, data, files, name)
qs = self.queryset.filter(**{'pk__in': list(values)})
pks = set(str(getattr(o, pk)) for o in qs)
cleaned_values = []
for val in value:
if str(val) not in pks:
val = queryset.create(title=val).pk
cleaned_values.append(val)
'''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
"""

1
docs/CONTRIBUTING.rst Normal file
View file

@ -0,0 +1 @@
.. include:: ../CONTRIBUTING.rst

View file

@ -49,8 +49,9 @@ DjangoSelect2 handles the initialization of select2 fields automatically. Just i
``{{ 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. It will handle both normal and
heavy fields. Simply call ``djangoSelect2(options)`` on your select fields.::
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();
@ -59,6 +60,9 @@ You can pass see `Select2 options <https://select2.github.io/options.html>`_ if
$('.django-select2').djangoSelect2({placeholder: 'Select an option'});
Please replace all your ``.select2`` invocations with the here provided
``.djangoSelect2``.
Security & Authentication
-------------------------

View file

@ -8,6 +8,12 @@ Overview
.. automodule:: django_select2
:members:
Assumptions
-----------
* You have a running Django up and running.
* You have form fully working without Django-Select2.
Installation
------------
@ -17,11 +23,12 @@ Installation
2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project settings.
3. Add ``django_select`` to your ``urlconf``::
3. Add ``django_select`` to your ``urlconf`` **if** you use any
:class:`ModelWidgets <.django_select2.forms.ModelSelect2Mixin>`::
path('select2/', include('django_select2.urls')),
url(r'^select2/', include('django_select2.urls')),
You can safely skip this one if you do not use any
:class:`ModelWidgets <.django_select2.forms.ModelSelect2Mixin>`
Quick Start
-----------
@ -30,19 +37,22 @@ Here is a quick example to get you started:
0. Follow the installation instructions above.
1. Add a select2 widget to the form. For example if you wanted Select2 with multi-select you would use
``Select2MultipleWidget``
Replacing::
1. Replace native Django forms widgets with one of the several ``django_select2.form`` widgets.
Start by importing them into your ``forms.py``, right next to Django own ones::
class MyForm(forms.Form):
things = ModelMultipleChoiceField(queryset=Thing.objects.all())
from django import forms
from django_select2 import forms as s2forms
with::
Then let's assume you have a model with a choice, a :class:`.ForeignKey`, and a
:class:`.ManyToManyField`, you would add this information to your Form Meta
class::
from django_select2.forms import Select2MultipleWidget
class MyForm(forms.Form):
things = ModelMultipleChoiceField(queryset=Thing.objects.all(), widget=Select2MultipleWidget)
widgets = {
'category': s2forms.Select2Widget,
'author': s2forms.ModelSelect2Widget(model=auth.get_user_model(),
search_fields=['first_name__istartswith', 'last_name__icontains']),
'attending': s2forms.ModelSelect2MultipleWidget …
}
2. Add the CSS to the ``head`` of your Django template::
@ -57,9 +67,9 @@ with::
External Dependencies
---------------------
* jQuery version 2
This is not included in the package since it is expected
that in most scenarios this would already be available.
* jQuery (version >=2)
jQuery is not included in the package since it is expected
that in most scenarios jQuery is already loaded.
Example Application
-------------------

View file

@ -15,6 +15,7 @@ Contents:
get_started
django_select2
extra
CONTRIBUTING
Indices and tables
==================

View file

@ -21,7 +21,7 @@ classifier =
include_package_data = True
packages = django_select2
install_requires =
django>=2.0
django>=2.2
django-appconf>=0.6.0
setup_requires =
setuptools_scm
@ -53,14 +53,13 @@ addopts = --cov=django_select2 --cov-report xml
DJANGO_SETTINGS_MODULE=tests.testapp.settings
[tox:tox]
envlist = py{35,36,37}-dj{22,21,20,master},docs
envlist = py{36,37,38}-dj{22,30,master},docs
[testenv]
passenv=CI
deps =
dj20: https://github.com/django/django/archive/stable/2.0.x.tar.gz#egg=django
dj21: https://github.com/django/django/archive/stable/2.1.x.tar.gz#egg=django
dj22: https://github.com/django/django/archive/stable/2.2.x.tar.gz#egg=django
dj22: django~=2.2
dj30: django~=3.0
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
commands = python setup.py test

View file

@ -2,4 +2,4 @@
from setuptools import setup
setup(use_scm_version=True)
setup(name='django-select2', use_scm_version=True)

View file

@ -21,7 +21,7 @@ def random_name(n):
@pytest.yield_fixture(scope='session')
def driver():
chrome_options = webdriver.ChromeOptions()
chrome_options.headless = False
chrome_options.headless = True
try:
b = webdriver.Chrome(options=chrome_options)
except WebDriverException as e:

View file

@ -95,7 +95,9 @@ class TestSelect2Mixin:
multiple_select = self.multiple_form.fields['featured_artists']
assert multiple_select.required is False
assert multiple_select.widget.allow_multiple_selected
assert '<option value=""></option>' not in multiple_select.widget.render('featured_artists', None)
output = multiple_select.widget.render('featured_artists', None)
assert '<option value=""></option>' not in output
assert 'data-placeholder=""' in output
def test_i18n(self):
translation.activate('de')
@ -412,6 +414,14 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
form = forms.GroupieForm(instance=groupie)
assert '<option value="Take That" selected>TAKE THAT</option>' 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):

View file

@ -186,7 +186,6 @@ class AddressChainedSelect2WidgetForm(forms.Form):
queryset=Country.objects.all(),
label='Country',
widget=ModelSelect2Widget(
model=Country,
search_fields=['name__icontains'],
max_results=500,
dependent_fields={'city': 'cities'},
@ -198,7 +197,6 @@ class AddressChainedSelect2WidgetForm(forms.Form):
queryset=City.objects.all(),
label='City',
widget=ModelSelect2Widget(
model=City,
search_fields=['name__icontains'],
dependent_fields={'country': 'country'},
max_results=500,