Full Code of ulule/django-safety for AI

master 6fdb56a55ee4 cached
57 files
53.5 KB
13.5k tokens
72 symbols
1 requests
Download .txt
Repository: ulule/django-safety
Branch: master
Commit: 6fdb56a55ee4
Files: 57
Total size: 53.5 KB

Directory structure:
gitextract_rc3yltz7/

├── .coveragerc
├── .editorconfig
├── .gitignore
├── .travis.yml
├── AUTHORS
├── LICENSE
├── Makefile
├── README.rst
├── data/
│   ├── .gitkeep
│   ├── geoip/
│   │   └── .gitkeep
│   └── geoip2/
│       └── .gitkeep
├── example/
│   ├── __init__.py
│   ├── models.py
│   ├── settings.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── home.html
│   │   └── safety/
│   │       ├── password_change/
│   │       │   └── base.html
│   │       └── session_list.html
│   └── urls.py
├── manage.py
├── pytest.ini
├── requirements/
│   ├── base.txt
│   ├── development.txt
│   ├── test.txt
│   └── tox.txt
├── safety/
│   ├── __init__.py
│   ├── admin.py
│   ├── app_settings.py
│   ├── apps.py
│   ├── compat.py
│   ├── decorators.py
│   ├── forms.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       └── clean_safety_sessions.py
│   ├── managers.py
│   ├── middleware.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── mixins.py
│   ├── models.py
│   ├── resolvers.py
│   ├── templates/
│   │   └── safety/
│   │       └── password_change/
│   │           ├── base.html
│   │           ├── done.html
│   │           └── form.html
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── models.py
│   │   ├── settings.py
│   │   ├── test_models.py
│   │   ├── test_views.py
│   │   └── urls.py
│   ├── urls.py
│   ├── utils.py
│   └── views.py
├── setup.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
include =
    safety/models.py
    safety/utils.py
    safety/views.py


================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true

[*]
charset                  = utf-8
end_of_line              = lf
indent_size              = 4
indent_style             = space
insert_final_newline     = true
trim_trailing_whitespace = true

[Makefile]
indent_style = tab
indent_size = 4


================================================
FILE: .gitignore
================================================
__pycache__/
*.py[cod]
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
*.manifest
*.spec
pip-log.txt
pip-delete-this-directory.txt
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
*.mo
*.pot
*.log
docs/_build/
target/
.vagrant
.venv
.env
*.sqlite3
*.db
*.mmdb
*.gz
*.dat


================================================
FILE: .travis.yml
================================================
language: python
env:
    - TOXENV=py27-django18
    - TOXENV=py34-django18
    - TOXENV=py35-django18
    - TOXENV=py27-django19
    - TOXENV=py34-django19
    - TOXENV=py35-django19
before_install:
    - sudo apt-get -qq update
    - sudo apt-get install -y libgeoip-dev
install:
    - travis_retry pip install tox
script:
    - travis_retry tox


================================================
FILE: AUTHORS
================================================
Gilles Fabio <gilles.fabio@gmail.com>
Louise Grandjonc <louise.grandjonc@gmail.com>
Florent Messa <florent.messa@gmail.com>


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Ulule

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
devenv:
	virtualenv -p python2.7 `pwd`/.venv
	. .venv/bin/activate && pip install -r requirements/development.txt

clean:
	@(rm -rvf .venv .tox .coverage build django-safety* *.egg-info)

pep8:
	@(flake8 safety --ignore=E501,E127,E128,E124)

test:
	@(py.test -s --cov-report term --cov-config .coveragerc --cov=safety --color=yes safety/tests)

geoip:
	@(wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz)
	@(wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz)
	@(wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz)
	@(gunzip -d GeoIP.dat.gz && mv GeoIP.dat data/geoip/)
	@(gunzip -d GeoLite2-City.mmdb.gz && mv GeoLite2-City.mmdb data/geoip2/)
	@(gunzip -d GeoLiteCity.dat.gz && mv GeoLiteCity.dat data/geoip/)

migrate:
	@(ENV=example python manage.py makemigrations safety)

example-clean:
	@(rm -rf example.db)

example-migrate:
	@(ENV=example python manage.py migrate)

example-user:
	@(ENV=example python manage.py createsuperuser --username='johndoe' --email='johndoe@example.com')

example-serve:
	@(ENV=example python manage.py runserver)

delpyc:
	@(find . -name '*.pyc' -delete)

release:
	@(python setup.py sdist register upload -s)


================================================
FILE: README.rst
================================================
django-safety
=============

.. image:: https://secure.travis-ci.org/ulule/django-safety.png?branch=master
    :alt: Build Status
    :target: http://travis-ci.org/ulule/django-safety


**Generic Django application for safer user accounts.**

Features
--------

Sessions
~~~~~~~~

* User can see all active sessions
* User can disable a given active session
* User can disable all active sessions

Force password change
~~~~~~~~~~~~~~~~~~~~~

* Administrators can require a password change for any user

Workflows
---------

Sessions
~~~~~~~~

1. User logs in
2. We connect the logic to the ``user_logged_in`` signal
3. We create a new ``safety.models.Session`` instance
4. User can see the list of her sessions (with IP, last activity and device information)
5. User can delete a given session in the list
6. We delete both the related ``safety.models.Session`` instance and related session in store
7. User can delete all active sessions excepted the current one
8. We proceed the same way: deleting instances and related sessions from store
9. User logs out
10. We connect the logic to the ``user_logged_out`` signal
11. We delete the related ``safety.models.Session`` instance

Force password change
~~~~~~~~~~~~~~~~~~~~~

1. Administrator creates a ``PasswordChange`` instance and sets ``required`` to ``True``
2. When user logs in, it will be redirected to password change form
3. Until the user does not change its password, it is not authorized to go elsewhere
4. User changes its password
5. It is now authorized to go elsewhere

Installation
------------

Installing prerequisites
~~~~~~~~~~~~~~~~~~~~~~~~

GeoIP library must be installed on your server.

On OS X with Homebrew:

.. code-block:: bash

    brew install geoip

You also need the GeoIP databases.

For Django >= 1.9, download City and Country databases as binary (not CSV):

http://dev.maxmind.com/geoip/geoip2/geolite2/

For Django 1.8, download City and Country legacy databases as binary (not CSV):

http://dev.maxmind.com/geoip/legacy/geolite/

Create a directory wherever you want and uncompress these archives this
directory. Once done, set ``GEOIP_PATH`` setting pointing to this directory:

.. code-block:: python

    GEOIP_PATH = '/absolute/path/to/maxmind/db/directory'

Installing django-safety
~~~~~~~~~~~~~~~~~~~~~~~~

Install

.. code-block:: bash

    $ pip install django-safety

In your ``settings.py``, add ``safety`` to ``INSTALLED_APPS``:

.. code-block:: python

    INSTALLED_APPS = (
        # Your other apps here.
        'safety',
    )

In your ``urls.py``, include ``safety.urls`` under ``safety`` namespace.

.. code-block:: python

    urlpatterns = [
        # Your other URLs here.
        url(r'^security/', include('safety.urls', namespace='safety')),
    ]

Synchronize the database:

.. code-block:: bash

    $ python manage.py migrate safety

Great. The session feature is ready.

If you want to enable the "force password change" feature, read the next.

Enabling "force password change" feature
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To enable this feature, you have two choices:

* You want to protect only specific views? Use ``password_change_required()`` decorator
* You want to protect your whole application? Use ``PasswordChangeMiddleware`` middleware

The decorator works as any Django view decorator.

.. code-block:: python

    #
    # In your urls.py
    #

    from safety.decorators import password_change_required
    from .views import protect_me

    urlpatterns = [
        # Other URLs here.
        url(r'^protect-me/$', password_change_required(protect_me)),
    ]

    #
    # Or in your views.py (it's up to you)
    #
    from django.shortcuts import render
    from safety.decorators import password_change_required

    @password_change_required
    def protect_me(request):
        return render(request, 'protect_me.html')

The middleware works as any Django middleware.

