diff --git a/.travis.yml b/.travis.yml index 88fc42a..985ab1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,41 +3,42 @@ language: python matrix: include: - - { python: 2.7, env: TOXENV=py27-dj_1.8-bootstrap_7.1-crispy_1.5-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.8-bootstrap_7.1-crispy_1.5-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap_7.1-crispy_1.5-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap_7.1-crispy_1.6-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap_8.1-crispy_1.5-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap_8.1-crispy_1.6-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap_8.2-crispy_1.5-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap_8.2-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap_7.1-crispy_1.5-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap_7.1-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap_8.1-crispy_1.5-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap_8.1-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap_8.2-crispy_1.5-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap_8.2-crispy_1.6-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap_7.1-crispy_1.5-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap_7.1-crispy_1.6-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap_8.1-crispy_1.5-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap_8.1-crispy_1.6-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap_8.2-crispy_1.5-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap_8.2-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap_7.1-crispy_1.5-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap_7.1-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap_8.1-crispy_1.5-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap_8.1-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap_8.2-crispy_1.5-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap_8.2-crispy_1.6-tests } - - { python: 2.7, env: TOXENV=py27-dj_1.11-bootstrap_8.2-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_1.11-bootstrap_8.2-crispy_1.6-tests } - - { python: 3.6, env: TOXENV=py36-dj_2.0-bootstrap_8.2-crispy_1.7-tests } - - { python: 3.6, env: TOXENV=py36-dj_2.1-bootstrap_8.2-crispy_1.7-tests } - { python: 2.7, env: TOXENV=py27-flake } - { python: 3.6, env: TOXENV=py36-flake } - - allow_failures: - - env: TOXENV=py27-dj_1.11-bootstrap_8.2-crispy_1.6-tests + - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap3_7.1-crispy_1.5-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap3_7.1-crispy_1.6-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap3_8.1-crispy_1.5-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap3_8.1-crispy_1.6-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap3_8.2-crispy_1.5-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.9-bootstrap3_8.2-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap3_7.1-crispy_1.5-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap3_7.1-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap3_8.1-crispy_1.5-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap3_8.1-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap3_8.2-crispy_1.5-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.9-bootstrap3_8.2-crispy_1.6-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap3_7.1-crispy_1.5-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap3_7.1-crispy_1.6-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap3_8.1-crispy_1.5-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap3_8.1-crispy_1.6-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap3_8.2-crispy_1.5-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.10-bootstrap3_8.2-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap3_7.1-crispy_1.5-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap3_7.1-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap3_8.1-crispy_1.5-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap3_8.1-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap3_8.2-crispy_1.5-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.10-bootstrap3_8.2-crispy_1.6-tests } + - { python: 2.7, env: TOXENV=py27-dj_1.11-bootstrap3_8.2-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_1.11-bootstrap3_8.2-crispy_1.6-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.0-latest_bootstraps-crispy_1.7-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.0-latest_bootstraps-crispy_1.8-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.1-latest_bootstraps-crispy_1.7-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.1-latest_bootstraps-crispy_1.8-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.2-latest_bootstraps-crispy_1.7-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.2-latest_bootstraps-crispy_1.8-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.1-latest_bootstraps-crispy_1.8-tests } + - { python: 3.6, env: TOXENV=py36-dj_2.2-latest_bootstraps-crispy_1.8-tests } install: - pip install tox>=2.5.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 534ee1a..4165d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG for django-popup-view-field +## 0.6.0 (2020-01-23) + +* Added support for Django 2.2 + +* Drop support for Django 1.8 + +* Added support for bootstrap4 (testing only with Django >= 2.1) + +* Added setting DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK + +* Templates for widgets was splitted to blocks + (for easy customization). Now `PopupViewWidget` can use + template for bootstrap3 or template for bootstrap4 + ## 0.5.0 (2019-04-17) * Support for Django 2.0 and 2.1 diff --git a/README.rst b/README.rst index c8fd505..4cbef14 100644 --- a/README.rst +++ b/README.rst @@ -22,19 +22,20 @@ You can create normal django View and load this view in dialog for form field. - Support: * Python: 2.7, 3.6 - * Django: 1.8, 1.9, 1.10, 1.11, 2.0, 2.1 + * Django: 1.9, 1.10, 1.11, 2.0, 2.1 * django-crispy-forms * django-bootstrap3 + * django-bootstrap4 (!Only for Django >= 2.1) - Require: * Django - * bootstrap3 + * bootstrap3 or bootstrap4 * JQuery - Recommended: - * django-bootstrap3 or + * django-bootstrap3 or django-bootstrap4 * django-crispy-forms - Locale: @@ -44,6 +45,8 @@ You can create normal django View and load this view in dialog for form field. - Tested on browsers: + * OK - Chromium 79.0.3945.79 - Ubuntu 18.04 + * OK - Firefox 72.0.1 (64 bity) - Ubuntu 18.04 * OK - Google Chrome 70.0 - Fedora 28 * OK - Firefox 62.0.3 - Fedora 28 * OK - Firefox 50.1.0 - Ubuntu 14.04 @@ -142,10 +145,13 @@ Add ``django_popup_view_field`` to your INSTALLED_APPS setting ... ] + # If you want use bootstrap4 then uncomment + # DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK = 'bootstrap4' + **Warning**: - Is recommended use django-bootstrap3 or django-crispy-forms - to render forms and fields, but this is not necessary. - You can still write django templates using pure CSS from bootstrap3. + Is recommended use django-bootstrap3/django-bootstrap4 or django-crispy-forms + to render forms and fields, but this is not necessary. + You can still write django templates using pure CSS from bootstrap3/4. More information about bootstrap forms in here: http://getbootstrap.com/css/#forms @@ -197,6 +203,27 @@ Tag should be append before body close tag and after jQuery and Bootstra +Settings +---------- + +DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Since version 0.6.0, django-popup-view-fields has built-in support for bootstrap4 also. +To enable support for `bootstrap4` you have to set `DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK` +to "bootstrap4" value. + +* `bootstrap3` - this setting will be load javascript and html templates for bootstrap3. + **This is a default value** + +* `bootstrap4` - this setting will be load javascript and html templates for bootstrap4. + +Value of DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK is changing behavior +of '{% django_popup_view_field_javascript %}' tag and `PopupViewWidget` class. +Template `scripts_include.html` is using this flag to decide which template +and javascript will be load. `PopupViewWidget` class is using this flag to decide +which template for field should be load. + + Simple Example ------------------------ @@ -618,4 +645,6 @@ Others * More about django-bootstrap3 in here : http://django-bootstrap3.readthedocs.io/en/latest/ +* More about django-bootstrap4 in here : https://django-bootstrap4.readthedocs.io/en/latest/ + * Documentation prepared with the help of **Online reStructuredText editor** : http://rst.ninjs.org/ diff --git a/demo/db.sqlite3 b/demo/db.sqlite3 index 6110e34..09bd731 100644 Binary files a/demo/db.sqlite3 and b/demo/db.sqlite3 differ diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 13e8ae9..6b8b34c 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -40,6 +40,7 @@ # Require 'bootstrap3', + 'bootstrap4', 'crispy_forms', # Added extra diff --git a/demo/demo/templates/base.html b/demo/demo/templates/base.html index 7cb15f3..748917f 100644 --- a/demo/demo/templates/base.html +++ b/demo/demo/templates/base.html @@ -1,6 +1,6 @@ {% load static %} -{% load bootstrap3 %} {% load django_popup_view_field_tags %} + @@ -11,7 +11,8 @@ django-popup-view-field - {% bootstrap_css %} + {% block css %} + {% endblock css %} @@ -21,8 +22,8 @@ - - {% bootstrap_javascript %} + {% block javascript %} + {% endblock javascript%} {# ! This tag add all js scripts and template scripts to this template ! #} {% django_popup_view_field_javascript %} diff --git a/demo/demo/templates/demo/demo_bootstrap_3.html b/demo/demo/templates/demo/demo_bootstrap_3.html new file mode 100644 index 0000000..3be8fdc --- /dev/null +++ b/demo/demo/templates/demo/demo_bootstrap_3.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} +{% load bootstrap3 %} +{% load crispy_forms_tags %} + +{% block css %} + {% bootstrap_css %} +{% endblock css %} + +{% block content %} + +

