├── .github └── workflows │ └── django.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Procfile ├── README.md ├── api ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── serializers.py ├── test_api.py ├── urls.py └── views.py ├── general ├── __init__.py ├── assets │ ├── general.js │ └── general.scss ├── context_processors.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200627_1611.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── base.py │ ├── customer.py │ └── person.py ├── templates │ └── base.html └── test_models.py ├── manage.py ├── multistep-form.png ├── multistepform ├── __init__.py ├── admin.py ├── apps.py ├── assets │ └── multistepform │ │ ├── form-bg.jpg │ │ ├── form.js │ │ └── form.scss ├── forms │ ├── __init__.py │ ├── customerForm.py │ ├── messageForm.py │ └── surveyForm.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200627_1611.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── formMessage.py │ └── formSurvey.py ├── templates │ └── multistepform │ │ ├── feedback.html │ │ └── form.html ├── test_forms.py ├── test_models.py ├── urls.py └── views │ ├── __init__.py │ ├── cancelView.py │ └── formView.py ├── requirements.txt └── web_project ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v1 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | - name: Run Tests 29 | run: | 30 | python manage.py test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | static/ 7 | staticfiles/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Django", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${workspaceFolder}\\manage.py", 12 | "console": "integratedTerminal", 13 | "args": [ 14 | "runserver", 15 | ], 16 | "django": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "env\\Scripts\\python.exe", 3 | "git.ignoreLimitWarning": true 4 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py migrate 2 | web: gunicorn web_project.wsgi --log-file - 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django multiple-steps form