Add ``safety.middleware.PasswordChangeMiddleware`` middleware in your ``settings.py``:

.. code-block:: python

    MIDDLEWARE_CLASSES = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
        'safety.middleware.PasswordChangeMiddleware',
    ]

Done.

Settings
--------

+-------------------------------------------+---------------------------------------------------------------------+
| Setting                                   | Description                                                         |
+===========================================+=====================================================================+
| ``SAFETY_LOGIN_REQUIRED_MIXIN_CLASS``     | The Python path to your own "login required" mixin class.           |
|                                           | Defaults to ``safety.mixins.LoginRequiredMixin``.                   |
+-------------------------------------------+---------------------------------------------------------------------+
| ``SAFETY_IP_RESOLVER``                    | The Python path to your own IP resolver callable.                   |
|                                           | Defaults to ``safety.resolvers.remote_addr_ip``.                    |
+-------------------------------------------+---------------------------------------------------------------------+
| ``SAFETY_DEVICE_RESOLVER``                | The Python path to your own device resolver callable.               |
|                                           | Defaults to ``safety.resolvers.device``.                            |
+-------------------------------------------+---------------------------------------------------------------------+
| ``SAFETY_LOCATION_RESOLVER``              | The Python path to your own location resolver callable.             |
|                                           | Defaults to ``safety.resolvers.location``.                          |
+-------------------------------------------+---------------------------------------------------------------------+

Development
-----------

.. code-block:: bash

    # Install pip and virtualenv
    $ sudo easy_install pip
    $ sudo pip install virtualenv

    # Clone repository
    $ git clone https://github.com/ulule/django-safety.git

    # Setup your development environment
    $ cd django-safety
    $ make devenv
    $ source .venv/bin/activate

    # Download GeoIP databases
    $ make geoip

    # Launch test suite
    $ make test

    # Launch test suite with tox to check compatibility
    $ tox

    # Run the example project (default user username is "johndoe")
    $ make example-migrate
    $ make example-user
    $ make example-serve

Contribute
----------

1. Create an issue (**before** submitting pull requests)
2. Submit your bug or feature request
3. You want to fix or code it yourself? Great! Fork the project
4. Create a branch, always add tests and make sure they all pass with ``tox``
5. Submit a pull request

Compatibility
-------------

- python 2.7: Django 1.8, 1.9
- Python 3.4: Django 1.8, 1.9
- Python 3.5: Django 1.8, 1.9


================================================
FILE: data/.gitkeep
================================================


================================================
FILE: data/geoip/.gitkeep
================================================


================================================
FILE: data/geoip2/.gitkeep
================================================


================================================
FILE: example/__init__.py
================================================


================================================
FILE: example/models.py
================================================


================================================
FILE: example/settings.py
================================================
import os

import django


BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

SECRET_KEY = 'zn=kw41f9nhe1lse8minnu0s-@7b+q(exccs5d-1vil$^ees&#'
DEBUG = True
ALLOWED_HOSTS = []

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'example',

    'safety',
]

MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'safety.middleware.PasswordChangeMiddleware',
]

ROOT_URLCONF = 'example.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'example.db'),
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

STATIC_URL = '/static/'

AUTH_USER_MODEL = 'auth.User'
LOGIN_URL = '/admin/login/'

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

GEOIP_DIR_PATH = os.path.join(BASE_DIR, 'data', 'geoip')
GEOIP2_DIR_PATH = os.path.join(BASE_DIR, 'data', 'geoip2')
GEOIP_PATH = GEOIP2_DIR_PATH if django.VERSION >= (1, 9) else GEOIP_DIR_PATH


================================================
FILE: example/templates/base.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/css/bootstrap.min.css" rel="stylesheet" media="screen">
        <!--[if lt IE 9]>
            <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.js"></script>
            <script src="//cdnjs.cloudflare.com/ajax/libs/respond.js/1.3.0/respond.js"></script>
        <![endif]-->
    </head>
    <body>
        {% block content_wrapper %}
        <div class="container">
            <header>
                <h3>Hello, {{ user }}!</h3>
                <p>
                    <a href="{% url 'home' %}">home</a> —
                    {% if request.user.is_authenticated %}<a href="{% url 'update-password' %}">update password</a> —{% endif %}
                    <a href="{% url 'admin:index' %}">admin</a> —
                    <a href="{% url 'safety:session_list' %}">active sessions</a>
                </p>
            </header>
            <hr>
            {% block content %}{% endblock %}
        </div>
        {% endblock %}
        <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/js/bootstrap.min.js"></script>
    </body>
</html>


================================================
FILE: example/templates/home.html
================================================
{% extends "base.html" %}

{% block content %}
    <p>I'm the homepage.</p>
{% endblock content %}


================================================
FILE: example/templates/safety/password_change/base.html
================================================
{% extends "base.html" %}


================================================
FILE: example/templates/safety/session_list.html
================================================
{% extends "base.html" %}
{% load i18n %}

{% block content %}
    <h1>{% trans "Active Sessions" %}</h1>
    <table class="table">
        <thead>
            <tr>
                <th>{% trans "Last Activity" %}</th>
                <th>{% trans "Location" %}</th>
                <th>{% trans "Device" %}</th>
                <th>{% trans "End Session" %}</th>
            </tr>
        </thead>
        {% for object in object_list %}
        <tr {% if object.session_key == request.session.session_key %}class="active"{% endif %}>
            <td>
                {% if object.session_key == request.session.session_key %}
                    {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %}
                {% else %}
                    {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago{% endblocktrans %}
                {% endif %}
            </td>
            <td>{{ object.location }}</td>
            <td>{{ object.device }}</td>
            <td>
                <form method="post" action="{% url 'safety:session_delete' object.pk %}">
                    {% csrf_token %}
                    {% if object.session_key == request.session.session_key %}
                        <button type="submit" class="btn btn-xs btn-link">{% trans "End Session" %}</button>
                    {% else %}
                        <button type="submit" class="btn btn-xs btn-warning">{% trans "End Session" %}</button>
                    {% endif %}
                </form>
            </td>
        </tr>
        {% endfor %}
    </table>

    {% if object_list.count > 1 %}
        <form method="post" action="{% url 'safety:session_delete_other' %}">
            {% csrf_token %}
            <p>
               {% blocktrans %}You can also end all other sessions but the current.
               This will log you out on all other devices.{% endblocktrans %}
           </p>
           <button type="submit" class="btn btn-default btn-warning">{% trans "End All Other Sessions" %}</button>
        </form>
    {% endif %}
{% endblock %}


================================================
FILE: example/urls.py
================================================
# -*- coding: utf-8 -*-
from django.conf.urls import url, include
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.shortcuts import redirect, render

from safety.models import PasswordChange


def home(request):
    return render(request, 'home.html')


def update_password(request):
    if request.user.is_authenticated():
        pr, created = PasswordChange.objects.get_or_create_for_user(request.user)
        pr.required = True
        pr.save()
    return redirect(reverse('home'))


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^account/', include('django.contrib.auth.urls')),
    url(r'^security/', include('safety.urls', namespace='safety')),
    url(r'^update-password/$', update_password, name='update-password'),
    url(r'^$', home, name='home'),
]


================================================
FILE: manage.py
================================================
#!/usr/bin/env python
import os
import sys

SUPPORTED_ENVS = (
    'test',
    'example',
)

SETTINGS_MODULES = {
    'test': 'safety.tests.settings',
    'example': 'example.settings'
}

ENV = os.environ.get('ENV', 'test')
ENV = ENV.lower()

if ENV not in SUPPORTED_ENVS:
    raise Exception('Unsupported environment: %s' % ENV)

if __name__ == "__main__":
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', SETTINGS_MODULES[ENV])
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)


================================================
FILE: pytest.ini
================================================
[pytest]
DJANGO_SETTINGS_MODULE=safety.tests.settings


================================================
FILE: requirements/base.txt
================================================
six
ua-parser
geoip2


================================================
FILE: requirements/development.txt
================================================
-r base.txt
-r test.txt

Django
flake8
tox


