├── testproject ├── app │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── urls.py │ └── tests.py ├── testproject │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── templates │ ├── 503.html │ ├── home.html │ ├── layouts │ │ └── base.html │ └── ignored.html └── manage.py ├── maintenancemode ├── conf │ ├── __init__.py │ └── urls │ │ ├── __init__.py │ │ └── defaults.py ├── utils │ ├── __init__.py │ └── settings.py ├── views │ ├── __init__.py │ └── defaults.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── setmaintenance.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20170920_1455.py │ ├── 0003_auto_20180227_0331.py │ └── 0001_initial.py ├── http.py ├── __init__.py ├── admin.py ├── models.py └── middleware.py ├── AUTHORS ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── setup.py ├── CHANGES ├── README.md ├── README.rst └── LICENSE /testproject/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testproject/app/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/conf/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testproject/testproject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancemode/conf/urls/defaults.py: -------------------------------------------------------------------------------- 1 | __all__ = ['handler503'] 2 | 3 | handler503 = 'maintenancemode.views.defaults.temporary_unavailable' 4 | -------------------------------------------------------------------------------- /maintenancemode/http.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | class HttpResponseTemporaryUnavailable(HttpResponse): 5 | status_code = 503 6 | -------------------------------------------------------------------------------- /testproject/templates/503.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% block title %}Temporary unavailable{% endblock title %} 4 | 5 | {% block body %} 6 |

Temporarily unavailable

7 | {% endblock body %} 8 | -------------------------------------------------------------------------------- /testproject/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% block title %}Home{% endblock title %} 4 | 5 | {% block body %} 6 |

Welcome

7 |

This is the home view.

