diff --git a/.gitignore b/.gitignore index 2c9d9656..875d21f9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ ghostdriver.log coverage.xml .eggs/ +db.sqlite3 diff --git a/README.rst b/README.rst index bbd72b68..3cb880d5 100644 --- a/README.rst +++ b/README.rst @@ -1,68 +1,29 @@ +============== Django-Select2 ============== -|version| |ci| |coverage| |license| +|version| |coverage| |license| This is a `Django`_ integration of `Select2`_. The app includes Select2 driven Django Widgets. -.. note:: - Django's admin comes with builtin support for Select2 - since version 2.0 via the `autocomplete_fields`_ feature. - -Installation ------------- - -1. Install ``django_select2`` - -.. code:: python - - pip install django_select2 - -2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project - settings. - -3. Add ``django_select`` to your urlconf if you use any "Auto" fields. - -.. code:: python - - url(r'^select2/', include('django_select2.urls')), - Documentation ------------- Documentation available at https://django-select2.readthedocs.io/. -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. - -Example Application -------------------- - -Please see ``tests/testapp`` application. This application is used to -manually test the functionalities of this package. This also serves as a -good example. - -Changelog ---------- - -See `Github releases`_ +.. 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 -.. _CHANGELOG.md: CHANGELOG.md -.. _Github releases: https://github.com/applegrew/django-select2/releases .. |version| image:: https://img.shields.io/pypi/v/Django-Select2.svg :target: https://pypi.python.org/pypi/Django-Select2/ -.. |ci| image:: https://travis-ci.org/applegrew/django-select2.svg?branch=master - :target: https://travis-ci.org/applegrew/django-select2 .. |coverage| image:: https://codecov.io/gh/applegrew/django-select2/branch/master/graph/badge.svg :target: https://codecov.io/gh/applegrew/django-select2 .. |license| image:: https://img.shields.io/badge/license-APL2-blue.svg diff --git a/docs/conf.py b/docs/conf.py index 52d38669..b339972f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,8 +1,11 @@ import 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. @@ -12,13 +15,13 @@ # 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, os.path.abspath("../tests.testapp")) -sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, str(BASE_DIR / "tests" / "testapp")) +sys.path.insert(0, str(BASE_DIR)) project = "Django-Select2" author = "Johannes Hoppe" -copyright = "2017, Johannes Hoppe" +copyright = "2017-2020, Johannes Hoppe" release = get_distribution("django_select2").version version = ".".join(release.split(".")[:2]) diff --git a/docs/get_started.rst b/docs/get_started.rst deleted file mode 100644 index 3af8b26d..00000000 --- a/docs/get_started.rst +++ /dev/null @@ -1,78 +0,0 @@ -=========== -Get Started -=========== - -Overview --------- - -.. automodule:: django_select2 - :members: - -Assumptions ------------ - -* You have a running Django up and running. -* You have form fully working without Django-Select2. - -Installation ------------- - -1. Install ``django_select2``:: - - pip install django_select2 - -2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project settings. - -3. Add ``django_select`` to your ``urlconf``:: - - path('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 ------------ - -Here is a quick example to get you started: - -0. Follow the installation instructions above. - -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:: - - from django import forms - from django_select2 import forms as s2forms - - 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:: - - 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:: - - {{ form.media.css }} - -3. Add the JavaScript to the end of the ``body`` of your Django template:: - - {{ form.media.js }} - -4. Done - enjoy the wonders of Select2! - -External Dependencies ---------------------- - -* jQuery (version >=2) - jQuery is not included in the package since it is expected - that in most scenarios jQuery is already loaded. - -Example Application -------------------- -Please see ``tests/testapp`` application. -This application is used to manually test the functionalities of this package. -This also serves as a good example. diff --git a/docs/index.rst b/docs/index.rst index dedf2e97..460460af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,189 @@ -.. Django-Select2 documentation master file, created by - sphinx-quickstart on Sat Aug 25 10:23:46 2012. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. include:: ../README.rst + +Installation +------------ + +Install ``django-select2`` + +.. code-block:: python + + 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 + ] + +Finally make sure you have a persistent cache backend setup (NOT +:class:`.DummyCache` or :class:`.LocMemCache`), we will use Redis in this +example. 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" + + +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/applegrew/django-select2/releases All Contents ============ @@ -12,7 +194,6 @@ Contents: :maxdepth: 2 :glob: - get_started django_select2 extra CONTRIBUTING diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt deleted file mode 100644 index fedb12ec..00000000 --- a/docs/spelling_wordlist.txt +++ /dev/null @@ -1,18 +0,0 @@ -jQuery -Django -mixin -backend -redis -memcached -AJAX -Cloudflare -lookup -QuerySet -pre -py -lookups -functionalities -plugin -multi -Indices -clearable diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..9da2c11e --- /dev/null +++ b/example/README.md @@ -0,0 +1,24 @@ +# 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/applegrew/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 +``` diff --git a/example/example/__init__.py b/example/example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/example/example/asgi.py b/example/example/asgi.py new file mode 100644 index 00000000..0042f21c --- /dev/null +++ b/example/example/asgi.py @@ -0,0 +1,16 @@ +""" +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() diff --git a/example/example/forms.py b/example/example/forms.py new file mode 100644 index 00000000..a6152cda --- /dev/null +++ b/example/example/forms.py @@ -0,0 +1,23 @@ +from 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, + } diff --git a/example/example/migrations/0001_initial.py b/example/example/migrations/0001_initial.py new file mode 100644 index 00000000..db45b1bb --- /dev/null +++ b/example/example/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# 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 + ), + ), + ], + ), + ] diff --git a/example/example/migrations/__init__.py b/example/example/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/example/example/models.py b/example/example/models.py new file mode 100644 index 00000000..7d76374b --- /dev/null +++ b/example/example/models.py @@ -0,0 +1,9 @@ +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" + ) diff --git a/example/example/settings.py b/example/example/settings.py new file mode 100644 index 00000000..83067236 --- /dev/null +++ b/example/example/settings.py @@ -0,0 +1,131 @@ +""" +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" + +# 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" diff --git a/example/example/templates/example/book_form.html b/example/example/templates/example/book_form.html new file mode 100644 index 00000000..c2ff60bf --- /dev/null +++ b/example/example/templates/example/book_form.html @@ -0,0 +1,20 @@ + + + + Create Book + {{ form.media.css }} + + + +