live at https://django-multistep-form.herokuapp.com/ 2 | 3 | This is an example of a Django application (my first experiment) implementing a form splitted through multiple screens 4 | 5 | ## Code 6 | 7 | * Global resources (for different projects) at [/general](./general): models, styles, ... 8 | * Multistep form project at [/multistepform](/multistepform); most of the multistep logic implemented in [formView.py](/multistepform/views/formView.py) 9 | * REST api (using Django Rest framework) at [/api](./api); live api [deployed at Heroku](https://django-multistep-form.herokuapp.com/api/) 10 | * Calls to get customer data using Vanilla JS: [JS module](general/assets/general.js) and [usage](/multistepform/assets/multistepform/form.js) 11 | * Compile CSS using SASS [global styles](/general/assets/general.scss) and [form styles](/multistepform/assets/multistepform/form.scss) 12 | 13 | ## Details 14 | 15 | * Temporary data are stored in session and stored to database in last step 16 | * Root url redirects to first non-filled step 17 | * Forms are automatically generated from models 18 | 19 | ### Devops 20 | 21 | * Includes some tests for [models](/general/test_models.py), [forms](/multistepform/test_forms.py), [API](/api/test_api.py), ... 22 | * [CI workflow (action) in GitHub](../../actions?query=workflow%3A%22Django+CI%22): build, tests 23 | * Automatically [deployed to Heroku](https://django-multistep-form.herokuapp.com/) 24 | 25 | ## (I'd like) To-do 26 | 27 | * Add info/placeholders for some fields 28 | * Last step showing all data before submitting 29 | * Multilanguage 30 | * Use TypeScript 31 | * Some more tests 32 | 33 | 34 | ![Screenshots](./multistep-form.png) 35 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/api/__init__.py -------------------------------------------------------------------------------- /api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = 'api' 6 | -------------------------------------------------------------------------------- /api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/api/migrations/__init__.py -------------------------------------------------------------------------------- /api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from general.models.customer import Customer 3 | from rest_framework import serializers 4 | 5 | 6 | class CustomerSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Customer 9 | fields = '__all__' 10 | lookup_field = 'email' 11 | -------------------------------------------------------------------------------- /api/test_api.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.test import APITestCase 3 | 4 | class TestAPI(APITestCase): 5 | def test_get_customers(self): 6 | ''' Get list of customers ''' 7 | response = self.client.get('/api/customers', format='json') 8 | self.assertEqual(response.status_code, status.HTTP_200_OK) -------------------------------------------------------------------------------- /api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | from .views import CustomerViewSet 4 | 5 | router = routers.DefaultRouter(trailing_slash=False) 6 | router.register(r'customers', CustomerViewSet) 7 | 8 | urlpatterns = [ 9 | path('', include(router.urls)), 10 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) 11 | ] -------------------------------------------------------------------------------- /api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from general.models.customer import Customer 4 | from .serializers import CustomerSerializer 5 | 6 | class CustomerViewSet(viewsets.ReadOnlyModelViewSet): 7 | queryset = Customer.objects.all() 8 | serializer_class = CustomerSerializer 9 | lookup_field = 'email' 10 | lookup_value_regex = '[^/]+' -------------------------------------------------------------------------------- /general/__init__.py: -------------------------------------------------------------------------------- 1 | """ Global resources for different projects """ -------------------------------------------------------------------------------- /general/assets/general.js: -------------------------------------------------------------------------------- 1 | // global namespace 2 | var general = general || {}; 3 | 4 | // module for API calls 5 | general.API = (function(){ 6 | 'use strict'; 7 | 8 | var _api_url = null; 9 | 10 | /** 11 | * Generic function to get JSON data from API 12 | * @param {string} path 13 | */ 14 | function _getJson(path) { 15 | document.getElementsByTagName('body')[0].classList.add('loading') 16 | return fetch(_api_url + '/' + path) 17 | .then(function(response) { 18 | if (response.ok) { 19 | return response.json(); 20 | } else { 21 | // if not found, just return null 22 | return Promise.resolve(null); 23 | } 24 | }) 25 | .catch(function(err) { 26 | console.log(err); 27 | return Promise.reject(); 28 | }) 29 | .finally(function() { 30 | document.getElementsByTagName("body")[0].classList.remove('loading') 31 | }) 32 | } 33 | 34 | /** 35 | * Get JSON customer data 36 | * @param {string} email 37 | */ 38 | function _getCustomerData(email) { 39 | return _getJson('customers/' + encodeURIComponent(email)); 40 | }; 41 | 42 | /** 43 | * Initialize 44 | */ 45 | function _init(api_url) { 46 | _api_url = api_url; 47 | }; 48 | 49 | return { 50 | getJson: _getJson, 51 | getCustomerData: _getCustomerData, 52 | init: _init 53 | }; 54 | })(); 55 | -------------------------------------------------------------------------------- /general/assets/general.scss: -------------------------------------------------------------------------------- 1 | // global styles (for all applications) 2 | body { 3 | font-family: 'Raleway', sans-serif; 4 | position: relative; 5 | 6 | &.loading { 7 | /* show overlay to block UI while waiting for AJAX responses */ 8 | opacity: .5; 9 | &.loading::after { 10 | content: ""; 11 | position: absolute; 12 | left: 0; 13 | right: 0; 14 | top: 0; 15 | bottom: 0; 16 | cursor: wait; 17 | background-color: transparent; 18 | } 19 | } 20 | } 21 | 22 | p { 23 | margin-top: 0.5rem; 24 | margin-bottom: 0.5rem; 25 | } 26 | 27 | footer { 28 | margin-top: 30px; 29 | text-align: center; 30 | font-size: 0.8rem; 31 | } 32 | 33 | .clear::before, 34 | .clear::after { 35 | clear: both; 36 | content: ""; 37 | display: table; 38 | } -------------------------------------------------------------------------------- /general/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def custom_settings(request): 5 | return { 6 | 'API_URL': settings.API_URL 7 | } -------------------------------------------------------------------------------- /general/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-12 16:24 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Person', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ('updated_at', models.DateTimeField(auto_now=True)), 21 | ('first_name', models.CharField(max_length=30)), 22 | ('last_name', models.CharField(max_length=100)), 23 | ], 24 | options={ 25 | 'abstract': False, 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='Customer', 30 | fields=[ 31 | ('person_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='general.Person')), 32 | ('email', models.EmailField(db_index=True, max_length=254, primary_key=True, serialize=False, unique=True)), 33 | ('phone_number', models.CharField(max_length=20)), 34 | ], 35 | options={ 36 | 'abstract': False, 37 | }, 38 | bases=('general.person',), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /general/migrations/0002_auto_20200627_1611.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-27 14:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('general', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='customer', 15 | name='phone_number', 16 | field=models.CharField(blank=True, max_length=20), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /general/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/general/migrations/__init__.py -------------------------------------------------------------------------------- /general/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ Global models (for different projects) """ -------------------------------------------------------------------------------- /general/models/base.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Base(models.Model): 4 | """ Base for all models """ 5 | 6 | created_at = models.DateTimeField(auto_now_add=True) 7 | updated_at = models.DateTimeField(auto_now=True) 8 | 9 | class Meta: 10 | abstract = True -------------------------------------------------------------------------------- /general/models/customer.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from .person import Person 3 | 4 | class Customer(Person): 5 | """ Customer (existing or future) """ 6 | 7 | email = models.EmailField(unique=True, db_index=True, primary_key=True) 8 | phone_number = models.CharField(max_length=20, blank=True) # PhoneNumberField() -------------------------------------------------------------------------------- /general/models/person.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from .base import Base 3 | 4 | class Person(Base): 5 | """ Any real person """ 6 | 7 | first_name = models.CharField(max_length=30) 8 | last_name = models.CharField(max_length=100) -------------------------------------------------------------------------------- /general/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load compress %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% compress css %} 11 | 12 | 13 | {% block styles %} 14 | 15 | {% endblock styles %} 16 | {% endcompress %} 17 | 18 | 19 | {% block title %} 20 | {{ page_title|default:"Untitled Page" }} 21 | {% endblock title %} 22 | 23 | 24 | 25 | 26 |
27 |

