├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── example ├── README.rst ├── __init__.py ├── manage.py ├── requirements.txt ├── settings.py ├── templates │ └── 503.html ├── urls.py └── views.py ├── maintenancemode ├── __init__.py ├── conf.py ├── http.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── maintenance.py ├── middleware.py ├── models.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── models.py │ ├── settings.py │ ├── templates │ │ └── 503.html │ ├── test_middleware.py │ └── urls.py ├── utils.py └── views.py ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */migrations/*,*__init__* 3 | source = maintenancemode 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | def __repr__ 9 | raise AssertionError 10 | raise NotImplementedError 11 | if __name__ == .__main__.: 12 | # -*- coding: utf-8 -*- 13 | pass -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | /.idea 23 | .venv 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | dist: xenial 3 | language: python 4 | cache: pip 5 | sudo: false 6 | 7 | matrix: 8 | include: 9 | - env: TOXENV=py36-dj22 10 | python: 3.6 11 | - env: TOXENV=py36-dj30 12 | python: 3.6 13 | - env: TOXENV=py36-dj31 14 | python: 3.6 15 | - env: TOXENV=py36-dj32 16 | python: 3.6 17 | - env: TOXENV=py37-dj22 18 | python: 3.7 19 | - env: TOXENV=py37-dj30 20 | python: 3.7 21 | - env: TOXENV=py37-dj31 22 | python: 3.7 23 | - env: TOXENV=py37-dj32 24 | python: 3.7 25 | - env: TOXENV=py38-dj22 26 | python: 3.8 27 | - env: TOXENV=py38-dj30 28 | python: 3.8 29 | - env: TOXENV=py38-dj31 30 | python: 3.8 31 | - env: TOXENV=py38-dj32 32 | python: 3.8 33 | - env: TOXENV=py39-dj22 34 | python: 3.9 35 | - env: TOXENV=py39-dj30 36 | python: 3.9 37 | - env: TOXENV=py39-dj31 38 | python: 3.9 39 | - env: TOXENV=py39-dj32 40 | python: 3.9 41 | # Adding jobs for ppc64le 42 | - env: TOXENV=py36-dj22 43 | python: 3.6 44 | arch: ppc64le 45 | - env: TOXENV=py36-dj30 46 | python: 3.6 47 | arch: ppc64le 48 | - env: TOXENV=py36-dj31 49 | python: 3.6 50 | arch: ppc64le 51 | - env: TOXENV=py36-dj32 52 | python: 3.6 53 | arch: ppc64le 54 | - env: TOXENV=py37-dj22 55 | python: 3.7 56 | arch: ppc64le 57 | - env: TOXENV=py37-dj30 58 | python: 3.7 59 | arch: ppc64le 60 | - env: TOXENV=py37-dj31 61 | python: 3.7 62 | arch: ppc64le 63 | - env: TOXENV=py37-dj32 64 | python: 3.7 65 | arch: ppc64le 66 | - env: TOXENV=py38-dj22 67 | python: 3.8 68 | arch: ppc64le 69 | - env: TOXENV=py38-dj30 70 | python: 3.8 71 | arch: ppc64le 72 | - env: TOXENV=py38-dj31 73 | python: 3.8 74 | arch: ppc64le 75 | - env: TOXENV=py38-dj32 76 | python: 3.8 77 | arch: ppc64le 78 | - env: TOXENV=py39-dj22 79 | python: 3.9 80 | arch: ppc64le 81 | - env: TOXENV=py39-dj30 82 | python: 3.9 83 | arch: ppc64le 84 | - env: TOXENV=py39-dj31 85 | python: 3.9 86 | arch: ppc64le 87 | - env: TOXENV=py39-dj32 88 | python: 3.9 89 | arch: ppc64le 90 | 91 | install: 92 | - pip install tox-travis 93 | 94 | script: 95 | - tox 96 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | django-maintenancemode is written by Remco Wendt 2 | 3 | Feel free to contact me by mail if you have any questions, or just want to say hi. 4 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ------- 3 | 4 | 0.11.7 5 | ~~~~~~ 6 | 7 | - Fixed broken tests 8 | 9 | 0.11.6 10 | ~~~~~~ 11 | 12 | - Nothing 13 | 14 | 0.11.5 15 | ~~~~~~ 16 | 17 | - Made sure the app runs on Django 3.2, dropped support for Django < 2.x. It may 18 | still work with Django 1.11, but this is no longer tested. 19 | 20 | 0.11.4 21 | ~~~~~~ 22 | 23 | - Changed the middleware to not fetch the user instance if both 24 | ``MAINTENANCE_ALLOW_STAFF`` and ``MAINTENANCE_ALLOW_SUPERUSER`` are 25 | ``False``. 26 | - Added support for django 3.1. 27 | 28 | 0.11.3 29 | ~~~~~~ 30 | 31 | - Added support for django 2.x, dropped support for django < 1.11. It may 32 | still work with django 1.8, but this is no longer tested. 33 | 34 | 0.11.2 35 | ~~~~~~ 36 | 37 | - Getting ready for Django 1.10 release. 38 | - Dropped support for Django 1.3 and older. 39 | 40 | 0.11.1 41 | ~~~~~~ 42 | 43 | - Enable network specify in INTERNAL_IPS 44 | 45 | 0.11.0 46 | ~~~~~~ 47 | 48 | - Added management command to set maintenance mode on/off 49 | 50 | 0.10.1 51 | ~~~~~~ 52 | 53 | - Made sure the app runs on Django 1.8. 54 | 55 | 0.10.0 56 | ~~~~~~ 57 | 58 | - Got rid of dependency on setuptools 59 | - Added ability to exclude specific paths from maintenance mode with the 60 | ``MAINTENANCE_IGNORE_URLS`` setting. 61 | - Use RequestContext when rending the ``503.html`` template. 62 | - Use tox for running the tests instead of buildout. 63 | - Made sure the app runs on Django 1.4. 64 | 65 | 0.9.3 66 | ~~~~~~ 67 | 68 | - Minor documentation updates for the switch to github, expect more changes to follow soon. 69 | 70 | 0.9.2 71 | ~~~~~~ 72 | 73 | - Fixed an issue with setuptools, thanks for reporting this ksato9700 74 | 75 | 0.9.1 76 | ~~~~~~ 77 | 78 | - Tested django-maintenancemode with django-1.0 release (following the 1.0.X release branch) 79 | - Bundled buildout.cfg and bootstrap with the source version of the project, allowing repeatable buildout 80 | - The middleware now uses its own default config file, thanks to a patch by semente 81 | - Use INTERNAL_IPS to check for users that need access. user.is_staff will stay in place 82 | for backwards incompatibility. Thanks for the idea Joshua Works 83 | - Have setup.py sdist only distribute maintenancemode itself, no longer distribute tests and buildout stuff 84 | - Use README and CHANGES in setup.py's long_description, stolen from Jeroen's djangorecipe :) 85 | - Updated the documentation and now use pypi as the documentation source (link there from google code) 86 | 87 | 0.9 88 | ~~~~~~ 89 | 90 | First release 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Maykin Media 2 | Copyright (c) 2012, enn.io 3 | Copyright (c) 2015-2019, Basil Shubin 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | * Neither the name of the author nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGES.rst 3 | include LICENSE 4 | include README.rst 5 | recursive-exclude example * 6 | recursive-include maintenancemode templates *.html 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-maintenancemode 2 | ====================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-maintenancemode.svg 5 | :target: https://pypi.python.org/pypi/django-maintenancemode/ 6 | 7 | .. image:: https://img.shields.io/pypi/dm/django-maintenancemode.svg 8 | :target: https://pypi.python.org/pypi/django-maintenancemode/ 9 | 10 | .. image:: https://img.shields.io/github/license/shanx/django-maintenancemode.svg 11 | :target: https://pypi.python.org/pypi/django-maintenancemode/ 12 | 13 | .. image:: https://app.travis-ci.com/bashu/django-maintenancemode.svg?branch=develop 14 | :target: https://app.travis-ci.com/github/bashu/django-maintenancemode 15 | 16 | .. image:: https://coveralls.io/repos/github/shanx/django-maintenancemode/badge.svg?branch=develop 17 | :target: https://coveralls.io/github/shanx/django-maintenancemode?branch=develop 18 | 19 | django-maintenancemode is a middleware that allows you to temporary shutdown 20 | your site for maintenance work. 21 | 22 | Logged in users having staff credentials can still fully use 23 | the site as can users visiting the site from an IP address defined in 24 | Django's ``INTERNAL_IPS``. 25 | 26 | Authored by `Remco Wendt `_, and some great `contributors `_. 27 | 28 | How it works 29 | ------------ 30 | 31 | ``maintenancemode`` works the same way as handling 404 or 500 error in 32 | Django work. It adds a ``handler503`` which you can override in your 33 | main ``urls.py`` or you can add a ``503.html`` to your templates 34 | directory. 35 | 36 | * If user is logged in and staff member, the maintenance page is 37 | not displayed. 38 | 39 | * If user's IP is in ``INTERNAL_IPS``, the maintenance page is 40 | not displayed. 41 | 42 | * To override the default view which is used if the maintenance mode 43 | is enabled you can simply define a ``handler503`` variable in your 44 | ROOT_URLCONF_, similar to how you would customize other `error handlers`_, 45 | e.g. : 46 | 47 | .. code-block:: python 48 | 49 | handler503 = 'example.views.maintenance_mode' 50 | 51 | Installation 52 | ------------ 53 | 54 | 1. Either checkout ``maintenancemode`` from GitHub, or install using pip : 55 | 56 | .. code-block:: bash 57 | 58 | pip install django-maintenancemode 59 | 60 | 2. Add ``maintenancemode`` to your ``INSTALLED_APPS`` : 61 | 62 | .. code-block:: python 63 | 64 | INSTALLED_APPS = ( 65 | ... 66 | 'maintenancemode', 67 | ) 68 | 69 | 3. Add ``MaintenanceModeMiddleware`` to ``MIDDLEWARE_CLASSES``, make sure it comes after ``AuthenticationMiddleware`` : 70 | 71 | .. code-block:: python 72 | 73 | MIDDLEWARE_CLASSES = ( 74 | ... 75 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 76 | 'maintenancemode.middleware.MaintenanceModeMiddleware', 77 | ) 78 | 79 | 4. Add variable called ``MAINTENANCE_MODE`` in your project's ``settings.py`` file : 80 | 81 | .. code-block:: python 82 | 83 | MAINTENANCE_MODE = True # Setting this variable to ``True`` activates the middleware. 84 | 85 | or set ``MAINTENANCE_MODE`` to ``False`` and use ``maintenance`` command : 86 | 87 | .. code-block:: shell 88 | 89 | python ./manage.py maintenance 90 | 91 | Please see ``example`` application. This application is used to 92 | manually test the functionalities of this package. This also serves as 93 | a good example... 94 | 95 | You need only Django 1.4 or above to run that. It might run on older 96 | versions but that is not tested. 97 | 98 | Configuration 99 | ------------- 100 | 101 | There are various optional configuration options you can set in your ``settings.py`` 102 | 103 | .. code-block:: python 104 | 105 | # Enable / disable maintenance mode. 106 | # Default: False 107 | MAINTENANCE_MODE = True # or ``False`` and use ``maintenance`` command 108 | 109 | # Sequence of URL path regexes to exclude from the maintenance mode. 110 | # Default: () 111 | MAINTENANCE_IGNORE_URLS = ( 112 | r'^/docs/.*', 113 | r'^/contact' 114 | ) 115 | 116 | License 117 | ------- 118 | 119 | ``django-maintenancemode`` is released under the BSD license. 120 | 121 | .. _ROOT_URLCONF: https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf 122 | .. _`error handlers`: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views 123 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | 4 | To run the example application, make sure you have the required 5 | packages installed. You can do this using following commands : 6 | 7 | .. code-block:: bash 8 | 9 | mkvirtualenv example 10 | pip install -r example/requirements.txt 11 | 12 | This assumes you already have ``virtualenv`` and ``virtualenvwrapper`` 13 | installed and configured. 14 | 15 | Next, you can setup the django instance using : 16 | 17 | .. code-block:: bash 18 | 19 | python example/manage.py syncdb --noinput 20 | 21 | And run it : 22 | 23 | .. code-block:: bash 24 | 25 | python example/manage.py runserver 26 | 27 | Good luck! 28 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-maintenancemode/dfbd5842c3df9b55c5f14efa7539b33749618cad/example/__init__.py -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | # Allow starting the app without installing the module. 11 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-appconf 3 | ipy 4 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for app project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | import re 14 | 15 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = "YOUR_SECRET_KEY" 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | PROJECT_APPS = [ 32 | "maintenancemode", 33 | ] 34 | 35 | INSTALLED_APPS = [ 36 | "django.contrib.auth", 37 | "django.contrib.admin", 38 | "django.contrib.sessions", 39 | "django.contrib.contenttypes", 40 | "django.contrib.messages", 41 | "django.contrib.sites", 42 | ] + PROJECT_APPS 43 | 44 | 45 | MIDDLEWARE_CLASSES = [ 46 | "django.contrib.sessions.middleware.SessionMiddleware", 47 | "django.middleware.common.CommonMiddleware", 48 | "django.middleware.csrf.CsrfViewMiddleware", 49 | "django.contrib.auth.middleware.AuthenticationMiddleware", 50 | #'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 51 | "django.contrib.messages.middleware.MessageMiddleware", 52 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 53 | #'django.middleware.security.SecurityMiddleware', 54 | "maintenancemode.middleware.MaintenanceModeMiddleware", 55 | ] 56 | 57 | MIDDLEWARE = MIDDLEWARE_CLASSES 58 | 59 | ROOT_URLCONF = "example.urls" 60 | 61 | SITE_ID = 1 62 | 63 | TEMPLATES = [ 64 | { 65 | "BACKEND": "django.template.backends.django.DjangoTemplates", 66 | "DIRS": [ 67 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates"), 68 | ], 69 | "APP_DIRS": True, 70 | "OPTIONS": { 71 | "context_processors": [ 72 | "django.template.context_processors.debug", 73 | "django.template.context_processors.request", 74 | "django.contrib.auth.context_processors.auth", 75 | "django.contrib.messages.context_processors.messages", 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 83 | 84 | DATABASES = { 85 | "default": { 86 | "ENGINE": "django.db.backends.sqlite3", 87 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 88 | } 89 | } 90 | 91 | # Internationalization 92 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 93 | 94 | LANGUAGE_CODE = "en-us" 95 | 96 | TIME_ZONE = "UTC" 97 | 98 | USE_I18N = True 99 | 100 | USE_L10N = True 101 | 102 | USE_TZ = True 103 | 104 | # Static files (CSS, JavaScript, Images) 105 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 106 | 107 | # Absolute filesystem path to the directory that will hold user-uploaded files. 108 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 109 | 110 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 111 | # trailing slash. 112 | MEDIA_URL = "/media/" 113 | 114 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 115 | 116 | STATIC_URL = "/static/" 117 | 118 | 119 | ## Maintenance Mode settings 120 | 121 | MAINTENANCE_MODE = True # or ``False`` and use ``maintenance`` command 122 | MAINTENANCE_IGNORE_URLS = (re.compile(r"^/ignored.*"),) 123 | -------------------------------------------------------------------------------- /example/templates/503.html: -------------------------------------------------------------------------------- 1 |

Temporary unavailable

2 | 3 |

You requested: {{ request_path }}

4 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import index 4 | 5 | urlpatterns = [ 6 | path('', index), 7 | path('ignored/', index), 8 | ] 9 | -------------------------------------------------------------------------------- /example/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def index(request): 5 | return HttpResponse("OK") 6 | -------------------------------------------------------------------------------- /maintenancemode/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-maintenancemode/dfbd5842c3df9b55c5f14efa7539b33749618cad/maintenancemode/__init__.py -------------------------------------------------------------------------------- /maintenancemode/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings # noqa 4 | 5 | from appconf import AppConf 6 | 7 | 8 | class MaintenanceSettings(AppConf): 9 | IGNORE_URLS = () 10 | LOCKFILE_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "maintenance.lock") 11 | MODE = False 12 | 13 | class Meta: 14 | prefix = "maintenance" 15 | holder = "maintenancemode.conf.settings" 16 | -------------------------------------------------------------------------------- /maintenancemode/http.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | class HttpResponseTemporaryUnavailable(HttpResponse): 5 | status_code = 503 6 | -------------------------------------------------------------------------------- /maintenancemode/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-maintenancemode/dfbd5842c3df9b55c5f14efa7539b33749618cad/maintenancemode/management/__init__.py -------------------------------------------------------------------------------- /maintenancemode/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-maintenancemode/dfbd5842c3df9b55c5f14efa7539b33749618cad/maintenancemode/management/commands/__init__.py -------------------------------------------------------------------------------- /maintenancemode/management/commands/maintenance.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | 3 | from maintenancemode import utils as maintenance 4 | 5 | 6 | class Command(BaseCommand): 7 | opts = ("on", "off", "activate", "deactivate") 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument("command", nargs="?", help="|".join(self.opts)) 11 | 12 | def handle(self, *args, **options): 13 | command = options.get("command", args[0] if len(args) > 0 else None) 14 | verbosity = int(options.get("verbosity")) 15 | 16 | if command is not None: 17 | if command.lower() in ("on", "activate"): 18 | maintenance.activate() 19 | if verbosity > 0: 20 | self.stdout.write("Maintenance mode was activated successfully") 21 | elif command.lower() in ("off", "deactivate"): 22 | maintenance.deactivate() 23 | if verbosity > 0: 24 | self.stdout.write("Maintenance mode was deactivated successfully") 25 | 26 | if command not in self.opts: 27 | raise CommandError("Allowed commands are: %s" % "|".join(self.opts)) 28 | -------------------------------------------------------------------------------- /maintenancemode/middleware.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django import VERSION as django_version 3 | from django.conf import urls 4 | from django.urls import get_resolver 5 | from django.urls import resolvers 6 | from django.utils.deprecation import MiddlewareMixin 7 | 8 | from . import utils as maintenance 9 | from .conf import settings 10 | 11 | urls.handler503 = "maintenancemode.views.temporary_unavailable" 12 | urls.__all__.append("handler503") 13 | 14 | IGNORE_URLS = tuple(re.compile(u) for u in settings.MAINTENANCE_IGNORE_URLS) 15 | DJANGO_VERSION_MAJOR = django_version[0] 16 | DJANGO_VERSION_MINOR = django_version[1] 17 | 18 | class MaintenanceModeMiddleware(MiddlewareMixin): 19 | def process_request(self, request): 20 | # Allow access if middleware is not activated 21 | allow_staff = getattr(settings, "MAINTENANCE_ALLOW_STAFF", True) 22 | allow_superuser = getattr(settings, "MAINTENANCE_ALLOW_SUPERUSER", True) 23 | 24 | if not (settings.MAINTENANCE_MODE or maintenance.status()): 25 | return None 26 | 27 | INTERNAL_IPS = maintenance.IPList(settings.INTERNAL_IPS) 28 | 29 | # Preferentially check HTTP_X_FORWARDED_FOR b/c a proxy 30 | # server might have obscured REMOTE_ADDR 31 | for ip in request.headers.get('X-Forwarded-For', "").split(","): 32 | if ip.strip() in INTERNAL_IPS: 33 | return None 34 | 35 | # Allow access if remote ip is in INTERNAL_IPS 36 | if request.META.get("REMOTE_ADDR") in INTERNAL_IPS: 37 | return None 38 | 39 | # Allow access if the user doing the request is logged in and a 40 | # staff member. 41 | if hasattr(request, "user"): 42 | if allow_staff and request.user.is_staff: 43 | return None 44 | 45 | if allow_superuser and request.user.is_superuser: 46 | return None 47 | 48 | # Check if a path is explicitly excluded from maintenance mode 49 | for url in IGNORE_URLS: 50 | if url.match(request.path_info): 51 | return None 52 | # Otherwise show the user the 503 page 53 | 54 | if (DJANGO_VERSION_MAJOR == 3 and DJANGO_VERSION_MINOR < 2) or DJANGO_VERSION_MAJOR < 3: 55 | # Checks if DJANGO version is less than 3.2.0 for breaking change 56 | resolver = get_resolver() 57 | 58 | callback, param_dict = resolver.resolve_error_handler("503") 59 | 60 | return callback(request, **param_dict) 61 | 62 | else: 63 | # Default behaviour for django 3.2 and higher 64 | resolver = resolvers.get_resolver(None) 65 | resolve = resolver.resolve_error_handler 66 | callback = resolve('503') 67 | 68 | return callback(request) 69 | 70 | -------------------------------------------------------------------------------- /maintenancemode/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-maintenancemode/dfbd5842c3df9b55c5f14efa7539b33749618cad/maintenancemode/models.py -------------------------------------------------------------------------------- /maintenancemode/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-maintenancemode/dfbd5842c3df9b55c5f14efa7539b33749618cad/maintenancemode/tests/__init__.py -------------------------------------------------------------------------------- /maintenancemode/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy conftest.py for maintenancemode. 3 | If you don't know what this is for, just leave it empty. 4 | Read more about conftest.py under: 5 | https://pytest.org/latest/plugins.html 6 | """ 7 | 8 | import pytest 9 | -------------------------------------------------------------------------------- /maintenancemode/tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-maintenancemode/dfbd5842c3df9b55c5f14efa7539b33749618cad/maintenancemode/tests/models.py -------------------------------------------------------------------------------- /maintenancemode/tests/settings.py: -------------------------------------------------------------------------------- 1 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 2 | import os 3 | import re 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | SECRET_KEY = "DUMMY_SECRET_KEY" 8 | 9 | INTERNAL_IPS = [] 10 | 11 | # Application definition 12 | 13 | PROJECT_APPS = ["maintenancemode.tests", "maintenancemode"] 14 | 15 | INSTALLED_APPS = [ 16 | "django.contrib.admin", 17 | "django.contrib.auth", 18 | "django.contrib.contenttypes", 19 | "django.contrib.sessions", 20 | "django.contrib.messages", 21 | "django.contrib.sites", 22 | ] + PROJECT_APPS 23 | 24 | MIDDLEWARE = [ 25 | "django.contrib.sessions.middleware.SessionMiddleware", 26 | "django.contrib.auth.middleware.AuthenticationMiddleware", 27 | "django.contrib.messages.middleware.MessageMiddleware", 28 | "maintenancemode.middleware.MaintenanceModeMiddleware", 29 | ] 30 | 31 | TEMPLATES = [ 32 | { 33 | "BACKEND": "django.template.backends.django.DjangoTemplates", 34 | "DIRS": [os.path.join(BASE_DIR, "tests", "templates")], 35 | "APP_DIRS": True, 36 | "OPTIONS": { 37 | "context_processors": [ 38 | "django.template.context_processors.debug", 39 | "django.template.context_processors.request", 40 | "django.contrib.auth.context_processors.auth", 41 | "django.contrib.messages.context_processors.messages", 42 | ] 43 | }, 44 | }, 45 | ] 46 | 47 | ROOT_URLCONF = "maintenancemode.tests.urls" 48 | 49 | SITE_ID = 1 50 | 51 | # Database 52 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 53 | 54 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 55 | 56 | 57 | MAINTENANCE_IGNORE_URLS = (re.compile(r"^/ignored.*"),) 58 | -------------------------------------------------------------------------------- /maintenancemode/tests/templates/503.html: -------------------------------------------------------------------------------- 1 | Temporary unavailable 2 | 3 | You requested: {{ request_path }} -------------------------------------------------------------------------------- /maintenancemode/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.core import management 4 | 5 | try: 6 | from django.utils.six import StringIO 7 | except ImportError: 8 | from six import StringIO 9 | 10 | from django.contrib.auth.models import User 11 | from django.template import TemplateDoesNotExist 12 | from django.test import TestCase 13 | from django.test.client import Client 14 | from django.test.utils import override_settings 15 | 16 | from maintenancemode import utils 17 | 18 | @override_settings(ROOT_URLCONF="maintenancemode.tests.urls", MAINTENANCE_MODE=False) 19 | class MaintenanceModeMiddlewareTestCase(TestCase): 20 | 21 | def setUp(self): 22 | utils.deactivate() # make sure maintenance mode is off 23 | 24 | self.user = User.objects.create_user( 25 | username="maintenance", email="maintenance@example.org", password="password" 26 | ) 27 | 28 | def tearDown(self): 29 | self.user.delete() 30 | 31 | def test_default_middleware(self): 32 | # Middleware should default to being disabled 33 | response = self.client.get("/") 34 | self.assertContains(response, text="Rendered response page", count=1, status_code=200) 35 | 36 | def test_disabled_middleware(self): 37 | # Explicitly disabling the ``MAINTENANCE_MODE`` should work 38 | with self.settings(MAINTENANCE_MODE=False): 39 | response = self.client.get("/") 40 | self.assertContains(response, text="Rendered response page", count=1, status_code=200) 41 | 42 | def test_enabled_middleware_without_template(self): 43 | # Enabling the middleware without a proper 503 template should 44 | # raise a template error 45 | with self.settings(MAINTENANCE_MODE=True, TEMPLATES=[]): 46 | self.assertRaises(TemplateDoesNotExist, self.client.get, "/") 47 | 48 | def test_enabled_middleware_with_template(self): 49 | # Enabling the middleware having a ``503.html`` in any of the 50 | # template locations should return the rendered template" 51 | with self.settings(MAINTENANCE_MODE=True): 52 | response = self.client.get("/") 53 | self.assertContains(response, text="Temporary unavailable", count=1, status_code=503) 54 | self.assertContains(response, text="You requested: /", count=1, status_code=503) 55 | 56 | def test_middleware_with_non_staff_user(self): 57 | # A logged in user that is not a staff user should see the 503 message 58 | self.client.login(username="maintenance", password="password") 59 | 60 | with self.settings(MAINTENANCE_MODE=True): 61 | response = self.client.get("/") 62 | self.assertContains(response, text="Temporary unavailable", count=1, status_code=503) 63 | 64 | def test_middleware_with_staff_user(self): 65 | # A logged in user that _is_ a staff user should be able to 66 | # use the site normally 67 | User.objects.filter(pk=self.user.pk).update(is_staff=True) 68 | 69 | self.client.login(username="maintenance", password="password") 70 | 71 | with self.settings(MAINTENANCE_MODE=True): 72 | response = self.client.get("/") 73 | self.assertContains(response, text="Rendered response page", count=1, status_code=200) 74 | 75 | def test_middleware_with_staff_user_denied(self): 76 | # A logged in user that _is_ a staff user should be able to 77 | # use the site normally 78 | User.objects.filter(pk=self.user.pk).update(is_staff=True) 79 | 80 | self.client.login(username="maintenance", password="password") 81 | 82 | with self.settings(MAINTENANCE_MODE=True, MAINTENANCE_ALLOW_STAFF=False): 83 | response = self.client.get("/") 84 | self.assertContains(response, text="Temporary unavailable", count=1, status_code=503) 85 | 86 | def test_middleware_with_superuser_user_denied(self): 87 | # A logged in user that _is_ a staff user should be able to 88 | # use the site normally 89 | User.objects.filter(pk=self.user.pk).update(is_superuser=True) 90 | 91 | self.client.login(username="maintenance", password="password") 92 | 93 | with self.settings(MAINTENANCE_MODE=True, MAINTENANCE_ALLOW_SUPERUSER=False): 94 | response = self.client.get("/") 95 | self.assertContains(response, text="Temporary unavailable", count=1, status_code=503) 96 | 97 | def test_middleware_with_superuser_user_allowed(self): 98 | # A logged in user that _is_ a staff user should be able to 99 | # use the site normally 100 | User.objects.filter(pk=self.user.pk).update(is_superuser=True) 101 | 102 | self.client.login(username="maintenance", password="password") 103 | 104 | with self.settings(MAINTENANCE_MODE=True, MAINTENANCE_ALLOW_STAFF=False): 105 | response = self.client.get("/") 106 | self.assertContains(response, text="Rendered response page", count=1, status_code=200) 107 | 108 | def test_middleware_with_internal_ips(self): 109 | # A user that visits the site from an IP in ``INTERNAL_IPS`` 110 | # should be able to use the site normally 111 | 112 | # Use a new Client instance to be able to set the REMOTE_ADDR used by INTERNAL_IPS 113 | client = Client(REMOTE_ADDR="127.0.0.1") 114 | 115 | with self.settings(MAINTENANCE_MODE=True, INTERNAL_IPS=("127.0.0.1",)): 116 | response = client.get("/") 117 | self.assertContains(response, text="Rendered response page", count=1, status_code=200) 118 | 119 | def test_middleware_with_internal_ips_range(self): 120 | client = Client(REMOTE_ADDR="10.10.10.1") 121 | 122 | with self.settings(MAINTENANCE_MODE=True, INTERNAL_IPS=("10.10.10.0/24",)): 123 | response = client.get("/") 124 | self.assertContains(response, text="Rendered response page", count=1, status_code=200) 125 | 126 | def test_ignored_path(self): 127 | # A path is ignored when applying the maintanance mode and 128 | # should be reachable normally 129 | with self.settings(MAINTENANCE_MODE=True): 130 | # Note that we cannot override the settings here, since they are 131 | # ONLY used when the middleware starts up. 132 | # For this reason, MAINTENANCE_IGNORE_URLS is set in the base 133 | # settings file. 134 | response = self.client.get("/ignored/") 135 | self.assertContains(response, text="Rendered response page", count=1, status_code=200) 136 | 137 | def test_management_command(self): 138 | out = StringIO() 139 | # Explicitly disabling the ``MAINTENANCE_MODE`` 140 | with self.settings(MAINTENANCE_MODE=False): 141 | management.call_command("maintenance", "on", stdout=out) 142 | self.assertContains(self.client.get("/"), text="Temporary unavailable", count=1, status_code=503) 143 | 144 | management.call_command("maintenance", "off", stdout=out) 145 | self.assertContains(self.client.get("/"), text="Rendered response page", count=1, status_code=200) 146 | -------------------------------------------------------------------------------- /maintenancemode/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from django.http import HttpResponse 4 | 5 | urlpatterns = [ 6 | re_path("^$", lambda r: HttpResponse("Rendered response page"), name="test"), 7 | re_path("^ignored/$", lambda r: HttpResponse("Rendered response page"), name="test"), 8 | ] 9 | -------------------------------------------------------------------------------- /maintenancemode/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .conf import settings 4 | 5 | 6 | class IPList(list): 7 | """Stolen from https://djangosnippets.org/snippets/1362/""" 8 | 9 | def __init__(self, ips): 10 | try: 11 | from IPy import IP 12 | 13 | for ip in ips: 14 | self.append(IP(ip)) 15 | except ImportError: 16 | pass 17 | 18 | def __contains__(self, ip): 19 | try: 20 | for net in self: 21 | if ip in net: 22 | return True 23 | except: # noqa 24 | pass 25 | return False 26 | 27 | 28 | def activate(): 29 | try: 30 | open(settings.MAINTENANCE_LOCKFILE_PATH, "ab", 0).close() 31 | except OSError: 32 | pass # shit happens 33 | 34 | 35 | def deactivate(): 36 | if os.path.isfile(settings.MAINTENANCE_LOCKFILE_PATH): 37 | os.remove(settings.MAINTENANCE_LOCKFILE_PATH) 38 | 39 | 40 | def status(): 41 | return settings.MAINTENANCE_MODE or os.path.isfile(settings.MAINTENANCE_LOCKFILE_PATH) 42 | -------------------------------------------------------------------------------- /maintenancemode/views.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | if django.get_version() >= "1.8": 4 | from django.template.loader import render_to_string 5 | else: 6 | from django.template import RequestContext, loader 7 | 8 | def render_to_string(template_name, context=None, request=None): 9 | context_instance = RequestContext(request) if request else None 10 | 11 | return loader.render_to_string(template_name, context, context_instance) 12 | 13 | 14 | from . import http 15 | 16 | 17 | def temporary_unavailable(request, template_name="503.html"): 18 | """ 19 | Default 503 handler, which looks for the requested URL in the 20 | redirects table, redirects if found, and displays 404 page if not 21 | redirected. 22 | 23 | Templates: ``503.html`` 24 | Context: 25 | request_path 26 | The path of the requested URL (e.g., '/app/pages/bad_page/') 27 | 28 | """ 29 | context = { 30 | "request_path": request.path, 31 | } 32 | return http.HttpResponseTemporaryUnavailable(render_to_string(template_name, context)) 33 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-maintenancemode 3 | version = 0.11.8 4 | description = django-maintenancemode allows you to temporary shutdown your site for maintenance work 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | author = Remco Wendt 8 | author_email = remco@maykinmedia.nl 9 | maintainer = Basil Shubin 10 | maintainer_email = basil.shubin@gmail.com 11 | url = https://github.com/shanx/django-maintenancemode 12 | download_url = https://github.com/shanx/django-maintenancemode/zipball/master 13 | license = BSD License 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Environment :: Web Environment 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: BSD License 19 | Operating System :: OS Independent 20 | Programming Language :: Python 21 | Programming Language :: Python :: 3 :: Only 22 | Programming Language :: Python :: 3.6 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Framework :: Django 27 | Framework :: Django :: 2.2 28 | Framework :: Django :: 3.0 29 | Framework :: Django :: 3.1 30 | Framework :: Django :: 3.2 31 | 32 | [options] 33 | zip_safe = False 34 | include_package_data = True 35 | packages = find: 36 | install_requires = 37 | django-appconf 38 | six>=1.9.0 39 | ipy 40 | 41 | [options.packages.find] 42 | exclude = example* 43 | 44 | [options.extras_require] 45 | develop = 46 | tox 47 | django 48 | pytest-django 49 | pytest 50 | test = 51 | pytest-django 52 | pytest-cov 53 | pytest 54 | 55 | [bdist_wheel] 56 | # No longer universal (Python 3 only) but leaving this section in here will 57 | # trigger zest to build a wheel. 58 | universal = 0 59 | 60 | [flake8] 61 | # Some sane defaults for the code style checker flake8 62 | # black compatibility 63 | max-line-length = 88 64 | # E203 and W503 have edge cases handled by black 65 | extend-ignore = E203, W503 66 | exclude = 67 | .tox 68 | build 69 | dist 70 | .eggs 71 | 72 | [tool:pytest] 73 | DJANGO_SETTINGS_MODULE = maintenancemode.tests.settings 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | distribute = False 3 | envlist = 4 | py{36,37,38,39}-dj{22,30,31,32} 5 | skip_missing_interpreters = True 6 | 7 | [travis] 8 | python = 9 | 3.6: py36 10 | 3.7: py37 11 | 3.8: py38 12 | 3.9: py39 13 | 14 | [testenv] 15 | usedevelop = True 16 | extras = test 17 | setenv = 18 | DJANGO_SETTINGS_MODULE = maintenancemode.tests.settings 19 | deps = 20 | dj22: Django>=2.2,<3.0 21 | dj30: Django>=3.0,<3.1 22 | dj31: Django>=3.1,<3.2 23 | dj32: Django>=3.2,<4.0 24 | commands = pytest --cov --cov-append --cov-report= 25 | --------------------------------------------------------------------------------