Create a new Book

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + {{ form.media.js }} + + diff --git a/example/example/urls.py b/example/example/urls.py new file mode 100644 index 00000000..ea4c18c3 --- /dev/null +++ b/example/example/urls.py @@ -0,0 +1,10 @@ +from 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), +] diff --git a/example/example/views.py b/example/example/views.py new file mode 100644 index 00000000..4bd09428 --- /dev/null +++ b/example/example/views.py @@ -0,0 +1,9 @@ +from django.views import generic + +from . import forms, models + + +class BookCreateView(generic.CreateView): + model = models.Book + form_class = forms.BookForm + success_url = "/" diff --git a/example/example/wsgi.py b/example/example/wsgi.py new file mode 100644 index 00000000..9f37c021 --- /dev/null +++ b/example/example/wsgi.py @@ -0,0 +1,16 @@ +""" +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() diff --git a/example/manage.py b/example/manage.py new file mode 100755 index 00000000..c6b69b9c --- /dev/null +++ b/example/manage.py @@ -0,0 +1,22 @@ +#!/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() diff --git a/example/requirements.txt b/example/requirements.txt new file mode 100644 index 00000000..53500ba2 --- /dev/null +++ b/example/requirements.txt @@ -0,0 +1,2 @@ +-e .. +django-redis diff --git a/setup.cfg b/setup.cfg index 95a47ada..ea4b66de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ build-dir = docs/_build [tool:pytest] addopts = + tests --doctest-glob='*.rst' --doctest-modules --cov=django_select2