{{ page_title }}

28 | 29 | {% block content %} 30 |

Placeholder text in base template. Replace with page content.

31 | {% endblock content %} 32 |
33 | 34 | 42 | 43 | 44 | 45 | 51 | 52 | {% block scripts %} 53 | 54 | {% endblock scripts %} 55 | 56 | -------------------------------------------------------------------------------- /general/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from general.models.customer import Customer 3 | from django.db.utils import IntegrityError 4 | 5 | class TestModels(TestCase): 6 | def test_no_duplicated_emails(self): 7 | ''' Make sure there can't be duplicated emails in database ''' 8 | 9 | customer1 = Customer(email='test@email.com') 10 | customer1.save() 11 | customer2 = Customer(email='test@email.com') 12 | with self.assertRaises(IntegrityError, msg='allows duplicated emails'): 13 | customer2.save() -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_project.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /multistep-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/multistep-form.png -------------------------------------------------------------------------------- /multistepform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/multistepform/__init__.py -------------------------------------------------------------------------------- /multistepform/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from general.models.person import Person 3 | from general.models.customer import Customer 4 | from .models.formMessage import FormMessage 5 | 6 | # Register your models here. 7 | admin.site.register(Person) 8 | admin.site.register(Customer) 9 | admin.site.register(FormMessage) 10 | -------------------------------------------------------------------------------- /multistepform/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MultistepformConfig(AppConfig): 5 | name = 'multistepform' 6 | -------------------------------------------------------------------------------- /multistepform/assets/multistepform/form-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/multistepform/assets/multistepform/form-bg.jpg -------------------------------------------------------------------------------- /multistepform/assets/multistepform/form.js: -------------------------------------------------------------------------------- 1 | // global namespace 2 | var general = general || {}; 3 | 4 | // module for multip-step form 5 | general.Multistepform = (function(){ 6 | 'use strict'; 7 | 8 | var _container = null; 9 | 10 | /** 11 | * Update form data from API using email 12 | * TODO: obvious privacy issues here 13 | * @param {*} email 14 | */ 15 | function _updateCustomerData(email) { 16 | general.API.getCustomerData(email).then(function(data) { 17 | if (!!data) { 18 | _container.querySelector('#id_first_name').value=data.first_name; 19 | _container.querySelector('#id_last_name').value=data.last_name; 20 | _container.querySelector('#id_phone_number').value=data.phone_number; 21 | } 22 | }); 23 | } 24 | 25 | /** 26 | * Initialize form 27 | * @param {*} container 28 | */ 29 | function _init(container) { 30 | _container = container; 31 | if (!! _container) { 32 | // attach listener to email field to get data from API 33 | var email_field = _container.querySelector('#id_email'); 34 | if (!! email_field) { 35 | email_field.addEventListener('blur', function(e) { 36 | _updateCustomerData(e.target.value); 37 | }); 38 | } 39 | } 40 | }; 41 | 42 | return { 43 | init: _init 44 | }; 45 | })(); 46 | -------------------------------------------------------------------------------- /multistepform/assets/multistepform/form.scss: -------------------------------------------------------------------------------- 1 | // styles only for form 2 | 3 | @import '../../../general/assets/general.scss'; 4 | 5 | html { 6 | background-color: #ccc; 7 | padding: 40px 20px; 8 | } 9 | 10 | main { 11 | background-color: transparent; 12 | padding: 20px; 13 | margin: 0 auto; 14 | max-width: 800px; 15 | position: relative; 16 | 17 | &::after { 18 | content: ""; 19 | background: url('form-bg.jpg'); 20 | background-size: cover; 21 | background-position: right top; 22 | opacity: 0.5; 23 | top: 0; 24 | left: 0; 25 | bottom: 0; 26 | right: 0; 27 | position: absolute; 28 | z-index: -1; 29 | } 30 | } 31 | 32 | form { 33 | & > label { 34 | display: block; 35 | margin-top: 30px; 36 | 37 | &.required::after { 38 | content: '\2732'; 39 | color: #dd3333; 40 | vertical-align: super; 41 | font-size: .7rem; 42 | padding-left: 5px; 43 | } 44 | } 45 | 46 | .field--helptext { 47 | opacity: .7; 48 | font-size: 0.8em; 49 | font-style: italic; 50 | padding-left: 10px; 51 | } 52 | 53 | ul, ol { 54 | /* used in radio buttons */ 55 | padding-left: 1.0rem; 56 | font-size: .9rem; 57 | 58 | li { 59 | list-style-type: none; 60 | } 61 | } 62 | 63 | .form--actions { 64 | @extend .clear; 65 | 66 | margin: 50px 0 0 0; 67 | padding: 30px 0 20px; 68 | border-top: solid 2px #fff; 69 | 70 | a { 71 | text-decoration: none; 72 | } 73 | 74 | .nav--prev, 75 | .nav--next, 76 | .nav--send { 77 | text-transform: uppercase; 78 | padding: 10px; 79 | display: inline-block; 80 | } 81 | 82 | .nav--next, 83 | .nav--send { 84 | border-radius: 5px; 85 | margin-left: 20px; 86 | text-transform: uppercase; 87 | font-weight: bold; 88 | } 89 | 90 | .nav--prev::before { 91 | content: '\21E6 '; 92 | } 93 | .nav--next::after { 94 | content: ' \21E8'; 95 | } 96 | .nav--send::after { 97 | content: ' \2713'; 98 | } 99 | 100 | 101 | &__main { 102 | /* main form actions right floated */ 103 | display: block; 104 | float: right; 105 | text-align: right; 106 | } 107 | } 108 | } 109 | 110 | nav { 111 | margin-top: 30px; 112 | margin-bottom: 30px; 113 | 114 | a { 115 | text-decoration: none; 116 | } 117 | } 118 | 119 | @media (max-width: 600px) { 120 | html { 121 | padding: 10px 5px; 122 | } 123 | 124 | input:not([type="checkbox"]):not([type="radio"]), 125 | textarea, 126 | select { 127 | width: 100%; 128 | box-sizing: border-box; 129 | } 130 | 131 | main { 132 | padding: 5px; 133 | background-color: #fff; 134 | } 135 | } -------------------------------------------------------------------------------- /multistepform/forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/multistepform/forms/__init__.py -------------------------------------------------------------------------------- /multistepform/forms/customerForm.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from general.models.customer import Customer 3 | 4 | class CustomerForm(forms.ModelForm): 5 | class Meta: 6 | model = Customer 7 | fields = ['email', 'first_name', 'last_name', 'phone_number'] 8 | help_texts = { 9 | 'email': 'if email already exists, stored data will be retrieved', 10 | } 11 | 12 | def validate_unique(self): 13 | ''' 14 | Do not check uniqueness of email address here as we want to allow existing emails 15 | to add new messages; we will do it at the view. 16 | This is not a 'new customer' form, but a form to provide customer data to the message. 17 | ''' 18 | pass 19 | -------------------------------------------------------------------------------- /multistepform/forms/messageForm.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from general.models.customer import Customer 3 | from ..models.formMessage import FormMessage 4 | 5 | class MessageForm(forms.ModelForm): 6 | class Meta: 7 | model = FormMessage 8 | exclude = ('customer',) # we will assign it from CustomerForm 9 | widgets = { 10 | 'additional_message': forms.Textarea(), 11 | } 12 | -------------------------------------------------------------------------------- /multistepform/forms/surveyForm.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from ..models.formSurvey import FormSurvey 3 | 4 | CHOICES = ( (True,'Yes'), 5 | (False,'No'), 6 | ) 7 | 8 | class SurveyForm(forms.ModelForm): 9 | are_you_happy = forms.TypedChoiceField( 10 | choices=CHOICES, widget=forms.RadioSelect 11 | ) 12 | do_you_know_it = forms.TypedChoiceField( 13 | choices=CHOICES, widget=forms.RadioSelect 14 | ) 15 | class Meta: 16 | model = FormSurvey 17 | exclude = ('customer',) # we will assign it from CustomerForm 18 | 19 | -------------------------------------------------------------------------------- /multistepform/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-12 16:24 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('general', '__first__'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='FormMessage', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ('subject', models.CharField(max_length=30)), 23 | ('message', models.CharField(max_length=500)), 24 | ('customer', models.ForeignKey(db_column='email', on_delete=django.db.models.deletion.CASCADE, to='general.Customer')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /multistepform/migrations/0002_auto_20200627_1611.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-27 14:11 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('general', '0002_auto_20200627_1611'), 11 | ('multistepform', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='formmessage', 17 | name='message', 18 | ), 19 | migrations.RemoveField( 20 | model_name='formmessage', 21 | name='subject', 22 | ), 23 | migrations.AddField( 24 | model_name='formmessage', 25 | name='additional_message', 26 | field=models.CharField(blank=True, max_length=500), 27 | ), 28 | migrations.CreateModel( 29 | name='FormSurvey', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('created_at', models.DateTimeField(auto_now_add=True)), 33 | ('updated_at', models.DateTimeField(auto_now=True)), 34 | ('are_you_happy', models.BooleanField()), 35 | ('do_you_know_it', models.BooleanField()), 36 | ('customer', models.ForeignKey(db_column='email', on_delete=django.db.models.deletion.CASCADE, to='general.Customer')), 37 | ], 38 | options={ 39 | 'abstract': False, 40 | }, 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /multistepform/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/multistepform/migrations/__init__.py -------------------------------------------------------------------------------- /multistepform/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/multistepform/models/__init__.py -------------------------------------------------------------------------------- /multistepform/models/formMessage.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from general.models.base import Base 3 | from general.models.customer import Customer 4 | 5 | class FormMessage(Base): 6 | """ Message from customer """ 7 | 8 | additional_message = models.CharField(max_length=500, blank=True) 9 | customer = models.ForeignKey(Customer, on_delete=models.CASCADE, db_column='email') 10 | -------------------------------------------------------------------------------- /multistepform/models/formSurvey.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from general.models.base import Base 3 | from general.models.customer import Customer 4 | 5 | class FormSurvey(Base): 6 | """ Ask some questions """ 7 | 8 | are_you_happy = models.BooleanField() 9 | do_you_know_it = models.BooleanField() 10 | customer = models.ForeignKey(Customer, on_delete=models.CASCADE, db_column='email') 11 | -------------------------------------------------------------------------------- /multistepform/templates/multistepform/feedback.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block styles %} 5 | 6 | {% endblock styles %} 7 | 8 | {% block content %} 9 |