================================================
FILE: requirements/test.txt
================================================
pytest
pytest-cov
pytest-django
exam


================================================
FILE: requirements/tox.txt
================================================
six
ua-parser

-r test.txt


================================================
FILE: safety/__init__.py
================================================
# -*- coding: utf-8 -*-
version = (0, 1, 0)

__version__ = '.'.join(map(str, version))

default_app_config = 'safety.apps.SafetyConfig'

__all__ = [
    'default_app_config',
    'version',
]


================================================
FILE: safety/admin.py
================================================
# -*- coding: utf-8 -*-
from django.contrib import admin
from django.utils.timezone import now

from .models import (
    PasswordChange,
    Session,
)


class PasswordChangeAdmin(admin.ModelAdmin):
    list_display = ('user', 'last_change_date', 'required')
    raw_id_fields = ('user',)


class SessionAdmin(admin.ModelAdmin):
    list_display = ('user', 'ip', 'last_activity', 'location', 'device', 'is_valid')
    raw_id_fields = ('user',)

    def is_valid(self, obj):
        return obj.expiration_date > now()
    is_valid.boolean = True


admin.site.register(PasswordChange, PasswordChangeAdmin)
admin.site.register(Session, SessionAdmin)


================================================
FILE: safety/app_settings.py
================================================
# -*- coding: utf-8 -*-
from django.conf import settings


# The Python path to the login_required mixin class applied to views.
LOGIN_REQUIRED_MIXIN_CLASS = getattr(
    settings,
    'SAFETY_LOGIN_REQUIRED_MIXIN_CLASS',
    'safety.mixins.LoginRequiredMixin')


# Path to callable that handles the IP resolving.
# Takes a django.http.HttpRequest instance and returns a string.
IP_RESOLVER = getattr(
    settings,
    'SAFETY_IP_RESOLVER',
    'safety.resolvers.remote_addr_ip')


# Path to callable that handles the device resolving.
# Takes a django.http.HttpRequest instance and returns a string.
DEVICE_RESOLVER = getattr(
    settings,
    'SAFETY_DEVICE_RESOLVER',
    'safety.resolvers.device')


# Path to callable that handles the location resolving.
# Takes a django.http.HttpRequest instance and returns a string.
LOCATION_RESOLVER = getattr(
    settings,
    'SAFETY_LOCATION_RESOLVER',
    'safety.resolvers.location')


================================================
FILE: safety/apps.py
================================================
# -*- coding: utf-8 -*-
from django.apps import AppConfig


class SafetyConfig(AppConfig):
    name = 'safety'
    verbose_name = 'Safety'

    def ready(self):
        super(SafetyConfig, self).ready()


================================================
FILE: safety/compat.py
================================================
# -*- coding: utf-8 -*-
import django


if django.VERSION >= (1, 9):
    from importlib import import_module  # noqa

    from django.contrib.gis.geoip2 import HAS_GEOIP2

    if HAS_GEOIP2:
        from django.contrib.gis.geoip2 import GeoIP2 as GeoIP  # noqa
    else:
        from django.contrib.gis.geoip import GeoIP  # noqa

else:
    from django.utils.importlib import import_module  # noqa
    from django.contrib.gis.geoip import GeoIP  # noqa


================================================
FILE: safety/decorators.py
================================================
# -*- coding: utf-8 -*-
from functools import wraps

from django.core.urlresolvers import reverse
from django.shortcuts import redirect

from .models import (
    PasswordChange,
    Session,
)


def password_change_required(func):
    @wraps(func)
    def inner(request, *args, **kwargs):
        if not request.user.is_authenticated():
            return func(request, *args, **kwargs)

        required = PasswordChange.objects.is_required_for_user(request.user)
        url = reverse('safety:password_change')
        is_excluded_url = request.path.startswith(url)

        if required and not is_excluded_url:
            return redirect(url)

        return func(request, *args, **kwargs)
    return inner


================================================
FILE: safety/forms.py
================================================
# -*- coding: utf-8 -*-
from django import forms
from django.contrib.auth.forms import PasswordChangeForm as BasePasswordChangeForm
from django.utils.translation import ugettext_lazy as _

from .models import PasswordChange


class PasswordChangeForm(BasePasswordChangeForm):
    error_messages = dict(BasePasswordChangeForm.error_messages, **{
        'same_password': _("Your old password and the new one are the same. You must use a different password."),
    })

    def clean_new_password2(self):
        password2 = super(PasswordChangeForm, self).clean_new_password2()

        if self.user.check_password(password2):
            raise forms.ValidationError(
                self.error_messages['same_password'],
                code='same_password')

        return password2

    def save(self, *args, **kwargs):
        pc, created = PasswordChange.objects.get_or_create_for_user(self.user)
        pc.required = False
        pc.save()
        super(PasswordChangeForm, self).save(*args, **kwargs)


================================================
FILE: safety/management/__init__.py
================================================


================================================
FILE: safety/management/commands/__init__.py
================================================


================================================
FILE: safety/management/commands/clean_safety_sessions.py
================================================
# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand

from django.utils.timezone import now
from safety.models import Session


class Command(BaseCommand):
    def handle(self, *args, **options):
        Session.objects.filter(expiration_date__lt=now()).delete()
        self.stdout.write(self.style.SUCCESS('Deleted expired sessions'))


================================================
FILE: safety/managers.py
================================================
# -*- coding: utf-8 -*-
from django.db import models, transaction, IntegrityError
from django.utils.timezone import now

from . import app_settings
from . import utils


class PasswordChangeManager(models.Manager):
    def get_or_create_for_user(self, user):
        return self.get_or_create(user=user)

    def is_required_for_user(self, user):
        obj, created = self.get_or_create_for_user(user=user)
        return obj.required


class SessionManager(models.Manager):
    def active(self, user=None):
        qs = self.filter(expiration_date__gt=now())
        if user is not None:
            qs = qs.filter(user=user)
        return qs.order_by('-last_activity')

    def create_session(self, request, user):
        ip = utils.resolve(app_settings.IP_RESOLVER, request)
        device = utils.resolve(app_settings.DEVICE_RESOLVER, request)
        location = utils.resolve(app_settings.LOCATION_RESOLVER, request)

        user_agent = request.META.get('HTTP_USER_AGENT', '')
        user_agent = user_agent[:200] if user_agent else user_agent

        try:
            with transaction.atomic():
                obj = self.create(
                    user=user,
                    session_key=request.session.session_key,
                    ip=ip,
                    user_agent=user_agent,
                    device=device,
                    location=location,
                    expiration_date=request.session.get_expiry_date(),
                    last_activity=now())
        except IntegrityError:
            obj = self.get(
                user=user,
                session_key=request.session.session_key)
            obj.last_activity = now()
            obj.save()

        return obj


================================================
FILE: safety/middleware.py
================================================
# -*- coding: utf-8 -*-
from .decorators import password_change_required


class PasswordChangeMiddleware(object):
    def process_view(self, request, view_func, view_args, view_kwargs):
        return password_change_required(view_func)(request, *view_args, **view_kwargs)


================================================
FILE: safety/migrations/0001_initial.py
================================================
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-03-16 12:51
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='PasswordChange',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('required', models.BooleanField(db_index=True, default=False, verbose_name='required')),
                ('last_change_date', models.DateTimeField(blank=True, null=True, verbose_name='last change date')),
                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='safety_password_change', to=settings.AUTH_USER_MODEL, verbose_name='user')),
            ],
            options={
                'verbose_name': 'password change',
                'verbose_name_plural': 'password changes',
            },
        ),
        migrations.CreateModel(
            name='Session',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('session_key', models.CharField(max_length=40, unique=True, verbose_name='session key')),
                ('ip', models.GenericIPAddressField(verbose_name='IP')),
                ('user_agent', models.CharField(max_length=200, verbose_name='user agent')),
                ('location', models.CharField(max_length=255, verbose_name='location')),
                ('device', models.CharField(max_length=200, verbose_name='device')),
                ('expiration_date', models.DateTimeField(db_index=True, verbose_name='expiration date')),
                ('last_activity', models.DateTimeField(verbose_name='last activity')),
                ('active', models.BooleanField(default=True)),
                ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
            ],
            options={
                'verbose_name': 'session',
                'verbose_name_plural': 'sessions',
            },
        ),
    ]