8 | {% endblock body %} 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | django-maintenancemode originally written by Remco Wendt 2 | 3 | This database-backed fork written by Brandon Taylor 4 | with contributions from Bruno Desthuilliers -------------------------------------------------------------------------------- /testproject/app/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class HomeView(TemplateView): 5 | template_name = "home.html" 6 | 7 | 8 | class IgnoredView(TemplateView): 9 | template_name = "ignored.html" 10 | -------------------------------------------------------------------------------- /testproject/templates/layouts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock title %} 5 | 6 | 7 | {% block body %}{% endblock body %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /testproject/templates/ignored.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html' %} 2 | 3 | {% block title %}A view you can still see!{% endblock title %} 4 | 5 | {% block body %} 6 |

Ignored

7 |

This view is ignore when the site is in maintenance mode.

8 | {% endblock body %} 9 | -------------------------------------------------------------------------------- /testproject/app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import views 4 | 5 | app_name = 'app' 6 | 7 | urlpatterns = [ 8 | re_path(r'^ignored-page/$', views.IgnoredView.as_view(), name='ignored'), 9 | re_path(r'^$', views.HomeView.as_view(), name='home'), 10 | ] 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGES 3 | include LICENSE 4 | include README.rst 5 | recursive-include maintenancemode/conf * 6 | recursive-include maintenancemode/south_migrations * 7 | recursive-include maintenancemode/utils * 8 | recursive-include maintenancemode/views * 9 | prune testproject 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.pyo 4 | *.swp 5 | *.orig 6 | *.kpf 7 | *.egg-info 8 | .project 9 | .pydevproject 10 | .komodoproject 11 | .DS_Store 12 | MANIFEST 13 | dist 14 | build 15 | parts/* 16 | eggs/* 17 | downloads/* 18 | .installed.cfg 19 | bin/* 20 | develop-eggs/*.egg-link 21 | testproject/db.sqlite3 22 | .settings 23 | .settings/*.project 24 | .pydevproject 25 | .project 26 | .vscode 27 | settings.json 28 | -------------------------------------------------------------------------------- /maintenancemode/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (2, 0, 0) # following PEP 386 2 | DEV_N = None 3 | 4 | 5 | def get_version(): 6 | version = '{0}.{1}'.format(VERSION[0], VERSION[1]) 7 | if VERSION[2]: 8 | version = '{0}.{1}'.format(version, VERSION[2]) 9 | try: 10 | if VERSION[3]: 11 | version = '{0}.{1}'.format(version, VERSION[3]) 12 | except IndexError: 13 | pass 14 | return version 15 | 16 | 17 | __version__ = get_version() 18 | -------------------------------------------------------------------------------- /testproject/testproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testproject 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/1.8/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", "testproject.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /testproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | # Adds the maintenancemode package from the cloned repository instead of 6 | # site_packages 7 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 8 | 9 | if __name__ == "__main__": 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") 11 | 12 | from django.core.management import execute_from_command_line 13 | 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /maintenancemode/migrations/0002_auto_20170920_1455.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-09-20 12:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('maintenancemode', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='maintenance', 18 | name='site', 19 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='sites.Site'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | 9 | env: 10 | - DJANGO_VERSION=1.7.7 TESTPROJECT=testproject 11 | - DJANGO_VERSION=1.8.4 TESTPROJECT=testproject 12 | - DJANGO_VERSION=1.9 TESTPROJECT=testproject 13 | - DJANGO_VERSION=1.10 TESTPROJECT=testproject 14 | - DJANGO_VERSION=1.11 TESTPROJECT=testproject 15 | 16 | branches: 17 | only: 18 | - develop 19 | 20 | matrix: 21 | exclude: 22 | 23 | - 24 | python: "3.5" 25 | env: DJANGO_VERSION=1.7.7 TESTPROJECT=testproject 26 | 27 | install: 28 | - pip install django==$DJANGO_VERSION 29 | 30 | script: 31 | - cd $TESTPROJECT 32 | - python manage.py test 33 | -------------------------------------------------------------------------------- /maintenancemode/migrations/0003_auto_20180227_0331.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-02-27 03:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('maintenancemode', '0002_auto_20170920_1455'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='ignoredurl', 17 | name='description', 18 | field=models.CharField(help_text='What this URL pattern covers.', max_length=75), 19 | ), 20 | migrations.AlterField( 21 | model_name='maintenance', 22 | name='is_being_performed', 23 | field=models.BooleanField(default=False, verbose_name='In Maintenance Mode'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /maintenancemode/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.sites.models import Site 3 | 4 | from maintenancemode.models import Maintenance, IgnoredURL, populate 5 | 6 | 7 | class IgnoredURLInline(admin.TabularInline): 8 | model = IgnoredURL 9 | extra = 3 10 | 11 | 12 | class MaintenanceAdmin(admin.ModelAdmin): 13 | inlines = [IgnoredURLInline, ] 14 | list_display = ['__str__', 'is_being_performed'] 15 | readonly_fields = ('site',) 16 | actions = None 17 | 18 | def get_queryset(self, request): 19 | # creates missing maintenances if necessary 20 | populate() 21 | return super(MaintenanceAdmin, self).get_queryset(request) 22 | 23 | def has_delete_permission(self, request, obj=None): 24 | return False 25 | 26 | def has_add_permission(self, request): 27 | return False 28 | 29 | 30 | admin.site.register(Maintenance, MaintenanceAdmin) 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | try: 4 | README = open('README.rst').read() 5 | except Exception: 6 | README = None 7 | 8 | setup( 9 | author='Brandon Taylor', 10 | author_email='alsoicode@gmail.com', 11 | classifiers=['Development Status :: 5 - Production/Stable', 12 | 'Environment :: Web Environment', 13 | 'Framework :: Django', 14 | 'Intended Audience :: Developers', 15 | 'License :: OSI Approved :: Apache Software License', 16 | 'Operating System :: OS Independent', 17 | 'Programming Language :: Python', 18 | 'Topic :: Utilities'], 19 | description='Database-driven way to put your Django site into maintenance mode.', 20 | include_package_data=True, 21 | install_requires=['django'], 22 | license='APL', 23 | long_description=README, 24 | name='django-maintenancemode-2', 25 | packages=find_packages(exclude=['testproject']), 26 | url='https://github.com/alsoicode/django-maintenancemode-2', 27 | version=__import__('maintenancemode').__version__, 28 | zip_safe=False 29 | ) 30 | -------------------------------------------------------------------------------- /testproject/testproject/urls.py: -------------------------------------------------------------------------------- 1 | """testproject URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/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: url(r'^$', 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: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from distutils.version import StrictVersion 17 | from django.conf.urls import include 18 | from django.contrib import admin 19 | from maintenancemode.utils.settings import DJANGO_VERSION 20 | 21 | if DJANGO_VERSION >= StrictVersion('2.0'): 22 | from django.urls import path 23 | urlpatterns = [ 24 | path(r'admin/', admin.site.urls), 25 | path(r'', include('app.urls')), 26 | ] 27 | 28 | else: 29 | from django.conf.urls import url 30 | urlpatterns = [ 31 | url(r'^admin/', include(admin.site.urls)), 32 | url(r'^', include('app.urls', namespace='app')), 33 | ] 34 | -------------------------------------------------------------------------------- /maintenancemode/views/defaults.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.template import loader, RequestContext 3 | 4 | from django import VERSION as DJANGO_VERSION 5 | from maintenancemode.http import HttpResponseTemporaryUnavailable 6 | from maintenancemode.utils.settings import MAINTENANCE_503_TEMPLATE 7 | 8 | 9 | def temporary_unavailable(request, template_name=MAINTENANCE_503_TEMPLATE): 10 | """ 11 | Default 503 handler 12 | """ 13 | # let's be kind to json api calls... 14 | # NB we specify that we're down for maintenance 15 | # so a plain 16 | if request.META.get("CONTENT_TYPE") == "application/json": 17 | content = json.dumps({ 18 | "code": 503, 19 | "error": "temporarily_unavailable", 20 | "reason": "maintenance", 21 | "error_description": "Sorry, the service is temporarily down for maintenance" 22 | }) 23 | content_type = "application/json" 24 | else: 25 | args = [template_name, {'request_path': request.path}] 26 | if DJANGO_VERSION < (1, 10, 0): 27 | args.append(RequestContext(request)) 28 | content = loader.render_to_string(*args) 29 | content_type = "text/html" 30 | 31 | return HttpResponseTemporaryUnavailable(content, content_type=content_type) 32 | -------------------------------------------------------------------------------- /maintenancemode/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | from django.db import IntegrityError 6 | from django.contrib.sites.models import Site 7 | 8 | 9 | class Maintenance(models.Model): 10 | site = models.OneToOneField(Site, on_delete=models.CASCADE) 11 | is_being_performed = models.BooleanField( 12 | _('In Maintenance Mode'), default=False 13 | ) 14 | 15 | class Meta: 16 | verbose_name = verbose_name_plural = _('Maintenance Mode') 17 | 18 | def __str__(self): 19 | return self.site.domain 20 | 21 | def ignored_url_patterns(self): 22 | qs = self.ignoredurl_set.values_list( 23 | "pattern", flat=True 24 | ) 25 | return list(qs) 26 | 27 | 28 | class IgnoredURL(models.Model): 29 | maintenance = models.ForeignKey(Maintenance, on_delete=models.CASCADE) 30 | pattern = models.CharField(max_length=255) 31 | description = models.CharField( 32 | max_length=75, help_text=_('What this URL pattern covers.') 33 | ) 34 | 35 | def __str__(self): 36 | return self.pattern 37 | 38 | 39 | def populate(): 40 | """ 41 | creates Maintenance objects for all sites (if necessary) 42 | """ 43 | for site in Site.objects.all(): 44 | try: 45 | Maintenance.objects.get_or_create(site=site) 46 | except IntegrityError as e: 47 | # MySQL can be a bit annoying sometimes 48 | pass 49 | 50 | -------------------------------------------------------------------------------- /maintenancemode/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('sites', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='IgnoredURL', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('pattern', models.CharField(max_length=255)), 19 | ('description', models.CharField(help_text=b'What this URL pattern covers.', max_length=75)), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='Maintenance', 24 | fields=[ 25 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 26 | ('is_being_performed', models.BooleanField(default=False, verbose_name=b'In Maintenance Mode')), 27 | ('site', models.ForeignKey(to='sites.Site', on_delete=models.CASCADE)), 28 | ], 29 | options={ 30 | 'verbose_name': 'Maintenance Mode', 31 | 'verbose_name_plural': 'Maintenance Mode', 32 | }, 33 | ), 34 | migrations.AddField( 35 | model_name='ignoredurl', 36 | name='maintenance', 37 | field=models.ForeignKey(to='maintenancemode.Maintenance', on_delete=models.CASCADE), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changes 2 | == == == = 3 | 4 | 1.1.8 5 | - - - - - 6 | - Added Python 3.6 support. 7 | - Added Django 1.11 support. 8 | - Documentation updates. 9 | - Syntax improvements. 10 | 11 | 1.1.7 12 | - - - - - 13 | Added `setmaintenance` management command. 14 | 15 | 1.1.6 16 | - - - - - 17 | Added Django 1.10 support. 18 | 19 | 1.1.5 20 | - - - - - 21 | Simplified admin ignore pattern settings. 22 | 23 | 1.1.4 24 | - - - - - 25 | Fixed logging into Django admin when maintenance mode is enabled. 26 | 27 | 1.1.3 28 | - - - - - 29 | Converstion of README.md to .rst for better compatibility with PyPi 30 | 31 | 1.1.2 32 | - - - - - 33 | - Documentation updates. 34 | - Added setting for 503.html template path 35 | 36 | 1.1.1 37 | - - - - - 38 | - Deleted 39 | 40 | 1.1.0 41 | - - - - - 42 | - Updated testproject 43 | - Django 1.8 compatibility. 44 | 45 | 0.9.3 46 | - - - - - 47 | - Moved maintenance mode and ignored url patterns from settings.py to database backed storage. 48 | 49 | 50 | 0.9.2 51 | - - - - - 52 | - Fixed an issue with setuptools, thanks for reporting this ksato9700 53 | 54 | 0.9.1 55 | - - - - - 56 | 57 | - Tested django - maintenancemode with django - 1.0 release (following the 1.0.X release branch) 58 | - Bundled buildout.cfg and bootstrap with the source version of the project, allowing repeatable buildout 59 | - The middleware now uses its own default config file, thanks to a patch by semente 60 | - Use INTERNAL_IPS to check for users that need access. user.is_staff will stay in place for backwards incompatibility. Thanks for the idea Joshua Works 61 | - Have setup.py sdist only distribute maintenancemode itself, no longer distribute tests and buildout stuff 62 | - Use README and CHANGES in setup.py's long_description, stolen from Jeroen's djangorecipe :) 63 | - Updated the documentation and now use pypi as the documentation source (link there from google code) 64 | 65 | 0.9 66 | - - - - - 67 | 68 | First release 69 | -------------------------------------------------------------------------------- /maintenancemode/middleware.py: -------------------------------------------------------------------------------- 1 | import re 2 | from distutils.version import StrictVersion 3 | 4 | import django.conf.urls as urls 5 | from django.conf import settings 6 | from django.contrib.sites.models import Site 7 | from django.urls import resolvers 8 | from django.utils import deprecation 9 | 10 | from maintenancemode.models import Maintenance 11 | from maintenancemode.utils.settings import ( 12 | DJANGO_VERSION, MAINTENANCE_ADMIN_IGNORED_URLS, MAINTENANCE_BLOCK_STAFF) 13 | 14 | urls.handler503 = 'maintenancemode.views.defaults.temporary_unavailable' 15 | urls.__all__.append('handler503') 16 | 17 | _base = deprecation.MiddlewareMixin if DJANGO_VERSION >= StrictVersion('1.10.0') else object 18 | 19 | 20 | class MaintenanceModeMiddleware(_base): 21 | def process_request(self, request): 22 | """ 23 | Check if the current site is in maintenance. 24 | """ 25 | 26 | # First check things that don't require a database access: 27 | 28 | # Allow access if remote ip is in INTERNAL_IPS 29 | if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: 30 | return None 31 | 32 | # Check if the staff the user is allowed 33 | if hasattr(request, 'user'): 34 | if request.user.is_superuser: 35 | return None 36 | 37 | if not MAINTENANCE_BLOCK_STAFF and request.user.is_staff: 38 | return None 39 | 40 | # ok let's look at the db 41 | site = Site.objects.get_current() 42 | try: 43 | maintenance = Maintenance.objects.get(site=site) 44 | except Maintenance.DoesNotExist: 45 | # Allow access if no matching Maintenance object exists 46 | return None 47 | 48 | # Allow access if maintenance is not being performed 49 | if not maintenance.is_being_performed: 50 | return None 51 | 52 | # Check if a path is explicitly excluded from maintenance mode 53 | ignored_url_list = set( 54 | maintenance.ignored_url_patterns() + MAINTENANCE_ADMIN_IGNORED_URLS 55 | ) 56 | 57 | ignored_url_patterns = tuple( 58 | re.compile(r'{}'.format(url)) for url in ignored_url_list 59 | ) 60 | 61 | request_path = request.path_info.lstrip("/") 62 | 63 | for url in ignored_url_patterns: 64 | if url.match(request_path): 65 | return None 66 | 67 | # Otherwise show the user the 503 page 68 | resolver = resolvers.get_resolver(None) 69 | 70 | resolve = resolver.resolve_error_handler 71 | callback = resolve('503') 72 | return callback(request) 73 | -------------------------------------------------------------------------------- /maintenancemode/management/commands/setmaintenance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django import VERSION as DJANGO_VERSION 3 | from django.conf import settings 4 | from django.core.management import BaseCommand, CommandError 5 | from django.contrib.sites.models import Site 6 | from maintenancemode.models import Maintenance 7 | 8 | 9 | class Command(BaseCommand): 10 | help = ( 11 | "Set maintenance-mode on or off for ." 12 | " If no site_id given, will use settings.SITE_ID" 13 | ) 14 | 15 | args = "status [site_id [site_id]*]" 16 | 17 | status_map = {"on": True, "off": False} 18 | status_rmap = {v: k for k, v in status_map.items()} 19 | 20 | if DJANGO_VERSION >= (1, 8, 0): 21 | def add_arguments(self, parser): 22 | parser.add_argument( 23 | "status", 24 | choices=("on", "off"), 25 | help=" (on/off)" 26 | ) 27 | 28 | parser.add_argument( 29 | "args", 30 | nargs="*", 31 | type=int, 32 | help="*" 33 | ) 34 | 35 | parser.add_argument( 36 | "-i", "--ignore-missing", 37 | action="store_true", 38 | # type=bool, 39 | default=False, 40 | help="ignore any inexistant site_id (default %(default)s)" 41 | ) 42 | 43 | else: 44 | # pre 1.8 compat using optparse 45 | # here we have to manually get 46 | # `status` from `*args` and set it as 47 | # `options['status']` 48 | from optparse import make_option 49 | 50 | option_list = BaseCommand.option_list + ( 51 | make_option( 52 | "-i", "--ignore-missing", 53 | action="store_true", 54 | # type=bool, 55 | default=False, 56 | help="ignore any inexistant site_id (default False)" 57 | ) 58 | ) 59 | 60 | def _compat_parse_args(self, args, options): 61 | if DJANGO_VERSION < (1, 8, 0): 62 | try: 63 | status, args = args[0], args[1:] 64 | except IndexError: 65 | raise CommandError("missing (on / off) argument") 66 | if status not in self.status_map: 67 | raise CommandError( 68 | "invalid argument '{}'" 69 | " (should be either 'on' or 'off')".format(status) 70 | ) 71 | options["status"] = status 72 | 73 | return args, options 74 | 75 | def handle(self, *args, **options): 76 | args, options = self._compat_parse_args(args, options) 77 | 78 | # print "args : {}".format(args) 79 | # print "options : {}".format(options) 80 | 81 | site_ids = args or (settings.SITE_ID, ) 82 | known_sites = Site.objects.all() 83 | missings = \ 84 | set(site_ids) - set(known_sites.values_list("id", flat=True)) 85 | 86 | if missings and not options["ignore_missing"]: 87 | raise CommandError( 88 | "Unknown site ids: {}".format(" ".join(map(str, missings))) 89 | ) 90 | 91 | status = self.status_map[options["status"]] 92 | for site_id in site_ids: 93 | # handle the case of --ignore-missing 94 | if site_id in missings: 95 | if options["verbosity"] >= 1: 96 | self.stderr.write( 97 | "unknown site {} - skipping\n".format(site_id) 98 | ) 99 | continue 100 | 101 | m, created = Maintenance.objects.get_or_create(site_id=site_id) 102 | 103 | if m.is_being_performed == status: 104 | if options["verbosity"] >= 1: 105 | self.stderr.write( 106 | "Site {site_id} ({site}) already {status}\n".format( 107 | site_id=site_id, 108 | site=m.site, 109 | status=self.status_rmap[status] 110 | ) 111 | ) 112 | continue 113 | 114 | m.is_being_performed = status 115 | m.save() 116 | if options["verbosity"] >= 1: 117 | self.stderr.write( 118 | "Site {site_id} ({site}) is now {status}\n".format( 119 | site_id=site_id, 120 | site=m.site, 121 | status=self.status_rmap[status] 122 | ) 123 | ) 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-maintenancemode-2 2 | 3 | [![Build Status](https://travis-ci.org/alsoicode/django-maintenancemode-2.svg)](https://travis-ci.org/alsoicode/django-maintenancemode-2) 4 | 5 | Current Version: 2.0.0 6 | 7 | This project makes it easy to put your Django site into "maintenance mode", or more technically, return an HTTP 503 response. 8 | 9 | This project differs slightly from other implementations in that the maintenance mode flag is stored in your database versus settings or an environment variable. If your site is deployed to multiple servers, the centralized database-based maintenance flag makes it a snap to bring them all up or down at once. 10 | 11 | ## Requirements 12 | - [django](https://www.djangoproject.com/download/) 13 | - [django.contrib.sites](https://docs.djangoproject.com/en/1.11/ref/contrib/sites/) 14 | 15 | ## Pre-Requisites 16 | You must have at least one Site entry in your database **before** installing django-maintenancemode-2. 17 | 18 | ## Supported Python Versions 19 | - 2.7, 3.x 20 | 21 | ## Supported Django Versions 22 | - 4.x use the latest version 23 | - 2.x >= 3.x, please use version 1.3.1 24 | - < 2, please use version 1.1.9 25 | 26 | ## Installation 27 | 1. `pip install django-maintenancemode-2` 28 | 29 | -- or -- 30 | 31 | 1. Download django-maintenancemode-2 from [source](https://github.com/alsoicode/django-maintenancemode-2/archive/master.zip) 32 | 2. *optional: Enable a virtualenv 33 | 3. Run `python setup.py install` or add `maintenancemode` to your PYTHONPATH 34 | 35 | ## Settings and Required Values 36 | - Ensure the [Sites Framework](https://docs.djangoproject.com/en/1.11/ref/contrib/sites/) is enabled, and you have at least one entry in the Sites table. 37 | - Add `maintenancemode.middleware.MaintenanceModeMiddleware` to your `MIDDLEWARE_CLASSES` 38 | - Add `maintenancemode` to your `INSTALLED_APPS` 39 | - Run `python manage.py migrate` to create the `maintenancemode` tables. 40 | - Run your project to automatically add the `maintenancemode` database records. 41 | - Add a 503.html template to the root of your templates directory, or optionally add a `MAINTENANCE_503_TEMPLATE` path to your 503.html file's location in settings. 42 | - `maintenancemode` will ignore any patterns beginning with the default Django Admin url: `^admin` so you can turn it off. If you use a custom url for admin, you may override the ignored admin patterns by adding the `MAINTENANCE_ADMIN_IGNORED_URLS` list in settings. Example: `['^my-custom-admin', '^my-other-custom-admin']` 43 | - You can also block staff users, who by default are ignored by maintenance mode, by setting `MAINTENANCE_BLOCK_STAFF` to `True` 44 | 45 | ## Usage 46 | 47 | ![Image of django-maintenancemode-2](http://res.cloudinary.com/alsoicode/image/upload/v1449537052/django-maintenancemode-2/maintenancemode.jpg) 48 | 49 | ### Turning Maintenance Mode **On** 50 | To put a site into "Maintenance Mode", check the "In Maintenance Mode" checkbox and save in Django Admin under the "Maintenancemode" section. The next time you visit the public side of the site, it will return a 503 if: 51 | 52 | - You are not logged in as a superuser or staff user 53 | - You are not viewing a URL in the ignored patterns list 54 | - Your `REMOTE_ADDR` does not appear in the `INTERNAL_IPS` setting 55 | 56 | 57 | Or you can alternatively use the `setmaintenance` management command: 58 | 59 | ``` 60 | # sets maintenance on for the current settings.SITE_ID 61 | ./manage.py setmaintenance on 62 | 63 | # sets maintenance on for sites 2 and 3 64 | ./manage.py setmaintenance on 2 3 65 | ``` 66 | 67 | which can be useful for `fabric` deployment scripts etc. 68 | 69 | ### Turning Maintenance Mode **Off** 70 | Log in, un-check the "In Maintenance Mode" checkbox and save. 71 | 72 | Or you can alternatively use the `setmaintenance` management command: 73 | 74 | ``` 75 | # sets maintenance off for the current settings.SITE_ID 76 | $ ./manage.py setmaintenance off 77 | 78 | # sets maintenance off for sites 2 and 3 79 | $ ./manage.py setmaintenance off 2 3 80 | ``` 81 | 82 | ## Testing and Sample Application 83 | A "testproject" application is included, which also contains unit and functional tests you can run via `python manage.py test` from the `testproject` directory. 84 | 85 | You will need to run `manage.py migrate` to create the test project database. 86 | 87 | There are only two views in the testproject: 88 | - / 89 | - /ignored-page 90 | 91 | To see `maintenancemode` in action, log into Django admin, and set the maintenance mode to true. Log out, then visit the home page, and instead, you'll be greeted with the maintenance page. 92 | 93 | To have `maintenancemode` ignore the "ignored-page" view, simply add it's url pattern to the Ignored URLs as: 94 | 95 | ^ignored-page/$ 96 | 97 | Now you should be able to visit the `ignored-page` view regardless of the maintenancemode status. This is useful for contact or help pages you still want people to be able to access while you're working on other parts of the site. 98 | 99 | ### Database migrations 100 | `./manage.py migrate` should add the necessary tables. 101 | -------------------------------------------------------------------------------- /maintenancemode/utils/settings.py: -------------------------------------------------------------------------------- 1 | from inspect import getmembers 2 | 3 | from django import get_version 4 | from django.conf import settings 5 | 6 | from distutils.version import StrictVersion 7 | DJANGO_VERSION = StrictVersion(get_version()) 8 | MAINTENANCE_503_TEMPLATE = getattr(settings, 9 | 'MAINTENANCE_503_TEMPLATE', 10 | '503.html') 11 | MAINTENANCE_ADMIN_IGNORED_URLS = getattr(settings, 12 | 'MAINTENANCE_ADMIN_IGNORED_URLS', 13 | ['^admin']) 14 | MAINTENANCE_BLOCK_STAFF = getattr(settings, 'MAINTENANCE_BLOCK_STAFF', False) 15 | 16 | 17 | class AppSettings(object): 18 | """ 19 | An app setting object to be used for handling app setting defaults 20 | gracefully and providing a nice API for them. Say you have an app 21 | called ``myapp`` and want to define a few defaults, and refer to the 22 | defaults easily in the apps code. Add a ``settings.py`` to your app:: 23 | 24 | from path.to.utils import AppSettings 25 | 26 | class MyAppSettings(AppSettings): 27 | SETTING_1 = "one" 28 | SETTING_2 = ( 29 | "two", 30 | ) 31 | 32 | Then initialize the setting with the correct prefix in the location of 33 | of your choice, e.g. ``conf.py`` of the app module:: 34 | 35 | settings = MyAppSettings(prefix="MYAPP") 36 | 37 | The ``MyAppSettings`` instance will automatically look at Django's 38 | global setting to determine each of the settings and respect the 39 | provided ``prefix``. E.g. adding this to your site's ``settings.py`` 40 | will set the ``SETTING_1`` setting accordingly:: 41 | 42 | MYAPP_SETTING_1 = "uno" 43 | 44 | Usage 45 | ----- 46 | 47 | Instead of using ``from django.conf import settings`` as you would 48 | usually do, you can switch to using your apps own settings module 49 | to access the app settings:: 50 | 51 | from myapp.conf import settings 52 | 53 | print myapp_settings.MYAPP_SETTING_1 54 | 55 | ``AppSettings`` instances also work as pass-throughs for other 56 | global settings that aren't related to the app. For example the 57 | following code is perfectly valid:: 58 | 59 | from myapp.conf import settings 60 | 61 | if "myapp" in settings.INSTALLED_APPS: 62 | print "yay, myapp is installed!" 63 | 64 | Custom handling 65 | --------------- 66 | 67 | Each of the settings can be individually configured with callbacks. 68 | For example, in case a value of a setting depends on other settings 69 | or other dependencies. The following example sets one setting to a 70 | different value depending on a global setting:: 71 | 72 | from django.conf import settings 73 | 74 | class MyCustomAppSettings(AppSettings): 75 | ENABLED = True 76 | 77 | def configure_enabled(self, value): 78 | return value and not self.DEBUG 79 | 80 | custom_settings = MyCustomAppSettings("MYAPP") 81 | 82 | The value of ``custom_settings.MYAPP_ENABLED`` will vary depending on the 83 | value of the global ``DEBUG`` setting. 84 | 85 | Each of the app settings can be customized by providing 86 | a method ``configure_`` that takes the default 87 | value as defined in the class attributes as the only parameter. 88 | The method needs to return the value to be use for the setting in 89 | question. 90 | """ 91 | def __dir__(self): 92 | return sorted(list(set(self.__dict__.keys() + dir(settings)))) 93 | 94 | __members__ = lambda self: self.__dir__() 95 | 96 | def __getattr__(self, name): 97 | if name.startswith(self._prefix): 98 | raise AttributeError( 99 | "{0} object has no attribute {1}".format( 100 | (self.__class__.__name__, name) 101 | ) 102 | ) 103 | return getattr(settings, name) 104 | 105 | def __setattr__(self, name, value): 106 | super(AppSettings, self).__setattr__(name, value) 107 | if name in dir(settings): 108 | setattr(settings, name, value) 109 | 110 | def __init__(self, prefix): 111 | super(AppSettings, self).__setattr__('_prefix', prefix) 112 | for setting, class_value in getmembers(self.__class__): 113 | if setting == setting.upper(): 114 | prefixed = "{0}_{1}".format(prefix.upper(), setting.upper()) 115 | configured_value = getattr(settings, prefixed, class_value) 116 | callback_name = "configure_{}".format(setting.lower()) 117 | callback = getattr(self, callback_name, None) 118 | if callable(callback): 119 | configured_value = callback(configured_value) 120 | delattr(self.__class__, setting) 121 | setattr(self, prefixed, configured_value) 122 | -------------------------------------------------------------------------------- /testproject/testproject/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for testproject project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import django 15 | import os 16 | 17 | DJANGO_MAJOR_VERSION = django.VERSION[0] 18 | DJANGO_MINOR_VERSION = django.VERSION[1] 19 | 20 | # Optionally, specify a custom 503 template path 21 | # MAINTENANCE_503_TEMPLATE = 'errors/503.html' 22 | 23 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 24 | 25 | # Quick-start development settings - unsuitable for production 26 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 27 | 28 | # SECURITY WARNING: keep the secret key used in production secret! 29 | SECRET_KEY = 'k=t$%@*+@=5c57g&f^&8$)evztgb1%b4l23zt*!2e-1))3@vue' 30 | 31 | # SECURITY WARNING: don't run with debug turned on in production! 32 | DEBUG = True 33 | 34 | SITE_ID = 1 35 | 36 | ALLOWED_HOSTS = [] 37 | 38 | 39 | # Application definition 40 | 41 | INSTALLED_APPS = ( 42 | 'django.contrib.admin', 43 | 'django.contrib.auth', 44 | 'django.contrib.contenttypes', 45 | 'django.contrib.messages', 46 | 'django.contrib.sessions', 47 | 'django.contrib.sites', 48 | 'django.contrib.staticfiles', 49 | 50 | 'app', 51 | 'maintenancemode', 52 | ) 53 | 54 | 55 | if DJANGO_MAJOR_VERSION == 1 and DJANGO_MAJOR_VERSION < 10: 56 | MIDDLEWARE_CLASSES = ( 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | ) 64 | else: 65 | MIDDLEWARE = [ 66 | 'django.middleware.security.SecurityMiddleware', 67 | 'django.contrib.sessions.middleware.SessionMiddleware', 68 | 'django.middleware.common.CommonMiddleware', 69 | 'django.middleware.csrf.CsrfViewMiddleware', 70 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 71 | 'django.contrib.messages.middleware.MessageMiddleware', 72 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 73 | ] 74 | 75 | TEMPLATE_DIRECTORY = os.path.join(BASE_DIR, 'templates') 76 | 77 | if DJANGO_MAJOR_VERSION >= 1: 78 | 79 | # Templates 80 | if DJANGO_MINOR_VERSION < 8 and DJANGO_MAJOR_VERSION == 1: 81 | TEMPLATE_DIRS = ( 82 | TEMPLATE_DIRECTORY, 83 | ) 84 | 85 | TEMPLATE_LOADERS = ( 86 | 'django.template.loaders.filesystem.Loader', 87 | 'django.template.loaders.app_directories.Loader', 88 | ) 89 | 90 | TEMPLATE_CONTEXT_PROCESSORS = ( 91 | 'django.contrib.auth.context_processors.auth', 92 | 'django.core.context_processors.i18n', 93 | 'django.core.context_processors.request', 94 | 'django.core.context_processors.media', 95 | 'django.core.context_processors.static', 96 | 'django.contrib.messages.context_processors.messages', 97 | ) 98 | else: 99 | TEMPLATES = [ 100 | { 101 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 102 | 'DIRS': [TEMPLATE_DIRECTORY], 103 | 'APP_DIRS': True, 104 | 'OPTIONS': { 105 | 'context_processors': [ 106 | 'django.template.context_processors.debug', 107 | 'django.template.context_processors.request', 108 | 'django.contrib.auth.context_processors.auth', 109 | 'django.contrib.messages.context_processors.messages', 110 | ], 111 | }, 112 | }, 113 | ] 114 | 115 | # Sessions 116 | if DJANGO_MAJOR_VERSION == 1 and DJANGO_MINOR_VERSION == 7: 117 | MIDDLEWARE_CLASSES += ( 118 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 119 | ) 120 | elif DJANGO_MAJOR_VERSION == 1: 121 | MIDDLEWARE_CLASSES += ( 122 | 'django.middleware.security.SecurityMiddleware', 123 | ) 124 | 125 | if DJANGO_MAJOR_VERSION == 1 and DJANGO_MAJOR_VERSION < 10: 126 | MIDDLEWARE_CLASSES += ('maintenancemode.middleware.MaintenanceModeMiddleware',) 127 | else: 128 | MIDDLEWARE += ['maintenancemode.middleware.MaintenanceModeMiddleware'] 129 | 130 | ROOT_URLCONF = 'testproject.urls' 131 | 132 | WSGI_APPLICATION = 'testproject.wsgi.application' 133 | 134 | 135 | # Database 136 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 137 | 138 | DATABASES = { 139 | 'default': { 140 | 'ENGINE': 'django.db.backends.sqlite3', 141 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 142 | } 143 | } 144 | 145 | 146 | # Internationalization 147 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 148 | 149 | LANGUAGE_CODE = 'en-us' 150 | 151 | TIME_ZONE = 'UTC' 152 | 153 | USE_I18N = True 154 | 155 | USE_L10N = True 156 | 157 | USE_TZ = True 158 | 159 | 160 | # Static files (CSS, JavaScript, Images) 161 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 162 | 163 | STATIC_URL = '/static/' 164 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-maintenancemode-2 2 | ======================== 3 | 4 | |Build Status| 5 | 6 | Current Version: 2.0.0 7 | 8 | This project makes it easy to put your Django site into “maintenance 9 | mode”, or more technically, return an HTTP 503 response. 10 | 11 | This project differs slightly from other implementations in that the 12 | maintenance mode flag is stored in your database versus settings or an 13 | environment variable. If your site is deployed to multiple servers, the 14 | centralized database-based maintenance flag makes it a snap to bring 15 | them all up or down at once. 16 | 17 | Requirements 18 | ------------ 19 | 20 | - `django `__ 21 | - `django.contrib.sites `__ 22 | 23 | Pre-Requisites 24 | -------------- 25 | 26 | You must have at least one Site entry in your database **before** 27 | installing django-maintenancemode-2. 28 | 29 | Supported Python Versions 30 | ------------------------- 31 | 32 | - 2.7, 3.x 33 | 34 | Supported Django Versions 35 | ------------------------- 36 | - 4.x use the latest version 37 | - 2.x >= 3.x, please use version 1.3.1 38 | - < 2, please use version 1.1.9 39 | 40 | Installation 41 | ------------ 42 | 43 | 1. ``pip install django-maintenancemode-2`` 44 | 45 | – or – 46 | 47 | 1. Download django-maintenancemode-2 from 48 | `source `__ 49 | 2. \*optional: Enable a virtualenv 50 | 3. Run ``python setup.py install`` or add ``maintenancemode`` to your 51 | PYTHONPATH 52 | 53 | Settings and Required Values 54 | ---------------------------- 55 | 56 | - Ensure the `Sites 57 | Framework `__ 58 | is enabled, and you have at least one entry in the Sites table. 59 | - Add ``maintenancemode.middleware.MaintenanceModeMiddleware`` to your 60 | ``MIDDLEWARE_CLASSES`` 61 | - Add ``maintenancemode`` to your ``INSTALLED_APPS`` 62 | - Run ``python manage.py migrate`` to create the ``maintenancemode`` 63 | tables. 64 | - Run your project to automatically add the ``maintenancemode`` 65 | database records. 66 | - Add a 503.html template to the root of your templates directory, or 67 | optionally add a ``MAINTENANCE_503_TEMPLATE`` path to your 503.html 68 | file’s location in settings. 69 | - ``maintenancemode`` will ignore any patterns beginning with the 70 | default Django Admin url: ``^admin`` so you can turn it off. If you 71 | use a custom url for admin, you may override the ignored admin 72 | patterns by adding the ``MAINTENANCE_ADMIN_IGNORED_URLS`` list in 73 | settings. Example: ``['^my-custom-admin', '^my-other-custom-admin']`` 74 | - You can also block staff users, who by default are ignored by 75 | maintenance mode, by setting ``MAINTENANCE_BLOCK_STAFF`` to ``True`` 76 | 77 | Usage 78 | ----- 79 | 80 | .. figure:: http://res.cloudinary.com/alsoicode/image/upload/v1449537052/django-maintenancemode-2/maintenancemode.jpg 81 | :alt: Image of django-maintenancemode-2 82 | 83 | Image of django-maintenancemode-2 84 | 85 | Turning Maintenance Mode **On** 86 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 87 | 88 | To put a site into “Maintenance Mode”, check the “In Maintenance Mode” 89 | checkbox and save in Django Admin under the “Maintenancemode” section. 90 | The next time you visit the public side of the site, it will return a 91 | 503 if: 92 | 93 | - You are not logged in as a superuser or staff user 94 | - You are not viewing a URL in the ignored patterns list 95 | - Your ``REMOTE_ADDR`` does not appear in the ``INTERNAL_IPS`` setting 96 | 97 | Or you can alternatively use the ``setmaintenance`` management command: 98 | 99 | # sets maintenance on for the current settings.SITE_ID 100 | ./manage.py setmaintenance on 101 | 102 | # sets maintenance on for sites 2 and 3 103 | ./manage.py setmaintenance on 2 3 104 | 105 | which can be useful for ``fabric`` deployment scripts etc. 106 | 107 | Turning Maintenance Mode **Off** 108 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 109 | 110 | Log in, un-check the “In Maintenance Mode” checkbox and save. 111 | 112 | Or you can alternatively use the ``setmaintenance`` management command: 113 | 114 | # sets maintenance off for the current settings.SITE_ID 115 | $ ./manage.py setmaintenance off 116 | 117 | # sets maintenance off for sites 2 and 3 118 | $ ./manage.py setmaintenance off 2 3 119 | 120 | Testing and Sample Application 121 | ------------------------------ 122 | 123 | A “testproject” application is included, which also contains unit and 124 | functional tests you can run via ``python manage.py test`` from the 125 | ``testproject`` directory. 126 | 127 | You will need to run ``manage.py migrate`` to create the test project 128 | database. 129 | 130 | There are only two views in the testproject: - / - /ignored-page 131 | 132 | To see ``maintenancemode`` in action, log into Django admin, and set the 133 | maintenance mode to true. Log out, then visit the home page, and 134 | instead, you’ll be greeted with the maintenance page. 135 | 136 | To have ``maintenancemode`` ignore the “ignored-page” view, simply add 137 | it’s url pattern to the Ignored URLs as: 138 | 139 | :: 140 | 141 | ^ignored-page/$ 142 | 143 | Now you should be able to visit the ``ignored-page`` view regardless of 144 | the maintenancemode status. This is useful for contact or help pages you 145 | still want people to be able to access while you’re working on other 146 | parts of the site. 147 | 148 | Database migrations 149 | ~~~~~~~~~~~~~~~~~~~ 150 | 151 | ``./manage.py migrate`` should add the necessary tables. 152 | 153 | .. |Build Status| image:: https://travis-ci.org/alsoicode/django-maintenancemode-2.svg 154 | :target: https://travis-ci.org/alsoicode/django-maintenancemode-2 155 | -------------------------------------------------------------------------------- /testproject/app/tests.py: -------------------------------------------------------------------------------- 1 | try: 2 | import httplib 3 | except ImportError: 4 | import http.client as httplib 5 | 6 | from django.contrib.auth.models import User 7 | from django.contrib.sites.models import Site 8 | from django.test import TestCase 9 | from django.test.client import Client 10 | from django.template import TemplateDoesNotExist 11 | from django.urls import reverse 12 | 13 | from maintenancemode import middleware as mw 14 | from maintenancemode.models import Maintenance, IgnoredURL 15 | 16 | from .urls import urlpatterns 17 | 18 | 19 | class MaintenanceModeMiddlewareTestCase(TestCase): 20 | def setUp(self): 21 | # Reset config options adapted in the individual tests 22 | 23 | mw.MAINTENANCE_MODE = False 24 | # settings.INTERNAL_IPS = () 25 | 26 | # Site 27 | site = Site.objects.get_current() 28 | self.maintenance, _ = Maintenance.objects.get_or_create(site=site) 29 | 30 | # User 31 | self.username = 'maintenance' 32 | self.password = 'maintenance_pw' 33 | 34 | try: 35 | user = User.objects.get(username=self.username) 36 | except User.DoesNotExist: 37 | user = User.objects.create_user( 38 | username=self.username, 39 | email='maintenance@example.org', 40 | password=self.password 41 | ) 42 | 43 | self.user = user 44 | 45 | # urls 46 | self.home_url = reverse('app:home') 47 | self.ignored_url = reverse('app:ignored') 48 | 49 | # Text checks 50 | self.home_page_text = 'This is the home view.' 51 | self.maintenance_page_text = 'Temporarily unavailable' 52 | 53 | def turn_maintenance_mode_on(self): 54 | self.maintenance.is_being_performed = True 55 | self.maintenance.save() 56 | 57 | def turn_maintenance_mode_off(self): 58 | self.maintenance.is_being_performed = False 59 | self.maintenance.save() 60 | 61 | def test_implicitly_disabled_middleware(self): 62 | # Middleware should default to being disabled 63 | response = self.client.get(self.home_url) 64 | self.assertContains( 65 | response, text=self.home_page_text, count=1, status_code=200 66 | ) 67 | 68 | def test_disabled_middleware(self): 69 | # Explicitly disabling the MAINTENANCE_MODE should work as expected 70 | mw.MAINTENANCE_MODE = False 71 | 72 | response = self.client.get(self.home_url) 73 | self.assertContains( 74 | response, text=self.home_page_text, count=1, status_code=200 75 | ) 76 | 77 | def test_enabled_middleware_without_template(self): 78 | """ 79 | Enabling the middleware without a proper 503 template should raise a 80 | template error 81 | """ 82 | templates_override = [ 83 | { 84 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 85 | 'DIRS': [], 86 | 'APP_DIRS': True, 87 | 'OPTIONS': { 88 | 'context_processors': [ 89 | 'django.template.context_processors.debug', 90 | 'django.template.context_processors.request', 91 | 'django.contrib.auth.context_processors.auth', 92 | 'django.contrib.messages.context_processors.messages', 93 | ], 94 | }, 95 | }, 96 | ] 97 | 98 | with self.settings(TEMPLATES=templates_override): 99 | mw.MAINTENANCE_MODE = True 100 | 101 | self.assertRaises( 102 | TemplateDoesNotExist, self.client.get, self.home_url 103 | ) 104 | 105 | def test_enabled_middleware_with_template(self): 106 | """ 107 | Enabling the middleware having a 503.html in any of the template 108 | locations should return the rendered template 109 | """ 110 | self.turn_maintenance_mode_on() 111 | 112 | # reset INTERNAL_IPS so that the request will appear to come 113 | # from the outside 114 | with self.settings(INTERNAL_IPS=()): 115 | response = self.client.get(self.home_url) 116 | self.assertContains( 117 | response, text=self.maintenance_page_text, count=1, 118 | status_code=503 119 | ) 120 | 121 | def test_middleware_with_non_staff_user(self): 122 | """ 123 | A logged in user that is not a staff user should see the 124 | 503 message 125 | """ 126 | self.turn_maintenance_mode_on() 127 | 128 | self.client.login(username=self.username, password=self.password) 129 | 130 | with self.settings(INTERNAL_IPS=()): 131 | response = self.client.get(self.home_url) 132 | self.assertContains( 133 | response, text=self.maintenance_page_text, count=1, 134 | status_code=503 135 | ) 136 | 137 | def test_middleware_with_staff_user(self): 138 | """ 139 | A logged in user that _is_ a staff user should be able to use 140 | the site normally 141 | """ 142 | self.user.is_staff = True 143 | self.user.save() 144 | 145 | self.client.login(username=self.username, password=self.password) 146 | 147 | response = self.client.get(self.home_url) 148 | self.assertContains( 149 | response, text=self.home_page_text, count=1, status_code=200 150 | ) 151 | 152 | def test_middleware_with_internal_ips(self): 153 | """ 154 | A user that visits the site from an IP in INTERNAL_IPS should 155 | be able to use the site normally 156 | """ 157 | self.turn_maintenance_mode_on() 158 | 159 | # Use a new Client instance to be able to set the REMOTE_ADDR 160 | # used by INTERNAL_IPS 161 | client = Client(REMOTE_ADDR='127.0.0.1') 162 | 163 | with self.settings(INTERNAL_IPS=('127.0.0.1',)): 164 | response = client.get(self.home_url) 165 | self.assertContains( 166 | response, text=self.home_page_text, count=1, status_code=200 167 | ) 168 | 169 | def test_ignored_path(self): 170 | """ 171 | A path is ignored when applying the maintanance mode and should 172 | be reachable normally 173 | """ 174 | self.turn_maintenance_mode_on() 175 | 176 | # Add a pattern to ignore 177 | maintenance = Maintenance.objects.all()[0] 178 | IgnoredURL.objects.get_or_create( 179 | maintenance=maintenance, 180 | pattern=urlpatterns[0].pattern # r'^ignored-page/$' 181 | ) 182 | 183 | response = self.client.get(self.ignored_url) 184 | self.assertContains(response, text='Ignored', count=1, status_code=200) 185 | 186 | def test_django_admin_accessable(self): 187 | """ 188 | Make sure we can still log into Django admin to turn 189 | maintenance mode off 190 | """ 191 | self.turn_maintenance_mode_on() 192 | response = self.client.get('/admin/login/') 193 | self.assertEqual( 194 | response.status_code, 195 | httplib.OK, 196 | 'Unable to reach Django Admin login' 197 | ) 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------