{{ msg }}

10 | 11 | 14 | {% endblock content %} -------------------------------------------------------------------------------- /multistepform/templates/multistepform/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block styles %} 5 | 6 | {% endblock styles %} 7 | 8 | {% block content %} 9 |
10 | 11 | {% if form.errors %} 12 | {% for field in form %} 13 | {% for error in field.errors %} 14 |
15 | {{ error|escape }} 16 |
17 | {% endfor %} 18 | {% endfor %} 19 | {% for error in form.non_field_errors %} 20 |
21 | {{ error|escape }} 22 |
23 | {% endfor %} 24 | {% endif %} 25 | 26 | {% csrf_token %} 27 | 28 | 29 | {% for field in form %} 30 | 31 | {{ field }} 32 | {% if field.help_text %} 33 | {{ field.help_text }} 34 | {% endif %} 35 | {% endfor %} 36 | 37 |
38 | 39 | {% if step > 1 %} 40 | Prev 41 | {% endif %} 42 | 43 | 44 |
45 | cancel 46 | 47 | {% if step < step_last %} 48 | 49 | {% else %} 50 | 51 | {% endif %} 52 |
53 | 54 |
55 |
56 | {% endblock content %} 57 | 58 | {% block scripts %} 59 | 60 | 66 | {% endblock scripts %} -------------------------------------------------------------------------------- /multistepform/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from .forms.surveyForm import SurveyForm 3 | from .forms.messageForm import MessageForm 4 | from .forms.customerForm import CustomerForm 5 | 6 | class TestForms(TestCase): 7 | 8 | def test_non_empty(self): 9 | ''' Make sure empty forms aren't allowed ''' 10 | 11 | form = SurveyForm() 12 | self.assertFalse(form.is_valid(), msg='empty messages allowed') 13 | form = CustomerForm() 14 | self.assertFalse(form.is_valid(), msg='empty customer allowed') 15 | 16 | def test_valid_data(self): 17 | ''' Make sure valid data is accepted ''' 18 | 19 | form_data = { 20 | 'additional_message': 'test message' 21 | } 22 | form = MessageForm(form_data) 23 | self.assertTrue(form.is_valid(), 'unable to validate message data') 24 | 25 | 26 | -------------------------------------------------------------------------------- /multistepform/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.db.utils import IntegrityError 3 | from .models.formMessage import FormMessage 4 | 5 | class TestModels(TestCase): 6 | def test_no_empty_customer(self): 7 | ''' Make sure there can't be a message without customer ''' 8 | 9 | message = FormMessage( 10 | additional_message = 'test message' 11 | ) 12 | with self.assertRaises(IntegrityError, msg='message without customer should not be allowed'): 13 | message.save() -------------------------------------------------------------------------------- /multistepform/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import path 3 | from multistepform.views.formView import FormView 4 | from multistepform.views.cancelView import CancelView 5 | 6 | urlpatterns = [ 7 | path('', FormView, {'step': None}), 8 | path('step/', FormView), 9 | path('cancel', CancelView), 10 | ] 11 | 12 | if settings.DEBUG: 13 | # test mode 14 | from django.conf.urls.static import static 15 | urlpatterns += static(settings.STATIC_URL, 16 | document_root=settings.STATIC_ROOT) 17 | # urlpatterns += static(settings.MEDIA_URL, 18 | # document_root=settings.MEDIA_ROOT) -------------------------------------------------------------------------------- /multistepform/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/multistepform/views/__init__.py -------------------------------------------------------------------------------- /multistepform/views/cancelView.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import render 3 | 4 | def CancelView(request): 5 | ''' View after finishing process ''' 6 | # remove all data stored in session 7 | request.session.flush() 8 | return render(request, 'multistepform/feedback.html', { 9 | 'page_title': 'Cancelled', 10 | 'msg': 'All your data has been removed. Thanks.' 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /multistepform/views/formView.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import render 3 | from django.forms.models import model_to_dict 4 | from django.shortcuts import redirect 5 | 6 | from ..forms.messageForm import MessageForm 7 | from ..forms.customerForm import CustomerForm 8 | from ..forms.surveyForm import SurveyForm 9 | 10 | from general.models.customer import Customer 11 | 12 | SESSIONKEY_PREFIX = 'multistepform_step_' 13 | 14 | STEPS = { 15 | 1: { 16 | 'form': 'SurveyForm' 17 | }, 18 | 2: { 19 | 'form': 'MessageForm' 20 | }, 21 | 3: { 22 | 'form': 'CustomerForm' 23 | } 24 | } 25 | 26 | def __getSessionData(request, step): 27 | ''' Get session data for a step ''' 28 | return request.session.get(SESSIONKEY_PREFIX + str(step)) 29 | 30 | def __getFormData(request, step): 31 | ''' Get form data stored in session, or empty otherwise ''' 32 | return globals()[STEPS[step]['form']](__getSessionData(request, step)) 33 | 34 | def __setFormData(request, step, data): 35 | ''' Store form data in session ''' 36 | request.session[SESSIONKEY_PREFIX + str(step)] = data 37 | 38 | def __getNextStep(request): 39 | ''' Try to get first step not completed by user ''' 40 | for i in range(1, len(STEPS)): 41 | if __getSessionData(request, i) == None: 42 | return i 43 | return len(STEPS) # there's data in all steps => go to last step 44 | 45 | 46 | def FormView(request, step): 47 | ''' View for multiple steps form ''' 48 | 49 | if step == None: 50 | # no step in url => check previously stored data and redirect to non-completed step 51 | step = __getNextStep(request) 52 | return redirect('/step/' + str(step)) 53 | 54 | form = globals()[STEPS[step]['form']]() # default form for current step 55 | 56 | if request.method == 'POST': 57 | if step == len(STEPS): 58 | # last step => save in database 59 | 60 | form = globals()[STEPS[step]['form']](request.POST) 61 | # check that user is not already on the database 62 | if form.is_valid(): 63 | existing_customers = Customer.objects.filter(email=form.instance.email) 64 | if existing_customers.count() > 0: 65 | # customer already exists => update data 66 | existing_customer = existing_customers[0] 67 | existing_customer.first_name = form.instance.first_name 68 | existing_customer.last_name = form.instance.last_name 69 | existing_customer.phone_number = form.instance.phone_number 70 | form.instance = existing_customer 71 | form.instance.save() 72 | else: 73 | form.save() # save new customer 74 | 75 | # retrieve previous forms (stored in session) and save them 76 | for i in range(1, len(STEPS)): 77 | form_stored = __getFormData(request, i) 78 | form_stored.instance.customer = form.instance # set saved customer in related entities 79 | form_stored.save() 80 | 81 | request.session.flush() # remove session data 82 | return render(request, 'multistepform/feedback.html', { 83 | 'page_title': 'Thanks', 84 | 'msg': 'Your data has been saved. We will contact you soon.' 85 | }) 86 | 87 | else: 88 | # not last step => store data in session (temporarily) using model_to_dict to make it serializable 89 | 90 | form = globals()[STEPS[step]['form']](request.POST) 91 | if form.is_valid(): 92 | __setFormData(request, step, model_to_dict(form.instance)) 93 | return redirect('/step/' + str(step + 1)) 94 | 95 | else: 96 | # GET => try to get data from session (in case it was previously stored) 97 | form = __getFormData(request, step) 98 | 99 | return render(request, 'multistepform/form.html', { 100 | 'page_title': 'Multiple steps form (' + str(step) + '/' + str(len(STEPS)) + ')', 101 | 'form': form, 102 | 'step': step, 103 | 'step_last': len(STEPS) 104 | }) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.7 2 | astroid==2.4.1 3 | Babel==2.8.0 4 | colorama==0.4.3 5 | dj-database-url==0.5.0 6 | Django==3.0.7 7 | django-appconf==1.0.4 8 | django-compressor==2.4 9 | django-heroku==0.3.1 10 | django-libsass==0.8 11 | django-phonenumber-field==4.0.0 12 | djangorestframework==3.11.0 13 | gunicorn==20.0.4 14 | isort==4.3.21 15 | lazy-object-proxy==1.4.3 16 | libsass==0.20.0 17 | mccabe==0.6.1 18 | phonenumberslite==8.12.5 19 | psycopg2==2.8.5 20 | pylint==2.5.2 21 | pytz==2020.1 22 | rcssmin==1.0.6 23 | rjsmin==1.1.0 24 | six==1.14.0 25 | sqlparse==0.3.1 26 | toml==0.10.0 27 | whitenoise==5.1.0 28 | wrapt==1.12.1 29 | -------------------------------------------------------------------------------- /web_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordisan/Django-multistep-form/2e370f5e1310885cd0763a2caea9975a611dec8d/web_project/__init__.py -------------------------------------------------------------------------------- /web_project/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for web_project project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_project.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /web_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for web_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | import django_heroku 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'od*(s3!p^1he%mp39i7_dw1u-h&)36*b7wlvxkut4r$1!0&!u*' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['127.0.0.1', 'localhost', 'django-demo-jordisan.herokuapp.com'] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'compressor', 42 | 'rest_framework', 43 | 'general', 44 | 'api', 45 | 'multistepform', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'web_project.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [ 64 | os.path.join(BASE_DIR, 'general/templates'), 65 | os.path.join(BASE_DIR, 'multistepform/templates') 66 | ], 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | 'general.context_processors.custom_settings', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'web_project.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.sqlite3', 89 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | 127 | # Static files (CSS, JavaScript, Images) 128 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 129 | 130 | STATIC_URL = '/static/' 131 | 132 | STATIC_ROOT = 'static' 133 | 134 | STATICFILES_DIRS = [ 135 | os.path.join(BASE_DIR, 'general/assets'), 136 | os.path.join(BASE_DIR, 'multistepform/assets'), 137 | ] 138 | 139 | STATICFILES_FINDERS = ( 140 | 'django.contrib.staticfiles.finders.FileSystemFinder', 141 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 142 | # other finders.. 143 | 'compressor.finders.CompressorFinder', 144 | ) 145 | 146 | COMPRESS_PRECOMPILERS = ( 147 | ('text/x-scss', 'django_libsass.SassCompiler'), 148 | ) 149 | 150 | # URL for REST API 151 | API_URL = '/api' 152 | 153 | # Activate Django-Heroku. 154 | django_heroku.settings(locals(), test_runner=False) -------------------------------------------------------------------------------- /web_project/urls.py: -------------------------------------------------------------------------------- 1 | """web_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path("", include("multistepform.urls")), 21 | path("api/", include("api.urls")), 22 | path('admin/', admin.site.urls), 23 | ] 24 | -------------------------------------------------------------------------------- /web_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for web_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_project.settings') 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------