================================================
FILE: safety/migrations/__init__.py
================================================


================================================
FILE: safety/mixins.py
================================================
# -*- coding: utf-8 -*-
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator

from .models import Session


class SessionMixin(object):
    def get_queryset(self):
        return Session.objects.active(user=self.request.user)


class LoginRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)


================================================
FILE: safety/models.py
================================================
# -*- coding: utf-8 -*-
from django.conf import settings

from django.contrib.auth.signals import (
    user_logged_in,
    user_logged_out,
)

from django.db.models.signals import post_delete
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _

from . import managers


@python_2_unicode_compatible
class PasswordChange(models.Model):
    user = models.OneToOneField(
        getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
        verbose_name=_('user'),
        related_name='safety_password_change')

    required = models.BooleanField(verbose_name=_('required'), db_index=True, default=False)
    last_change_date = models.DateTimeField(verbose_name=_('last change date'), null=True, blank=True)

    objects = managers.PasswordChangeManager()

    class Meta:
        verbose_name = _('password change')
        verbose_name_plural = _('password changes')

    def __str__(self):
        return '%s - %s' % (self.user, self.last_change_date)


@python_2_unicode_compatible
class Session(models.Model):
    user = models.ForeignKey(getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), verbose_name=_('user'), null=True)
    session_key = models.CharField(verbose_name=_('session key'), max_length=40, unique=True)
    ip = models.GenericIPAddressField(verbose_name=_('IP'))
    user_agent = models.CharField(verbose_name=_('user agent'), max_length=200)
    location = models.CharField(verbose_name=_('location'), max_length=255)
    device = models.CharField(verbose_name=_('device'), max_length=200)
    expiration_date = models.DateTimeField(verbose_name=_('expiration date'), db_index=True)
    last_activity = models.DateTimeField(verbose_name=_('last activity'))
    active = models.BooleanField(default=True)

    objects = managers.SessionManager()

    class Meta:
        verbose_name = _('session')
        verbose_name_plural = _('sessions')

    def __str__(self):
        return '%s (%s)' % (self.user, self.device)

    def delete_store_session(self):
        from .utils import get_session_store

        store = get_session_store()

        if store.exists(session_key=self.session_key):
            store.delete(session_key=self.session_key)


# -----------------------------------------------------------------------------
# Signal handlers
# -----------------------------------------------------------------------------

# Connected to user_logged_in
def create_session(sender, request, user, **kwargs):
    Session.objects.create_session(request, user)


# Connected to user_logged_out
def deactivate_session(sender, request, user, **kwargs):
    try:
        key = request.session.session_key
        instance = Session.objects.get(user=user, session_key=key)
        instance.active = False
        instance.save()
    except Session.DoesNotExist:
        pass


# Connected to post_delete for Session model
def post_delete_session(sender, instance, **kwargs):
    instance.delete_store_session()


user_logged_in.connect(create_session)
user_logged_out.connect(deactivate_session)
post_delete.connect(post_delete_session, sender=Session)


================================================
FILE: safety/resolvers.py
================================================
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext

from ua_parser import user_agent_parser

from . import app_settings
from . import utils
from .compat import GeoIP


def remote_addr_ip(request):
    """
    This is for the development setup.
    """
    return request.META.get('REMOTE_ADDR') or None


def x_forwarded_ip(request):
    """
    Amazon ELB stores the IP in the HTTP_X_FORWARDED_FOR META attribute.
    It is realiably the first one of the IP adresses sent and can be
    trusted (eg.: cannot be spoofed) Warning: This might not be true for
    other load balancers
    This function assumes that your Nginx is configured with:
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    """
    ip_address_list = request.META.get('HTTP_X_FORWARDED_FOR')

    if ip_address_list:
        ip_address_list = ip_address_list.split(',')
        return ip_address_list[0]


def real_ip(request):
    """
    Behind a Wsgi (Nginx) server.
    """
    return request.META.get('HTTP_X_REAL_IP') or None


def device(request):
    """
    Default device resolver using ua-parser.
    """
    ua = request.META.get('HTTP_USER_AGENT', '')
    parsed = user_agent_parser.Parse(ua)

    ua_parsed = (ugettext('unknown browser'), parsed['user_agent'])
    os_parsed = (ugettext('unknown system'), parsed['os'])

    infos = []
    for unknown, dct in (ua_parsed, os_parsed):
        d = dct.copy()

        family = d.pop('family')
        if family is None or family == 'Other':
            infos.append(unknown)
            continue

        version = '.'.join([d.get(k) for k in ('major', 'minor', 'patch') if d.get(k) is not None])
        if version:
            family = '%s %s' % (family, version)
        infos.append(family)

    return ' - '. join(infos)


def location(request):
    """
    Transform an IP address into an approximate location.
    """
    ip = utils.resolve(app_settings.IP_RESOLVER, request)

    try:
        location = GeoIP() and GeoIP().city(ip)
    except:
        # Handle 127.0.0.1 and not found IPs
        return ugettext('unknown')

    if location and location['country_name']:
        if location['city']:
            return '%s, %s' % (location['city'], location['country_name'])
        else:
            return location['country_name']

    return ugettext('unknown')


================================================
FILE: safety/templates/safety/password_change/base.html
================================================
{% block title %}
    {{ title }}
{% endblock %}

{% block content %}
{% endblock %}


================================================
FILE: safety/templates/safety/password_change/done.html
================================================
{% extends "safety/password_change/base.html" %}
{% load i18n %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
<p>{% trans 'Your password was changed.' %}</p>
{% endblock %}


================================================
FILE: safety/templates/safety/password_change/form.html
================================================
{% extends "safety/password_change/base.html" %}
{% load i18n %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
<div id="safety-password-change-form">
    <form method="post">{% csrf_token %}
        <div>
            {% if form.errors %}
                <p class="error">
                {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
                </p>
            {% endif %}
            <p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
            <fieldset>
                <div class="form-row">
                    {{ form.old_password.errors }}
                    {{ form.old_password.label_tag }} {{ form.old_password }}
                </div>
                <div class="form-row">
                    {{ form.new_password1.errors }}
                    {{ form.new_password1.label_tag }} {{ form.new_password1 }}
                    {% if form.new_password1.help_text %}
                    <p class="help">{{ form.new_password1.help_text|safe }}</p>
                    {% endif %}
                </div>
                <div class="form-row">
                    {{ form.new_password2.errors }}
                    {{ form.new_password2.label_tag }} {{ form.new_password2 }}
                    {% if form.new_password2.help_text %}
                    <p class="help">{{ form.new_password2.help_text|safe }}</p>
                    {% endif %}
                </div>
            </fieldset>
            <div class="submit-row">
                <input type="submit" value="{% trans 'Change my password' %}" class="default" />
            </div>
        </div>
    </form>
</div>
{% endblock %}


================================================
FILE: safety/tests/__init__.py
================================================


================================================
FILE: safety/tests/base.py
================================================
# -*- coding: utf-8 -*-
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils.timezone import now

from exam.cases import Exam
from exam.decorators import fixture

from safety.models import Session
from safety.utils import get_session_store


class Fixtures(Exam):
    UA = ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) '
          'AppleWebKit/536.26.17 (KHTML, like Gecko) Version/6.0.2 '
          'Safari/536.26.17')

    DEVICE = 'Safari 6.0.2 - Mac OS X 10.8.2'
    LOCATION = 'Mountain View, United States'
    REMOTE_ADDR = '66.249.64.0'
    USER_PASSWORD = 'secret'

    @fixture
    def user(self):
        return User.objects.create_superuser(
            username='johndoe',
            email='johndoe@example.com',
            password=self.USER_PASSWORD)

    def reload(self):
        self.user = User.objects.get(pk=self.user.pk)


class BaseTestCase(Fixtures, TestCase):
    def login_user(self, password=None):
        admin_login_url = reverse('admin:login')
        password = password or self.USER_PASSWORD

        self.client.post(
            admin_login_url,
            data={
                'username': self.user.username,
                'password': password,
                'next': '/admin/',
            },
            REMOTE_ADDR=self.REMOTE_ADDR,
            HTTP_USER_AGENT=self.UA)

    def create_fake_sessions(self):
        store = get_session_store()

        sessions = []
        for x in range(10):
            store.create()
            session = Session.objects.create(
                user=self.user,
                session_key=store.session_key,
                ip=self.REMOTE_ADDR,
                location=self.LOCATION,
                device=self.DEVICE,
                user_agent=self.UA,
                last_activity=now(),
                expiration_date=store.get_expiry_date())
            sessions.append(session)

        return sessions


================================================
FILE: safety/tests/models.py
================================================


================================================
FILE: safety/tests/settings.py
================================================
import os

import django


BASE_DIR = os.path.dirname(os.path.dirname((os.path.dirname(os.path.abspath(__file__)))))

SECRET_KEY = 'zn=kw41f9nhe1lse8minnu0s-@7b+q(exccs5d-1vil$^ees&#'
DEBUG = True
ALLOWED_HOSTS = []

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'example',
    'safety',
    'safety.tests',
]

MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'safety.middleware.PasswordChangeMiddleware',
]

ROOT_URLCONF = 'safety.tests.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'test.db'),
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

STATIC_URL = '/static/'

AUTH_USER_MODEL = 'auth.User'
LOGIN_URL = '/admin/login/'

GEOIP_DIR_PATH = os.path.join(BASE_DIR, 'data', 'geoip')
GEOIP2_DIR_PATH = os.path.join(BASE_DIR, 'data', 'geoip2')
GEOIP_PATH = os.getenv('GEOIP_PATH', GEOIP2_DIR_PATH) if django.VERSION >= (1, 9) else GEOIP_DIR_PATH


================================================
FILE: safety/tests/test_models.py
================================================
# -*- coding: utf-8 -*-
from .base import BaseTestCase

from safety.models import (
    PasswordChange,
    Session,
)


class ModelsTest(BaseTestCase):
    def test_session(self):
        self.assertEqual(Session.objects.count(), 0)
        self.login_user()
        self.assertEqual(Session.objects.count(), 1)
        session = Session.objects.first()
        self.assertTrue(session.active)
        self.client.logout()
        self.assertEqual(Session.objects.count(), 1)
        session = Session.objects.first()
        self.assertFalse(session.active)

    def test_password_change(self):
        obj, created = PasswordChange.objects.get_or_create_for_user(self.user)
        self.assertTrue(created)
        self.assertEqual(obj.user, self.user)
        self.assertIsNone(obj.last_change_date)
        self.assertFalse(obj.required)


================================================
FILE: safety/tests/test_views.py
================================================
# -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse

from safety.forms import PasswordChangeForm
from safety.models import PasswordChange, Session

from .base import BaseTestCase


class SessionsViewsTest(BaseTestCase):
    def tearDown(self, *args, **kwargs):
        self.client.logout()

    def test_session_list_view(self):
        self.login_user()

        r = self.client.get(reverse('safety:session_list'))
        self.assertEqual(r.status_code, 200)

        objects = r.context['object_list']
        obj = r.context['object_list'][0]

        self.assertEqual(len(objects), 1)
        self.assertEqual(obj.user, self.user)
        self.assertTrue(obj.session_key)
        self.assertTrue(obj.active)
        self.assertEqual(obj.ip, self.REMOTE_ADDR)
        self.assertEqual(obj.device, self.DEVICE)
        self.assertEqual(obj.location, self.LOCATION)

    def test_session_delete_view(self):
        admin_login_url = reverse('admin:login')
        session_list_url = reverse('safety:session_list')

        # Let's delete our current session.
        # We must be redirected to login page.

        self.login_user()
        session = Session.objects.first()

        r = self.client.post(reverse('safety:session_delete', kwargs={'pk': session.pk}), follow=True)
        self.assertRedirects(r, '%s?next=%s' % (admin_login_url, session_list_url))
        self.assertEqual(Session.objects.count(), 0)

        self.client.logout()

        # Let's delete an other session, not our current one.
        # We must be redirected to session list.

        fake_sessions = self.create_fake_sessions()
        self.assertEqual(Session.objects.count(), 10)

        self.login_user()

        sessions_count = Session.objects.count()
        self.assertEqual(sessions_count, 11)

        for session in fake_sessions:
            r = self.client.post(reverse('safety:session_delete', kwargs={'pk': session.pk}), follow=True)
            self.assertRedirects(r, session_list_url)

        self.assertEqual(Session.objects.count(), 1)

    def test_session_delete_other_view(self):
        session_list_url = reverse('safety:session_list')

        fake_sessions = self.create_fake_sessions()
        self.assertEqual(Session.objects.count(), 10)

        self.login_user()
        self.assertEqual(Session.objects.count(), 11)

        r = self.client.post(reverse('safety:session_delete_other'), follow=True)
        self.assertRedirects(r, session_list_url)
        self.assertEqual(Session.objects.count(), 1)
        current_session = Session.objects.get(session_key=self.client.session.session_key)

        r = self.client.get(reverse('safety:session_list'))
        self.assertEqual(r.status_code, 200)
        self.assertTrue(self.client.session.exists(session_key=current_session.session_key))

        for session in fake_sessions:
            self.assertFalse(self.client.session.exists(session_key=session.session_key))


class PasswordChangeViewsTest(BaseTestCase):
    def test_password_change(self):
        self.login_user()

        r = self.client.get(reverse('safety:password_change'))
        self.assertTemplateUsed(r, 'safety/password_change/form.html')
        self.assertEqual(r.status_code, 200)

        self.client.logout()

    def test_password_change_done(self):
        self.login_user()

        r = self.client.get(reverse('safety:password_change_done'))
        self.assertTemplateUsed(r, 'safety/password_change/done.html')
        self.assertEqual(r.status_code, 200)

        self.client.logout()

    def test_workflow(self):
        change_form_url = reverse('safety:password_change')

        # We don't have any PasswordChange instance yet.
        # Because we just plugged the app.
        self.assertEqual(PasswordChange.objects.count(), 0)

        # Let's login user.
        self.login_user()

        # We still have no instance.
        self.assertEqual(PasswordChange.objects.count(), 0)

        # Let's create an instance for a given user.
        # By default, "required" field is set to False.
        # So nothing changes.
        pr, created = PasswordChange.objects.get_or_create_for_user(self.user)
        self.assertTrue(created)
        self.assertFalse(pr.required)

        # Let's logout user and login again, than go elsewhere
        # on the site. As we don't set required to True yet,
        # nothing changes again.
        self.client.logout()
        self.login_user()

        r = self.client.get(reverse('home'))
        self.assertEqual(r.status_code, 200)

        r = self.client.get(reverse('admin:index'))
        self.assertEqual(r.status_code, 200)

        # Time to force user to reset its password.
        pr.required = True
        pr.save()

        # Now, it should be effictive because we use the decorator
        # (the middleware or the decorator provides the same behavior).
        # So we must be redirected to password change form. No other choice.
        r = self.client.get(reverse('home'))
        self.assertRedirects(r, change_form_url)

        r = self.client.get(reverse('admin:index'))
        self.assertRedirects(r, change_form_url)

        # Now, ok. User is stuck. Time to change password.
        # Let's change password!

        r = self.client.post(
            reverse('safety:password_change'),
            data={
                'old_password': self.USER_PASSWORD,
                'new_password1': 'superpassword',
                'new_password2': 'superpassword',
            })

        self.assertRedirects(r, reverse('safety:password_change_done'))

        # Done. New password.
        #
        # A few days later, we required user to change its password again.
        # But! We do not accept the old one again. User must provide a
        # different new password. Let's check it.
        pr, created = PasswordChange.objects.get_or_create_for_user(self.user)
        self.assertFalse(created)

        # As we changed our last password, required field must be now set to False.
        self.assertFalse(pr.required)

        # Let's switch.
        pr.required = True
        pr.save()

        # Login with new password.
        self.login_user(password='superpassword')

        # Boom! Redirected.
        r = self.client.get(reverse('home'))
        self.assertRedirects(r, change_form_url)

        # Let's reload our fixture.
        self.reload()

        # Let's try to reset our password with the same as old one.
        r = self.client.post(
            reverse('safety:password_change'),
            data={
                'old_password': 'superpassword',
                'new_password1': 'superpassword',
                'new_password2': 'superpassword',
            })

        # Boom! Error. Not authorized.
        self.assertFormError(r, 'form', 'new_password2', PasswordChangeForm.error_messages['same_password'])

        # OK. Choose a new one.
        r = self.client.post(
            reverse('safety:password_change'),
            data={
                'old_password': 'superpassword',
                'new_password1': 'ournewpassword',
                'new_password2': 'ournewpassword',
            })

        # Everything is OK.
        self.assertRedirects(r, reverse('safety:password_change_done'))

        # Login with new password.
        self.login_user(password='ournewpassword')

        # No redirect. Everything is OK.
        r = self.client.get(reverse('home'))
        self.assertEqual(r.status_code, 200)


