From 209a3eb24d316f49bb00adac7690e1527581ca4c Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Sun, 9 Apr 2023 17:34:42 +0200 Subject: [PATCH 01/31] Updated JavaScript Added documentation. Added new property for settings: credentials. --- .../static/js/bootstrap5.modal.forms.js | 85 ++++++++++++++----- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 3008b69..4edb260 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,10 +1,15 @@ +'use strict'; /* django-bootstrap-modal-forms version : 2.2.1 Copyright (c) 2023 Marcel Rupp */ -// Open modal & load the form at formURL to the modalContent element +/** + * Open modal & load the form as inner HTML from "formURL" to the "modalContent" element + * + * @param {Object} settings Configuration/Settings for associated modal + */ const modalFormCallback = function (settings) { let modal = document.querySelector(settings.modalID); let content = modal.querySelector(settings.modalContent); @@ -17,20 +22,31 @@ const modalFormCallback = function (settings) { } fetch(settings.formURL).then(res => { + // Get content from target URL return res.text(); }).then(data => { + // Set content to inner HTML content.innerHTML = data; }).then(() => { + // Finally show the modal with new content modalInstance.show(); let form = modal.querySelector(settings.modalForm); if (form) { form.action = settings.formURL; - addEventHandlers(modal, form, settings) + // Add handler for form validation + addEventHandlers(modal, form, settings); } }); }; +/** + * Adds event handler for form validation cycle. + * + * @param {HTMLElement} modal The modal + * @param {HTMLElement} form The actual form, that should be evaluated by the server + * @param {Object} settings Configuration/Settings for associated modal + */ const addEventHandlers = function (modal, form, settings) { form.addEventListener('submit', (event) => { if (settings.isDeleteForm === false) { @@ -40,7 +56,7 @@ const addEventHandlers = function (modal, form, settings) { } }); - modal.addEventListener('hidden.bs.modal', (event) => { + modal.addEventListener('hidden.bs.modal', () => { let content = modal.querySelector(settings.modalContent); while (content.lastChild) { content.removeChild(content.lastChild); @@ -48,23 +64,33 @@ const addEventHandlers = function (modal, form, settings) { }); }; -// Check if form.is_valid() & either show errors or submit it via callback +/** + * Sends the form to the server & processes the result. If the form is valid the redirect from the + * form will be executed. If the form is invalid the errors are shown and no redirect will be executed. + * + * @param {Object} settings Configuration/Settings for associated modal + * @param {Function} callback Callback to break out of form validation cycle + */ const isFormValid = function (settings, callback) { let modal = document.querySelector(settings.modalID); let form = modal.querySelector(settings.modalForm); - const headers = new Headers(); + let headers = new Headers(); headers.append('X-Requested-With', 'XMLHttpRequest'); let btnSubmit = modal.querySelector('button[type="submit"]'); btnSubmit.disabled = true; + fetch(form.action, { headers: headers, method: form.method, body: new FormData(form), + credentials: settings.credentials, }).then(res => { return res.text(); }).then(data => { + console.log(data) if (data.includes(settings.errorClass)) { + // Form is invalid, therefore set the returned form (with marked invalid fields) to new inner HTML modal.querySelector(settings.modalContent).innerHTML = data; form = modal.querySelector(settings.modalForm); @@ -73,6 +99,7 @@ const isFormValid = function (settings, callback) { return; } + // Start from the beginning form.action = settings.formURL; addEventHandlers(modal, form, settings) } else { @@ -81,7 +108,11 @@ const isFormValid = function (settings, callback) { }); }; -// Submit form callback function +/** + * Submit form callback function + * + * @param {Object} settings Configuration/Settings for associated modal + */ const submitForm = function (settings) { let modal = document.querySelector(settings.modalID); let form = modal.querySelector(settings.modalForm); @@ -89,7 +120,7 @@ const submitForm = function (settings) { if (!settings.asyncUpdate) { form.submit(); } else { - let asyncSettingsValid = validateAsyncSettings(settings.asyncSettings); + const asyncSettingsValid = validateAsyncSettings(settings.asyncSettings); if (asyncSettingsValid) { let asyncSettings = settings.asyncSettings; // Serialize form data @@ -155,9 +186,14 @@ const submitForm = function (settings) { } }; +/** + * Validates given settings/configuration for asynchronous calls. + * + * @param {Object} settings Configuration/Settings for associated modal + * @return {boolean} True if given configuration/settings is valid, false otherwise + */ const validateAsyncSettings = function (settings) { - console.log(settings) - var missingSettings = []; + let missingSettings = []; if (!settings.successMessage) { missingSettings.push("successMessage"); @@ -179,15 +215,23 @@ const validateAsyncSettings = function (settings) { missingSettings.push("addModalFormFunction"); console.error("django-bootstrap-modal-forms: 'addModalFormFunction' in asyncSettings is missing."); } - - if (missingSettings.length > 0) { - return false; - } - - return true; + return missingSettings.length < 1; }; -const modalForm = function(elem, options) { +/** + * Adds click listener to given button. If button is clicked, associated + * modal makes a call to given URL("formURL") to load its inner HTML. + * + * credentials: + * Prevent browser to share credentials (Cookies, Authorization headers & TLS client certificates for future + * authentication) secrets with malicious 3rd parties. Defaults to "same-origin". + * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included + * + * @param {HTMLElement} trigger_btn Button that triggers the modal to open/close + * @param {Object} settings Configuration/Settings for this given modal + * @return {HTMLElement} The button with an event listener + */ +const modalForm = function (trigger_btn, settings) { // Default settings let defaults = { modalID: "#modal", @@ -196,6 +240,7 @@ const modalForm = function(elem, options) { formURL: null, isDeleteForm: false, errorClass: "is-invalid", + credentials: 'same-origin', // Choices: omit, include, same-origin asyncUpdate: false, asyncSettings: { closeOnSubmit: false, @@ -207,11 +252,11 @@ const modalForm = function(elem, options) { } }; - let settings = {...defaults, ...options} + const replenished_settings = {...defaults, ...settings} - elem.addEventListener('click', () => { - modalFormCallback(settings); + trigger_btn.addEventListener('click', () => { + modalFormCallback(replenished_settings); }) - return elem; + return trigger_btn; } From 36e159729dff5172507cb89e0e731ab7e3fa6de6 Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Sun, 9 Apr 2023 17:40:45 +0200 Subject: [PATCH 02/31] Updated JavaScript Added documentation. Added new property for settings: credentials. --- bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 4edb260..757a35f 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -21,7 +21,7 @@ const modalFormCallback = function (settings) { }) } - fetch(settings.formURL).then(res => { + fetch(settings.formURL, {credentials: settings.credentials}).then(res => { // Get content from target URL return res.text(); }).then(data => { @@ -131,6 +131,7 @@ const submitForm = function (settings) { fetch(form.action, { method: form.method, body: formData, + credentials: settings.credentials, }).then(res => { return res.text(); }).then(data => { @@ -145,7 +146,7 @@ const submitForm = function (settings) { if (asyncSettings.dataUrl) { // Update page without refresh - fetch(asyncSettings.dataUrl).then(res => res.json()).then(data => { + fetch(asyncSettings.dataUrl, {credentials: settings.credentials}).then(res => res.json()).then(data => { // Update page let dataElement = document.querySelector(asyncSettings.dataElementId); if (dataElement) { @@ -161,7 +162,7 @@ const submitForm = function (settings) { bootstrap.Modal.getInstance(modal).hide(); } else { // Reload form - fetch(settings.formURL).then(res => { + fetch(settings.formURL, {credentials: settings.credentials}).then(res => { return res.text(); }).then(data => { let content = modal.querySelector(settings.modalContent); From 024e41ac4103cb03fd42fa7e1f21eea81f1877b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Trstenjak?= <33464584+trco@users.noreply.github.com> Date: Mon, 1 May 2023 18:12:35 +0200 Subject: [PATCH 03/31] New major release 3.0.0 (#216) * Fix waring from BSModalDeleteView / DeleteMessageMixin with Django 4 * fix when form has field named method * add new mixin for FormValidation, remove SuccessMessageMixin dependecy * Updated project to current Django LTS version Updated deprecated "is_safe_url" function to "url_has_allowed_host_and_scheme". Added automatically database population, if DB is empty. Updated requirements to current version. Updated settings. Removed outdated version support. Updated gitignore. * Minor refactoring * Updated Project Added type hints. Updated test cases. Removed last remaining snippets for outdated Django versions. * Removed unused constant * Updated required version * Minor Bugfix Removed __slots__ from dataclass, to match Python 3.8 support. * refactor save method in CreateUpdateAjaxMixin * remove compatibility.py * remove utils.py * remove types * add is_ajax method and remove imports * remove unneeded class inheritence * remove obsolete class name parameter * revert examples to version in master branch * remove static folder * remove types from tests * remove unneeded comments * update get and set for form action and method attributes * update bootstrap5.modal.forms.min.js * update assert string to pass the test * update DeleteMessageMixin comment * cleanup .gitignore --------- Co-authored-by: Christian Wiegand Co-authored-by: Mark Monaghan Co-authored-by: aDramaQueen --- .gitignore | 2 +- bootstrap_modal_forms/compatibility.py | 84 -------------- bootstrap_modal_forms/generic.py | 20 +--- bootstrap_modal_forms/mixins.py | 79 ++++++++----- .../static/js/bootstrap5.modal.forms.js | 105 +++++------------- .../static/js/bootstrap5.modal.forms.min.js | 2 +- bootstrap_modal_forms/utils.py | 8 -- examples/admin.py | 3 - examples/apps.py | 2 +- examples/forms.py | 2 +- examples/migrations/0001_initial.py | 4 +- examples/models.py | 2 +- examples/tests.py | 3 - examples/urls.py | 2 +- examples/views.py | 5 +- manage.py | 7 +- requirements.txt | 9 +- setup.py | 23 ++-- setup/settings.py | 39 +++---- setup/wsgi.py | 9 -- tests/base.py | 76 +++++++++++-- tests/tests_functional.py | 42 +++---- 22 files changed, 215 insertions(+), 313 deletions(-) delete mode 100644 bootstrap_modal_forms/compatibility.py delete mode 100644 bootstrap_modal_forms/utils.py delete mode 100644 examples/admin.py delete mode 100644 examples/tests.py diff --git a/.gitignore b/.gitignore index 17613c7..4e2f06d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ database/db.sqlite3 geckodriver.log __pycache__ *.pyc -.env/ +.env/ \ No newline at end of file diff --git a/bootstrap_modal_forms/compatibility.py b/bootstrap_modal_forms/compatibility.py deleted file mode 100644 index 60b0168..0000000 --- a/bootstrap_modal_forms/compatibility.py +++ /dev/null @@ -1,84 +0,0 @@ -from django.conf import settings -from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model, login as auth_login -from django.contrib.auth.forms import AuthenticationForm -from django.contrib.sites.shortcuts import get_current_site -from django.http import HttpResponseRedirect -from django.shortcuts import resolve_url -from django.utils.decorators import method_decorator -from django.utils.http import is_safe_url -from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_protect -from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.edit import FormView - - -class SuccessURLAllowedHostsMixin: - success_url_allowed_hosts = set() - - def get_success_url_allowed_hosts(self): - return {self.request.get_host(), *self.success_url_allowed_hosts} - - -class LoginView(SuccessURLAllowedHostsMixin, FormView): - """ - Display the login form and handle the login action. - """ - form_class = AuthenticationForm - authentication_form = None - redirect_field_name = REDIRECT_FIELD_NAME - template_name = 'registration/login.html' - redirect_authenticated_user = False - extra_context = None - - @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) - @method_decorator(never_cache) - def dispatch(self, request, *args, **kwargs): - if self.redirect_authenticated_user and self.request.user.is_authenticated: - redirect_to = self.get_success_url() - if redirect_to == self.request.path: - raise ValueError( - 'Redirection loop for authenticated user detected. Check that ' - 'your LOGIN_REDIRECT_URL doesn\'t point to a login page.' - ) - return HttpResponseRedirect(redirect_to) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - url = self.get_redirect_url() - return url or resolve_url(settings.LOGIN_REDIRECT_URL) - - def get_redirect_url(self): - """Return the user-originating redirect URL if it's safe.""" - redirect_to = self.request.POST.get( - self.redirect_field_name, - self.request.GET.get(self.redirect_field_name, '') - ) - url_is_safe = is_safe_url( - url=redirect_to - ) - return redirect_to if url_is_safe else '' - - def get_form_class(self): - return self.authentication_form or self.form_class - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['request'] = self.request - return kwargs - - def form_valid(self, form): - """Security check complete. Log the user in.""" - auth_login(self.request, form.get_user()) - return HttpResponseRedirect(self.get_success_url()) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - current_site = get_current_site(self.request) - context.update({ - self.redirect_field_name: self.get_redirect_url(), - 'site': current_site, - 'site_name': current_site.name, - **(self.extra_context or {}) - }) - return context \ No newline at end of file diff --git a/bootstrap_modal_forms/generic.py b/bootstrap_modal_forms/generic.py index 3df0873..d341f27 100644 --- a/bootstrap_modal_forms/generic.py +++ b/bootstrap_modal_forms/generic.py @@ -1,20 +1,10 @@ -import django -from django.contrib.messages.views import SuccessMessageMixin from django.views import generic -from .mixins import PassRequestMixin, DeleteMessageMixin, LoginAjaxMixin +from django.contrib.auth.views import LoginView -DJANGO_VERSION = django.get_version().split('.') -DJANGO_MAJOR_VERSION = DJANGO_VERSION[0] -DJANGO_MINOR_VERSION = DJANGO_VERSION[1] +from .mixins import PassRequestMixin, DeleteMessageMixin, LoginAjaxMixin, FormValidationMixin -# Import custom LoginView for Django versions < 1.11 -if DJANGO_MAJOR_VERSION == '1' and '11' not in DJANGO_MINOR_VERSION: - from .compatibility import LoginView -else: - from django.contrib.auth.views import LoginView - -class BSModalLoginView(LoginAjaxMixin, SuccessMessageMixin, LoginView): +class BSModalLoginView(LoginAjaxMixin, LoginView): pass @@ -22,11 +12,11 @@ class BSModalFormView(PassRequestMixin, generic.FormView): pass -class BSModalCreateView(PassRequestMixin, SuccessMessageMixin, generic.CreateView): +class BSModalCreateView(PassRequestMixin, FormValidationMixin, generic.CreateView): pass -class BSModalUpdateView(PassRequestMixin, SuccessMessageMixin, generic.UpdateView): +class BSModalUpdateView(PassRequestMixin, FormValidationMixin, generic.UpdateView): pass diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index b275538..b984932 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -1,27 +1,24 @@ from django.contrib import messages from django.contrib.auth import login as auth_login -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, HttpResponse -from .utils import is_ajax - - -class PassRequestMixin(object): +class PassRequestMixin: """ - Mixin which puts the request into the form's kwargs. + Form Mixin which puts the request into the form's kwargs. Note: Using this mixin requires you to pop the `request` kwarg out of the dict in the super of your form's `__init__`. """ def get_form_kwargs(self): - kwargs = super(PassRequestMixin, self).get_form_kwargs() - kwargs.update({'request': self.request}) + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request return kwargs -class PopRequestMixin(object): +class PopRequestMixin: """ - Mixin which pops request out of the kwargs and attaches it to the form's + Form Mixin which pops request out of the kwargs and attaches it to the form's instance. Note: This mixin must precede forms.ModelForm/forms.Form. The form is not @@ -29,45 +26,71 @@ class PopRequestMixin(object): anything else is done. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: self.request = kwargs.pop('request', None) - super(PopRequestMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) -class CreateUpdateAjaxMixin(object): +class CreateUpdateAjaxMixin: """ - Mixin which passes or saves object based on request type. + ModelForm Mixin which passes or saves object based on request type. """ def save(self, commit=True): - if not is_ajax(self.request.META) or self.request.POST.get('asyncUpdate') == 'True': - instance = super(CreateUpdateAjaxMixin, self).save(commit=commit) - else: - instance = super(CreateUpdateAjaxMixin, self).save(commit=False) - return instance + isAjaxRequest = is_ajax(self.request.META) + asyncUpdate = self.request.POST.get('asyncUpdate') == 'True' + + if not isAjaxRequest or asyncUpdate: + return super().save(commit=commit) + if isAjaxRequest: + return super().save(commit=False) -class DeleteMessageMixin(object): +class DeleteMessageMixin: """ - Mixin which adds message to BSModalDeleteView and only calls the delete method if request - is not ajax request. + Generic View Mixin which adds message to BSModalDeleteView and only calls the post method if request + is not ajax request. In case request is ajax post method calls delete method, which redirects to success url. """ - - def delete(self, request, *args, **kwargs): + + def post(self, request, *args, **kwargs): if not is_ajax(request.META): messages.success(request, self.success_message) - return super(DeleteMessageMixin, self).delete(request, *args, **kwargs) + return super().post(request, *args, **kwargs) else: self.object = self.get_object() return HttpResponseRedirect(self.get_success_url()) -class LoginAjaxMixin(object): + +class LoginAjaxMixin: """ - Mixin which authenticates user if request is not ajax request. + Generic View Mixin which authenticates user if request is not ajax request. """ def form_valid(self, form): if not is_ajax(self.request.META): auth_login(self.request, form.get_user()) messages.success(self.request, self.success_message) - return HttpResponseRedirect(self.get_success_url()) \ No newline at end of file + return HttpResponseRedirect(self.get_success_url()) + + +class FormValidationMixin: + """ + Generic View Mixin which saves object and redirects to success_url if request is not ajax request. Otherwise response 204 No content is returned. + """ + + def form_valid(self, form): + isAjaxRequest = is_ajax(self.request.META) + asyncUpdate = self.request.POST.get('asyncUpdate') == 'True' + + if isAjaxRequest: + if asyncUpdate: + form.save() + return HttpResponse(status=204) + + form.save() + messages.success(self.request, self.success_message) + return HttpResponseRedirect(self.success_url) + + +def is_ajax(meta): + return 'HTTP_X_REQUESTED_WITH' in meta and meta['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 757a35f..37dab68 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,15 +1,10 @@ -'use strict'; /* django-bootstrap-modal-forms version : 2.2.1 Copyright (c) 2023 Marcel Rupp */ -/** - * Open modal & load the form as inner HTML from "formURL" to the "modalContent" element - * - * @param {Object} settings Configuration/Settings for associated modal - */ +// Open modal & load the form at formURL to the modalContent element const modalFormCallback = function (settings) { let modal = document.querySelector(settings.modalID); let content = modal.querySelector(settings.modalContent); @@ -21,32 +16,21 @@ const modalFormCallback = function (settings) { }) } - fetch(settings.formURL, {credentials: settings.credentials}).then(res => { - // Get content from target URL + fetch(settings.formURL).then(res => { return res.text(); }).then(data => { - // Set content to inner HTML content.innerHTML = data; }).then(() => { - // Finally show the modal with new content modalInstance.show(); let form = modal.querySelector(settings.modalForm); if (form) { - form.action = settings.formURL; - // Add handler for form validation - addEventHandlers(modal, form, settings); + form.setAttribute("action", settings.formURL); + addEventHandlers(modal, form, settings) } }); }; -/** - * Adds event handler for form validation cycle. - * - * @param {HTMLElement} modal The modal - * @param {HTMLElement} form The actual form, that should be evaluated by the server - * @param {Object} settings Configuration/Settings for associated modal - */ const addEventHandlers = function (modal, form, settings) { form.addEventListener('submit', (event) => { if (settings.isDeleteForm === false) { @@ -56,7 +40,7 @@ const addEventHandlers = function (modal, form, settings) { } }); - modal.addEventListener('hidden.bs.modal', () => { + modal.addEventListener('hidden.bs.modal', (event) => { let content = modal.querySelector(settings.modalContent); while (content.lastChild) { content.removeChild(content.lastChild); @@ -64,33 +48,23 @@ const addEventHandlers = function (modal, form, settings) { }); }; -/** - * Sends the form to the server & processes the result. If the form is valid the redirect from the - * form will be executed. If the form is invalid the errors are shown and no redirect will be executed. - * - * @param {Object} settings Configuration/Settings for associated modal - * @param {Function} callback Callback to break out of form validation cycle - */ +// Check if form.is_valid() & either show errors or submit it via callback const isFormValid = function (settings, callback) { let modal = document.querySelector(settings.modalID); let form = modal.querySelector(settings.modalForm); - let headers = new Headers(); + const headers = new Headers(); headers.append('X-Requested-With', 'XMLHttpRequest'); let btnSubmit = modal.querySelector('button[type="submit"]'); btnSubmit.disabled = true; - - fetch(form.action, { + fetch(form.getAttribute("action"), { headers: headers, - method: form.method, + method: form.getAttribute("method"), body: new FormData(form), - credentials: settings.credentials, }).then(res => { return res.text(); }).then(data => { - console.log(data) if (data.includes(settings.errorClass)) { - // Form is invalid, therefore set the returned form (with marked invalid fields) to new inner HTML modal.querySelector(settings.modalContent).innerHTML = data; form = modal.querySelector(settings.modalForm); @@ -99,8 +73,7 @@ const isFormValid = function (settings, callback) { return; } - // Start from the beginning - form.action = settings.formURL; + form.setAttribute("action", settings.formURL); addEventHandlers(modal, form, settings) } else { callback(settings); @@ -108,11 +81,7 @@ const isFormValid = function (settings, callback) { }); }; -/** - * Submit form callback function - * - * @param {Object} settings Configuration/Settings for associated modal - */ +// Submit form callback function const submitForm = function (settings) { let modal = document.querySelector(settings.modalID); let form = modal.querySelector(settings.modalForm); @@ -120,7 +89,7 @@ const submitForm = function (settings) { if (!settings.asyncUpdate) { form.submit(); } else { - const asyncSettingsValid = validateAsyncSettings(settings.asyncSettings); + let asyncSettingsValid = validateAsyncSettings(settings.asyncSettings); if (asyncSettingsValid) { let asyncSettings = settings.asyncSettings; // Serialize form data @@ -128,10 +97,9 @@ const submitForm = function (settings) { // Add asyncUpdate and check for it in save method of CreateUpdateAjaxMixin formData.append("asyncUpdate", "True"); - fetch(form.action, { - method: form.method, + fetch(form.getAttribute("action"), { + method: form.getAttribute("method"), body: formData, - credentials: settings.credentials, }).then(res => { return res.text(); }).then(data => { @@ -146,7 +114,7 @@ const submitForm = function (settings) { if (asyncSettings.dataUrl) { // Update page without refresh - fetch(asyncSettings.dataUrl, {credentials: settings.credentials}).then(res => res.json()).then(data => { + fetch(asyncSettings.dataUrl).then(res => res.json()).then(data => { // Update page let dataElement = document.querySelector(asyncSettings.dataElementId); if (dataElement) { @@ -162,7 +130,7 @@ const submitForm = function (settings) { bootstrap.Modal.getInstance(modal).hide(); } else { // Reload form - fetch(settings.formURL, {credentials: settings.credentials}).then(res => { + fetch(settings.formURL).then(res => { return res.text(); }).then(data => { let content = modal.querySelector(settings.modalContent); @@ -174,7 +142,7 @@ const submitForm = function (settings) { return; } - form.action = settings.formURL; + form.setAttribute("action", settings.formURL); addEventHandlers(modal, form, settings) }); } @@ -187,14 +155,8 @@ const submitForm = function (settings) { } }; -/** - * Validates given settings/configuration for asynchronous calls. - * - * @param {Object} settings Configuration/Settings for associated modal - * @return {boolean} True if given configuration/settings is valid, false otherwise - */ const validateAsyncSettings = function (settings) { - let missingSettings = []; + var missingSettings = []; if (!settings.successMessage) { missingSettings.push("successMessage"); @@ -216,23 +178,15 @@ const validateAsyncSettings = function (settings) { missingSettings.push("addModalFormFunction"); console.error("django-bootstrap-modal-forms: 'addModalFormFunction' in asyncSettings is missing."); } - return missingSettings.length < 1; + + if (missingSettings.length > 0) { + return false; + } + + return true; }; -/** - * Adds click listener to given button. If button is clicked, associated - * modal makes a call to given URL("formURL") to load its inner HTML. - * - * credentials: - * Prevent browser to share credentials (Cookies, Authorization headers & TLS client certificates for future - * authentication) secrets with malicious 3rd parties. Defaults to "same-origin". - * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included - * - * @param {HTMLElement} trigger_btn Button that triggers the modal to open/close - * @param {Object} settings Configuration/Settings for this given modal - * @return {HTMLElement} The button with an event listener - */ -const modalForm = function (trigger_btn, settings) { +const modalForm = function(elem, options) { // Default settings let defaults = { modalID: "#modal", @@ -241,7 +195,6 @@ const modalForm = function (trigger_btn, settings) { formURL: null, isDeleteForm: false, errorClass: "is-invalid", - credentials: 'same-origin', // Choices: omit, include, same-origin asyncUpdate: false, asyncSettings: { closeOnSubmit: false, @@ -253,11 +206,11 @@ const modalForm = function (trigger_btn, settings) { } }; - const replenished_settings = {...defaults, ...settings} + let settings = {...defaults, ...options} - trigger_btn.addEventListener('click', () => { - modalFormCallback(replenished_settings); + elem.addEventListener('click', () => { + modalFormCallback(settings); }) - return trigger_btn; + return elem; } diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.min.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.min.js index a54abe5..f0bdf64 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.min.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.min.js @@ -1 +1 @@ -const modalFormCallback=function(settings){let modal=document.querySelector(settings.modalID);let content=modal.querySelector(settings.modalContent);let modalInstance=bootstrap.Modal.getInstance(modal);if(modalInstance===null){modalInstance=new bootstrap.Modal(modal,{keyboard:false})}fetch(settings.formURL).then(res=>{return res.text()}).then(data=>{content.innerHTML=data}).then(()=>{modalInstance.show();let form=modal.querySelector(settings.modalForm);if(form){form.action=settings.formURL;addEventHandlers(modal,form,settings)}})};const addEventHandlers=function(modal,form,settings){form.addEventListener("submit",event=>{if(settings.isDeleteForm===false){event.preventDefault();isFormValid(settings,submitForm);return false}});modal.addEventListener("hidden.bs.modal",event=>{let content=modal.querySelector(settings.modalContent);while(content.lastChild){content.removeChild(content.lastChild)}})};const isFormValid=function(settings,callback){let modal=document.querySelector(settings.modalID);let form=modal.querySelector(settings.modalForm);const headers=new Headers;headers.append("X-Requested-With","XMLHttpRequest");let btnSubmit=modal.querySelector('button[type="submit"]');btnSubmit.disabled=true;fetch(form.action,{headers:headers,method:form.method,body:new FormData(form)}).then(res=>{return res.text()}).then(data=>{if(data.includes(settings.errorClass)){modal.querySelector(settings.modalContent).innerHTML=data;form=modal.querySelector(settings.modalForm);if(!form){console.error("no form present in response");return}form.action=settings.formURL;addEventHandlers(modal,form,settings)}else{callback(settings)}})};const submitForm=function(settings){let modal=document.querySelector(settings.modalID);let form=modal.querySelector(settings.modalForm);if(!settings.asyncUpdate){form.submit()}else{let asyncSettingsValid=validateAsyncSettings(settings.asyncSettings);if(asyncSettingsValid){let asyncSettings=settings.asyncSettings;let formData=new FormData(form);formData.append("asyncUpdate","True");fetch(form.action,{method:form.method,body:formData}).then(res=>{return res.text()}).then(data=>{let body=document.body;if(body===undefined){console.error("django-bootstrap-modal-forms: element missing in your html.");return}let doc=(new DOMParser).parseFromString(asyncSettings.successMessage,"text/xml");body.insertBefore(doc.firstChild,body.firstChild);if(asyncSettings.dataUrl){fetch(asyncSettings.dataUrl).then(res=>res.json()).then(data=>{let dataElement=document.querySelector(asyncSettings.dataElementId);if(dataElement){dataElement.innerHTML=data[asyncSettings.dataKey]}if(asyncSettings.addModalFormFunction){asyncSettings.addModalFormFunction()}if(asyncSettings.closeOnSubmit){bootstrap.Modal.getInstance(modal).hide()}else{fetch(settings.formURL).then(res=>{return res.text()}).then(data=>{let content=modal.querySelector(settings.modalContent);content.innerHTML=data;form=modal.querySelector(settings.modalForm);if(!form){console.error("no form present in response");return}form.action=settings.formURL;addEventHandlers(modal,form,settings)})}})}else if(asyncSettings.closeOnSubmit){bootstrap.Modal.getInstance(modal).hide()}})}}};const validateAsyncSettings=function(settings){console.log(settings);var missingSettings=[];if(!settings.successMessage){missingSettings.push("successMessage");console.error("django-bootstrap-modal-forms: 'successMessage' in asyncSettings is missing.")}if(!settings.dataUrl){missingSettings.push("dataUrl");console.error("django-bootstrap-modal-forms: 'dataUrl' in asyncSettings is missing.")}if(!settings.dataElementId){missingSettings.push("dataElementId");console.error("django-bootstrap-modal-forms: 'dataElementId' in asyncSettings is missing.")}if(!settings.dataKey){missingSettings.push("dataKey");console.error("django-bootstrap-modal-forms: 'dataKey' in asyncSettings is missing.")}if(!settings.addModalFormFunction){missingSettings.push("addModalFormFunction");console.error("django-bootstrap-modal-forms: 'addModalFormFunction' in asyncSettings is missing.")}if(missingSettings.length>0){return false}return true};const modalForm=function(elem,options){let defaults={modalID:"#modal",modalContent:".modal-content",modalForm:".modal-content form",formURL:null,isDeleteForm:false,errorClass:"is-invalid",asyncUpdate:false,asyncSettings:{closeOnSubmit:false,successMessage:null,dataUrl:null,dataElementId:null,dataKey:null,addModalFormFunction:null}};let settings={...defaults,...options};elem.addEventListener("click",()=>{modalFormCallback(settings)});return elem}; \ No newline at end of file +const modalFormCallback=function(e){let t=document.querySelector(e.modalID),n=t.querySelector(e.modalContent),o=bootstrap.Modal.getInstance(t);null===o&&(o=new bootstrap.Modal(t,{keyboard:!1})),fetch(e.formURL).then(e=>e.text()).then(e=>{n.innerHTML=e}).then(()=>{o.show();let n=t.querySelector(e.modalForm);n&&(n.setAttribute("action",e.formURL),addEventHandlers(t,n,e))})},addEventHandlers=function(e,t,n){t.addEventListener("submit",e=>{if(!1===n.isDeleteForm)return e.preventDefault(),isFormValid(n,submitForm),!1}),e.addEventListener("hidden.bs.modal",t=>{let o=e.querySelector(n.modalContent);for(;o.lastChild;)o.removeChild(o.lastChild)})},isFormValid=function(e,t){let n=document.querySelector(e.modalID),o=n.querySelector(e.modalForm),r=new Headers;r.append("X-Requested-With","XMLHttpRequest");n.querySelector('button[type="submit"]').disabled=!0,fetch(o.getAttribute("action"),{headers:r,method:o.getAttribute("method"),body:new FormData(o)}).then(e=>e.text()).then(r=>{if(r.includes(e.errorClass)){if(n.querySelector(e.modalContent).innerHTML=r,!(o=n.querySelector(e.modalForm))){console.error("no form present in response");return}o.setAttribute("action",e.formURL),addEventHandlers(n,o,e)}else t(e)})},submitForm=function(e){let t=document.querySelector(e.modalID),n=t.querySelector(e.modalForm);if(e.asyncUpdate){if(validateAsyncSettings(e.asyncSettings)){let o=e.asyncSettings,r=new FormData(n);r.append("asyncUpdate","True"),fetch(n.getAttribute("action"),{method:n.getAttribute("method"),body:r}).then(e=>e.text()).then(r=>{let a=document.body;if(void 0===a){console.error("django-bootstrap-modal-forms: element missing in your html.");return}let s=new DOMParser().parseFromString(o.successMessage,"text/xml");a.insertBefore(s.firstChild,a.firstChild),o.dataUrl?fetch(o.dataUrl).then(e=>e.json()).then(r=>{let a=document.querySelector(o.dataElementId);a&&(a.innerHTML=r[o.dataKey]),o.addModalFormFunction&&o.addModalFormFunction(),o.closeOnSubmit?bootstrap.Modal.getInstance(t).hide():fetch(e.formURL).then(e=>e.text()).then(o=>{if(t.querySelector(e.modalContent).innerHTML=o,!(n=t.querySelector(e.modalForm))){console.error("no form present in response");return}n.setAttribute("action",e.formURL),addEventHandlers(t,n,e)})}):o.closeOnSubmit&&bootstrap.Modal.getInstance(t).hide()})}}else n.submit()},validateAsyncSettings=function(e){var t=[];return e.successMessage||(t.push("successMessage"),console.error("django-bootstrap-modal-forms: 'successMessage' in asyncSettings is missing.")),e.dataUrl||(t.push("dataUrl"),console.error("django-bootstrap-modal-forms: 'dataUrl' in asyncSettings is missing.")),e.dataElementId||(t.push("dataElementId"),console.error("django-bootstrap-modal-forms: 'dataElementId' in asyncSettings is missing.")),e.dataKey||(t.push("dataKey"),console.error("django-bootstrap-modal-forms: 'dataKey' in asyncSettings is missing.")),e.addModalFormFunction||(t.push("addModalFormFunction"),console.error("django-bootstrap-modal-forms: 'addModalFormFunction' in asyncSettings is missing.")),!(t.length>0)},modalForm=function(e,t){let n={modalID:"#modal",modalContent:".modal-content",modalForm:".modal-content form",formURL:null,isDeleteForm:!1,errorClass:"is-invalid",asyncUpdate:!1,asyncSettings:{closeOnSubmit:!1,successMessage:null,dataUrl:null,dataElementId:null,dataKey:null,addModalFormFunction:null},...t};return e.addEventListener("click",()=>{modalFormCallback(n)}),e}; \ No newline at end of file diff --git a/bootstrap_modal_forms/utils.py b/bootstrap_modal_forms/utils.py deleted file mode 100644 index 1519708..0000000 --- a/bootstrap_modal_forms/utils.py +++ /dev/null @@ -1,8 +0,0 @@ -def is_ajax(meta): - if 'HTTP_X_REQUESTED_WITH' not in meta: - return False - - if meta['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest': - return True - - return False \ No newline at end of file diff --git a/examples/admin.py b/examples/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/examples/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/examples/apps.py b/examples/apps.py index 5940f52..8ad7c7a 100644 --- a/examples/apps.py +++ b/examples/apps.py @@ -2,4 +2,4 @@ class ExamplesConfig(AppConfig): - name = 'examples' + name = 'examples' \ No newline at end of file diff --git a/examples/forms.py b/examples/forms.py index f0715d8..9d599b2 100644 --- a/examples/forms.py +++ b/examples/forms.py @@ -33,4 +33,4 @@ class Meta: class CustomAuthenticationForm(AuthenticationForm): class Meta: model = User - fields = ['username', 'password'] + fields = ['username', 'password'] \ No newline at end of file diff --git a/examples/migrations/0001_initial.py b/examples/migrations/0001_initial.py index eed1f88..8feaf5f 100644 --- a/examples/migrations/0001_initial.py +++ b/examples/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1 on 2019-03-30 16:15 +# Generated by Django 3.2 on 2023-04-09 15:22 from django.db import migrations, models @@ -14,7 +14,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Book', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=50)), ('publication_date', models.DateField(null=True)), ('author', models.CharField(blank=True, max_length=30)), diff --git a/examples/models.py b/examples/models.py index fecc6d7..f17effb 100644 --- a/examples/models.py +++ b/examples/models.py @@ -17,4 +17,4 @@ class Book(models.Model): pages = models.IntegerField(blank=True, null=True) book_type = models.PositiveSmallIntegerField(choices=BOOK_TYPES) - timestamp = models.DateField(auto_now_add=True, auto_now=False) + timestamp = models.DateField(auto_now_add=True, auto_now=False) \ No newline at end of file diff --git a/examples/tests.py b/examples/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/examples/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/examples/urls.py b/examples/urls.py index 8baa565..9e0e1e9 100644 --- a/examples/urls.py +++ b/examples/urls.py @@ -13,4 +13,4 @@ path('books/', views.books, name='books'), path('signup/', views.SignUpView.as_view(), name='signup'), path('login/', views.CustomLoginView.as_view(), name='login'), -] +] \ No newline at end of file diff --git a/examples/views.py b/examples/views.py index 53c6410..0551253 100644 --- a/examples/views.py +++ b/examples/views.py @@ -1,6 +1,5 @@ from django.http import JsonResponse from django.template.loader import render_to_string -from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse_lazy from django.views import generic @@ -89,7 +88,7 @@ class CustomLoginView(BSModalLoginView): def books(request): - data = dict() + data = {} if request.method == 'GET': books = Book.objects.all() data['table'] = render_to_string( @@ -97,4 +96,4 @@ def books(request): {'books': books}, request=request ) - return JsonResponse(data) + return JsonResponse(data) \ No newline at end of file diff --git a/manage.py b/manage.py index 2c6b4a3..00d0b34 100755 --- a/manage.py +++ b/manage.py @@ -4,12 +4,9 @@ if __name__ == '__main__': os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'setup.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 + 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) diff --git a/requirements.txt b/requirements.txt index e0e9e30..68c7651 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -Django==2.2.28 -django-widget-tweaks==1.4.2 -selenium==3.14.0 -pytz==2018.5 +# End of life Django 3.2: April 2024 +# @see https://www.djangoproject.com/download/#supported-versions +Django==3.2 +django-widget-tweaks~=1.4 +selenium~=3.14 diff --git a/setup.py b/setup.py index 044863a..5c95dc9 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,12 @@ import os +from pathlib import Path + from setuptools import find_packages, setup -with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: - README = readme.read() +PROJECT_ROOT_DIR = Path(__file__).resolve().parent + +with open(Path(PROJECT_ROOT_DIR, 'README.rst')) as readme_file: + README = readme_file.read() # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) @@ -19,7 +23,7 @@ author='Uros Trstenjak', author_email='uros.trstenjak@gmail.com', install_requires=[ - 'Django>=1.8', + 'Django>=3.2', ], classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -27,15 +31,10 @@ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Software Development :: Libraries :: Python Modules' ], ) diff --git a/setup/settings.py b/setup/settings.py index 35172a0..7124919 100644 --- a/setup/settings.py +++ b/setup/settings.py @@ -1,7 +1,7 @@ import os +from pathlib import Path - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = 'ke2rim3a=ukld9cjh6$d$fb%ztgobvrs807i^d!_whg%@n^%v#' DEBUG = True @@ -32,12 +32,9 @@ ROOT_URLCONF = 'setup.urls' -PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'examples/templates'), ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -55,37 +52,27 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'database/db.sqlite3'), + 'NAME': str(Path(BASE_DIR, 'db.sqlite3')), } } -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', - }, -] +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# No password rules in development +AUTH_PASSWORD_VALIDATORS = [] +# Simple (and unsecure) but fast password hasher in development +PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' LANGUAGE_CODE = 'en-us' - TIME_ZONE = 'UTC' -USE_I18N = True - -USE_L10N = True - -USE_TZ = True +# No internationalization for this project +USE_I18N = False +USE_L10N = False +USE_TZ = False STATICFILES_FINDERS = [ # searches in STATICFILES_DIRS diff --git a/setup/wsgi.py b/setup/wsgi.py index 1ea09b8..0b84e9e 100644 --- a/setup/wsgi.py +++ b/setup/wsgi.py @@ -1,12 +1,3 @@ -""" -WSGI config for setup 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/2.1/howto/deployment/wsgi/ -""" - import os from django.core.wsgi import get_wsgi_application diff --git a/tests/base.py b/tests/base.py index 85a05c0..50d4393 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,23 +1,83 @@ +from pathlib import Path + from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from setup import settings + from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait -MAX_WAIT = 10 - class FunctionalTest(StaticLiveServerTestCase): + """ + Download your driver of choice, copy & paste it into the root directory of this project and change + the `BROWSER_DRIVER_PATH` variable to your downloaded driver file. + + FireFox + - Driver Download: https://github.com/mozilla/geckodriver/releases + - Compatibility: https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html + Chrome + - Driver Download: https://chromedriver.chromium.org/downloads + - Compatibility: https://chromedriver.chromium.org/downloads/version-selection + Edge (May also work with preinstalled version. Just try it. If it works, you're good. If not, download the files.) + - Driver Download: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ + - Compatibility: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ + Safari (May also work with preinstalled version. Just try it. If it works, you're good. If not, download the files.) + - Driver Download: https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari + - Compatibility: https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari + """ + + BROWSER = None + # Change this, to your browser type of choice + BROWSER_TYPE = webdriver.Chrome + # Change this, to your driver file of your chosen browser + BROWSER_DRIVER_PATH: Path = Path(settings.BASE_DIR, 'chromedriver') + # If you're using Firefox, and you have installed firefox in a none-standard directory, change this to the executable wherever + # you have installed Firefox. E.g.: Path('C:/My/None/Standard/directory/firefox.exe') + FIRE_FOX_BINARY = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.BROWSER = cls.get_browser() + # cls.BROWSER.implicitly_wait(5) - # Basic setUp & tearDown - def setUp(self): - self.browser = webdriver.Firefox() + @classmethod + def tearDownClass(cls): + cls.BROWSER.quit() + super().tearDownClass() - def tearDown(self): - self.browser.quit() + @classmethod + def get_browser(cls): + if cls.BROWSER_TYPE is webdriver.Firefox: + if cls.BROWSER_DRIVER_PATH is None: + raise ValueError('Firefox needs a path to a browser driver file!') + else: + if cls.FIRE_FOX_BINARY is None: + return webdriver.Firefox(executable_path=cls.BROWSER_DRIVER_PATH) + else: + return webdriver.Firefox(firefox_binary=str(cls.FIRE_FOX_BINARY), executable_path=cls.BROWSER_DRIVER_PATH) + elif cls.BROWSER_TYPE is webdriver.Chrome: + if cls.BROWSER_DRIVER_PATH is None: + raise ValueError('Chrome needs a path to a browser driver file!') + else: + return webdriver.Chrome(executable_path=cls.BROWSER_DRIVER_PATH) + elif cls.BROWSER_TYPE is webdriver.Edge: + if cls.BROWSER_DRIVER_PATH is None: + return webdriver.Edge() + else: + return webdriver.Edge(executable_path=cls.BROWSER_DRIVER_PATH) + elif cls.BROWSER_TYPE is webdriver.Safari: + if cls.BROWSER_DRIVER_PATH is None: + return webdriver.Safari() + else: + return webdriver.Safari(executable_path=cls.BROWSER_DRIVER_PATH) + else: + raise RuntimeError(f'Unsupported browser type: {cls.BROWSER_TYPE}') def wait_for(self, class_name=None, element_id=None, tag=None, xpath=None): - return WebDriverWait(self.browser, 20).until( + return WebDriverWait(self.BROWSER, 20).until( expected_conditions.element_to_be_clickable ((By.ID, element_id) if element_id else (By.CLASS_NAME, class_name) if class_name else diff --git a/tests/tests_functional.py b/tests/tests_functional.py index 8c4e321..f56b00d 100644 --- a/tests/tests_functional.py +++ b/tests/tests_functional.py @@ -7,13 +7,13 @@ class SignUpLoginTest(FunctionalTest): def test_signup_login(self): # User visits homepage and checks the content - self.browser.get(self.live_server_url) - self.assertIn('django-bootstrap-modal-forms', self.browser.title) - header_text = self.browser.find_element_by_tag_name('h1').text + self.BROWSER.get(self.live_server_url) + self.assertIn('django-bootstrap-modal-forms', self.BROWSER.title) + header_text = self.BROWSER.find_element_by_tag_name('h1').text self.assertIn('django-bootstrap-modal-forms', header_text) # User clicks Sign up button - self.browser.find_element_by_id('signup-btn').click() + self.BROWSER.find_element_by_id('signup-btn').click() # Sign up modal opens modal = self.wait_for(element_id='modal') @@ -41,14 +41,14 @@ def test_signup_login(self): form.submit() # User sees success message after page redirection - redirect_url = self.browser.current_url + redirect_url = self.BROWSER.current_url self.assertRegex(redirect_url, '/') # Slice removes '\nx' since alert is dismissible and contains 'times' button success_msg = self.wait_for(class_name='alert').text[:-2] self.assertEqual(success_msg, 'Success: Sign up succeeded. You can now Log in.') # User clicks log in button - self.browser.find_element_by_id('login-btn').click() + self.BROWSER.find_element_by_id('login-btn').click() # Log in modal opens modal = self.wait_for(element_id='modal') @@ -80,7 +80,7 @@ def test_signup_login(self): # User sees log out button after page redirection logout_btn_txt = self.wait_for(element_id='logout-btn').text self.assertEqual(logout_btn_txt, 'Log out') - redirect_url = self.browser.current_url + redirect_url = self.BROWSER.current_url self.assertRegex(redirect_url, '/') @@ -98,10 +98,10 @@ def setUp(self): def test_create_object(self): # User visits homepage - self.browser.get(self.live_server_url) + self.BROWSER.get(self.live_server_url) # User clicks create book button - self.browser.find_element_by_id('create-book-sync').click() + self.BROWSER.find_element_by_id('create-book-sync').click() # Create book modal opens modal = self.wait_for(element_id='create-modal') @@ -140,7 +140,7 @@ def test_create_object(self): form.submit() # User sees success message after page redirection - redirect_url = self.browser.current_url + redirect_url = self.BROWSER.current_url self.assertRegex(redirect_url, '/') # Slice removes '\nx' since alert is dismissible and contains 'times' button @@ -160,7 +160,7 @@ def test_create_object(self): def test_filter_object(self): # User visits homepage - self.browser.get(self.live_server_url) + self.BROWSER.get(self.live_server_url) # User clicks filter book button self.wait_for(element_id='filter-book').click() @@ -171,7 +171,7 @@ def test_filter_object(self): # User changes book type form = modal.find_element_by_tag_name('form') - book_type = self.browser.find_element_by_id("id_type") + book_type = self.BROWSER.find_element_by_id("id_type") book_type_select = Select(book_type) book_type_select.select_by_index(0) @@ -179,15 +179,15 @@ def test_filter_object(self): # User is redirected to the homepage with a querystring with the filter self.wait_for(class_name='filtered-books') - redirect_url = self.browser.current_url + redirect_url = self.BROWSER.current_url self.assertRegex(redirect_url, '/?type=1$') def test_update_object(self): # User visits homepage - self.browser.get(self.live_server_url) + self.BROWSER.get(self.live_server_url) # User clicks update book button - self.browser.find_element_by_class_name('update-book').click() + self.BROWSER.find_element_by_class_name('update-book').click() # Update book modal opens modal = self.wait_for(element_id='modal') @@ -206,7 +206,7 @@ def test_update_object(self): form.submit() # User sees success message after page redirection - redirect_url = self.browser.current_url + redirect_url = self.BROWSER.current_url self.assertRegex(redirect_url, '/') # Slice removes '\nx' since alert is dismissible and contains 'times' button @@ -234,10 +234,10 @@ def test_update_object(self): def test_read_object(self): # User visits homepage - self.browser.get(self.live_server_url) + self.BROWSER.get(self.live_server_url) # User clicks Read book button - self.browser.find_element_by_class_name('read-book').click() + self.BROWSER.find_element_by_class_name('read-book').click() # Read book modal opens modal = self.wait_for(element_id='modal') @@ -254,10 +254,10 @@ def test_read_object(self): def test_delete_object(self): # User visits homepage - self.browser.get(self.live_server_url) + self.BROWSER.get(self.live_server_url) # User clicks Delete book button - self.browser.find_element_by_class_name('delete-book').click() + self.BROWSER.find_element_by_class_name('delete-book').click() # Delete book modal opens modal = self.wait_for(element_id='modal') @@ -271,7 +271,7 @@ def test_delete_object(self): delete_btn.click() # User sees success message after page redirection - redirect_url = self.browser.current_url + redirect_url = self.BROWSER.current_url self.assertRegex(redirect_url, '/') # Slice removes '\nx' since alert is dismissible and contains 'times' button From 327e100def0dc798de15d1f875cf3d02c3dafd34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 18:17:11 +0200 Subject: [PATCH 04/31] Bump django from 3.2 to 3.2.18 (#217) Bumps [django](https://github.com/django/django) from 3.2 to 3.2.18. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.2...3.2.18) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 68c7651..c4c4186 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # End of life Django 3.2: April 2024 # @see https://www.djangoproject.com/download/#supported-versions -Django==3.2 +Django==3.2.18 django-widget-tweaks~=1.4 selenium~=3.14 From 7d1f9792b4637e2756faaa7bcc04f029b625315d Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 18:32:49 +0200 Subject: [PATCH 05/31] update README.rst --- README.rst | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 8e049d8..c6f7aab 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,13 @@ This repository includes ``Dockerfile`` and ``docker-compose.yml`` files so you $ docker compose up (use -d flag to run app in detached mode in the background) $ visit 0.0.0.0:8000 +Tests +===== + +Run unit and functional tests inside of project folder:: + + $ python manage.py test + Installation ============ @@ -36,9 +43,7 @@ Installation ... ] -3. Include Bootstrap, jQuery and ``jquery.bootstrap.modal.forms.js`` on every page where you would like to set up the AJAX driven Django forms in Bootstrap modal. - -IMPORTANT: Adjust Bootstrap and jQuery file paths to match yours, but include ``jquery.bootstrap.modal.forms.js`` exactly as in code bellow. +3. Include Bootstrap, jQuery and ``jquery.bootstrap(5).modal.forms.js`` on every page where you would like to set up the AJAX driven Django forms in Bootstrap modal. **IMPORTANT:** Adjust Bootstrap and jQuery file paths to match yours, but include ``jquery.bootstrap.modal.forms.js`` exactly as in code bellow. .. code-block:: html+django @@ -50,12 +55,14 @@ IMPORTANT: Adjust Bootstrap and jQuery file paths to match yours, but include `` - - + + + - + + @@ -444,33 +451,39 @@ Mixins Import mixins with ``from bootstrap_modal_forms.mixins import PassRequestMixin``. PassRequestMixin - Puts the request into the form's kwargs. + Form Mixin which puts the request into the form's kwargs. Note: Using this mixin requires you to pop the `request` kwarg out of the dict in the super of your form's `__init__`. See PopRequestMixin. PopRequestMixin - Pops request out of the kwargs and attaches it to the form's instance. + Form Mixin which pops request out of the kwargs and attaches it to the form's instance. Note: This mixin must precede forms.ModelForm/forms.Form. The form is not expecting these kwargs to be passed in, so they must be popped off before anything else is done. CreateUpdateAjaxMixin - Saves or doesn't save the object based on the request type. + ModelForm Mixin which passes or saves object based on request type. DeleteMessageMixin - Deletes object if request is not ajax request. + Generic View Mixin which adds message to BSModalDeleteView and only calls the post method if request is not ajax request. In case request is ajax post method calls delete method, which redirects to success url. + +FormValidationMixin + Generic View Mixin which saves object and redirects to success_url if request is not ajax request. Otherwise response 204 No content is returned. LoginAjaxMixin - Authenticates user if request is not ajax request. + Generic View Mixin which authenticates user if request is not ajax request. Generic views ============= Import generic views with ``from bootstrap_modal_forms.generic import BSModalFormView``. +BSModalLoginView + Inhertis LoginAjaxMixin and Django's LoginView. + BSModalFormView Inherits PassRequestMixin and Django's generic.FormView. BSModalCreateView - Inherits PassRequestMixin and Django's SuccessMessageMixin and generic.CreateView. + Inherits PassRequestMixin, FormValidationMixin and generic.CreateView. BSModalUpdateView - Inherits PassRequestMixin and Django's SuccessMessageMixin and generic.UpdateView. + Inherits PassRequestMixin, FormValidationMixin and generic.UpdateView. BSModalReadView Inherits Django's generic.DetailView. @@ -489,13 +502,6 @@ To see ``django-bootstrap-modal-forms`` in action clone the repository and run t $ python manage.py migrate $ python manage.py runserver -Tests -===== - -Run unit and functional tests inside of project folder:: - - $ python manage.py test - Example 1: Signup form in Bootstrap modal ***************************************** From 60c80659a88a4fb804e1bd49fa4930e9760f031d Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 18:33:48 +0200 Subject: [PATCH 06/31] update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c6f7aab..b82dc2d 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,7 @@ Installation - + From 620a7c0ecc0d79a4f7643f0f6f3b805464a54f1e Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 10:10:05 +0200 Subject: [PATCH 07/31] update django to latest version --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c4c4186..c5610b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -# End of life Django 3.2: April 2024 +# End of life Django 4.2: April 2026 # @see https://www.djangoproject.com/download/#supported-versions -Django==3.2.18 +Django==4.2 django-widget-tweaks~=1.4 selenium~=3.14 From 16b792e0afd6b244f40d716b07d22863a6c6e3ce Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 10:10:13 +0200 Subject: [PATCH 08/31] cleanup --- docker-compose.yml | 1 - setup/settings.py | 4 ---- 2 files changed, 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fb3ff39..c08ce99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,6 @@ version: '3.9' services: django-bootstrap-modal-forms: - container_name: django-bootstrap-modal-forms build: . command: > sh -c "python manage.py migrate && diff --git a/setup/settings.py b/setup/settings.py index 7124919..65963cb 100644 --- a/setup/settings.py +++ b/setup/settings.py @@ -82,7 +82,3 @@ ] STATIC_URL = '/static/' - -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), -] From 09eb39945340c123f2a40edbfe44571ca6420fa1 Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 10:41:39 +0200 Subject: [PATCH 09/31] set fixed package versions --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c5610b0..ec2cb56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # End of life Django 4.2: April 2026 # @see https://www.djangoproject.com/download/#supported-versions Django==4.2 -django-widget-tweaks~=1.4 -selenium~=3.14 +django-widget-tweaks==1.4 +selenium==3.14 From 0c875df0cd4cd4154ad2e65727eab2fb2af0700d Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 10:42:14 +0200 Subject: [PATCH 10/31] prepare for 3.0.0 release --- CHANGELOG.rst | 11 +++++++++++ .../static/js/bootstrap5.modal.forms.js | 2 +- .../static/js/jquery.bootstrap.modal.forms.js | 2 +- setup.py | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 20ed062..e4a9911 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,17 @@ Changelog ========= +3.0.0 (2023-05-02) +================== + +- deprecate support for python < 3.8 +- deprecate support for Django < 3.2 +- deprecate SuccessMessageMixin +- add FormValidationMixin +- add support for Chrome, Firefox, Edge and Safari drivers when running funtional tests +- update examples to Django=4.2 +- update README.rst + 2.2.1 (2023-03-05) ================== diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 37dab68..2503f7b 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 2.2.1 +version : 3.0.0 Copyright (c) 2023 Marcel Rupp */ diff --git a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js index 05f0cda..c4cf6bf 100644 --- a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js +++ b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 2.2.1 +version : 3.0.0 Copyright (c) 2023 Uroš Trstenjak https://github.com/trco/django-bootstrap-modal-forms */ diff --git a/setup.py b/setup.py index 5c95dc9..5f64d65 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='django-bootstrap-modal-forms', - version='2.2.1', + version='3.0.0', packages=find_packages(), include_package_data=True, license='MIT License', From 3aed06c6862c9ae756ddfca12b3a2538a075118b Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 12:01:15 +0200 Subject: [PATCH 11/31] add general information to README.rst --- README.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b82dc2d..b44799e 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,19 @@ Demo_ .. _Demo: http://trco.silkym.com/dbmf/ + +General information +=================== + +Opening an issue +**************** +When reproting an issue for ``django-bootstrap-modal-forms`` package, please prepare a publicly available repository having the issue you are reporting. The clear reproduce is the optimal way towards reoslution. + +Contribute +********** +This is an Open Source project and any contribution is highly appreciated. + + Test and experiment on your machine =================================== @@ -1121,11 +1134,6 @@ For explanation how all the parts of the code work together see paragraph **Usag }); -Contribute -========== - -This is an Open Source project and any contribution is appreciated. - License ======= From c1da8a590ef134eb0b06c2ba670644975e8d9eb2 Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 12:02:20 +0200 Subject: [PATCH 12/31] fix typos --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b44799e..dd9b305 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ General information Opening an issue **************** -When reproting an issue for ``django-bootstrap-modal-forms`` package, please prepare a publicly available repository having the issue you are reporting. The clear reproduce is the optimal way towards reoslution. +When reporting an issue for ``django-bootstrap-modal-forms`` package, please prepare a publicly available repository having the issue you are reporting. The clear reproduce is the optimal way towards resolution. Contribute ********** From b41f368cd9e21bbfdd25e1e8ab1b062d38926da1 Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 12:12:28 +0200 Subject: [PATCH 13/31] add table of contents to README.rst --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index dd9b305..aa0e3ca 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,11 @@ Django Bootstrap Modal Forms A Django plugin for creating AJAX driven forms in Bootstrap modal. +.. contents:: Table of Contents + :depth: 2 + :local: + :backlinks: none + Live Demo ========= From 9f6a3105f69fdcdc26bb7ca6ad90d6b4b76c9d11 Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 12:14:28 +0200 Subject: [PATCH 14/31] update Table of Contents title --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index aa0e3ca..d6ad772 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Django Bootstrap Modal Forms A Django plugin for creating AJAX driven forms in Bootstrap modal. -.. contents:: Table of Contents +.. contents:: **Table of Contents** :depth: 2 :local: :backlinks: none From 342371f053c0e98fd4fc715293619af902c0ba4a Mon Sep 17 00:00:00 2001 From: Christian Wiegand Date: Tue, 2 May 2023 15:02:30 +0200 Subject: [PATCH 15/31] Fix redirect of FormValidationMixin to be compliant to standard django: (#219) If return class property success_url if exists, if not return value from classes get_success_url method. If that doesn't exist return value from models get_absolute_url method. --- bootstrap_modal_forms/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index b984932..2618bae 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -89,7 +89,7 @@ def form_valid(self, form): form.save() messages.success(self.request, self.success_message) - return HttpResponseRedirect(self.success_url) + return HttpResponseRedirect(self.get_success_url()) def is_ajax(meta): From 30fc75aaaebb00644850527e80b39965af550c23 Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 15:04:50 +0200 Subject: [PATCH 16/31] prepare for 3.0.1 release --- CHANGELOG.rst | 5 +++++ bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js | 2 +- .../static/js/jquery.bootstrap.modal.forms.js | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4a9911..3af6be1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +3.0.1 (2023-05-02) +================== + +- fix redirect in FormValidationMixin + 3.0.0 (2023-05-02) ================== diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 2503f7b..f1e5afc 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.0 +version : 3.0.1 Copyright (c) 2023 Marcel Rupp */ diff --git a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js index c4cf6bf..9281d4e 100644 --- a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js +++ b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.0 +version : 3.0.1 Copyright (c) 2023 Uroš Trstenjak https://github.com/trco/django-bootstrap-modal-forms */ diff --git a/setup.py b/setup.py index 5f64d65..4c9d265 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='django-bootstrap-modal-forms', - version='3.0.0', + version='3.0.1', packages=find_packages(), include_package_data=True, license='MIT License', From 1610edef8ed8621e595fbd36061d96de0d9d13da Mon Sep 17 00:00:00 2001 From: Christian Wiegand Date: Tue, 2 May 2023 19:46:58 +0200 Subject: [PATCH 17/31] Fix form validation success url (#221) * Fix redirect of FormValidationMixin to be compliant to standard django: If return class property success_url if exists, if not return value from classes get_success_url method. If that doesn't exist return value from models get_absolute_url method. * Fix success url to work with all constellations --- bootstrap_modal_forms/mixins.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index 2618bae..253c9f1 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -77,7 +77,12 @@ class FormValidationMixin: """ Generic View Mixin which saves object and redirects to success_url if request is not ajax request. Otherwise response 204 No content is returned. """ - + + def get_success_url(self): + if self.success_url: + return self.success_url + return super().get_success_url() + def form_valid(self, form): isAjaxRequest = is_ajax(self.request.META) asyncUpdate = self.request.POST.get('asyncUpdate') == 'True' @@ -86,7 +91,7 @@ def form_valid(self, form): if asyncUpdate: form.save() return HttpResponse(status=204) - + form.save() messages.success(self.request, self.success_message) return HttpResponseRedirect(self.get_success_url()) From 5b89deede252895015126619281245adcd210065 Mon Sep 17 00:00:00 2001 From: trco Date: Tue, 2 May 2023 19:49:34 +0200 Subject: [PATCH 18/31] prepare for 3.0.1 release --- CHANGELOG.rst | 5 +++++ bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js | 2 +- .../static/js/jquery.bootstrap.modal.forms.js | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3af6be1..073f186 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +3.0.2 (2023-05-02) +================== + +- fix call to get_success_url method in FormValidationMixin + 3.0.1 (2023-05-02) ================== diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index f1e5afc..456fb35 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.1 +version : 3.0.2 Copyright (c) 2023 Marcel Rupp */ diff --git a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js index 9281d4e..e13fb65 100644 --- a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js +++ b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.1 +version : 3.0.2 Copyright (c) 2023 Uroš Trstenjak https://github.com/trco/django-bootstrap-modal-forms */ diff --git a/setup.py b/setup.py index 4c9d265..66d9d89 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='django-bootstrap-modal-forms', - version='3.0.1', + version='3.0.2', packages=find_packages(), include_package_data=True, license='MIT License', From b3f9e42c5b8b01a3610a06fde917b66544992207 Mon Sep 17 00:00:00 2001 From: Christian Wiegand Date: Sun, 4 Jun 2023 15:44:32 +0200 Subject: [PATCH 19/31] Make success messages more dynamic and compliant with standard django (#220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the success message so that it can be used with either a static message (class property) or a dynamic success message from get_success_message method (so we're able to use model instance attributes in the message) or even without a message. Co-authored-by: Uroš Trstenjak <33464584+trco@users.noreply.github.com> --- bootstrap_modal_forms/mixins.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index 253c9f1..eeaff5d 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -78,6 +78,10 @@ class FormValidationMixin: Generic View Mixin which saves object and redirects to success_url if request is not ajax request. Otherwise response 204 No content is returned. """ + def get_success_message(self): + if hasattr(self, 'success_message'): + return self.success_message + def get_success_url(self): if self.success_url: return self.success_url @@ -93,7 +97,7 @@ def form_valid(self, form): return HttpResponse(status=204) form.save() - messages.success(self.request, self.success_message) + messages.success(self.request, self.get_success_message()) return HttpResponseRedirect(self.get_success_url()) From bb881310fdf529db2a9a624b5f0bd2acf04eb210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Jun 2023 15:46:12 +0200 Subject: [PATCH 20/31] Bump django from 4.2 to 4.2.1 (#222) Bumps [django](https://github.com/django/django) from 4.2 to 4.2.1. - [Commits](https://github.com/django/django/compare/4.2...4.2.1) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ec2cb56..45f6c3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # End of life Django 4.2: April 2026 # @see https://www.djangoproject.com/download/#supported-versions -Django==4.2 +Django==4.2.1 django-widget-tweaks==1.4 selenium==3.14 From e0f850414b6c318162a3dda5a1a576e2057262ef Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 4 Jun 2023 18:52:34 +0200 Subject: [PATCH 21/31] prepare for 3.0.3 release --- bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js | 2 +- bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 456fb35..b0abbd0 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.2 +version : 3.0.3 Copyright (c) 2023 Marcel Rupp */ diff --git a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js index e13fb65..d64434f 100644 --- a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js +++ b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.2 +version : 3.0.3 Copyright (c) 2023 Uroš Trstenjak https://github.com/trco/django-bootstrap-modal-forms */ diff --git a/setup.py b/setup.py index 66d9d89..d353b96 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='django-bootstrap-modal-forms', - version='3.0.2', + version='3.0.3', packages=find_packages(), include_package_data=True, license='MIT License', From ea382b8e48ce27e5175a7c70450ce7d7ca70d443 Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 5 Jun 2023 07:20:20 +0200 Subject: [PATCH 22/31] Update CHANGELOG.rst --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 073f186..b7b407c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +3.0.3 (2023-06-04) +================== + +- add get_success_message method in FormValidationMixin + + 3.0.2 (2023-05-02) ================== From c523a8fc0e4bdb5675b7d8754a339d8c05c1cd2f Mon Sep 17 00:00:00 2001 From: Jeffrey McAthey Date: Sun, 18 Jun 2023 14:46:51 +0100 Subject: [PATCH 23/31] Bump django-widget-tweaks from 1.4 to 1.4.12 (#226) Fix #225 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 45f6c3f..a4b23f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # End of life Django 4.2: April 2026 # @see https://www.djangoproject.com/download/#supported-versions Django==4.2.1 -django-widget-tweaks==1.4 +django-widget-tweaks==1.4.12 selenium==3.14 From 84285919d7d4fd86624eb3765ff568e7833d53ac Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 18 Jun 2023 15:51:58 +0200 Subject: [PATCH 24/31] prepare for 3.0.4 release --- CHANGELOG.rst | 5 +++++ bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js | 2 +- .../static/js/jquery.bootstrap.modal.forms.js | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b7b407c..da0590c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog ========= +3.0.4 (2023-06-18) +================== + +- fix broken examples by bumping django-widget-tweaks dependency version + 3.0.3 (2023-06-04) ================== diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index b0abbd0..2aaa5fe 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.3 +version : 3.0.4 Copyright (c) 2023 Marcel Rupp */ diff --git a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js index d64434f..dca793a 100644 --- a/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js +++ b/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js @@ -1,6 +1,6 @@ /* django-bootstrap-modal-forms -version : 3.0.3 +version : 3.0.4 Copyright (c) 2023 Uroš Trstenjak https://github.com/trco/django-bootstrap-modal-forms */ diff --git a/setup.py b/setup.py index d353b96..c544415 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='django-bootstrap-modal-forms', - version='3.0.3', + version='3.0.4', packages=find_packages(), include_package_data=True, license='MIT License', From fc1305882603eba878635490ee66f99df17879a3 Mon Sep 17 00:00:00 2001 From: trco Date: Sat, 1 Jul 2023 12:08:23 +0200 Subject: [PATCH 25/31] update README.rst --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d6ad772..1c07f3c 100644 --- a/README.rst +++ b/README.rst @@ -158,6 +158,7 @@ Define form's html and save it as Django template. - Form will POST to ``formURL`` defined in #6. - Add ``class="invalid"`` or custom ``errorClass`` (see paragraph **Options**) to the elements that wrap the fields. - ``class="invalid"`` acts as a flag for the fields having errors after the form has been POSTed. +- IMPORTANT NOTE: ``class="invalid"`` is default setting for Bootstrap 4, while the default setting for Bootstrap 5 version of this package is ``class="is-invalid"``. .. code-block:: html @@ -406,7 +407,7 @@ isDeleteForm Defines if form is used for deletion. Should be set to ``true`` for deletion forms. ``Default: false`` errorClass - Sets the custom class for the form fields having errors. ``Default: ".invalid"`` + Sets the custom class for the form fields having errors. ``Default: ".invalid" for Boostrap 4 and ".is-invalid" for Bootstrap 5.`` asyncUpdate Sets asynchronous content update after form submission. ``Default: false`` @@ -440,6 +441,7 @@ modalForm default settings object and it's structure modalForm: ".modal-content form", formURL: null, isDeleteForm: false, + // .invalid is default class for Bootstrap 4, while the default class for Bootstrap 5 version of this package is .is-invalid. errorClass: ".invalid", asyncUpdate: false, asyncSettings: { From bb6c10447a93fdc26c7bf738ad5034a45c74114f Mon Sep 17 00:00:00 2001 From: trco Date: Sat, 1 Jul 2023 12:16:22 +0200 Subject: [PATCH 26/31] update README.rst --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 1c07f3c..2b1c2b5 100644 --- a/README.rst +++ b/README.rst @@ -154,11 +154,10 @@ Define BookModelForm and inherit built-in form ``BSModalModelForm``. Define form's html and save it as Django template. -- Bootstrap 4 modal elements are used in this example. - Form will POST to ``formURL`` defined in #6. -- Add ``class="invalid"`` or custom ``errorClass`` (see paragraph **Options**) to the elements that wrap the fields. +- Add ``class="invalid"`` or custom ``errorClass`` (see paragraph **Options**) to the elements that wrap the fields - ``class="invalid"`` acts as a flag for the fields having errors after the form has been POSTed. -- IMPORTANT NOTE: ``class="invalid"`` is default setting for Bootstrap 4, while the default setting for Bootstrap 5 version of this package is ``class="is-invalid"``. +- **IMPORTANT NOTE:** Bootstrap 4 modal elements are used in this example. ``class="invalid"`` is the default for Bootstrap 4. ``class="is-invalid"`` is the default for Bootstrap 5. .. code-block:: html @@ -441,7 +440,7 @@ modalForm default settings object and it's structure modalForm: ".modal-content form", formURL: null, isDeleteForm: false, - // .invalid is default class for Bootstrap 4, while the default class for Bootstrap 5 version of this package is .is-invalid. + // ".invalid" is the default for Bootstrap 4. ".is-invalid" is the default for Bootstrap 5. errorClass: ".invalid", asyncUpdate: false, asyncSettings: { From 6c3825342b49bd8047ed823150fe77055f384517 Mon Sep 17 00:00:00 2001 From: trco Date: Sat, 1 Jul 2023 19:39:47 +0200 Subject: [PATCH 27/31] update README.rst --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2b1c2b5..f3d9ebf 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,11 @@ This is an Open Source project and any contribution is highly appreciated. Test and experiment on your machine =================================== -This repository includes ``Dockerfile`` and ``docker-compose.yml`` files so you can easily setup and start to experiment with ``django-bootstrap-modal-forms`` running inside of a container on your local machine. Any changes you make in ``bootstrap_modal_forms``, ``examples`` and ``test`` folders are reflected in the container (see docker-compose.yml) and the data stored in sqlite3 database are persistent even if you remove stopped container. Follow the steps below to run the app:: +This repository includes ``Dockerfile`` and ``docker-compose.yml`` files so you can easily setup and start to experiment with ``django-bootstrap-modal-forms`` running inside of a container on your local machine. Any changes you make in ``bootstrap_modal_forms``, ``examples`` and ``test`` folders are reflected in the container (see docker-compose.yml) and the data stored in sqlite3 database are persistent even if you remove stopped container. + +Note that ``master branch`` contains Bootstrap 4 examples, while ``bootstrap5-examples branch`` contains Bootstrap 5 examples. To experiment with Bootstrap 5 examples simply switch the branch. + +Follow the steps below to run the app:: $ clone repository $ cd django-bootstrap-modal-forms From 3db496b84ac6f7a43abf162bc487f7b29a08bc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Trstenjak?= <33464584+trco@users.noreply.github.com> Date: Thu, 3 Aug 2023 11:26:45 +0200 Subject: [PATCH 28/31] update README.rst --- README.rst | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index f3d9ebf..0a87e1d 100644 --- a/README.rst +++ b/README.rst @@ -9,13 +9,19 @@ A Django plugin for creating AJAX driven forms in Bootstrap modal. :local: :backlinks: none -Live Demo -========= +Test and experiment on your machine +=================================== -Demo_ +This repository includes ``Dockerfile`` and ``docker-compose.yml`` files so you can easily setup and start to experiment with ``django-bootstrap-modal-forms`` running inside of a container on your local machine. Any changes you make in ``bootstrap_modal_forms``, ``examples`` and ``test`` folders are reflected in the container (see docker-compose.yml) and the data stored in sqlite3 database are persistent even if you remove stopped container. -.. _Demo: http://trco.silkym.com/dbmf/ +Note that ``master branch`` contains Bootstrap 4 examples, while ``bootstrap5-examples branch`` contains Bootstrap 5 examples. To experiment with Bootstrap 5 examples simply switch the branch. +Follow the steps below to run the app:: + + $ clone repository + $ cd django-bootstrap-modal-forms + $ docker compose up (use -d flag to run app in detached mode in the background) + $ visit 0.0.0.0:8000 General information =================== @@ -28,21 +34,6 @@ Contribute ********** This is an Open Source project and any contribution is highly appreciated. - -Test and experiment on your machine -=================================== - -This repository includes ``Dockerfile`` and ``docker-compose.yml`` files so you can easily setup and start to experiment with ``django-bootstrap-modal-forms`` running inside of a container on your local machine. Any changes you make in ``bootstrap_modal_forms``, ``examples`` and ``test`` folders are reflected in the container (see docker-compose.yml) and the data stored in sqlite3 database are persistent even if you remove stopped container. - -Note that ``master branch`` contains Bootstrap 4 examples, while ``bootstrap5-examples branch`` contains Bootstrap 5 examples. To experiment with Bootstrap 5 examples simply switch the branch. - -Follow the steps below to run the app:: - - $ clone repository - $ cd django-bootstrap-modal-forms - $ docker compose up (use -d flag to run app in detached mode in the background) - $ visit 0.0.0.0:8000 - Tests ===== From 32a1df374865c7eda2cd555be8a6ab26f8f508fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:59:41 +0200 Subject: [PATCH 29/31] Bump django from 4.2.1 to 4.2.3 (#228) Bumps [django](https://github.com/django/django) from 4.2.1 to 4.2.3. - [Commits](https://github.com/django/django/compare/4.2.1...4.2.3) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a4b23f9..aac648d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # End of life Django 4.2: April 2026 # @see https://www.djangoproject.com/download/#supported-versions -Django==4.2.1 +Django==4.2.3 django-widget-tweaks==1.4.12 selenium==3.14 From 1113ac44a46bad39b390deabcf9cb41b94883ca3 Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Thu, 30 Nov 2023 18:12:10 +0100 Subject: [PATCH 30/31] Added additional Fetch API parameters and updated README.rst --- README.rst | 46 ++++++- .../static/js/bootstrap5.modal.forms.js | 128 +++++++++++++----- 2 files changed, 136 insertions(+), 38 deletions(-) diff --git a/README.rst b/README.rst index 0a87e1d..ff1d948 100644 --- a/README.rst +++ b/README.rst @@ -424,6 +424,40 @@ asyncSettings.dataKey asyncSettings.addModalFormFunction Sets the method needed for reinstantiation of event listeners on buttons (single or all CRUD buttons) after asynchronous update. ``Default: null`` +Fetch API parameters +******************** + +(@see `MDN Web DOCs - Using the Fetch API `_). + +credentials: + Choices: omit, include, same-origin + + ``Default: same-origin`` +method: + Choices: GET, POST, PUT, DELETE + + ``Default: POST`` +mode: + Choices: no-cors, cors, same-origin + + ``Default: cors`` +cache: + Choices: default, no-cache, reload, force-cache, only-if-cached + + ``Default: no-cache`` +headers: + Choices: @see `MDN Web DOCs - HTTP headers `_ + + ``Default: { "Content-Type": "application/json" }`` +redirect: + Choices: manual, follow, error + + ``Default: follow`` +referrerPolicy: + Choices: no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + + ``Default: no-referrer`` + modalForm default settings object and it's structure **************************************************** @@ -445,7 +479,17 @@ modalForm default settings object and it's structure dataElementId: null, dataKey: null, addModalFormFunction: null - } + }, + // At this point Fetch API parameters + credentials: 'same-origin', + method: 'POST', + mode: 'cors', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'follow', + referrerPolicy: 'no-referrer' }); Forms diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 2aaa5fe..2923767 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,10 +1,15 @@ +'use strict'; /* django-bootstrap-modal-forms -version : 3.0.4 +version : 2.2.1 Copyright (c) 2023 Marcel Rupp */ -// Open modal & load the form at formURL to the modalContent element +/** + * Open modal & load the form as inner HTML from "formURL" to the "modalContent" element + * + * @param {Object} settings Configuration/Settings for associated modal + */ const modalFormCallback = function (settings) { let modal = document.querySelector(settings.modalID); let content = modal.querySelector(settings.modalContent); @@ -16,21 +21,32 @@ const modalFormCallback = function (settings) { }) } - fetch(settings.formURL).then(res => { + fetch(settings.formURL, {credentials: settings.credentials}).then(res => { + // Get content from target URL return res.text(); }).then(data => { + // Set content to inner HTML content.innerHTML = data; }).then(() => { + // Finally show the modal with new content modalInstance.show(); let form = modal.querySelector(settings.modalForm); if (form) { - form.setAttribute("action", settings.formURL); - addEventHandlers(modal, form, settings) + form.action = settings.formURL; + // Add handler for form validation + addEventHandlers(modal, form, settings); } }); }; +/** + * Adds event handler for form validation cycle. + * + * @param {HTMLElement} modal The modal + * @param {HTMLElement} form The actual form, that should be evaluated by the server + * @param {Object} settings Configuration/Settings for associated modal + */ const addEventHandlers = function (modal, form, settings) { form.addEventListener('submit', (event) => { if (settings.isDeleteForm === false) { @@ -40,7 +56,7 @@ const addEventHandlers = function (modal, form, settings) { } }); - modal.addEventListener('hidden.bs.modal', (event) => { + modal.addEventListener('hidden.bs.modal', () => { let content = modal.querySelector(settings.modalContent); while (content.lastChild) { content.removeChild(content.lastChild); @@ -48,23 +64,33 @@ const addEventHandlers = function (modal, form, settings) { }); }; -// Check if form.is_valid() & either show errors or submit it via callback +/** + * Sends the form to the server & processes the result. If the form is valid the redirect from the + * form will be executed. If the form is invalid the errors are shown and no redirect will be executed. + * + * @param {Object} settings Configuration/Settings for associated modal + * @param {Function} callback Callback to break out of form validation cycle + */ const isFormValid = function (settings, callback) { let modal = document.querySelector(settings.modalID); let form = modal.querySelector(settings.modalForm); - const headers = new Headers(); + let headers = new Headers(); headers.append('X-Requested-With', 'XMLHttpRequest'); let btnSubmit = modal.querySelector('button[type="submit"]'); btnSubmit.disabled = true; - fetch(form.getAttribute("action"), { + + fetch(form.action, { headers: headers, - method: form.getAttribute("method"), + method: form.method, body: new FormData(form), + credentials: settings.credentials, }).then(res => { return res.text(); }).then(data => { + // console.log(data) if (data.includes(settings.errorClass)) { + // Form is invalid, therefore set the returned form (with marked invalid fields) to new inner HTML modal.querySelector(settings.modalContent).innerHTML = data; form = modal.querySelector(settings.modalForm); @@ -73,7 +99,8 @@ const isFormValid = function (settings, callback) { return; } - form.setAttribute("action", settings.formURL); + // Start from the beginning + form.action = settings.formURL; addEventHandlers(modal, form, settings) } else { callback(settings); @@ -81,7 +108,11 @@ const isFormValid = function (settings, callback) { }); }; -// Submit form callback function +/** + * Submit form callback function + * + * @param {Object} settings Configuration/Settings for associated modal + */ const submitForm = function (settings) { let modal = document.querySelector(settings.modalID); let form = modal.querySelector(settings.modalForm); @@ -89,7 +120,7 @@ const submitForm = function (settings) { if (!settings.asyncUpdate) { form.submit(); } else { - let asyncSettingsValid = validateAsyncSettings(settings.asyncSettings); + const asyncSettingsValid = validateAsyncSettings(settings.asyncSettings); if (asyncSettingsValid) { let asyncSettings = settings.asyncSettings; // Serialize form data @@ -97,9 +128,10 @@ const submitForm = function (settings) { // Add asyncUpdate and check for it in save method of CreateUpdateAjaxMixin formData.append("asyncUpdate", "True"); - fetch(form.getAttribute("action"), { - method: form.getAttribute("method"), + fetch(form.action, { + method: form.method, body: formData, + credentials: settings.credentials, }).then(res => { return res.text(); }).then(data => { @@ -114,7 +146,7 @@ const submitForm = function (settings) { if (asyncSettings.dataUrl) { // Update page without refresh - fetch(asyncSettings.dataUrl).then(res => res.json()).then(data => { + fetch(asyncSettings.dataUrl, {credentials: settings.credentials}).then(res => res.json()).then(data => { // Update page let dataElement = document.querySelector(asyncSettings.dataElementId); if (dataElement) { @@ -130,7 +162,7 @@ const submitForm = function (settings) { bootstrap.Modal.getInstance(modal).hide(); } else { // Reload form - fetch(settings.formURL).then(res => { + fetch(settings.formURL, {credentials: settings.credentials}).then(res => { return res.text(); }).then(data => { let content = modal.querySelector(settings.modalContent); @@ -142,7 +174,7 @@ const submitForm = function (settings) { return; } - form.setAttribute("action", settings.formURL); + form.action = settings.formURL; addEventHandlers(modal, form, settings) }); } @@ -155,8 +187,14 @@ const submitForm = function (settings) { } }; +/** + * Validates given settings/configuration for asynchronous calls. + * + * @param {Object} settings Configuration/Settings for associated modal + * @return {boolean} True if given configuration/settings is valid, false otherwise + */ const validateAsyncSettings = function (settings) { - var missingSettings = []; + let missingSettings = []; if (!settings.successMessage) { missingSettings.push("successMessage"); @@ -178,23 +216,30 @@ const validateAsyncSettings = function (settings) { missingSettings.push("addModalFormFunction"); console.error("django-bootstrap-modal-forms: 'addModalFormFunction' in asyncSettings is missing."); } - - if (missingSettings.length > 0) { - return false; - } - - return true; + return missingSettings.length < 1; }; -const modalForm = function(elem, options) { - // Default settings +/** + * Adds click listener to given button. If button is clicked, associated + * modal makes a call to given URL("formURL") to load its inner HTML. + * + * credentials: + * Prevent browser to share credentials (Cookies, Authorization headers & TLS client certificates for future + * authentication) secrets with malicious 3rd parties. Defaults to "same-origin". + * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included + * + * @param {HTMLElement} trigger_btn Button that triggers the modal to open/close + * @param {Object} settings Configuration/Settings for this given modal + * @return {HTMLElement} The button with an event listener + */ +const modalForm = function (trigger_btn, settings) { let defaults = { - modalID: "#modal", - modalContent: ".modal-content", - modalForm: ".modal-content form", + modalID: '#modal', + modalContent: '.modal-content', + modalForm: '.modal-content form', formURL: null, isDeleteForm: false, - errorClass: "is-invalid", + errorClass: 'is-invalid', asyncUpdate: false, asyncSettings: { closeOnSubmit: false, @@ -203,14 +248,23 @@ const modalForm = function(elem, options) { dataElementId: null, dataKey: null, addModalFormFunction: null - } + }, + credentials: 'same-origin', + method: 'POST', + mode: 'cors', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'follow', + referrerPolicy: 'no-referrer' }; - let settings = {...defaults, ...options} + const replenished_settings = {...defaults, ...settings} - elem.addEventListener('click', () => { - modalFormCallback(settings); + trigger_btn.addEventListener('click', () => { + modalFormCallback(replenished_settings); }) - return elem; -} + return trigger_btn; +} \ No newline at end of file From 9ecdc2217f3efc74b2d90ff8827e98de26ec6d9a Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Thu, 30 Nov 2023 18:26:23 +0100 Subject: [PATCH 31/31] Updated fetch call to retrieve values from settings object --- bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 2923767..5b5e86a 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -1,7 +1,7 @@ 'use strict'; /* django-bootstrap-modal-forms -version : 2.2.1 +version : 3.0.4 Copyright (c) 2023 Marcel Rupp */ @@ -21,7 +21,7 @@ const modalFormCallback = function (settings) { }) } - fetch(settings.formURL, {credentials: settings.credentials}).then(res => { + fetch(settings.formURL, {credentials: settings.credentials, method: settings.method, mode: settings.mode, cache: settings.cache, headers: settings.headers, redirect: settings.redirect, referrerPolicy: settings.referrerPolicy}).then(res => { // Get content from target URL return res.text(); }).then(data => {