├── 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 | [](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 | 
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 |
--------------------------------------------------------------------------------