================================================
FILE: safety/tests/urls.py
================================================
# -*- coding: utf-8 -*-
from django.conf.urls import include, url
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect

from safety.models import PasswordChange


def home(request):
    return render(request, 'home.html')


def update_password(request):
    if request.user.is_authenticated():
        pr, created = PasswordChange.objects.get_or_create_for_user(request.user)
        pr.required = True
        pr.save()
    return redirect(reverse('home'))


urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^account/', include('django.contrib.auth.urls')),
    url(r'^security/', include('safety.urls', namespace='safety')),
    url(r'^update-password/$', update_password, name='update-password'),
    url(r'^$', home, name='home'),
]


================================================
FILE: safety/urls.py
================================================
# -*- coding: utf-8 -*-
from django.conf.urls import url

from . import views


urlpatterns = [
    url(
        r'^sessions/$',
        views.SessionListView.as_view(),
        name='session_list'),

    url(
        r'^sessions/other/delete/$',
        views.SessionDeleteOtherView.as_view(),
        name='session_delete_other'),

    url(
        r'^sessions/(?P<pk>\w+)/delete/$',
        views.SessionDeleteView.as_view(),
        name='session_delete'),

    url(
        r'^password-change/$',
        views.password_change,
        name='password_change'),

    url(
        r'^password-change/done/$',
        views.password_change_done,
        name='password_change_done'),
]


================================================
FILE: safety/utils.py
================================================
# -*- coding: utf-8 -*-
from django.conf import settings

from .compat import import_module


def get_session_store():
    mod = getattr(settings, 'SESSION_ENGINE', 'django.contrib.sessions.backends.db')
    engine = import_module(mod)
    store = engine.SessionStore()
    return store


def resolve(path, request):
    obj = import_from_path(path)
    return obj(request)


def import_from_path(path):
    try:
        module_path, attr = path.rsplit('.', 1)
        module = import_module(module_path)
        obj = getattr(module, attr)
    except Exception as e:
        raise e
    return obj


================================================
FILE: safety/views.py
================================================
# -*- coding: utf-8 -*-

try:
    from urllib.parse import urlparse, urlunparse
except ImportError:  # pragma: no cover
    # Python 2 fallback
    from urlparse import urlparse, urlunparse  # noqa

from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import login_required
from django.contrib.auth.tokens import default_token_generator
from django.core.urlresolvers import reverse, reverse_lazy
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import ListView, DeleteView, View
from django.views.generic.edit import DeletionMixin

from . import app_settings
from . import forms
from . import utils

from .mixins import SessionMixin


LoginRequiredMixin = utils.import_from_path(
    app_settings.LOGIN_REQUIRED_MIXIN_CLASS)


# -----------------------------------------------------------------------------
# Sessions
# -----------------------------------------------------------------------------

class SessionListView(LoginRequiredMixin, SessionMixin, ListView):
    pass


class SessionDeleteView(LoginRequiredMixin, SessionMixin, DeleteView):
    def get_success_url(self):
        return str(reverse_lazy('safety:session_list'))


class SessionDeleteOtherView(LoginRequiredMixin, SessionMixin, DeletionMixin, View):
    def get_object(self):
        qs = super(SessionDeleteOtherView, self).get_queryset()
        qs = qs.exclude(session_key=self.request.session.session_key)
        return qs

    def get_success_url(self):
        return str(reverse_lazy('safety:session_list'))


# -----------------------------------------------------------------------------
# Password Change
# -----------------------------------------------------------------------------

@sensitive_post_parameters()
@csrf_protect
@login_required
def password_change(request):
    return auth_views.password_change(
        request=request,
        template_name='safety/password_change/form.html',
        post_change_redirect=reverse('safety:password_change_done'),
        password_change_form=forms.PasswordChangeForm)


@login_required
def password_change_done(request):
    return auth_views.password_change_done(
        request=request,
        template_name='safety/password_change/done.html')


================================================
FILE: setup.py
================================================
# -*- coding: utf-8 -*-
import os
import sys

from setuptools import setup, find_packages

root = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(root, 'README.rst')) as f:
    readme = f.read()

version = __import__('safety').__version__

install_requires = [
    'six',
    'ua-parser',
    'geoip2'
]

setup(
    name='django-safety',
    version=version,
    description='Generic Django application for safer user accounts',
    long_description=readme,
    author='Gilles Fabio',
    author_email='gilles.fabio@gmail.com',
    url='http://github.com/ulule/django-safety',
    packages=find_packages(),
    zip_safe=False,
    include_package_data=True,
    install_requires=install_requires,
    classifiers=[
        'Environment :: Web Environment',
        'Framework :: Django',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: MIT License',
        'Operating System :: OS Independent',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
    ]
)


================================================
FILE: tox.ini
================================================
[tox]
skipsdist = True
envlist =
    {py27,py34,py35}-django{18,19}
    {py27,py34,py35}-django19nogeoip2

[testenv]
basepython =
    py27: python2.7
    py34: python3.4
    py35: python3.5

deps =
    -r{toxinidir}/requirements/tox.txt
    {py27,py34,py35}-django18: Django>=1.8,<1.9
    {py27,py34,py35}-django19: Django>=1.9,<1.10
    {py27,py34,py35}-django19: geoip2
    {py27,py34,py35}-django19nogeoip2: Django>=1.9,<1.10

setenv =
    PYTHONPATH = {toxinidir}
whitelist_externals =
    make
changedir = {toxinidir}
commands =
    make geoip
    make test

[testenv:py27-django19nogeoip2]
setenv =
    GEOIP_PATH = {toxinidir}/data/geoip

[testenv:py34-django19nogeoip2]
setenv =
    GEOIP_PATH = {toxinidir}/data/geoip

[testenv:py35-django19nogeoip2]
setenv =
    GEOIP_PATH = {toxinidir}/data/geoip
Download .txt
gitextract_rc3yltz7/

├── .coveragerc
├── .editorconfig
├── .gitignore
├── .travis.yml
├── AUTHORS
├── LICENSE
├── Makefile
├── README.rst
├── data/
│   ├── .gitkeep
│   ├── geoip/
│   │   └── .gitkeep
│   └── geoip2/
│       └── .gitkeep
├── example/
│   ├── __init__.py
│   ├── models.py
│   ├── settings.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── home.html
│   │   └── safety/
│   │       ├── password_change/
│   │       │   └── base.html
│   │       └── session_list.html
│   └── urls.py
├── manage.py
├── pytest.ini
├── requirements/
│   ├── base.txt
│   ├── development.txt
│   ├── test.txt
│   └── tox.txt
├── safety/
│   ├── __init__.py
│   ├── admin.py
│   ├── app_settings.py
│   ├── apps.py
│   ├── compat.py
│   ├── decorators.py
│   ├── forms.py
│   ├── management/
│   │   ├── __init__.py
│   │   └── commands/
│   │       ├── __init__.py
│   │       └── clean_safety_sessions.py
│   ├── managers.py
│   ├── middleware.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── mixins.py
│   ├── models.py
│   ├── resolvers.py
│   ├── templates/
│   │   └── safety/
│   │       └── password_change/
│   │           ├── base.html
│   │           ├── done.html
│   │           └── form.html
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── models.py
│   │   ├── settings.py
│   │   ├── test_models.py
│   │   ├── test_views.py
│   │   └── urls.py
│   ├── urls.py
│   ├── utils.py
│   └── views.py
├── setup.py
└── tox.ini
Download .txt
SYMBOL INDEX (72 symbols across 18 files)