Demo - django-popup-view-field using bootstrap 3

+ +
+ + + {# ------------------------------- #} +
+ +
+

Render with django-bootstrap3

+
+ {% csrf_token %} + {% bootstrap_form form_1 %} + {% buttons %} + + {% endbuttons %} +
+
+ +
+

Crispy + helper

+ {% crispy form_2 form_2.helper %} +
+ +
+

Crispy without helper

+ {% crispy form_3 %} +
+ +
+ +
+ +{% endblock content %} + +{% block javascript %} + + {% bootstrap_javascript %} +{% endblock javascript %} + + diff --git a/demo/demo/templates/demo/demo_bootstrap_4.html b/demo/demo/templates/demo/demo_bootstrap_4.html new file mode 100644 index 0000000..c964708 --- /dev/null +++ b/demo/demo/templates/demo/demo_bootstrap_4.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% load bootstrap4 %} +{% load crispy_forms_tags %} + +{% block css %} + {% bootstrap_css %} +{% endblock css %} + +{% block content %} +

Demo - django-popup-view-field using bootstrap 4

+ +
+ + + + {# ------------------------------- #} +
+ +
+

Render with django-bootstrap4

+
+ {% csrf_token %} + {% bootstrap_form form_1 %} + {% buttons %} + + {% endbuttons %} +
+
+ +
+

Crispy + helper

+ {% crispy form_2 form_2.helper %} +
+ +
+

Crispy without helper

+ {% crispy form_3 %} +
+ +
+ +
+{% endblock content %} + +{% block javascript %} + {% bootstrap_javascript jquery='full'%} +{% endblock javascript %} + + diff --git a/demo/demo/templates/demo/index.html b/demo/demo/templates/demo/index.html index 3864af5..e4a800f 100644 --- a/demo/demo/templates/demo/index.html +++ b/demo/demo/templates/demo/index.html @@ -1,67 +1,32 @@ {% extends "base.html" %} -{% load bootstrap3 %} -{% load crispy_forms_tags %} +{% load bootstrap4 %} +{% block css %} + {% bootstrap_css %} +{% endblock css %} {% block content %} -

Demo - django-popup-view-field

+
+
+

Choose bootstrap version

+
+
-
- - - {# ------------------------------- #} -
- -
-

Render with django-bootstrap3

-
- {% csrf_token %} - {% bootstrap_form form_1 %} - {% buttons %} - - {% endbuttons %} -
-
- -
-

Crispy + helper

- {% crispy form_2 form_2.helper %} -
- -
-

Crispy without helper

- {% crispy form_3 %}
-
-
{% endblock content %} - - - diff --git a/demo/demo/urls.py b/demo/demo/urls.py index 4853b95..4289479 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -1,11 +1,13 @@ from django.conf.urls import include, url from django.contrib import admin -from .views import Index +from .views import Index, DemoBootstrap3, DemoBootstrap4 urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^$', Index.as_view(), name="index"), + url(r'^bootstrap3/', DemoBootstrap3.as_view(), name="demo_bootstrap_3"), + url(r'^bootstrap4/', DemoBootstrap4.as_view(), name="demo_bootstrap_4"), # django-popup-view-field url(r'^django_popup_view_field/', include('django_popup_view_field.urls')), diff --git a/demo/demo/views.py b/demo/demo/views.py index 6d3f7ef..4973eb8 100644 --- a/demo/demo/views.py +++ b/demo/demo/views.py @@ -1,26 +1,28 @@ from django.views.generic import TemplateView - +from django.conf import settings from .forms import DemoForm class Index(TemplateView): - template_name = "demo/index.html" - form_class = DemoForm + template_name = 'demo/index.html' + +class DemoBase(TemplateView): + form_class = DemoForm form_1 = None form_2 = None form_3 = None def get(self, request, *args, **kwargs): - self.form_1 = self.form_class(prefix="form_1") - self.form_2 = self.form_class(prefix="form_2") - self.form_3 = self.form_class(prefix="form_3") - return super(Index, self).get(request, *args, **kwargs) + self.form_1 = self.form_class(prefix='form_1') + self.form_2 = self.form_class(prefix='form_2') + self.form_3 = self.form_class(prefix='form_3') + return super(DemoBase, self).get(request, *args, **kwargs) def post(self, request, *args, **kwargs): - self.form_1 = self.form_class(prefix="form_1", data=request.POST) - self.form_2 = self.form_class(prefix="form_2", data=request.POST) - self.form_3 = self.form_class(prefix="form_3", data=request.POST) + self.form_1 = self.form_class(prefix='form_1', data=request.POST) + self.form_2 = self.form_class(prefix='form_2', data=request.POST) + self.form_3 = self.form_class(prefix='form_3', data=request.POST) self.form_1.is_valid() self.form_2.is_valid() @@ -29,8 +31,33 @@ def post(self, request, *args, **kwargs): return self.render_to_response(self.get_context_data()) def get_context_data(self, **kwargs): - context = super(Index, self).get_context_data(**kwargs) - context["form_1"] = self.form_1 - context["form_2"] = self.form_2 - context["form_3"] = self.form_3 + context = super(DemoBase, self).get_context_data(**kwargs) + context['form_1'] = self.form_1 + context['form_2'] = self.form_2 + context['form_3'] = self.form_3 return context + + +class DemoBootstrap3(DemoBase): + template_name = 'demo/demo_bootstrap_3.html' + + def __init__(self, *args, **kwargs): + # For this case we must set DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK + settings.DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK = 'bootstrap3' + super(DemoBootstrap3, self).__init__(*args, **kwargs) + + +class DemoBootstrap4(DemoBase): + template_name = 'demo/demo_bootstrap_4.html' + + def __init__(self, *args, **kwargs): + # This is a hack. + # For this case we must override + # CRISPY_TEMPLATE_PACK settings + settings.CRISPY_TEMPLATE_PACK = 'bootstrap4' + # For this case we must set DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK + # This view show howe django_popup working with bootstrap4 + # If you use bootstrap4 you should set 'bootstrap4' as a value + # of DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK in settings.py + settings.DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK = 'bootstrap4' + super(DemoBootstrap4, self).__init__(*args, **kwargs) diff --git a/demo/requirements.txt b/demo/requirements.txt index 56baa08..8a48215 100644 --- a/demo/requirements.txt +++ b/demo/requirements.txt @@ -1,4 +1,5 @@ -Django==2.1.8 -django-bootstrap3==11.0.0 +Django==2.2.9 +django-bootstrap3==12.0.3 +django-bootstrap4==1.1.1 django-crispy-forms==1.7.2 ../. diff --git a/django_popup_view_field/__init__.py b/django_popup_view_field/__init__.py index de131cb..4f7799b 100644 --- a/django_popup_view_field/__init__.py +++ b/django_popup_view_field/__init__.py @@ -1,2 +1,3 @@ -VERSION = (0, 5, 0) +VERSION = (0, 6, 0) __version__ = ".".join(str(i) for i in VERSION) +SUPPORTED_BOOTSTRAP_VER = ('bootstrap3', 'bootstrap4') diff --git a/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field.min.js b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field.min.js deleted file mode 100644 index 1109b00..0000000 --- a/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field.min.js +++ /dev/null @@ -1 +0,0 @@ -if("undefined"==typeof jQuery)throw new Error("django-popup-view-field - requires jQuery");$(document).ready(function(){var a=0,b=function(a){var b=a.data("url");return b},c=function(a,b){var c=a.find(".modal-body");c.find("*").on("click",function(c){var e=null,g=$(this),h=g.data("popup-view-value");if(void 0!==h&&("html()"===h&&(h=g.html(),h&&(h=h.trim())),e=b.data("target"),target=$("#"+e),c.stopPropagation(),a.modal("hide"),target.val(h)),g.is("a[href]")===!0){var i=g.prop("href");return c.stopPropagation(),d(a,b,i,"GET",null),!1}}),c.find("form").on("submit",function(c){var e=$(this),f=e.attr("method")||"POST",g=e.attr("action")||".",h=e.serialize();return c.stopPropagation(),d(a,b,g,f,h),!1})},d=function(a,b,d,e,f){var g={url:d,method:e,cache:!1};null!=f&&(g.data=f);var h=$.ajax(g);h.done(function(d){a.find(".modal-body").html(d),c(a,b)}),h.fail(function(b){throw a.modal("hide"),new Error("django-popup-view-field - Ajax request to url: "+d)})},e=function(b){var c=$("#template-django-popup-view-field").html(),d=$(c),e=d.attr("id")+a,f=b.data("popup-dialog-title"),g=gettext("Data is loading ..."),h=gettext("Close");return d.attr("id",e),d.find(".modal-title").html(f),d.find(".modal-body").html(g),d.find(".modal-footer-close").html(h),a++,d};$(".popup-view-btn-load").on("click",function(){var a=$(this),c=e(a),f=b(a);c.modal({backdrop:!1}),c.on("hidden.bs.modal",function(a){$(this).remove()}),d(c,a,f,"GET",null)}),$(".popup-view-btn-clear").on("click",function(){var a=$(this),b=a.data("target"),c=$("#"+b);c.val("")})}); diff --git a/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field.js b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap3.js similarity index 99% rename from django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field.js rename to django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap3.js index e13b635..c285746 100644 --- a/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field.js +++ b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap3.js @@ -21,7 +21,7 @@ $(document).ready(function(){ var bind_events = function($dialog, $button){ var $dial_body = $dialog.find(".modal-body"); - // Click to evrithing + // Click to everything $dial_body.find("*").on("click", function(event){ var target_id = null, $target = null; var $elem = $(this); diff --git a/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap3.min.js b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap3.min.js new file mode 100644 index 0000000..5325709 --- /dev/null +++ b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap3.min.js @@ -0,0 +1 @@ +if("undefined"==typeof jQuery)throw new Error("django-popup-view-field - requires jQuery");$(document).ready(function(){var t=0,o=function(t,a,i,n,e){var r={url:i,method:n,cache:!1};null!=e&&(r.data=e);var d=$.ajax(r);d.done(function(i){t.find(".modal-body").html(i),function(t,a){var i=t.find(".modal-body");i.find("*").on("click",function(i){var n=null,e=$(this),r=e.data("popup-view-value");if(void 0!==r&&("html()"===r&&(r=e.html())&&(r=r.trim()),n=a.data("target"),target=$("#"+n),i.stopPropagation(),t.modal("hide"),target.val(r)),!0===e.is("a[href]")){var d=e.prop("href");return i.stopPropagation(),o(t,a,d,"GET",null),!1}}),i.find("form").on("submit",function(i){var n=$(this),e=n.attr("method")||"POST",r=n.attr("action")||".",d=n.serialize();return i.stopPropagation(),o(t,a,r,e,d),!1})}(t,a)}),d.fail(function(o){throw t.modal("hide"),new Error("django-popup-view-field - Ajax request to url: "+i)})};$(".popup-view-btn-load").on("click",function(){var a=$(this),i=function(o){var a=$("#template-django-popup-view-field").html(),i=$(a),n=i.attr("id")+t,e=o.data("popup-dialog-title"),r=gettext("Data is loading ..."),d=gettext("Close");return i.attr("id",n),i.find(".modal-title").html(e),i.find(".modal-body").html(r),i.find(".modal-footer-close").html(d),t++,i}(a),n=function(t){return t.data("url")}(a);i.modal({backdrop:!1}),i.on("hidden.bs.modal",function(t){$(this).remove()}),o(i,a,n,"GET",null)}),$(".popup-view-btn-clear").on("click",function(){var t=$(this).data("target");$("#"+t).val("")})}); diff --git a/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap4.js b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap4.js new file mode 100644 index 0000000..31db0c2 --- /dev/null +++ b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap4.js @@ -0,0 +1,131 @@ +// Author: Grzegorz Tężycki +// https://github.com/djk2/django-popup-view-field + +if ("undefined"==typeof jQuery) { + throw new Error("django-popup-view-field - requires jQuery"); +} + +$(document).ready(function(){ + + var dialog_counter = 0; + + var get_url = function($button){ + var url = $button.data("url"); + return url; + }; + + // Bind all events for dialog content after content loaded + // bind click to elements + // override behavior for anchors (all anchors loaded by ajax) + // override behavior for submit buttons for forms + var bind_events = function($dialog, $button){ + var $dial_body = $dialog.find(".modal-body"); + + // Click anywhere + $dial_body.find("*").on("click", function(event){ + var target_id = null, $target = null; + var $elem = $(this); + var value = $elem.data("popup-view-value"); + + // Set value in field + if (value !== undefined) { + + // If data-popup-view-value is html() then + // full html content of element is set as value + if ( value === "html()" ) { + value = $elem.html(); + if (value) { + value = value.trim(); + } + } + + target_id = $button.data("target"); + target = $("#" + target_id); + event.stopPropagation(); + $dialog.modal('hide'); + target.val(value); + } + + // Override behavior for anchors + if ($elem.is("a[href]") === true) { + var href = $elem.prop("href"); + event.stopPropagation(); + get_content($dialog, $button, href, "GET", null); + return false; + } + }); + + $dial_body.find("form").on("submit", function(event){ + var $form = $(this); + var method = $form.attr("method") || "POST"; + var action = $form.attr("action") || "."; + var data = $form.serialize(); + event.stopPropagation(); + get_content($dialog, $button, action, method, data); + return false; + }); + }; + + // Load content for dialog from popup view by ajax request + var get_content = function($dialog, $button, url, method, data){ + + var ajax_param = { + 'url' : url, + 'method' : method, + 'cache' : false + }; + + if (data != null) { + ajax_param['data']=data; + } + + var request = $.ajax(ajax_param); + + request.done(function(response){ + $dialog.find(".modal-body").html(response); + bind_events($dialog, $button); + }); + request.fail(function(response){ + $dialog.modal('dispose'); + throw new Error("django-popup-view-field - Ajax request to url: " + url); + }); + }; + + var set_dialog = function($button){ + var $dialog = $("#django-popup-view-field"); + var dial_body = gettext('Data is loading ...'); + var dial_close = gettext('Close'); + if ($button !== null) { + var dial_title = $button.data('popup-dialog-title'); + } else { + dial_title = 'Modal title'; + } + $dialog.find(".modal-title").html(dial_title); + $dialog.find(".modal-body").html(dial_body); + $dialog.find(".modal-footer-close").html(dial_close); + }; + + $(".popup-view-btn-load").on("click", function(){ + var $button = $(this); + var url = get_url($button); + var $dialog = $("#django-popup-view-field"); + set_dialog($button); + get_content($dialog, $button, url, "GET", null); + + $dialog.on('hidden.bs.modal', function (e) { + $dialog.modal('hide'); + set_dialog(null); + }); + + $dialog.modal({}); + + }); + + $(".popup-view-btn-clear").on("click", function(){ + var $button = $(this); + var target_id = $button.data("target"); + var $target = $("#" + target_id); + $target.val(""); + }); + +}); diff --git a/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap4.min.js b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap4.min.js new file mode 100644 index 0000000..4ba5950 --- /dev/null +++ b/django_popup_view_field/static/django_popup_view_field/js/django_popup_view_field_bootstrap4.min.js @@ -0,0 +1 @@ +if("undefined"==typeof jQuery)throw new Error("django-popup-view-field - requires jQuery");$(document).ready(function(){var t=function(o,a,i,n,e){var l={url:i,method:n,cache:!1};null!=e&&(l.data=e);var d=$.ajax(l);d.done(function(i){o.find(".modal-body").html(i),function(o,a){var i=o.find(".modal-body");i.find("*").on("click",function(i){var n=null,e=$(this),l=e.data("popup-view-value");if(void 0!==l&&("html()"===l&&(l=e.html())&&(l=l.trim()),n=a.data("target"),target=$("#"+n),i.stopPropagation(),o.modal("hide"),target.val(l)),!0===e.is("a[href]")){var d=e.prop("href");return i.stopPropagation(),t(o,a,d,"GET",null),!1}}),i.find("form").on("submit",function(i){var n=$(this),e=n.attr("method")||"POST",l=n.attr("action")||".",d=n.serialize();return i.stopPropagation(),t(o,a,l,e,d),!1})}(o,a)}),d.fail(function(t){throw o.modal("dispose"),new Error("django-popup-view-field - Ajax request to url: "+i)})},o=function(t){var o=$("#django-popup-view-field"),a=gettext("Data is loading ..."),i=gettext("Close");if(null!==t)var n=t.data("popup-dialog-title");else n="Modal title";o.find(".modal-title").html(n),o.find(".modal-body").html(a),o.find(".modal-footer-close").html(i)};$(".popup-view-btn-load").on("click",function(){var a=$(this),i=function(t){return t.data("url")}(a),n=$("#django-popup-view-field");o(a),t(n,a,i,"GET",null),n.on("hidden.bs.modal",function(t){n.modal("hide"),o(null)}),n.modal({})}),$(".popup-view-btn-clear").on("click",function(){var t=$(this).data("target");$("#"+t).val("")})}); diff --git a/django_popup_view_field/templates/django_popup_view_field/popup_view_dialog_bootstrap3.html b/django_popup_view_field/templates/django_popup_view_field/popup_view_dialog_bootstrap3.html new file mode 100644 index 0000000..f517719 --- /dev/null +++ b/django_popup_view_field/templates/django_popup_view_field/popup_view_dialog_bootstrap3.html @@ -0,0 +1,27 @@ +{% spaceless %} + +{% endspaceless %} diff --git a/django_popup_view_field/templates/django_popup_view_field/popup_view_dialog_bootstrap4.html b/django_popup_view_field/templates/django_popup_view_field/popup_view_dialog_bootstrap4.html new file mode 100644 index 0000000..0faa39e --- /dev/null +++ b/django_popup_view_field/templates/django_popup_view_field/popup_view_dialog_bootstrap4.html @@ -0,0 +1,34 @@ +{% spaceless %} + +{% endspaceless %} diff --git a/django_popup_view_field/templates/django_popup_view_field/popup_view_widget.html b/django_popup_view_field/templates/django_popup_view_field/popup_view_widget.html deleted file mode 100644 index 3704e22..0000000 --- a/django_popup_view_field/templates/django_popup_view_field/popup_view_widget.html +++ /dev/null @@ -1,29 +0,0 @@ -{% spaceless %} -
- - - - - -
-{% endspaceless %} diff --git a/django_popup_view_field/templates/django_popup_view_field/popup_view_widget_bootstrap3.html b/django_popup_view_field/templates/django_popup_view_field/popup_view_widget_bootstrap3.html new file mode 100644 index 0000000..dbf1725 --- /dev/null +++ b/django_popup_view_field/templates/django_popup_view_field/popup_view_widget_bootstrap3.html @@ -0,0 +1,42 @@ +{% spaceless %} + +{% block popup-view-field-widget %} +
+ + {% block popup-view-field-input %} + + {% endblock popup-view-field-input %} + + {% block popup-view-field-buttons %} + {% block popup-view-field-button-clear %} + + {% endblock popup-view-field-button-clear %} + + {% block popup-view-field-button-load %} + + {% endblock popup-view-field-button-load %} + {% endblock popup-view-field-buttons %} +
+{% endblock popup-view-field-widget %} + +{% endspaceless %} diff --git a/django_popup_view_field/templates/django_popup_view_field/popup_view_widget_bootstrap4.html b/django_popup_view_field/templates/django_popup_view_field/popup_view_widget_bootstrap4.html new file mode 100644 index 0000000..c080dad --- /dev/null +++ b/django_popup_view_field/templates/django_popup_view_field/popup_view_widget_bootstrap4.html @@ -0,0 +1,44 @@ +{% spaceless %} + +{% block popup-view-field-widget %} +
+ + {% block popup-view-field-input %} + + {% endblock popup-view-field-input %} + + {% block popup-view-field-buttons %} + + {% endblock popup-view-field-buttons %} + +
+{% endblock popup-view-field-widget %} + +{% endspaceless %} diff --git a/django_popup_view_field/templates/django_popup_view_field/scripts_include.html b/django_popup_view_field/templates/django_popup_view_field/scripts_include.html index e5638d2..b36b698 100644 --- a/django_popup_view_field/templates/django_popup_view_field/scripts_include.html +++ b/django_popup_view_field/templates/django_popup_view_field/scripts_include.html @@ -1,34 +1,23 @@ {% load static %} +{% load django_popup_view_field_tags %} + - - - +{% django_popup_view_field_bootstrap_version as bootstrap_version %} +{% if bootstrap_version == 'bootstrap3' %} + {% include 'django_popup_view_field/popup_view_dialog_bootstrap3.html' %} + +{% elif bootstrap_version == 'bootstrap4' %} + {% include 'django_popup_view_field/popup_view_dialog_bootstrap4.html' %} + +{% endif %} diff --git a/django_popup_view_field/templatetags/django_popup_view_field_tags.py b/django_popup_view_field/templatetags/django_popup_view_field_tags.py index 9bb2ccb..d33a29b 100644 --- a/django_popup_view_field/templatetags/django_popup_view_field_tags.py +++ b/django_popup_view_field/templatetags/django_popup_view_field_tags.py @@ -1,4 +1,6 @@ from django.template import Library, loader +from django.conf import settings + register = Library() @@ -17,3 +19,27 @@ def django_popup_view_field_javascript(): """ temp = loader.get_template('django_popup_view_field/scripts_include.html') return temp.render({}) + + +@register.simple_tag +def django_popup_view_field_bootstrap_version(): + """ + Return value of DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK setting + By default return `bootstrap3` + Is used in `templates/scripts_include.html` to decide which js or html + should be load + + **Tag name**:: + django_popup_view_field_bootstrap_version + + **Usage**:: + {% django_popup_view_field_bootstrap_version as bv %} + {% if bv == 'bootstrap4' %} + ... + {% endif %} + """ + return getattr( + settings, + 'DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK', + 'bootstrap3' + ) diff --git a/django_popup_view_field/tests/settings.py b/django_popup_view_field/tests/settings.py index d00f5c8..869c75e 100644 --- a/django_popup_view_field/tests/settings.py +++ b/django_popup_view_field/tests/settings.py @@ -50,3 +50,10 @@ LANGUAGE_CODE = 'en-us' MEDIA_URL = '/media/' STATIC_URL = '/static/' + + +MIDDLEWARE = [ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] diff --git a/django_popup_view_field/tests/test_templatetags.py b/django_popup_view_field/tests/test_templatetags.py index fec4e39..24e7213 100644 --- a/django_popup_view_field/tests/test_templatetags.py +++ b/django_popup_view_field/tests/test_templatetags.py @@ -1,11 +1,11 @@ # encoding: utf-8 from django.template import engines -from django.test import TestCase +from django.test import TestCase, override_settings class TemplatetagsTest(TestCase): - def test_django_popup_view_field_javascript(self): + def test_django_popup_view_field_javascript_bootstrap3(self): template_code = """ {% load django_popup_view_field_tags %} @@ -15,9 +15,52 @@ def test_django_popup_view_field_javascript(self): template = engines['django'].from_string(template_code) html = template.render({}) - self.assertInHTML('''''', html) - self.assertInHTML(''' - - ''', html) + ''', + html + ) + self.assertInHTML( + ''' + + ''', + html + ) assert html.find('''id="template-django-popup-view-field"''') != -1 + + @override_settings(DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK='bootstrap4') + def test_django_popup_view_field_javascript_bootstrap4(self): + template_code = """ + {% load django_popup_view_field_tags %} + {% django_popup_view_field_javascript %} + """ + + template = engines['django'].from_string(template_code) + html = template.render({}) + + self.assertInHTML( + ''' + + ''', + html + ) + self.assertInHTML( + ''' + + ''', + html + ) + assert html.find('''id="django-popup-view-field-dialog"''') != -1 diff --git a/django_popup_view_field/tests/test_views.py b/django_popup_view_field/tests/test_views.py index 1469a57..f5766c2 100644 --- a/django_popup_view_field/tests/test_views.py +++ b/django_popup_view_field/tests/test_views.py @@ -1,6 +1,6 @@ # encoding: utf-8 import django -from django.test import Client, TestCase +from django.test import Client, TestCase, override_settings if django.VERSION < (1, 10): from django.core.urlresolvers import reverse @@ -26,7 +26,7 @@ def test_status(self): status = response.status_code assert status == 200 - def test_get_response(self): + def test_get_response_bootstrap3(self): response = self.client.get(self.url) html = response.content.decode("utf-8") self.assertTrue(response.context['form'] is not None) @@ -36,6 +36,17 @@ def test_get_response(self): assert html.find('''data-popup-dialog-title="Test PopupView1 Title"''') != -1 assert html.find('''data-url = "/django_popup_view_field/PopupView1/?"''') != -1 + @override_settings(DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK='bootstrap4') + def test_get_response_bootstrap4(self): + response = self.client.get(self.url) + html = response.content.decode("utf-8") + self.assertTrue(response.context['form'] is not None) + self.assertInHTML("View 1", html) + assert html.find('''class="btn btn-outline-secondary popup-view-btn-load"''') != -1 + assert html.find('''data-target="id_field"''') != -1 + assert html.find('''data-popup-dialog-title="Test PopupView1 Title"''') != -1 + assert html.find('''data-url = "/django_popup_view_field/PopupView1/?"''') != -1 + def test_post_response(self): response = self.client.post(self.url, {"field": "Test Value"}) assert response.status_code == 200 diff --git a/django_popup_view_field/tests/test_widgets.py b/django_popup_view_field/tests/test_widgets.py new file mode 100644 index 0000000..29d690a --- /dev/null +++ b/django_popup_view_field/tests/test_widgets.py @@ -0,0 +1,94 @@ +# encoding: utf-8 +from collections import OrderedDict +from django.test import TestCase, override_settings +from django.core.exceptions import ImproperlyConfigured +from django.forms import TextInput + +from django_popup_view_field.widgets import PopupViewWidget + + +class WidgetTest(TestCase): + + def test_subclass_widget(self): + self.assertTrue(issubclass(PopupViewWidget, TextInput)) + + def test_get_view_url(self): + widget = PopupViewWidget( + view_class_name='TestView', + popup_dialog_title='Title', + callback_data="callback=Yes" + ) + self.assertEqual( + widget.get_view_url(), + '/django_popup_view_field/TestView/?callback=Yes' + ) + + def test_get_template_name_default(self): + """ + This test check which template will be return + if in settings DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK + flag is not set + """ + widget = PopupViewWidget( + view_class_name='TestView', + popup_dialog_title='Title', + callback_data="callback=Yes" + ) + self.assertEqual( + widget.get_template_name(), + 'django_popup_view_field/popup_view_widget_bootstrap3.html' + ) + + @override_settings(DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK='bootstrap3') + def test_get_template_name_botstrap3(self): + """ + This test check which template will be return + if in settings DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK + flag is set to 'bootstrap3' + """ + widget = PopupViewWidget( + view_class_name='TestView', + popup_dialog_title='Title', + callback_data="callback=Yes" + ) + self.assertEqual( + widget.get_template_name(), + 'django_popup_view_field/popup_view_widget_bootstrap3.html' + ) + + @override_settings(DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK='bootstrap4') + def test_get_template_name_botsstrap4(self): + """ + This test check which template will be return + if in settings DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK + flag is set to 'bootstrap4' + """ + widget = PopupViewWidget( + view_class_name='TestView', + popup_dialog_title='Title', + callback_data="callback=Yes" + ) + self.assertEqual( + widget.get_template_name(), + 'django_popup_view_field/popup_view_widget_bootstrap4.html' + ) + + @override_settings(DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK='bad_value') + def test_get_template_name_exception(self): + """ + This test check if 'get_template_name method' + will raise exception if we set DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK + to incorrect value + """ + widget = PopupViewWidget( + view_class_name='TestView', + popup_dialog_title='Title', + callback_data="callback=Yes" + ) + with self.assertRaisesMessage( + ImproperlyConfigured, + 'incorrect value for ' + 'settings.DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK ' + 'availables values are: bootstrap3, bootstrap4' + ): + widget.get_template_name() diff --git a/django_popup_view_field/widgets.py b/django_popup_view_field/widgets.py index 6beeb85..42bb6db 100644 --- a/django_popup_view_field/widgets.py +++ b/django_popup_view_field/widgets.py @@ -2,6 +2,12 @@ from django import forms from django.template import loader from django.utils.encoding import force_text +from django.core.exceptions import ImproperlyConfigured + +from django_popup_view_field import SUPPORTED_BOOTSTRAP_VER +from django_popup_view_field.templatetags.django_popup_view_field_tags import ( + django_popup_view_field_bootstrap_version +) try: from django.urls import reverse @@ -17,7 +23,7 @@ class PopupViewWidget(forms.TextInput): second button to call popup dialog """ - template_name = 'django_popup_view_field/popup_view_widget.html' + template_name = 'django_popup_view_field/popup_view_widget_bootstrap3.html' def __init__(self, view_class_name, popup_dialog_title, callback_data, attrs=None): self.view_class_name = view_class_name @@ -39,8 +45,27 @@ def get_view_url(self): cd=self.callback_data ) - def get_context(self, name, value, attrs=None): + def get_template_name(self, *args, **kwargs): + bootstrap_version = django_popup_view_field_bootstrap_version() + if bootstrap_version not in SUPPORTED_BOOTSTRAP_VER: + raise ImproperlyConfigured( + "incorrect value for " + "settings.DJANGO_POPUP_VIEW_FIELD_TEMPLATE_PACK " + "availables values are: {supported}".format( + supported=", ".join(SUPPORTED_BOOTSTRAP_VER) + ) + ) + self.template_name = ( + 'django_popup_view_field/' + 'popup_view_widget_{}.html'.format( + bootstrap_version + ) + ) + return self.template_name + def get_context(self, name, value, attrs=None): + # Call to force set correct temlatename + template_name = self.get_template_name() # For Django >= 1.11 try: context = super(PopupViewWidget, self).get_context(name, value, attrs) @@ -54,7 +79,7 @@ def get_context(self, name, value, attrs=None): 'is_hidden': self.is_hidden, 'type': self.input_type, 'attrs': self.build_attrs(attrs), - 'template_name': self.template_name + 'template_name': template_name } } @@ -80,7 +105,7 @@ def render(self, name, value, attrs=None, **kwargs): context = self.get_context(name, value, attrs) if django.VERSION < (1, 11): - template = loader.get_template(self.template_name) + template = loader.get_template(self.get_template_name()) return template.render(context).strip() else: diff --git a/requirements-dev.txt b/requirements-dev.txt index 01ef758..a7b3bcf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,8 @@ -Django==2.1.8 +Django==2.2.9 tox>=2.5.0 -django-bootstrap3 -django-crispy-forms +django-bootstrap3>=12.0.3 +django-bootstrap4>=1.1.1 +django-crispy-forms>=1.8.1 twine flake8 isort diff --git a/tox.ini b/tox.ini index de45a62..c5fa227 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] minversion = 1.8 envlist= - {py27,py36}-dj_1.8-bootstrap_7.1-crispy_1.5-tests, - {py27,py36}-dj_1.9-bootstrap_{7.1,8.1,8.2}-crispy_{1.5,1.6}-tests, - {py27,py36}-dj_1.10-bootstrap_{7.1,8.1,8.2}-crispy_{1.5,1.6}-tests, - {py27,py36}-dj_1.11-bootstrap_{8.2}-crispy_{1.6}-tests, - {py36}-dj_{2.0,2.1}-bootstrap_{8.2}-crispy_{1.7}-tests, + {py27,py36}-dj_1.9-bootstrap3_{7.1,8.1,8.2}-crispy_{1.5,1.6}-tests, + {py27,py36}-dj_1.10-bootstrap3_{7.1,8.1,8.2}-crispy_{1.5,1.6}-tests, + {py27,py36}-dj_1.11-bootstrap3_{8.2}-crispy_{1.6}-tests, + {py36}-dj_{2.0,2.1,2.2}-latest_bootstraps-crispy_{1.7,1.8}-tests, + {py36}-dj_{2.1,2.2}-latest_bootstraps-crispy_{1.8}-tests, {py27,py36}-flake [testenv] @@ -17,18 +17,26 @@ pip_pre = true deps = flake8 - dj_1.8: Django>=1.8,<1.9 dj_1.9: Django>=1.9,<1.10 dj_1.10: Django>=1.9,<1.11 dj_1.11: Django>=1.11,<2.0 dj_2.0: Django>=2.0,<2.1 dj_2.1: Django>=2.1,<2.2 + dj_2.2: Django>=2.2,<2.3 crispy_1.5: django-crispy-forms==1.5.1 crispy_1.6: django-crispy-forms==1.6.1 crispy_1.7: django-crispy-forms==1.7.2 - bootstrap_7.1: django-bootstrap3==7.1.0 - bootstrap_8.1: django-bootstrap3==8.1.0 - bootstrap_8.2: django-bootstrap3==8.2.1 + crispy_1.8: django-crispy-forms==1.8.1 + bootstrap3_7.1: django-bootstrap3==7.1.0 + bootstrap3_8.1: django-bootstrap3==8.1.0 + bootstrap3_8.2: django-bootstrap3==8.2.1 + bootstrap3_12.0: django-bootstrap3==12.0.3 + + # Since version 0.6.0 django-popup-view-field + # has support for bootstrap 4. Now tests are requiring + # both versions of bootstraps + latest_bootstraps: django-bootstrap3==12.0.3 + latest_bootstraps: django-bootstrap4==1.1.1 commands = tests: ./run_test.py