From b9e4ad76138614243594d106352be1b579cebd57 Mon Sep 17 00:00:00 2001 From: Christian Wiegand Date: Tue, 19 Apr 2022 20:09:14 +0200 Subject: [PATCH 01/25] Fix waring from BSModalDeleteView / DeleteMessageMixin with Django 4 --- bootstrap_modal_forms/mixins.py | 9 +++++---- tests/tests_functional.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index b275538..1fdf1ef 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -52,15 +52,16 @@ class DeleteMessageMixin(object): Mixin which adds message to BSModalDeleteView and only calls the delete method if request is not ajax request. """ - - 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): """ Mixin which authenticates user if request is not ajax request. @@ -70,4 +71,4 @@ 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()) diff --git a/tests/tests_functional.py b/tests/tests_functional.py index 8c4e321..059a0e5 100644 --- a/tests/tests_functional.py +++ b/tests/tests_functional.py @@ -30,7 +30,7 @@ def test_signup_login(self): # User sees error in form error = self.wait_for(class_name='help-block') - self.assertEqual(error.text, 'The two password fields didn’t match.') + self.assertEqual(error.text, 'The two password fields didn\'t match.') # User fills in and submits sign up form correctly form = modal.find_element_by_tag_name('form') From 53ca042caa811ac5b193f7bd7df95e31975ceb39 Mon Sep 17 00:00:00 2001 From: Mark Monaghan Date: Sat, 18 Mar 2023 13:28:48 +1100 Subject: [PATCH 02/25] fix when form has field named method --- bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js | 4 ++-- bootstrap_modal_forms/static/js/bootstrap5.modal.forms.min.js | 2 +- 2 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 3008b69..154f50a 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -59,7 +59,7 @@ const isFormValid = function (settings, callback) { btnSubmit.disabled = true; fetch(form.action, { headers: headers, - method: form.method, + method: form.getAttribute('method'), body: new FormData(form), }).then(res => { return res.text(); @@ -98,7 +98,7 @@ const submitForm = function (settings) { formData.append("asyncUpdate", "True"); fetch(form.action, { - method: form.method, + method: form.getAttribute('method'), body: formData, }).then(res => { return res.text(); 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..d3f575d 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(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.getAttribute('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.getAttribute('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 From 8699c8cd83122f8da92eeb9e43ed23a5e1df33f9 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 19 Mar 2023 18:49:11 +0100 Subject: [PATCH 03/25] add new mixin for FormValidation, remove SuccessMessageMixin dependecy --- bootstrap_modal_forms/generic.py | 11 ++++++----- bootstrap_modal_forms/mixins.py | 34 +++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/bootstrap_modal_forms/generic.py b/bootstrap_modal_forms/generic.py index 3df0873..52829a1 100644 --- a/bootstrap_modal_forms/generic.py +++ b/bootstrap_modal_forms/generic.py @@ -1,7 +1,8 @@ import django -from django.contrib.messages.views import SuccessMessageMixin from django.views import generic -from .mixins import PassRequestMixin, DeleteMessageMixin, LoginAjaxMixin +from .mixins import PassRequestMixin, DeleteMessageMixin, LoginAjaxMixin, FormValidationMixin + +from .utils import is_ajax DJANGO_VERSION = django.get_version().split('.') DJANGO_MAJOR_VERSION = DJANGO_VERSION[0] @@ -14,7 +15,7 @@ from django.contrib.auth.views import LoginView -class BSModalLoginView(LoginAjaxMixin, SuccessMessageMixin, LoginView): +class BSModalLoginView(LoginAjaxMixin, LoginView): pass @@ -22,11 +23,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..e7e4bd9 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -1,13 +1,13 @@ 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): """ - 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__`. @@ -21,7 +21,7 @@ def get_form_kwargs(self): class PopRequestMixin(object): """ - 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 @@ -36,7 +36,7 @@ def __init__(self, *args, **kwargs): class CreateUpdateAjaxMixin(object): """ - 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): @@ -49,7 +49,7 @@ def save(self, commit=True): class DeleteMessageMixin(object): """ - Mixin which adds message to BSModalDeleteView and only calls the delete method if request + Generic View Mixin which adds message to BSModalDeleteView and only calls the delete method if request is not ajax request. """ @@ -61,13 +61,33 @@ def delete(self, request, *args, **kwargs): self.object = self.get_object() return HttpResponseRedirect(self.get_success_url()) + class LoginAjaxMixin(object): """ - 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(object): + """ + Generic View Mixin which saves object and redirects to success_url if request is not ajax request. Otherwise resoponse 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) \ No newline at end of file From aec0024b5d41fafba7d9db020e04c305d3eda5e4 Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Sun, 9 Apr 2023 15:59:01 +0200 Subject: [PATCH 04/25] 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. --- .gitignore | 366 ++++++++++++++++++++++++- bootstrap_modal_forms/compatibility.py | 8 +- examples/apps.py | 32 +++ examples/migrations/0001_initial.py | 4 +- examples/models.py | 10 +- examples/tests.py | 3 - examples/views.py | 2 - manage.py | 7 +- requirements.txt | 9 +- setup.py | 23 +- setup/settings.py | 39 +-- setup/wsgi.py | 9 - static/.gitkeep | 0 13 files changed, 437 insertions(+), 75 deletions(-) delete mode 100644 examples/tests.py create mode 100644 static/.gitkeep diff --git a/.gitignore b/.gitignore index 17613c7..934e888 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,363 @@ -database/db.sqlite3 +################### +##### Windows ##### +################### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +################# +##### Linux ##### +################# +*~ + +# Temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +################# +##### macOS ##### +################# +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +#################### +##### IntelliJ ##### +#################### +.idea/ +out/ + +# Run configurations +.run/ + +# CMake +cmake-build-*/ + +# File-based project format +*.iws + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### Intellij Patch ### +# @see https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +*.iml +# *.ipr +# modules.xml + +################### +##### Eclipse ##### +################### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. Typically, this file would be tracked if +# it would contain build/dependency configurations +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +############################ +##### VisualStudioCode ##### +############################ +.vscode/ +.history/ +*.code-workspace + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +################## +##### Python ##### +################## +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template before PyInstaller builds the exe, +# so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.pot + +# Django +*.log +local_settings.py +*.sqlite3 +*.sqlite3-journal + +# Flask +instance/ +.webassets-cache + +# Scrapy +.scrapy + +# Sphinx +doc/build/ +doc/source/_autosummary/* + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is intended to run in multiple +# environments; otherwise, check them in +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. However, in case of +# collaboration, if having platform-specific dependencies or dependencies having no cross-platform support, pipenv may +# install dependencies that don't work, or not install all needed dependencies. +# Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. This is especially +# recommended for binary packages to ensure reproducibility, and is more commonly ignored for libraries. +# @see https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock + +# pdm +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it in version control. +# @see https://pdm.fming.dev/#use-with-ide +.pdm.toml +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +### Python Patch ### +# Poetry local configuration file +# @see https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +################## +##### Docker ##### +################## +!docker/.env + +############################### +##### Application ##### +############################### geckodriver.log -__pycache__ -*.pyc -.env/ diff --git a/bootstrap_modal_forms/compatibility.py b/bootstrap_modal_forms/compatibility.py index 60b0168..f8ef69d 100644 --- a/bootstrap_modal_forms/compatibility.py +++ b/bootstrap_modal_forms/compatibility.py @@ -1,11 +1,11 @@ from django.conf import settings -from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model, login as auth_login +from django.contrib.auth import REDIRECT_FIELD_NAME, 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.utils.http import url_has_allowed_host_and_scheme 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 @@ -54,8 +54,8 @@ def get_redirect_url(self): self.redirect_field_name, self.request.GET.get(self.redirect_field_name, '') ) - url_is_safe = is_safe_url( - url=redirect_to + url_is_safe = url_has_allowed_host_and_scheme( + redirect_to, settings.ALLOWED_HOSTS ) return redirect_to if url_is_safe else '' diff --git a/examples/apps.py b/examples/apps.py index 5940f52..2f4379e 100644 --- a/examples/apps.py +++ b/examples/apps.py @@ -1,5 +1,37 @@ +import sys +from dataclasses import dataclass +from datetime import datetime +from typing import Set + from django.apps import AppConfig +@dataclass(slots=True, frozen=True) +class ExampleBook: + title: str + publication_date: datetime + author: str + price: float + pages: int + book_type: int # REMEMBER: 1 = 'Hardcover', 2 = 'Paperback', 3 = 'E-book' + + +def get_example_books() -> Set[ExampleBook]: + return { + ExampleBook('Lord of the Rings - 3-Book Paperback Box Set', datetime(year=1954, month=7, day=29), 'J.R.R. Tolkien', 19.99, 1536, 2), + ExampleBook('Lord of the Flies - Large Print Edition', datetime(year=1954, month=9, day=17), 'William Golding', 25.95, 286, 2), + } + + class ExamplesConfig(AppConfig): name = 'examples' + + def ready(self) -> None: + # ATTENTION: Leave the imports here!!! + from examples.models import Book + # Pushing during any migration operation, is not that clever... + if not any(arg in ('makemigrations', 'migrate') for arg in sys.argv): + # Push some examples to play around, if DB is empty... + if Book.objects.all().count() < 1: + books = [Book(title=book.title, publication_date=book.publication_date, author=book.author, price=book.price, pages=book.pages, book_type=book.book_type) for book in get_example_books()] + Book.objects.bulk_create(books) 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..d7f2373 100644 --- a/examples/models.py +++ b/examples/models.py @@ -2,14 +2,12 @@ class Book(models.Model): - HARDCOVER = 1 - PAPERBACK = 2 - EBOOK = 3 BOOK_TYPES = ( - (HARDCOVER, 'Hardcover'), - (PAPERBACK, 'Paperback'), - (EBOOK, 'E-book'), + (1, 'Hardcover'), + (2, 'Paperback'), + (3, 'E-book'), ) + title = models.CharField(max_length=50) publication_date = models.DateField(null=True) author = models.CharField(max_length=30, blank=True) 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/views.py b/examples/views.py index 53c6410..ae4f7cb 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 @@ -12,7 +11,6 @@ BSModalReadView, BSModalDeleteView ) - from .forms import ( BookModelForm, CustomUserCreationForm, 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..b24afd3 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,18 @@ 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))) +# Python version is given implicitly by Django version. If Django requires Python 3.6, this project automatically also needs Python 3.6. +# Django version is defined in "requirements.txt" setup( name='django-bootstrap-modal-forms', version='2.2.1', @@ -19,7 +25,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 +33,12 @@ '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/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 From ea9cba95d1a74a2e72e0cd800169d8071edc43aa Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Sun, 9 Apr 2023 16:15:10 +0200 Subject: [PATCH 05/25] Minor refactoring --- .gitignore | 5 ----- bootstrap_modal_forms/compatibility.py | 2 +- setup.py | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 934e888..64ec8fc 100644 --- a/.gitignore +++ b/.gitignore @@ -352,11 +352,6 @@ cython_debug/ # @see https://python-poetry.org/docs/configuration/#local-configuration poetry.toml -################## -##### Docker ##### -################## -!docker/.env - ############################### ##### Application ##### ############################### diff --git a/bootstrap_modal_forms/compatibility.py b/bootstrap_modal_forms/compatibility.py index f8ef69d..b33620b 100644 --- a/bootstrap_modal_forms/compatibility.py +++ b/bootstrap_modal_forms/compatibility.py @@ -81,4 +81,4 @@ def get_context_data(self, **kwargs): 'site_name': current_site.name, **(self.extra_context or {}) }) - return context \ No newline at end of file + return context diff --git a/setup.py b/setup.py index b24afd3..fe2f768 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) -# Python version is given implicitly by Django version. If Django requires Python 3.6, this project automatically also needs Python 3.6. +# Python version is given implicitly by Django version. If Django 3.2 requires Python 3.6, this project automatically also needs Python 3.6. # Django version is defined in "requirements.txt" setup( name='django-bootstrap-modal-forms', From a1ce4883ba4431da222f6d575bf758400655053b Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Wed, 12 Apr 2023 00:11:40 +0200 Subject: [PATCH 06/25] Updated Project Added type hints. Updated test cases. Removed last remaining snippets for outdated Django versions. --- .gitignore | 8 +-- bootstrap_modal_forms/compatibility.py | 34 ++++++------ bootstrap_modal_forms/generic.py | 13 +---- bootstrap_modal_forms/mixins.py | 46 ++++++++------- bootstrap_modal_forms/utils.py | 51 ++++++++++++++++- examples/admin.py | 3 - examples/views.py | 13 +++-- tests/base.py | 77 ++++++++++++++++++++++++-- tests/tests_functional.py | 42 +++++++------- 9 files changed, 200 insertions(+), 87 deletions(-) delete mode 100644 examples/admin.py diff --git a/.gitignore b/.gitignore index 64ec8fc..7287195 100644 --- a/.gitignore +++ b/.gitignore @@ -352,7 +352,7 @@ cython_debug/ # @see https://python-poetry.org/docs/configuration/#local-configuration poetry.toml -############################### -##### Application ##### -############################### -geckodriver.log +############################################ +##### django-bootstrap-modal-forms ##### +############################################ +geckodriver.exe diff --git a/bootstrap_modal_forms/compatibility.py b/bootstrap_modal_forms/compatibility.py index b33620b..ee50f6e 100644 --- a/bootstrap_modal_forms/compatibility.py +++ b/bootstrap_modal_forms/compatibility.py @@ -1,8 +1,12 @@ +from typing import Dict, Any, Set, Type + from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, 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.http.request import HttpRequest +from django.http.response import HttpResponse from django.shortcuts import resolve_url from django.utils.decorators import method_decorator from django.utils.http import url_has_allowed_host_and_scheme @@ -11,15 +15,10 @@ 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} +from bootstrap_modal_forms.mixins import AuthForm -class LoginView(SuccessURLAllowedHostsMixin, FormView): +class LoginView(FormView): """ Display the login form and handle the login action. """ @@ -29,26 +28,26 @@ class LoginView(SuccessURLAllowedHostsMixin, FormView): template_name = 'registration/login.html' redirect_authenticated_user = False extra_context = None + success_url_allowed_hosts = set() @method_decorator(sensitive_post_parameters()) @method_decorator(csrf_protect) @method_decorator(never_cache) - def dispatch(self, request, *args, **kwargs): + def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: 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.' + "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): + def get_success_url(self) -> str: url = self.get_redirect_url() return url or resolve_url(settings.LOGIN_REDIRECT_URL) - def get_redirect_url(self): + def get_redirect_url(self) -> str: """Return the user-originating redirect URL if it's safe.""" redirect_to = self.request.POST.get( self.redirect_field_name, @@ -59,20 +58,23 @@ def get_redirect_url(self): ) return redirect_to if url_is_safe else '' - def get_form_class(self): + def get_form_class(self) -> Type[AuthForm]: return self.authentication_form or self.form_class - def get_form_kwargs(self): + def get_form_kwargs(self) -> Dict[str, Any]: kwargs = super().get_form_kwargs() kwargs['request'] = self.request return kwargs - def form_valid(self, form): + def form_valid(self, form) -> HttpResponse: """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): + def get_success_url_allowed_hosts(self) -> Set[str]: + return {self.request.get_host(), *self.success_url_allowed_hosts} + + def get_context_data(self, **kwargs) -> Dict[str, Any]: context = super().get_context_data(**kwargs) current_site = get_current_site(self.request) context.update({ diff --git a/bootstrap_modal_forms/generic.py b/bootstrap_modal_forms/generic.py index 3df0873..2109f36 100644 --- a/bootstrap_modal_forms/generic.py +++ b/bootstrap_modal_forms/generic.py @@ -1,17 +1,8 @@ -import django +from django.contrib.auth.views import LoginView from django.contrib.messages.views import SuccessMessageMixin from django.views import generic -from .mixins import PassRequestMixin, DeleteMessageMixin, LoginAjaxMixin - -DJANGO_VERSION = django.get_version().split('.') -DJANGO_MAJOR_VERSION = DJANGO_VERSION[0] -DJANGO_MINOR_VERSION = DJANGO_VERSION[1] -# 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 +from .mixins import PassRequestMixin, DeleteMessageMixin, LoginAjaxMixin class BSModalLoginView(LoginAjaxMixin, SuccessMessageMixin, LoginView): diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index b275538..9da3887 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -1,11 +1,19 @@ +from typing import TypeVar, Any + from django.contrib import messages from django.contrib.auth import login as auth_login +from django.contrib.auth.forms import AuthenticationForm +from django.db.models import Model from django.http import HttpResponseRedirect +from django.http.request import HttpRequest + +from .utils import * -from .utils import is_ajax +AuthForm = TypeVar('AuthForm', bound=AuthenticationForm) +DjangoModel = TypeVar('DjangoModel', bound=Model) -class PassRequestMixin(object): +class PassRequestMixin: """ Mixin which puts the request into the form's kwargs. @@ -13,13 +21,13 @@ class PassRequestMixin(object): 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}) + def get_form_kwargs(self: DjangoView) -> Any: + 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 instance. @@ -29,45 +37,45 @@ 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. """ - def save(self, commit=True): + def save(self: DjangoView, commit: bool = True) -> DjangoModel: if not is_ajax(self.request.META) or self.request.POST.get('asyncUpdate') == 'True': - instance = super(CreateUpdateAjaxMixin, self).save(commit=commit) + return super().save(commit=commit) else: - instance = super(CreateUpdateAjaxMixin, self).save(commit=False) - return instance + 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. """ - def delete(self, request, *args, **kwargs): + def delete(self: DeleteMessageMixinProtocol, request: HttpRequest, *args, **kwargs) -> HttpResponseRedirect: if not is_ajax(request.META): messages.success(request, self.success_message) - return super(DeleteMessageMixin, self).delete(request, *args, **kwargs) + return super().delete(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. """ - def form_valid(self, form): + def form_valid(self: LoginAjaxMixinProtocol, form: AuthForm) -> HttpResponseRedirect: 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()) diff --git a/bootstrap_modal_forms/utils.py b/bootstrap_modal_forms/utils.py index 1519708..4882268 100644 --- a/bootstrap_modal_forms/utils.py +++ b/bootstrap_modal_forms/utils.py @@ -1,8 +1,55 @@ -def is_ajax(meta): +__all__ = ('is_ajax', 'DjangoView', 'LoginAjaxMixinProtocol', 'DeleteMessageMixinProtocol',) + +from typing import Protocol, Any, Dict + +from django.http import HttpRequest + + +def is_ajax(meta: Dict): 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 + return False + + +class DjangoView(Protocol): + """ + This is a pure supporting, type hinting class, for mixins that require a HttpRequest attribute. + + @see https://docs.python.org/3/library/typing.html#typing.Protocol + """ + + @property + def request(self) -> HttpRequest: + ... + + +class LoginAjaxMixinProtocol(DjangoView, Protocol): + """ + This is a pure supporting, type hinting class, for mixins that require a success_message + attribute and a get_success_url(...) method. + + @see https://docs.python.org/3/library/typing.html#typing.Protocol + """ + + @property + def success_message(self) -> str: + ... + + def get_success_url(self) -> str: + ... + + +class DeleteMessageMixinProtocol(LoginAjaxMixinProtocol, Protocol): + """ + This is a pure supporting, type hinting class, for mixins that require a success_message attribute, + a get_success_url(...) and a get_object(...) method. + + @see https://docs.python.org/3/library/typing.html#typing.Protocol + """ + + def get_object(self) -> Any: + ... 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/views.py b/examples/views.py index ae4f7cb..3da7ac1 100644 --- a/examples/views.py +++ b/examples/views.py @@ -1,4 +1,7 @@ +from django.db.models import QuerySet from django.http import JsonResponse +from django.http.request import HttpRequest +from django.http.response import HttpResponse from django.template.loader import render_to_string from django.urls import reverse_lazy from django.views import generic @@ -25,7 +28,7 @@ class Index(generic.ListView): context_object_name = 'books' template_name = 'index.html' - def get_queryset(self): + def get_queryset(self) -> QuerySet: qs = super().get_queryset() if 'type' in self.request.GET: qs = qs.filter(book_type=int(self.request.GET['type'])) @@ -36,12 +39,12 @@ class BookFilterView(BSModalFormView): template_name = 'examples/filter_book.html' form_class = BookFilterForm - def form_valid(self, form): + def form_valid(self, form) -> HttpResponse: self.filter = '?type=' + form.cleaned_data['type'] response = super().form_valid(form) return response - def get_success_url(self): + def get_success_url(self) -> str: return reverse_lazy('index') + self.filter @@ -86,8 +89,8 @@ class CustomLoginView(BSModalLoginView): success_url = reverse_lazy('index') -def books(request): - data = dict() +def books(request: HttpRequest) -> HttpResponse: + data = {} if request.method == 'GET': books = Book.objects.all() data['table'] = render_to_string( diff --git a/tests/base.py b/tests/base.py index 85a05c0..3baf551 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,23 +1,88 @@ +from pathlib import Path +from typing import Optional, Union, Type + from django.contrib.staticfiles.testing import StaticLiveServerTestCase 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 +from setup import settings + +WebDriver = Union[webdriver.Firefox, webdriver.Chrome, webdriver.Edge, webdriver.Safari] + 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: Optional[WebDriver] = None + # Change this, to your browser type of choice + BROWSER_TYPE: Type[WebDriver] = webdriver.Firefox + # Change this, to your driver file of your chosen browser + BROWSER_DRIVER_PATH: Path = Path(settings.BASE_DIR, 'geckodriver.exe') + # 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: Optional[Path] = 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) -> WebDriver: + if cls.BROWSER_TYPE is webdriver.Firefox: + if cls.BROWSER_DRIVER_PATH is None: + raise ValueError('Chrome 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('Firefox 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 4e0138983846da2bb4e54620690d1df8dc16bb0c Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Wed, 12 Apr 2023 00:13:11 +0200 Subject: [PATCH 07/25] Removed unused constant --- tests/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/base.py b/tests/base.py index 3baf551..97982e5 100644 --- a/tests/base.py +++ b/tests/base.py @@ -11,8 +11,6 @@ WebDriver = Union[webdriver.Firefox, webdriver.Chrome, webdriver.Edge, webdriver.Safari] -MAX_WAIT = 10 - class FunctionalTest(StaticLiveServerTestCase): """ From 3822486b92c66a4993d72d338aa7b1cab0462916 Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Wed, 12 Apr 2023 00:37:30 +0200 Subject: [PATCH 08/25] Updated required version --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index fe2f768..fd5e935 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ # Python version is given implicitly by Django version. If Django 3.2 requires Python 3.6, this project automatically also needs Python 3.6. # Django version is defined in "requirements.txt" +# Right now, we need Python 3.8. Because in Python 3.8 type hints with Protocols were introduced. setup( name='django-bootstrap-modal-forms', version='2.2.1', @@ -33,8 +34,6 @@ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - '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', From a663e7a2addc41b5da0310d4553fb81908edc305 Mon Sep 17 00:00:00 2001 From: aDramaQueen Date: Thu, 13 Apr 2023 10:23:24 +0200 Subject: [PATCH 09/25] Minor Bugfix Removed __slots__ from dataclass, to match Python 3.8 support. --- examples/apps.py | 2 +- tests/base.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/apps.py b/examples/apps.py index 2f4379e..f17dc66 100644 --- a/examples/apps.py +++ b/examples/apps.py @@ -6,7 +6,7 @@ from django.apps import AppConfig -@dataclass(slots=True, frozen=True) +@dataclass(frozen=True) class ExampleBook: title: str publication_date: datetime diff --git a/tests/base.py b/tests/base.py index 97982e5..973b9c7 100644 --- a/tests/base.py +++ b/tests/base.py @@ -55,7 +55,7 @@ def tearDownClass(cls): def get_browser(cls) -> WebDriver: if cls.BROWSER_TYPE is webdriver.Firefox: if cls.BROWSER_DRIVER_PATH is None: - raise ValueError('Chrome needs a path to a browser driver file!') + 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) @@ -63,7 +63,7 @@ def get_browser(cls) -> WebDriver: 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('Firefox needs a path to a browser driver file!') + 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: From 17ac8cf1647d1fac213b60fb2666a68b530016c0 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 19:25:25 +0200 Subject: [PATCH 10/25] refactor save method in CreateUpdateAjaxMixin --- bootstrap_modal_forms/mixins.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index e7e4bd9..d5239d6 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -40,11 +40,13 @@ class CreateUpdateAjaxMixin(object): """ 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(CreateUpdateAjaxMixin, self).save(commit=commit) + if isAjaxRequest: + return super(CreateUpdateAjaxMixin, self).save(commit=False) class DeleteMessageMixin(object): @@ -76,7 +78,7 @@ def form_valid(self, form): class FormValidationMixin(object): """ - Generic View Mixin which saves object and redirects to success_url if request is not ajax request. Otherwise resoponse 204 No content is returned. + 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): From 2c83a7e43100767577e07999d3e5da4c38b4f8d3 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 19:55:55 +0200 Subject: [PATCH 11/25] remove compatibility.py --- bootstrap_modal_forms/compatibility.py | 86 -------------------------- 1 file changed, 86 deletions(-) delete mode 100644 bootstrap_modal_forms/compatibility.py diff --git a/bootstrap_modal_forms/compatibility.py b/bootstrap_modal_forms/compatibility.py deleted file mode 100644 index ee50f6e..0000000 --- a/bootstrap_modal_forms/compatibility.py +++ /dev/null @@ -1,86 +0,0 @@ -from typing import Dict, Any, Set, Type - -from django.conf import settings -from django.contrib.auth import REDIRECT_FIELD_NAME, 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.http.request import HttpRequest -from django.http.response import HttpResponse -from django.shortcuts import resolve_url -from django.utils.decorators import method_decorator -from django.utils.http import url_has_allowed_host_and_scheme -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 - -from bootstrap_modal_forms.mixins import AuthForm - - -class LoginView(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 - success_url_allowed_hosts = set() - - @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) - @method_decorator(never_cache) - def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - 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) -> str: - url = self.get_redirect_url() - return url or resolve_url(settings.LOGIN_REDIRECT_URL) - - def get_redirect_url(self) -> str: - """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 = url_has_allowed_host_and_scheme( - redirect_to, settings.ALLOWED_HOSTS - ) - return redirect_to if url_is_safe else '' - - def get_form_class(self) -> Type[AuthForm]: - return self.authentication_form or self.form_class - - def get_form_kwargs(self) -> Dict[str, Any]: - kwargs = super().get_form_kwargs() - kwargs['request'] = self.request - return kwargs - - def form_valid(self, form) -> HttpResponse: - """Security check complete. Log the user in.""" - auth_login(self.request, form.get_user()) - return HttpResponseRedirect(self.get_success_url()) - - def get_success_url_allowed_hosts(self) -> Set[str]: - return {self.request.get_host(), *self.success_url_allowed_hosts} - - def get_context_data(self, **kwargs) -> Dict[str, Any]: - 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 From fb095684ca54e2abd861f04d84891b438ac8f815 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 19:57:04 +0200 Subject: [PATCH 12/25] remove utils.py --- bootstrap_modal_forms/utils.py | 55 ---------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 bootstrap_modal_forms/utils.py diff --git a/bootstrap_modal_forms/utils.py b/bootstrap_modal_forms/utils.py deleted file mode 100644 index 4882268..0000000 --- a/bootstrap_modal_forms/utils.py +++ /dev/null @@ -1,55 +0,0 @@ -__all__ = ('is_ajax', 'DjangoView', 'LoginAjaxMixinProtocol', 'DeleteMessageMixinProtocol',) - -from typing import Protocol, Any, Dict - -from django.http import HttpRequest - - -def is_ajax(meta: Dict): - if 'HTTP_X_REQUESTED_WITH' not in meta: - return False - - if meta['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest': - return True - - return False - - -class DjangoView(Protocol): - """ - This is a pure supporting, type hinting class, for mixins that require a HttpRequest attribute. - - @see https://docs.python.org/3/library/typing.html#typing.Protocol - """ - - @property - def request(self) -> HttpRequest: - ... - - -class LoginAjaxMixinProtocol(DjangoView, Protocol): - """ - This is a pure supporting, type hinting class, for mixins that require a success_message - attribute and a get_success_url(...) method. - - @see https://docs.python.org/3/library/typing.html#typing.Protocol - """ - - @property - def success_message(self) -> str: - ... - - def get_success_url(self) -> str: - ... - - -class DeleteMessageMixinProtocol(LoginAjaxMixinProtocol, Protocol): - """ - This is a pure supporting, type hinting class, for mixins that require a success_message attribute, - a get_success_url(...) and a get_object(...) method. - - @see https://docs.python.org/3/library/typing.html#typing.Protocol - """ - - def get_object(self) -> Any: - ... From ffe2a5f103739cc36681d4bef051515489afe71f Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 19:57:41 +0200 Subject: [PATCH 13/25] remove types --- bootstrap_modal_forms/mixins.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index 9da3887..a422c6e 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -1,5 +1,3 @@ -from typing import TypeVar, Any - from django.contrib import messages from django.contrib.auth import login as auth_login from django.contrib.auth.forms import AuthenticationForm @@ -9,10 +7,6 @@ from .utils import * -AuthForm = TypeVar('AuthForm', bound=AuthenticationForm) -DjangoModel = TypeVar('DjangoModel', bound=Model) - - class PassRequestMixin: """ Mixin which puts the request into the form's kwargs. @@ -21,7 +15,7 @@ class PassRequestMixin: out of the dict in the super of your form's `__init__`. """ - def get_form_kwargs(self: DjangoView) -> Any: + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['request'] = self.request return kwargs @@ -47,7 +41,7 @@ class CreateUpdateAjaxMixin: Mixin which passes or saves object based on request type. """ - def save(self: DjangoView, commit: bool = True) -> DjangoModel: + def save(self, commit = True): if not is_ajax(self.request.META) or self.request.POST.get('asyncUpdate') == 'True': return super().save(commit=commit) else: @@ -60,7 +54,7 @@ class DeleteMessageMixin: is not ajax request. """ - def delete(self: DeleteMessageMixinProtocol, request: HttpRequest, *args, **kwargs) -> HttpResponseRedirect: + def delete(self, request, *args, **kwargs): if not is_ajax(request.META): messages.success(request, self.success_message) return super().delete(request, *args, **kwargs) @@ -74,7 +68,7 @@ class LoginAjaxMixin: Mixin which authenticates user if request is not ajax request. """ - def form_valid(self: LoginAjaxMixinProtocol, form: AuthForm) -> HttpResponseRedirect: + 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) From d5e2e698c7e83bf761e24400737308400a6b676d Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 20:08:39 +0200 Subject: [PATCH 14/25] add is_ajax method and remove imports --- bootstrap_modal_forms/mixins.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index a422c6e..3c0e542 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -1,11 +1,6 @@ from django.contrib import messages from django.contrib.auth import login as auth_login -from django.contrib.auth.forms import AuthenticationForm -from django.db.models import Model from django.http import HttpResponseRedirect -from django.http.request import HttpRequest - -from .utils import * class PassRequestMixin: """ @@ -73,3 +68,7 @@ def form_valid(self, form): auth_login(self.request, form.get_user()) messages.success(self.request, self.success_message) return HttpResponseRedirect(self.get_success_url()) + + +def is_ajax(meta): + return 'HTTP_X_REQUESTED_WITH' in meta and meta['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' \ No newline at end of file From 8a1822c80c78b6ed0e5f3b49d8e9128c91918270 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 20:18:15 +0200 Subject: [PATCH 15/25] remove unneeded class inheritence --- 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 d3f5a15..8ef37e4 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -73,7 +73,7 @@ def form_valid(self, form): return HttpResponseRedirect(self.get_success_url()) -class FormValidationMixin(object): +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. """ From 2d2017ab739f03ccf22dfebf5bd72c5e0d25bf05 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 20:25:57 +0200 Subject: [PATCH 16/25] remove obsolete class name parameter --- bootstrap_modal_forms/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index 8ef37e4..abb6613 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -41,9 +41,9 @@ def save(self, commit=True): asyncUpdate = self.request.POST.get('asyncUpdate') == 'True' if not isAjaxRequest or asyncUpdate: - return super(CreateUpdateAjaxMixin, self).save(commit=commit) + return super().save(commit=commit) if isAjaxRequest: - return super(CreateUpdateAjaxMixin, self).save(commit=False) + return super().save(commit=False) class DeleteMessageMixin: From 82b7a2c0e6859ad04def80d7b03667d71fdb0531 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 20:26:17 +0200 Subject: [PATCH 17/25] revert examples to version in master branch --- examples/apps.py | 34 +--------------------------------- examples/forms.py | 2 +- examples/models.py | 12 +++++++----- examples/urls.py | 2 +- examples/views.py | 14 ++++++-------- 5 files changed, 16 insertions(+), 48 deletions(-) diff --git a/examples/apps.py b/examples/apps.py index f17dc66..8ad7c7a 100644 --- a/examples/apps.py +++ b/examples/apps.py @@ -1,37 +1,5 @@ -import sys -from dataclasses import dataclass -from datetime import datetime -from typing import Set - from django.apps import AppConfig -@dataclass(frozen=True) -class ExampleBook: - title: str - publication_date: datetime - author: str - price: float - pages: int - book_type: int # REMEMBER: 1 = 'Hardcover', 2 = 'Paperback', 3 = 'E-book' - - -def get_example_books() -> Set[ExampleBook]: - return { - ExampleBook('Lord of the Rings - 3-Book Paperback Box Set', datetime(year=1954, month=7, day=29), 'J.R.R. Tolkien', 19.99, 1536, 2), - ExampleBook('Lord of the Flies - Large Print Edition', datetime(year=1954, month=9, day=17), 'William Golding', 25.95, 286, 2), - } - - class ExamplesConfig(AppConfig): - name = 'examples' - - def ready(self) -> None: - # ATTENTION: Leave the imports here!!! - from examples.models import Book - # Pushing during any migration operation, is not that clever... - if not any(arg in ('makemigrations', 'migrate') for arg in sys.argv): - # Push some examples to play around, if DB is empty... - if Book.objects.all().count() < 1: - books = [Book(title=book.title, publication_date=book.publication_date, author=book.author, price=book.price, pages=book.pages, book_type=book.book_type) for book in get_example_books()] - Book.objects.bulk_create(books) + 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/models.py b/examples/models.py index d7f2373..f17effb 100644 --- a/examples/models.py +++ b/examples/models.py @@ -2,12 +2,14 @@ class Book(models.Model): + HARDCOVER = 1 + PAPERBACK = 2 + EBOOK = 3 BOOK_TYPES = ( - (1, 'Hardcover'), - (2, 'Paperback'), - (3, 'E-book'), + (HARDCOVER, 'Hardcover'), + (PAPERBACK, 'Paperback'), + (EBOOK, 'E-book'), ) - title = models.CharField(max_length=50) publication_date = models.DateField(null=True) author = models.CharField(max_length=30, blank=True) @@ -15,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/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 3da7ac1..0551253 100644 --- a/examples/views.py +++ b/examples/views.py @@ -1,7 +1,4 @@ -from django.db.models import QuerySet from django.http import JsonResponse -from django.http.request import HttpRequest -from django.http.response import HttpResponse from django.template.loader import render_to_string from django.urls import reverse_lazy from django.views import generic @@ -14,6 +11,7 @@ BSModalReadView, BSModalDeleteView ) + from .forms import ( BookModelForm, CustomUserCreationForm, @@ -28,7 +26,7 @@ class Index(generic.ListView): context_object_name = 'books' template_name = 'index.html' - def get_queryset(self) -> QuerySet: + def get_queryset(self): qs = super().get_queryset() if 'type' in self.request.GET: qs = qs.filter(book_type=int(self.request.GET['type'])) @@ -39,12 +37,12 @@ class BookFilterView(BSModalFormView): template_name = 'examples/filter_book.html' form_class = BookFilterForm - def form_valid(self, form) -> HttpResponse: + def form_valid(self, form): self.filter = '?type=' + form.cleaned_data['type'] response = super().form_valid(form) return response - def get_success_url(self) -> str: + def get_success_url(self): return reverse_lazy('index') + self.filter @@ -89,7 +87,7 @@ class CustomLoginView(BSModalLoginView): success_url = reverse_lazy('index') -def books(request: HttpRequest) -> HttpResponse: +def books(request): data = {} if request.method == 'GET': books = Book.objects.all() @@ -98,4 +96,4 @@ def books(request: HttpRequest) -> HttpResponse: {'books': books}, request=request ) - return JsonResponse(data) + return JsonResponse(data) \ No newline at end of file From 12c206ddcc6a32dc8c35d798c8a4b9e838c89806 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 20:27:34 +0200 Subject: [PATCH 18/25] remove static folder --- static/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 static/.gitkeep diff --git a/static/.gitkeep b/static/.gitkeep deleted file mode 100644 index e69de29..0000000 From 28c706e9753a176cafbae66b4b085b68410b0701 Mon Sep 17 00:00:00 2001 From: trco Date: Sun, 30 Apr 2023 21:25:32 +0200 Subject: [PATCH 19/25] remove types from tests --- tests/base.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/base.py b/tests/base.py index 973b9c7..50d4393 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,16 +1,13 @@ from pathlib import Path -from typing import Optional, Union, Type 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 -from setup import settings - -WebDriver = Union[webdriver.Firefox, webdriver.Chrome, webdriver.Edge, webdriver.Safari] - class FunctionalTest(StaticLiveServerTestCase): """ @@ -31,14 +28,14 @@ class FunctionalTest(StaticLiveServerTestCase): - Compatibility: https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari """ - BROWSER: Optional[WebDriver] = None + BROWSER = None # Change this, to your browser type of choice - BROWSER_TYPE: Type[WebDriver] = webdriver.Firefox + BROWSER_TYPE = webdriver.Chrome # Change this, to your driver file of your chosen browser - BROWSER_DRIVER_PATH: Path = Path(settings.BASE_DIR, 'geckodriver.exe') + 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: Optional[Path] = None + FIRE_FOX_BINARY = None @classmethod def setUpClass(cls): @@ -52,7 +49,7 @@ def tearDownClass(cls): super().tearDownClass() @classmethod - def get_browser(cls) -> WebDriver: + 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!') From 9cf89fc66fe3c2eb05ac44697a27c1f97e505e80 Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 16:14:59 +0200 Subject: [PATCH 20/25] remove unneeded comments --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index fd5e935..5c95dc9 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,6 @@ # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) -# Python version is given implicitly by Django version. If Django 3.2 requires Python 3.6, this project automatically also needs Python 3.6. -# Django version is defined in "requirements.txt" -# Right now, we need Python 3.8. Because in Python 3.8 type hints with Protocols were introduced. setup( name='django-bootstrap-modal-forms', version='2.2.1', From 8492ffcb7031043e8586cf2f4db06ad41e3c38af Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 17:08:08 +0200 Subject: [PATCH 21/25] update get and set for form action and method attributes --- .../static/js/bootstrap5.modal.forms.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js index 154f50a..37dab68 100644 --- a/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js +++ b/bootstrap_modal_forms/static/js/bootstrap5.modal.forms.js @@ -25,7 +25,7 @@ const modalFormCallback = function (settings) { let form = modal.querySelector(settings.modalForm); if (form) { - form.action = settings.formURL; + form.setAttribute("action", settings.formURL); addEventHandlers(modal, form, settings) } }); @@ -57,9 +57,9 @@ const isFormValid = function (settings, callback) { let btnSubmit = modal.querySelector('button[type="submit"]'); btnSubmit.disabled = true; - fetch(form.action, { + fetch(form.getAttribute("action"), { headers: headers, - method: form.getAttribute('method'), + method: form.getAttribute("method"), body: new FormData(form), }).then(res => { return res.text(); @@ -73,7 +73,7 @@ const isFormValid = function (settings, callback) { return; } - form.action = settings.formURL; + form.setAttribute("action", settings.formURL); addEventHandlers(modal, form, settings) } else { callback(settings); @@ -97,8 +97,8 @@ const submitForm = function (settings) { // Add asyncUpdate and check for it in save method of CreateUpdateAjaxMixin formData.append("asyncUpdate", "True"); - fetch(form.action, { - method: form.getAttribute('method'), + fetch(form.getAttribute("action"), { + method: form.getAttribute("method"), body: formData, }).then(res => { return res.text(); @@ -142,7 +142,7 @@ const submitForm = function (settings) { return; } - form.action = settings.formURL; + form.setAttribute("action", settings.formURL); addEventHandlers(modal, form, settings) }); } @@ -156,7 +156,6 @@ const submitForm = function (settings) { }; const validateAsyncSettings = function (settings) { - console.log(settings) var missingSettings = []; if (!settings.successMessage) { From 373b159d1b723849078392d674648c823d915aca Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 17:09:07 +0200 Subject: [PATCH 22/25] update bootstrap5.modal.forms.min.js --- bootstrap_modal_forms/static/js/bootstrap5.modal.forms.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d3f575d..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.getAttribute('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.getAttribute('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 From db3950287a9ed75147ed136462d8649601797b1f Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 17:58:15 +0200 Subject: [PATCH 23/25] update assert string to pass the test --- tests/tests_functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests_functional.py b/tests/tests_functional.py index 21884bf..f56b00d 100644 --- a/tests/tests_functional.py +++ b/tests/tests_functional.py @@ -30,7 +30,7 @@ def test_signup_login(self): # User sees error in form error = self.wait_for(class_name='help-block') - self.assertEqual(error.text, 'The two password fields didn\'t match.') + self.assertEqual(error.text, 'The two password fields didn’t match.') # User fills in and submits sign up form correctly form = modal.find_element_by_tag_name('form') From bd7b6b4c3d732e94cd24ee6b18c2592d0e81b811 Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 18:06:54 +0200 Subject: [PATCH 24/25] update DeleteMessageMixin comment --- bootstrap_modal_forms/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstrap_modal_forms/mixins.py b/bootstrap_modal_forms/mixins.py index 486492d..b984932 100644 --- a/bootstrap_modal_forms/mixins.py +++ b/bootstrap_modal_forms/mixins.py @@ -48,8 +48,8 @@ def save(self, commit=True): class DeleteMessageMixin: """ - Generic View 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 post(self, request, *args, **kwargs): From bae3e6c178c1311f05b757725dd7eb3c3642569d Mon Sep 17 00:00:00 2001 From: trco Date: Mon, 1 May 2023 18:09:54 +0200 Subject: [PATCH 25/25] cleanup .gitignore --- .gitignore | 363 +---------------------------------------------------- 1 file changed, 5 insertions(+), 358 deletions(-) diff --git a/.gitignore b/.gitignore index 7287195..4e2f06d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,358 +1,5 @@ -################### -##### Windows ##### -################### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -################# -##### Linux ##### -################# -*~ - -# Temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -################# -##### macOS ##### -################# -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### macOS Patch ### -# iCloud generated files -*.icloud - -#################### -##### IntelliJ ##### -#################### -.idea/ -out/ - -# Run configurations -.run/ - -# CMake -cmake-build-*/ - -# File-based project format -*.iws - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -### Intellij Patch ### -# @see https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -*.iml -# *.ipr -# modules.xml - -################### -##### Eclipse ##### -################### -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# CDT- autotools -.autotools - -# Java annotation processor (APT) -.factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) -.springBeans - -# Code Recommenders -.recommenders/ - -# Annotation Processing -.apt_generated/ -.apt_generated_test/ - -# Scala IDE specific (Scala & Java development for Eclipse) -.cache-main -.scala_dependencies -.worksheet - -# Uncomment this line if you wish to ignore the project description file. Typically, this file would be tracked if -# it would contain build/dependency configurations -#.project - -### Eclipse Patch ### -# Spring Boot Tooling -.sts4-cache/ - -############################ -##### VisualStudioCode ##### -############################ -.vscode/ -.history/ -*.code-workspace - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -################## -##### Python ##### -################## -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template before PyInstaller builds the exe, -# so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.pot - -# Django -*.log -local_settings.py -*.sqlite3 -*.sqlite3-journal - -# Flask -instance/ -.webassets-cache - -# Scrapy -.scrapy - -# Sphinx -doc/build/ -doc/source/_autosummary/* - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is intended to run in multiple -# environments; otherwise, check them in -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. However, in case of -# collaboration, if having platform-specific dependencies or dependencies having no cross-platform support, pipenv may -# install dependencies that don't work, or not install all needed dependencies. -# Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. This is especially -# recommended for binary packages to ensure reproducibility, and is more commonly ignored for libraries. -# @see https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -# poetry.lock - -# pdm -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it in version control. -# @see https://pdm.fming.dev/#use-with-ide -.pdm.toml -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -### Python Patch ### -# Poetry local configuration file -# @see https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -############################################ -##### django-bootstrap-modal-forms ##### -############################################ -geckodriver.exe +database/db.sqlite3 +geckodriver.log +__pycache__ +*.pyc +.env/ \ No newline at end of file