FILE: example/urls.py
  function home (line 10) | def home(request):
  function update_password (line 14) | def update_password(request):

FILE: safety/admin.py
  class PasswordChangeAdmin (line 11) | class PasswordChangeAdmin(admin.ModelAdmin):
  class SessionAdmin (line 16) | class SessionAdmin(admin.ModelAdmin):
    method is_valid (line 20) | def is_valid(self, obj):

FILE: safety/apps.py
  class SafetyConfig (line 5) | class SafetyConfig(AppConfig):
    method ready (line 9) | def ready(self):

FILE: safety/decorators.py
  function password_change_required (line 13) | def password_change_required(func):

FILE: safety/forms.py
  class PasswordChangeForm (line 9) | class PasswordChangeForm(BasePasswordChangeForm):
    method clean_new_password2 (line 14) | def clean_new_password2(self):
    method save (line 24) | def save(self, *args, **kwargs):

FILE: safety/management/commands/clean_safety_sessions.py
  class Command (line 8) | class Command(BaseCommand):
    method handle (line 9) | def handle(self, *args, **options):

FILE: safety/managers.py
  class PasswordChangeManager (line 9) | class PasswordChangeManager(models.Manager):
    method get_or_create_for_user (line 10) | def get_or_create_for_user(self, user):
    method is_required_for_user (line 13) | def is_required_for_user(self, user):
  class SessionManager (line 18) | class SessionManager(models.Manager):
    method active (line 19) | def active(self, user=None):
    method create_session (line 25) | def create_session(self, request, user):

FILE: safety/middleware.py
  class PasswordChangeMiddleware (line 5) | class PasswordChangeMiddleware(object):
    method process_view (line 6) | def process_view(self, request, view_func, view_args, view_kwargs):

FILE: safety/migrations/0001_initial.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: safety/mixins.py
  class SessionMixin (line 8) | class SessionMixin(object):
    method get_queryset (line 9) | def get_queryset(self):
  class LoginRequiredMixin (line 13) | class LoginRequiredMixin(object):
    method dispatch (line 15) | def dispatch(self, request, *args, **kwargs):

FILE: safety/models.py
  class PasswordChange (line 18) | class PasswordChange(models.Model):
    class Meta (line 29) | class Meta:
    method __str__ (line 33) | def __str__(self):
  class Session (line 38) | class Session(models.Model):
    class Meta (line 51) | class Meta:
    method __str__ (line 55) | def __str__(self):
    method delete_store_session (line 58) | def delete_store_session(self):
  function create_session (line 72) | def create_session(sender, request, user, **kwargs):
  function deactivate_session (line 77) | def deactivate_session(sender, request, user, **kwargs):
  function post_delete_session (line 88) | def post_delete_session(sender, instance, **kwargs):

FILE: safety/resolvers.py
  function remote_addr_ip (line 11) | def remote_addr_ip(request):
  function x_forwarded_ip (line 18) | def x_forwarded_ip(request):
  function real_ip (line 34) | def real_ip(request):
  function device (line 41) | def device(request):
  function location (line 68) | def location(request):

FILE: safety/tests/base.py
  class Fixtures (line 14) | class Fixtures(Exam):
    method user (line 25) | def user(self):
    method reload (line 31) | def reload(self):
  class BaseTestCase (line 35) | class BaseTestCase(Fixtures, TestCase):
    method login_user (line 36) | def login_user(self, password=None):
    method create_fake_sessions (line 50) | def create_fake_sessions(self):

FILE: safety/tests/test_models.py
  class ModelsTest (line 10) | class ModelsTest(BaseTestCase):
    method test_session (line 11) | def test_session(self):
    method test_password_change (line 22) | def test_password_change(self):

FILE: safety/tests/test_views.py
  class SessionsViewsTest (line 10) | class SessionsViewsTest(BaseTestCase):
    method tearDown (line 11) | def tearDown(self, *args, **kwargs):
    method test_session_list_view (line 14) | def test_session_list_view(self):
    method test_session_delete_view (line 31) | def test_session_delete_view(self):
    method test_session_delete_other_view (line 64) | def test_session_delete_other_view(self):
  class PasswordChangeViewsTest (line 86) | class PasswordChangeViewsTest(BaseTestCase):
    method test_password_change (line 87) | def test_password_change(self):
    method test_password_change_done (line 96) | def test_password_change_done(self):
    method test_workflow (line 105) | def test_workflow(self):

FILE: safety/tests/urls.py
  function home (line 10) | def home(request):
  function update_password (line 14) | def update_password(request):

FILE: safety/utils.py
  function get_session_store (line 7) | def get_session_store():
  function resolve (line 14) | def resolve(path, request):
  function import_from_path (line 19) | def import_from_path(path):

FILE: safety/views.py
  class SessionListView (line 34) | class SessionListView(LoginRequiredMixin, SessionMixin, ListView):
  class SessionDeleteView (line 38) | class SessionDeleteView(LoginRequiredMixin, SessionMixin, DeleteView):
    method get_success_url (line 39) | def get_success_url(self):
  class SessionDeleteOtherView (line 43) | class SessionDeleteOtherView(LoginRequiredMixin, SessionMixin, DeletionM...
    method get_object (line 44) | def get_object(self):
    method get_success_url (line 49) | def get_success_url(self):
  function password_change (line 60) | def password_change(request):
  function password_change_done (line 69) | def password_change_done(request):
Condensed preview — 57 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (60K chars).
[
  {
    "path": ".coveragerc",
    "chars": 77,
    "preview": "[run]\ninclude =\n    safety/models.py\n    safety/utils.py\n    safety/views.py\n"
  },
  {
    "path": ".editorconfig",
    "chars": 272,
    "preview": "# editorconfig.org\nroot = true\n\n[*]\ncharset                  = utf-8\nend_of_line              = lf\nindent_size          "
  },
  {
    "path": ".gitignore",
    "chars": 359,
    "preview": "__pycache__/\n*.py[cod]\n*.so\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.eg"
  },
  {
    "path": ".travis.yml",
    "chars": 348,
    "preview": "language: python\nenv:\n    - TOXENV=py27-django18\n    - TOXENV=py34-django18\n    - TOXENV=py35-django18\n    - TOXENV=py27"
  },
  {
    "path": "AUTHORS",
    "chars": 124,
    "preview": "Gilles Fabio <gilles.fabio@gmail.com>\nLouise Grandjonc <louise.grandjonc@gmail.com>\nFlorent Messa <florent.messa@gmail.c"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Ulule\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "Makefile",
    "chars": 1230,
    "preview": "devenv:\n\tvirtualenv -p python2.7 `pwd`/.venv\n\t. .venv/bin/activate && pip install -r requirements/development.txt\n\nclean"
  },
  {
    "path": "README.rst",
    "chars": 7430,
    "preview": "django-safety\n=============\n\n.. image:: https://secure.travis-ci.org/ulule/django-safety.png?branch=master\n    :alt: Bui"
  },
  {
    "path": "data/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "data/geoip/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "data/geoip2/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "example/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "example/models.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "example/settings.py",
    "chars": 2434,
    "preview": "import os\n\nimport django\n\n\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\nSECRET_KEY = 'zn=kw41"
  },
  {
    "path": "example/templates/base.html",
    "chars": 1417,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <title>{% block title %}{% endblock %}</title>\n        <meta name=\"viewport\" c"
  },
  {
    "path": "example/templates/home.html",
    "chars": 99,
    "preview": "{% extends \"base.html\" %}\n\n{% block content %}\n    <p>I'm the homepage.</p>\n{% endblock content %}\n"
  },
  {
    "path": "example/templates/safety/password_change/base.html",
    "chars": 26,
    "preview": "{% extends \"base.html\" %}\n"
  },
  {
    "path": "example/templates/safety/session_list.html",
    "chars": 2117,
    "preview": "{% extends \"base.html\" %}\n{% load i18n %}\n\n{% block content %}\n    <h1>{% trans \"Active Sessions\" %}</h1>\n    <table cla"
  },
  {
    "path": "example/urls.py",
    "chars": 823,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.conf.urls import url, include\nfrom django.contrib import admin\nfrom django.core.urlr"
  },
  {
    "path": "manage.py",
    "chars": 538,
    "preview": "#!/usr/bin/env python\nimport os\nimport sys\n\nSUPPORTED_ENVS = (\n    'test',\n    'example',\n)\n\nSETTINGS_MODULES = {\n    't"
  },
  {
    "path": "pytest.ini",
    "chars": 54,
    "preview": "[pytest]\nDJANGO_SETTINGS_MODULE=safety.tests.settings\n"
  },
  {
    "path": "requirements/base.txt",
    "chars": 21,
    "preview": "six\nua-parser\ngeoip2\n"
  },
  {
    "path": "requirements/development.txt",
    "chars": 43,
    "preview": "-r base.txt\n-r test.txt\n\nDjango\nflake8\ntox\n"
  },
  {
    "path": "requirements/test.txt",
    "chars": 37,
    "preview": "pytest\npytest-cov\npytest-django\nexam\n"
  },
  {
    "path": "requirements/tox.txt",
    "chars": 27,
    "preview": "six\nua-parser\n\n-r test.txt\n"
  },
  {
    "path": "safety/__init__.py",
    "chars": 192,
    "preview": "# -*- coding: utf-8 -*-\nversion = (0, 1, 0)\n\n__version__ = '.'.join(map(str, version))\n\ndefault_app_config = 'safety.app"
  },
  {
    "path": "safety/admin.py",
    "chars": 648,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.contrib import admin\nfrom django.utils.timezone import now\n\nfrom .models import (\n  "
  },
  {
    "path": "safety/app_settings.py",
    "chars": 935,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.conf import settings\n\n\n# The Python path to the login_required mixin class applied t"
  },
  {
    "path": "safety/apps.py",
    "chars": 203,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.apps import AppConfig\n\n\nclass SafetyConfig(AppConfig):\n    name = 'safety'\n    verbo"
  },
  {
    "path": "safety/compat.py",
    "chars": 453,
    "preview": "# -*- coding: utf-8 -*-\nimport django\n\n\nif django.VERSION >= (1, 9):\n    from importlib import import_module  # noqa\n\n  "
  },
  {
    "path": "safety/decorators.py",
    "chars": 712,
    "preview": "# -*- coding: utf-8 -*-\nfrom functools import wraps\n\nfrom django.core.urlresolvers import reverse\nfrom django.shortcuts "
  },
  {
    "path": "safety/forms.py",
    "chars": 1009,
    "preview": "# -*- coding: utf-8 -*-\nfrom django import forms\nfrom django.contrib.auth.forms import PasswordChangeForm as BasePasswor"
  },
  {
    "path": "safety/management/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "safety/management/commands/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "safety/management/commands/clean_safety_sessions.py",
    "chars": 360,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.core.management.base import BaseCommand\n\nfrom django.utils.timezone import now\nfrom "
  },
  {
    "path": "safety/managers.py",
    "chars": 1716,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.db import models, transaction, IntegrityError\nfrom django.utils.timezone import now\n"
  },
  {
    "path": "safety/middleware.py",
    "chars": 274,
    "preview": "# -*- coding: utf-8 -*-\nfrom .decorators import password_change_required\n\n\nclass PasswordChangeMiddleware(object):\n    d"
  },
  {
    "path": "safety/migrations/0001_initial.py",
    "chars": 2389,
    "preview": "# -*- coding: utf-8 -*-\n# Generated by Django 1.9.2 on 2016-03-16 12:51\nfrom __future__ import unicode_literals\n\nfrom dj"
  },
  {
    "path": "safety/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "safety/mixins.py",
    "chars": 490,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.contrib.auth.decorators import login_required\nfrom django.utils.decorators import me"
  },
  {
    "path": "safety/models.py",
    "chars": 3163,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.conf import settings\n\nfrom django.contrib.auth.signals import (\n    user_logged_in,\n"
  },
  {
    "path": "safety/resolvers.py",
    "chars": 2343,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.utils.translation import ugettext\n\nfrom ua_parser import user_agent_parser\n\nfrom . i"
  },
  {
    "path": "safety/templates/safety/password_change/base.html",
    "chars": 85,
    "preview": "{% block title %}\n    {{ title }}\n{% endblock %}\n\n{% block content %}\n{% endblock %}\n"
  },
  {
    "path": "safety/templates/safety/password_change/done.html",
    "chars": 193,
    "preview": "{% extends \"safety/password_change/base.html\" %}\n{% load i18n %}\n\n{% block title %}{{ title }}{% endblock %}\n\n{% block c"
  },
  {
    "path": "safety/templates/safety/password_change/form.html",
    "chars": 1840,
    "preview": "{% extends \"safety/password_change/base.html\" %}\n{% load i18n %}\n\n{% block title %}{{ title }}{% endblock %}\n\n{% block c"
  },
  {
    "path": "safety/tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "safety/tests/base.py",
    "chars": 1988,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.contrib.auth.models import User\nfrom django.core.urlresolvers import reverse\nfrom dj"
  },
  {
    "path": "safety/tests/models.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "safety/tests/settings.py",
    "chars": 2433,
    "preview": "import os\n\nimport django\n\n\nBASE_DIR = os.path.dirname(os.path.dirname((os.path.dirname(os.path.abspath(__file__)))))\n\nSE"
  },
  {
    "path": "safety/tests/test_models.py",
    "chars": 843,
    "preview": "# -*- coding: utf-8 -*-\nfrom .base import BaseTestCase\n\nfrom safety.models import (\n    PasswordChange,\n    Session,\n)\n\n"
  },
  {
    "path": "safety/tests/test_views.py",
    "chars": 7501,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.core.urlresolvers import reverse\n\nfrom safety.forms import PasswordChangeForm\nfrom s"
  },
  {
    "path": "safety/tests/urls.py",
    "chars": 832,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.conf.urls import include, url\nfrom django.contrib import admin\nfrom django.core.urlr"
  },
  {
    "path": "safety/urls.py",
    "chars": 688,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.conf.urls import url\n\nfrom . import views\n\n\nurlpatterns = [\n    url(\n        r'^sess"
  },
  {
    "path": "safety/utils.py",
    "chars": 599,
    "preview": "# -*- coding: utf-8 -*-\nfrom django.conf import settings\n\nfrom .compat import import_module\n\n\ndef get_session_store():\n "
  },
  {
    "path": "safety/views.py",
    "chars": 2372,
    "preview": "# -*- coding: utf-8 -*-\n\ntry:\n    from urllib.parse import urlparse, urlunparse\nexcept ImportError:  # pragma: no cover\n"
  },
  {
    "path": "setup.py",
    "chars": 1189,
    "preview": "# -*- coding: utf-8 -*-\nimport os\nimport sys\n\nfrom setuptools import setup, find_packages\n\nroot = os.path.abspath(os.pat"
  },
  {
    "path": "tox.ini",
    "chars": 809,
    "preview": "[tox]\nskipsdist = True\nenvlist =\n    {py27,py34,py35}-django{18,19}\n    {py27,py34,py35}-django19nogeoip2\n\n[testenv]\nbas"
  }
]

About this extraction

This page contains the full source code of the ulule/django-safety GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 57 files (53.5 KB), approximately 13.5k tokens, and a symbol index with 72 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!