1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 07:27:04 +00:00
This commit is contained in:
zhengxie
2016-07-15 10:47:32 +08:00
parent 126da528b5
commit 41f9a3950c
24 changed files with 1180 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
"""Django Terms and Conditions Module"""
from __future__ import unicode_literals

View File

@@ -0,0 +1,25 @@
"""Django Admin Site configuration"""
# pylint: disable=R0904
from django.contrib import admin
from .models import TermsAndConditions, UserTermsAndConditions
class TermsAndConditionsAdmin(admin.ModelAdmin):
"""Sets up the custom Terms and Conditions admin display"""
list_display = ('slug', 'name', 'date_active', 'version_number',)
verbose_name = "Terms and Conditions"
class UserTermsAndConditionsAdmin(admin.ModelAdmin):
"""Sets up the custom User Terms and Conditions admin display"""
#fields = ('terms', 'user', 'date_accepted', 'ip_address',)
readonly_fields = ('date_accepted',)
list_display = ('terms', 'user', 'date_accepted', 'ip_address',)
date_hierarchy = 'date_accepted'
list_select_related = True
admin.site.register(TermsAndConditions, TermsAndConditionsAdmin)
admin.site.register(UserTermsAndConditions, UserTermsAndConditionsAdmin)

View File

@@ -0,0 +1,8 @@
"""Django Apps Config"""
from django.apps import AppConfig
class TermsAndConditionsConfig(AppConfig):
name = 'termsandconditions'
verbose_name = "Terms and Conditions"

View File

@@ -0,0 +1,31 @@
"""View Decorators for termsandconditions module"""
try:
from urllib.parse import urlparse, urlunparse
except ImportError:
from urlparse import urlparse, urlunparse
from functools import wraps
from django.http import HttpResponseRedirect, QueryDict
from django.utils.decorators import available_attrs
from .models import TermsAndConditions
from .middleware import ACCEPT_TERMS_PATH
def terms_required(view_func):
"""
This decorator checks to see if the user is logged in, and if so, if they have accepted the site terms.
"""
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
"""Method to wrap the view passed in"""
if not request.user.is_authenticated() or TermsAndConditions.agreed_to_latest(request.user):
return view_func(request, *args, **kwargs)
currentPath = request.path
login_url_parts = list(urlparse(ACCEPT_TERMS_PATH))
querystring = QueryDict(login_url_parts[4], mutable=True)
querystring['returnTo'] = currentPath
login_url_parts[4] = querystring.urlencode(safe='/')
return HttpResponseRedirect(urlunparse(login_url_parts))
return _wrapped_view

View File

@@ -0,0 +1,26 @@
"""Django forms for the termsandconditions application"""
# pylint: disable=E1120,W0613
from django import forms
from termsandconditions.models import UserTermsAndConditions, TermsAndConditions
class UserTermsAndConditionsModelForm(forms.ModelForm):
"""Form used when accepting Terms and Conditions - returnTo is used to catch where to end up."""
returnTo = forms.CharField(required=False, initial="/", widget=forms.HiddenInput())
class Meta(object):
"""Configuration for this Modelform"""
model = UserTermsAndConditions
exclude = ('date_accepted', 'ip_address', 'user')
widgets = {'terms': forms.HiddenInput()}
class EmailTermsForm(forms.Form):
"""Form used to collect email address to send terms and conditions to."""
email_subject = forms.CharField(widget=forms.HiddenInput())
email_address = forms.EmailField()
returnTo = forms.CharField(required=False, initial="/", widget=forms.HiddenInput())
terms = forms.ModelChoiceField(queryset=TermsAndConditions.objects.all(), widget=forms.HiddenInput())

View File

@@ -0,0 +1,55 @@
"""Terms and Conditions Middleware"""
from .models import TermsAndConditions
from django.conf import settings
import logging
from .pipeline import redirect_to_terms_accept
LOGGER = logging.getLogger(name='termsandconditions')
ACCEPT_TERMS_PATH = getattr(settings, 'ACCEPT_TERMS_PATH', '/terms/accept/')
TERMS_EXCLUDE_URL_PREFIX_LIST = getattr(settings, 'TERMS_EXCLUDE_URL_PREFIX_LIST', {'/admin', '/terms'})
TERMS_EXCLUDE_URL_LIST = getattr(settings, 'TERMS_EXCLUDE_URL_LIST', {'/', '/termsrequired/', '/logout/', '/securetoo/'})
class TermsAndConditionsRedirectMiddleware(object):
"""
This middleware checks to see if the user is logged in, and if so,
if they have accepted the site terms.
"""
def process_request(self, request):
"""Process each request to app to ensure terms have been accepted"""
LOGGER.debug('termsandconditions.middleware')
current_path = request.META['PATH_INFO']
protected_path = is_path_protected(current_path)
if request.user.is_authenticated() and protected_path:
for term in TermsAndConditions.get_active_list():
if not TermsAndConditions.agreed_to_latest(request.user, term):
return redirect_to_terms_accept(current_path, term)
return None
def is_path_protected(path):
"""
returns True if given path is to be protected, otherwise False
The path is not to be protected when it appears on:
TERMS_EXCLUDE_URL_PREFIX_LIST, TERMS_EXCLUDE_URL_LIST or as
ACCEPT_TERMS_PATH
"""
protected = True
for exclude_path in TERMS_EXCLUDE_URL_PREFIX_LIST:
if path.startswith(exclude_path):
protected = False
if path in TERMS_EXCLUDE_URL_LIST:
protected = False
if path.startswith(ACCEPT_TERMS_PATH):
protected = False
return protected

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TermsAndConditions',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('slug', models.SlugField(default=b'site-terms')),
('name', models.TextField(max_length=255)),
('version_number', models.DecimalField(default=1.0, max_digits=6, decimal_places=2)),
('text', models.TextField(null=True, blank=True)),
('date_active', models.DateTimeField(help_text=b'Leave Null To Never Make Active', null=True, blank=True)),
('date_created', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-date_active'],
'get_latest_by': 'date_active',
'verbose_name': 'Terms and Conditions',
'verbose_name_plural': 'Terms and Conditions',
},
),
migrations.CreateModel(
name='UserTermsAndConditions',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('ip_address', models.GenericIPAddressField(null=True, verbose_name=b'IP Address', blank=True)),
('date_accepted', models.DateTimeField(auto_now_add=True, verbose_name=b'Date Accepted')),
('terms', models.ForeignKey(related_name='userterms', to='termsandconditions.TermsAndConditions')),
('user', models.ForeignKey(related_name='userterms', to=settings.AUTH_USER_MODEL)),
],
options={
'get_latest_by': 'date_accepted',
'verbose_name': 'User Terms and Conditions',
'verbose_name_plural': 'User Terms and Conditions',
},
),
migrations.AddField(
model_name='termsandconditions',
name='users',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, through='termsandconditions.UserTermsAndConditions', blank=True),
),
migrations.AlterUniqueTogether(
name='usertermsandconditions',
unique_together=set([('user', 'terms')]),
),
]

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('termsandconditions', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='termsandconditions',
name='info',
field=models.TextField(
help_text=b"Provide users with some info about "
b"what's changed and why", null=True, blank=True),
preserve_default=True,
),
]

View File

@@ -0,0 +1,122 @@
"""Django Models for TermsAndConditions App"""
# pylint: disable=C1001,E0202,W0613
from collections import OrderedDict
from django.db import models
from django.conf import settings
from django.http import Http404
from django.utils import timezone
import logging
LOGGER = logging.getLogger(name='termsandconditions')
DEFAULT_TERMS_SLUG = getattr(settings, 'DEFAULT_TERMS_SLUG', 'site-terms')
class UserTermsAndConditions(models.Model):
"""Holds mapping between TermsAndConditions and Users"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="userterms")
terms = models.ForeignKey("TermsAndConditions", related_name="userterms")
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP Address')
date_accepted = models.DateTimeField(auto_now_add=True, verbose_name='Date Accepted')
class Meta:
"""Model Meta Information"""
get_latest_by = 'date_accepted'
verbose_name = 'User Terms and Conditions'
verbose_name_plural = 'User Terms and Conditions'
unique_together = ('user', 'terms',)
class TermsAndConditions(models.Model):
"""Holds Versions of TermsAndConditions
Active one for a given slug is: date_active is not Null and is latest not in future"""
slug = models.SlugField(default=DEFAULT_TERMS_SLUG)
name = models.TextField(max_length=255)
users = models.ManyToManyField(settings.AUTH_USER_MODEL, through=UserTermsAndConditions, blank=True)
version_number = models.DecimalField(default=1.0, decimal_places=2, max_digits=6)
text = models.TextField(null=True, blank=True)
info = models.TextField(null=True, blank=True, help_text="Provide users with some info about what's changed and why")
date_active = models.DateTimeField(blank=True, null=True, help_text="Leave Null To Never Make Active")
date_created = models.DateTimeField(blank=True, auto_now_add=True)
class Meta:
"""Model Meta Information"""
ordering = ['-date_active', ]
get_latest_by = 'date_active'
verbose_name = 'Terms and Conditions'
verbose_name_plural = 'Terms and Conditions'
def __str__(self):
return "{0}-{1:.2f}".format(self.slug, self.version_number)
@models.permalink
def get_absolute_url(self):
return ('tc_view_specific_version_page', [self.slug, self.version_number]) # pylint: disable=E1101
@staticmethod
def create_default_terms():
"""Create a default TermsAndConditions Object"""
default_terms = TermsAndConditions.objects.create(
slug=DEFAULT_TERMS_SLUG,
name=DEFAULT_TERMS_SLUG,
date_active=timezone.now(),
version_number=1,
text=DEFAULT_TERMS_SLUG + " Text. CHANGE ME.")
return default_terms
@staticmethod
def get_active(slug=DEFAULT_TERMS_SLUG):
"""Finds the latest of a particular terms and conditions"""
try:
active_terms = TermsAndConditions.objects.filter(
date_active__isnull=False,
date_active__lte=timezone.now(),
slug=slug).latest('date_active')
except TermsAndConditions.DoesNotExist:
if slug == DEFAULT_TERMS_SLUG:
active_terms = TermsAndConditions.create_default_terms()
else: # pragma: nocover
raise Http404
return active_terms
@staticmethod
def get_active_list():
"""Finds the latest of all terms and conditions"""
terms_list = {}
try:
all_terms_list = TermsAndConditions.objects.filter(
date_active__isnull=False,
date_active__lte=timezone.now()).order_by('slug')
for term in all_terms_list:
terms_list.update({term.slug: TermsAndConditions.get_active(slug=term.slug)})
except TermsAndConditions.DoesNotExist: # pragma: nocover
terms_list.update({DEFAULT_TERMS_SLUG: TermsAndConditions.create_default_terms()})
terms_list = OrderedDict(sorted(terms_list.items(), key=lambda t: t[0]))
return terms_list
@staticmethod
def agreed_to_latest(user, slug=DEFAULT_TERMS_SLUG):
"""Checks to see if a specified user has agreed to the latest of a particular terms and conditions"""
try:
UserTermsAndConditions.objects.get(user=user, terms=TermsAndConditions.get_active(slug))
return True
except UserTermsAndConditions.MultipleObjectsReturned: # pragma: nocover
return True
except UserTermsAndConditions.DoesNotExist:
return False
@staticmethod
def agreed_to_terms(user, terms=None):
"""Checks to see if a specified user has agreed to a specific terms and conditions"""
try:
UserTermsAndConditions.objects.get(user=user, terms=terms)
return True
except UserTermsAndConditions.DoesNotExist:
return False

View File

@@ -0,0 +1,40 @@
"""This file contains functions used as part of a user creation pipeline, such as django-social-auth."""
# pylint: disable=W0613
try:
from urllib.parse import urlparse, urlunparse
except ImportError:
from urlparse import urlparse, urlunparse
from .models import TermsAndConditions
from django.http import HttpResponseRedirect, QueryDict
from django.conf import settings
from django.core.urlresolvers import reverse
import logging
ACCEPT_TERMS_PATH = getattr(settings, 'ACCEPT_TERMS_PATH', '/terms/accept/')
TERMS_RETURNTO_PARAM = getattr(settings, 'TERMS_RETURNTO_PARAM', 'returnTo')
LOGGER = logging.getLogger(name='termsandconditions')
def user_accept_terms(backend, user, uid, social_user=None, *args, **kwargs):
"""Check if the user has accepted the terms and conditions after creation."""
LOGGER.debug('user_accept_terms')
if not TermsAndConditions.agreed_to_latest(user):
return redirect_to_terms_accept('/')
else:
return {'social_user': social_user, 'user': user}
def redirect_to_terms_accept(current_path='/', slug='default'):
"""Redirect the user to the terms and conditions accept page."""
redirect_url_parts = list(urlparse(ACCEPT_TERMS_PATH))
if slug != 'default':
redirect_url_parts[2] += slug
querystring = QueryDict(redirect_url_parts[4], mutable=True)
querystring[TERMS_RETURNTO_PARAM] = current_path
redirect_url_parts[4] = querystring.urlencode(safe='/')
return HttpResponseRedirect(urlunparse(redirect_url_parts))

View File

@@ -0,0 +1,65 @@
.termsandconditions-modal {
visibility: hidden;
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
text-align:center;
z-index: 999;
background: rgba(4, 10, 30, 0.8);
-moz-transition: all 0.5s ease-out;
-webkit-transition: all 0.5s ease-out;
-o-transition: all 0.5s ease-out;
transition: all 0.5s ease-out;
}
.termsandconditions-modal div {
poistion: absolute;
z-index: 1000;
top: 33%;
left: 25%;
width: 50%;
margin: 10% auto;
padding: 5px 20px 13px 20px;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
background: #ffffff;
background: -moz-linear-gradient(#ffffff, #cccccc);
background: -webkit-linear-gradient(#ffffff, #cccccc);
background: -o-linear-gradient(#ffffff, #cccccc);
box-shadow: 0 0 10px #000000;
-moz-box-shadow: 0 0 10px #000000;
-webkit-box-shadow: 0 0 10px #000000;
}
.termsandconditions-close {
display: block;
background: #606061;
color: #FFFFFF;
line-height: 25px;
position: relative;
left: -32px;
top: -16px;
width: 24px;
text-align: center;
text-decoration: none;
font-weight: bold;
-webkit-border-radius: 12px;
-moz-border-radius: 12px;
border-radius: 12px;
box-shadow: 0 0 10px #000000;
-moz-box-shadow: 0 0 10px #000000;
-webkit-box-shadow: 0 0 10px #000000;
}
.termsandconditions-close:hover {
background: #bd362f;
}
body {
height:100%;
}

View File

@@ -0,0 +1,7 @@
function termsandconditions_overlay() {
var el = document.getElementsByClassName("termsandconditions-modal");
var i;
for (i = 0; i < el.length; i++) {
el[i].style.visibility = (el[i].style.visibility == "visible") ? "hidden" : "visible";
};
};

View File

@@ -0,0 +1,29 @@
{% load staticfiles %}
{% load i18n %}
{% if terms %}
<link rel="stylesheet" type="text/css" href="{% static 'termsandconditions/css/modal.css' %}">
<script src="{% static 'termsandconditions/js/modal.js' %}" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
termsandconditions_overlay();
}, false);
</script>
<div id="termsandconditions" class="termsandconditions-modal">
<div>
<a href='javascript:void(0)' onclick='termsandconditions_overlay();' title="{% trans 'Close' %}" class="termsandconditions-close">X</a>
<h2>
{% if terms.name %}
{{ terms.name|safe }}
{% else %}
{% trans 'Terms and Conditions' %}
{% endif %}
</h2>
{% if terms.info %}{{ terms.info|safe }}{% endif %}
{% trans 'To accept new Terms, visit' %}
<a href="{% url 'tc_accept_specific_page' terms.slug %}">{% url 'tc_accept_specific_page' terms.slug %}</a>
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block headtitle %}Accept Terms and Conditions{% endblock %}
{% block header %}{% endblock %}
{% block content %}
<section title="termsandconditions" data-role="content">
<h1>Please Accept {{ form.initial.terms.name|safe }}</h1>
{{ form.errors }}
<div id="tc-terms-html">
{{ form.initial.terms.text|safe }}
</div>
<form action="{% url 'tc_accept_page' %}" method="post" id="tc-terms-form" data-ajax="false">
{% csrf_token %}
{{ form.terms }}
{{ form.returnTo }}
<p><input type="submit" value="Accept" data-role="button"></p>
</form>
<p><a href="{% url "tc_print_page" form.initial.terms.slug form.initial.terms.version_number %}"
target="_blank">Print Terms & Conditions</a></p>
</section>
{% endblock %}
{% block footer %}{% endblock %}

View File

@@ -0,0 +1,5 @@
{{ terms.name|safe }}
Version {{ terms.version_number|safe }}
{{ terms.text|safe }}

View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block headtitle %}Email Terms and Conditions{% endblock %}
{% block content %}
<section title="termsandconditions" data-role="content">
<h1>Email {{ form.initial.terms.name|safe }}</h1>
<form action="{% url 'tc_email_page' %}" method="post" id="tc-email-form" data-ajax="false">
{% csrf_token %}
<label for="id_email_address">Email To:</label>{{ form.email_address }}
{{ form.terms }}
<input type="hidden" id="id_email_subject" name="email_subject" value="You Requested: {{ form.initial.terms }}">
<p><input type="submit" value="Send" data-role="button"></p>
</form>
</section>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block headtitle %}Print Terms and Conditions{% endblock %}
{% block body_class %}{% endblock %}
{% block head %}
<script>
window.print();
</script>
{% endblock %}
{% block header %}{% endblock %}
{% block content %}
<h1>{{ terms.name|safe }}</h1>
<h3>Version {{ terms.version_number|safe }}</h3>
<div>
{{ terms.text|safe }}
</div>
{% endblock %}
{% block footer %}{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block content %}
<section title="Terms and Conditions" data-role="content">
<h1>{{ terms.name|safe }}</h1>
<div>
{{ terms.text|safe }}
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,42 @@
"""Django Tags"""
from django import template
from ..models import TermsAndConditions, DEFAULT_TERMS_SLUG
from ..middleware import is_path_protected
from django.conf import settings
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
register = template.Library()
DEFAULT_HTTP_PATH_FIELD = 'PATH_INFO'
TERMS_HTTP_PATH_FIELD = getattr(settings, 'TERMS_HTTP_PATH_FIELD', DEFAULT_HTTP_PATH_FIELD)
@register.inclusion_tag('termsandconditions/snippets/termsandconditions.html',
takes_context=True)
def show_terms_if_not_agreed(context, slug=DEFAULT_TERMS_SLUG, field=TERMS_HTTP_PATH_FIELD):
"""Displays a modal on a current page if a user has not yet agreed to the
given terms. If terms are not specified, the default slug is used.
How it works? A small snippet is included into your template if a user
who requested the view has not yet agreed the terms. The snippet takes
care of displaying a respective modal.
"""
request = context['request']
terms = TermsAndConditions.get_active(slug)
agreed = TermsAndConditions.agreed_to_terms(request.user, terms)
# stop here, if terms has been agreed
if agreed:
return {}
# handle excluded url's
url = urlparse(request.META[field])
protected = is_path_protected(url.path)
if (not agreed) and terms and protected:
return {'terms': terms}
return {}

View File

@@ -0,0 +1,392 @@
"""Unit Tests for the termsandconditions module"""
# pylint: disable=R0904, C0103
from django.core import mail
from django.http import HttpResponseRedirect
from django.conf import settings
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.template import Context, Template
from django.utils import timezone
from .models import TermsAndConditions, UserTermsAndConditions, DEFAULT_TERMS_SLUG
from .pipeline import user_accept_terms
from .templatetags.terms_tags import show_terms_if_not_agreed
import logging
from importlib import import_module
LOGGER = logging.getLogger(name='termsandconditions')
class TermsAndConditionsTests(TestCase):
"""Tests Terms and Conditions Module"""
def setUp(self):
"""Setup for each test"""
LOGGER.debug('Test Setup')
self.user1 = User.objects.create_user('user1', 'user1@user1.com', 'user1password')
self.user2 = User.objects.create_user('user2', 'user2@user2.com', 'user2password')
self.terms1 = TermsAndConditions.objects.create(slug="site-terms", name="Site Terms",
text="Site Terms and Conditions 1", version_number=1.0,
date_active="2012-01-01")
self.terms2 = TermsAndConditions.objects.create(slug="site-terms", name="Site Terms",
text="Site Terms and Conditions 2", version_number=2.0,
date_active="2012-01-05")
self.terms3 = TermsAndConditions.objects.create(slug="contrib-terms", name="Contributor Terms",
text="Contributor Terms and Conditions 1.5", version_number=1.5,
date_active="2012-01-01")
self.terms4 = TermsAndConditions.objects.create(slug="contrib-terms", name="Contributor Terms",
text="Contributor Terms and Conditions 2", version_number=2.0,
date_active="2100-01-01")
def tearDown(self):
"""Teardown for each test"""
LOGGER.debug('Test TearDown')
User.objects.all().delete()
TermsAndConditions.objects.all().delete()
UserTermsAndConditions.objects.all().delete()
def test_agreed_to(self):
"""Test the agreed_to_terms static method"""
LOGGER.debug("Test that user1 has not agreed to terms1.")
self.assertFalse(TermsAndConditions.agreed_to_terms(self.user1, self.terms1))
def test_social_redirect(self):
"""Test the agreed_to_terms redirect from social pipeline"""
LOGGER.debug("Test the social pipeline")
response = user_accept_terms('backend', self.user1, '123')
self.assertIsInstance(response, HttpResponseRedirect)
UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2)
response = user_accept_terms('backend', self.user1, '123')
self.assertIsInstance(response, dict)
def test_get_active_list(self):
"""Test get list of active T&Cs"""
active_list = TermsAndConditions.get_active_list()
self.assertEqual(2, len(active_list))
def test_terms_and_conditions_models(self):
"""Various tests of the TermsAndConditions Module"""
# Testing Direct Assignment of Acceptance
UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms1)
UserTermsAndConditions.objects.create(user=self.user2, terms=self.terms3)
self.assertEquals(1.0, self.user1.userterms.get().terms.version_number)
self.assertEquals(1.5, self.user2.userterms.get().terms.version_number)
self.assertEquals('user1', self.terms1.users.all()[0].username)
# Testing the get_active static method of TermsAndConditions
self.assertEquals(2.0, TermsAndConditions.get_active(slug='site-terms').version_number)
self.assertEquals(1.5, TermsAndConditions.get_active(slug='contrib-terms').version_number)
# Testing the agreed_to_latest static method of TermsAndConditions
self.assertEquals(False, TermsAndConditions.agreed_to_latest(user=self.user1, slug='site-terms'))
self.assertEquals(True, TermsAndConditions.agreed_to_latest(user=self.user2, slug='contrib-terms'))
# Testing the unicode method of TermsAndConditions
self.assertEquals('site-terms-2.00', str(TermsAndConditions.get_active(slug='site-terms')))
self.assertEquals('contrib-terms-1.50', str(TermsAndConditions.get_active(slug='contrib-terms')))
def test_middleware_redirect(self):
"""Validate that a user is redirected to the terms accept page if they are logged in, and decorator is on method"""
UserTermsAndConditions.objects.all().delete()
LOGGER.debug('Test user1 login for middleware')
login_response = self.client.login(username='user1', password='user1password')
self.assertTrue(login_response)
LOGGER.debug('Test /secure/ after login')
logged_in_response = self.client.get('/secure/', follow=True)
self.assertRedirects(logged_in_response, 'http://testserver/terms/accept/contrib-terms?returnTo=/secure/')
def test_terms_required_redirect(self):
"""Validate that a user is redirected to the terms accept page if logged in, and decorator is on method"""
LOGGER.debug('Test /termsrequired/ pre login')
not_logged_in_response = self.client.get('/termsrequired/', follow=True)
self.assertRedirects(not_logged_in_response, 'http://testserver/accounts/login/?next=/termsrequired/')
LOGGER.debug('Test user1 login')
login_response = self.client.login(username='user1', password='user1password')
self.assertTrue(login_response)
LOGGER.debug('Test /termsrequired/ after login')
logged_in_response = self.client.get('/termsrequired/', follow=True)
self.assertRedirects(logged_in_response, 'http://testserver/terms/accept/?returnTo=/termsrequired/')
LOGGER.debug('Test no redirect for /termsrequired/ after accept')
accepted_response = self.client.post('/terms/accept/', {'terms': 2, 'returnTo': '/termsrequired/'}, follow=True)
self.assertContains(accepted_response, "Terms and Conditions Acceptance Required")
LOGGER.debug('Test response after termsrequired accept')
terms_required_response = self.client.get('/termsrequired/', follow=True)
self.assertContains(terms_required_response, "Terms and Conditions Acceptance Required")
def test_accept(self):
"""Validate that accepting terms works"""
LOGGER.debug('Test user1 login for accept')
login_response = self.client.login(username='user1', password='user1password')
self.assertTrue(login_response)
LOGGER.debug('Test /terms/accept/ get')
accept_response = self.client.get('/terms/accept/', follow=True)
self.assertContains(accept_response, "Accept")
LOGGER.debug('Test /terms/accept/ post')
chained_terms_response = self.client.post('/terms/accept/', {'terms': 2, 'returnTo': '/secure/'}, follow=True)
self.assertContains(chained_terms_response, "Contributor")
self.assertEquals(True, TermsAndConditions.agreed_to_latest(user=self.user1, slug='site-terms'))
LOGGER.debug('Test /terms/accept/contrib-terms/1.5/ post')
accept_version_response = self.client.get('/terms/accept/contrib-terms/1.5/', follow=True)
self.assertContains(accept_version_response, "Contributor Terms and Conditions 1.5")
LOGGER.debug('Test /terms/accept/contrib-terms/3/ post')
accept_version_post_response = self.client.post('/terms/accept/', {'terms': 3, 'returnTo': '/secure/'}, follow=True)
self.assertContains(accept_version_post_response, "Secure")
self.assertTrue(TermsAndConditions.agreed_to_terms(user=self.user1, terms=self.terms3))
def test_accept_store_ip_address(self):
"""Test with IP address storage setting true (default)"""
self.client.login(username='user1', password='user1password')
self.client.post('/terms/accept/', {'terms': 2, 'returnTo': '/secure/'}, follow=True)
user_terms = UserTermsAndConditions.objects.all()[0]
self.assertEqual(user_terms.user, self.user1)
self.assertEqual(user_terms.terms, self.terms2)
self.assertTrue(user_terms.ip_address)
def test_accept_no_ip_address(self):
"""Test with IP address storage setting false"""
self.client.login(username='user1', password='user1password')
with self.settings(TERMS_STORE_IP_ADDRESS=False):
self.client.post('/terms/accept/', {'terms': 2, 'returnTo': '/secure/'}, follow=True)
user_terms = UserTermsAndConditions.objects.all()[0]
self.assertFalse(user_terms.ip_address)
def test_auto_create(self):
"""Validate that a terms are auto created if none exist"""
LOGGER.debug('Test auto create terms')
TermsAndConditions.objects.all().delete()
num_terms = TermsAndConditions.objects.count()
self.assertEquals(0, num_terms)
LOGGER.debug('Test user1 login for autocreate')
login_response = self.client.login(username='user1', password='user1password')
self.assertTrue(login_response)
LOGGER.debug('Test /termsrequired/ after login with no TermsAndConditions')
logged_in_response = self.client.get('/termsrequired/', follow=True)
self.assertRedirects(logged_in_response, 'http://testserver/terms/accept/?returnTo=/termsrequired/')
LOGGER.debug('Test TermsAndConditions Object Was Created')
num_terms = TermsAndConditions.objects.count()
self.assertEquals(1, num_terms)
terms = TermsAndConditions.objects.get()
self.assertEquals('site-terms-1.00', str(terms))
LOGGER.debug('Test Not Creating Non-Default TermsAndConditions')
non_default_response = self.client.get('/terms/accept/contrib-terms/', follow=True)
self.assertEquals(404, non_default_response.status_code)
def test_terms_upgrade(self):
"""Validate a user is prompted to accept terms again when new version comes out"""
UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2)
LOGGER.debug('Test user1 login pre upgrade')
login_response = self.client.login(username='user1', password='user1password')
self.assertTrue(login_response)
LOGGER.debug('Test user1 not redirected after login')
logged_in_response = self.client.get('/secure/', follow=True)
self.assertContains(logged_in_response, "Contributor")
# First, Accept Contributor Terms
LOGGER.debug('Test /terms/accept/contrib-terms/3/ post')
self.client.post('/terms/accept/', {'terms': 3, 'returnTo': '/secure/'}, follow=True)
LOGGER.debug('Test upgrade terms')
self.terms5 = TermsAndConditions.objects.create(slug="site-terms", name="Site Terms",
text="Terms and Conditions2", version_number=2.5,
date_active="2012-02-05")
LOGGER.debug('Test user1 is redirected when changing pages')
post_upgrade_response = self.client.get('/secure/', follow=True)
self.assertRedirects(post_upgrade_response, 'http://testserver/terms/accept/site-terms?returnTo=/secure/')
def test_no_middleware(self):
"""Test a secure page with the middleware excepting it"""
UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2)
LOGGER.debug('Test user1 login no middleware')
login_response = self.client.login(username='user1', password='user1password')
self.assertTrue(login_response)
LOGGER.debug('Test user1 not redirected after login')
logged_in_response = self.client.get('/securetoo/', follow=True)
self.assertContains(logged_in_response, "SECOND")
LOGGER.debug("Test startswith '/admin' pages not redirecting")
admin_response = self.client.get('/admin', follow=True)
self.assertContains(admin_response, "administration")
def test_terms_view(self):
"""Test Accessing the View Terms and Conditions Functions"""
LOGGER.debug('Test /terms/')
root_response = self.client.get('/terms/', follow=True)
self.assertContains(root_response, 'Terms and Conditions')
LOGGER.debug('Test /terms/view/site-terms')
slug_response = self.client.get('/terms/view/site-terms', follow=True)
self.assertContains(slug_response, 'Terms and Conditions')
LOGGER.debug('Test /terms/view/site-terms/1.5')
version_response = self.client.get(self.terms3.get_absolute_url(), follow=True)
self.assertContains(version_response, 'Terms and Conditions')
def test_user_pipeline(self):
"""Test the case of a user being partially created via the django-socialauth pipeline"""
LOGGER.debug('Test /terms/accept/ post for no user')
no_user_response = self.client.post('/terms/accept/', {'terms': 2}, follow=True)
self.assertContains(no_user_response, "Home")
user = {'pk': 1}
kwa = {'user': user}
partial_pipeline = {'kwargs': kwa}
engine = import_module(settings.SESSION_ENGINE)
store = engine.SessionStore()
store.save()
self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key
session = self.client.session
session["partial_pipeline"] = partial_pipeline
session.save()
self.assertTrue('partial_pipeline' in self.client.session)
LOGGER.debug('Test /terms/accept/ post for pipeline user')
pipeline_response = self.client.post('/terms/accept/', {'terms': 2, 'returnTo': '/anon'}, follow=True)
self.assertContains(pipeline_response, "Anon")
def test_email_terms(self):
"""Test emailing terms and conditions"""
LOGGER.debug('Test /terms/email/')
email_form_response = self.client.get('/terms/email/', follow=True)
self.assertContains(email_form_response, 'Email')
LOGGER.debug('Test /terms/email/ post, expecting email fail')
email_send_response = self.client.post('/terms/email/',
{'email_address': 'foo@foo.com', 'email_subject': 'Terms Email',
'terms': 2, 'returnTo': '/'}, follow=True)
self.assertEqual(len(mail.outbox), 1) # Check that there is one email in the test outbox
self.assertContains(email_send_response, 'Sent')
LOGGER.debug('Test /terms/email/ post, expecting email fail')
email_fail_response = self.client.post('/terms/email/', {'email_address': 'INVALID EMAIL ADDRESS',
'email_subject': 'Terms Email', 'terms': 2,
'returnTo': '/'}, follow=True)
self.assertContains(email_fail_response, 'Invalid')
class TermsAndConditionsTemplateTagsTestCase(TestCase):
def setUp(self):
"""Setup for each test"""
self.user1 = User.objects.create_user(
'user1', 'user1@user1.com', 'user1password')
self.template_string_1 = (
'{% load terms_tags %}'
'{% show_terms_if_not_agreed %}'
)
self.template_string_2 = (
'{% load terms_tags %}'
'{% show_terms_if_not_agreed slug="specific-terms" %}'
)
def _make_context(self, url):
"""Build Up Context - Used in many tests"""
context = dict()
context['request'] = RequestFactory()
context['request'].user = self.user1
context['request'].META = {'PATH_INFO': url}
return context
def render_template(self, string, context=None):
"""a helper method to render simplistic test templates"""
request = RequestFactory().get('/test')
request.user = self.user1
request.context = context or {}
return Template(string).render(Context({'request': request}))
def test_show_terms_if_not_agreed(self):
"""test if show_terms_if_not_agreed template tag renders html code"""
rendered = self.render_template(self.template_string_1)
terms = TermsAndConditions.get_active()
self.assertIn(terms.slug, rendered)
def test_not_show_terms_if_agreed(self):
"""test if show_terms_if_not_agreed template tag does not load if user
agreed terms"""
terms = TermsAndConditions.get_active()
UserTermsAndConditions.objects.create(terms=terms, user=self.user1)
rendered = self.render_template(self.template_string_1)
self.assertNotIn(terms.slug, rendered)
def test_show_terms_if_not_agreed_by_slug(self):
"""Test if show_terms not agreed to by looking up slug"""
terms = TermsAndConditions.objects.create(
slug='specific-terms',
date_active=timezone.now()
)
rendered = self.render_template(self.template_string_2)
self.assertIn(terms.slug, rendered)
def test_show_terms_if_not_agreed_on_protected_url_not_agreed(self):
"""Check terms on protected url if not agreed"""
context = self._make_context('/test')
result = show_terms_if_not_agreed(context)
terms = TermsAndConditions.get_active(slug=DEFAULT_TERMS_SLUG)
self.assertDictEqual(result, {'terms': terms})
def test_show_terms_if_not_agreed_on_unprotected_url_not_agreed(self):
"""Check terms on unprotected url if not agreed"""
context = self._make_context('/')
result = show_terms_if_not_agreed(context)
self.assertDictEqual(result, {})
class TermsAndConditionsModelsTestCase(TestCase):
"""Tests Models for T&C"""
def setUp(self):
"""Set Up T&C Model Tests"""
self.terms_1 = TermsAndConditions.objects.create(
name='terms_1',
date_active=timezone.now()
)
self.terms_2 = TermsAndConditions.objects.create(
name='terms_2',
date_active=None
)
def test_tnc_default_slug(self):
"""test if default slug is used"""
self.assertEqual(self.terms_1.slug, DEFAULT_TERMS_SLUG)
def test_tnc_get_active(self):
"""test if right terms are active"""
active = TermsAndConditions.get_active()
self.assertEqual(active.name, self.terms_1.name)
self.assertNotEqual(active.name, self.terms_2.name)

View File

@@ -0,0 +1,43 @@
"""
Master URL Pattern List for the application. Most of the patterns here should be top-level
pass-offs to sub-modules, who will have their own urls.py defining actions within.
"""
# pylint: disable=W0401, W0614, E1120
from django.conf.urls import url
from django.contrib import admin
from .views import TermsView, AcceptTermsView, EmailTermsView
from .models import DEFAULT_TERMS_SLUG
admin.autodiscover()
urlpatterns = (
# View Default Terms
url(r'^$', TermsView.as_view(), {"slug": DEFAULT_TERMS_SLUG}, name="tc_view_page"),
# View Specific Active Terms
url(r'^view/(?P<slug>[a-zA-Z0-9_.-]+)/$', TermsView.as_view(), name="tc_view_specific_page"),
# View Specific Version of Terms
url(r'^view/(?P<slug>[a-zA-Z0-9_.-]+)/(?P<version>[0-9.]+)/$', TermsView.as_view(), name="tc_view_specific_version_page"),
# Print Specific Version of Terms
url(r'^print/(?P<slug>[a-zA-Z0-9_.-]+)/(?P<version>[0-9.]+)/$', TermsView.as_view(template_name="termsandconditions/tc_print_terms.html"), name="tc_print_page"),
# Accept Terms
url(r'^accept/$', AcceptTermsView.as_view(), name="tc_accept_page"),
# Accept Specific Terms
url(r'^accept/(?P<slug>[a-zA-Z0-9_.-]+)$', AcceptTermsView.as_view(), name="tc_accept_specific_page"),
# Accept Specific Terms Version
url(r'^accept/(?P<slug>[a-zA-Z0-9_.-]+)/(?P<version>[0-9\.]+)/$', AcceptTermsView.as_view(), name="tc_accept_specific_version_page"),
# Email Terms
url(r'^email/$', EmailTermsView.as_view(), name="tc_email_page"),
# Email Specific Terms Version
url(r'^email/(?P<slug>[a-zA-Z0-9_.-]+)/(?P<version>[0-9\.]+)/$', EmailTermsView.as_view(), name="tc_specific_version_page"),
)

View File

@@ -0,0 +1,133 @@
"""Django Views for the termsandconditions module"""
# pylint: disable=E1120,R0901,R0904
from django.contrib.auth.models import User
from .forms import UserTermsAndConditionsModelForm, EmailTermsForm
from .models import TermsAndConditions, UserTermsAndConditions, DEFAULT_TERMS_SLUG
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.views.generic import DetailView, CreateView, FormView
from django.template.loader import get_template
from django.core.mail import send_mail
import logging
from smtplib import SMTPException
LOGGER = logging.getLogger(name='termsandconditions')
def get_terms(kwargs):
"""Checks URL parameters for slug and/or version to pull the right TermsAndConditions object"""
slug = kwargs.get("slug", DEFAULT_TERMS_SLUG)
if kwargs.get("version"):
terms = TermsAndConditions.objects.get(slug=slug, version_number=kwargs.get("version"))
else:
terms = TermsAndConditions.get_active(slug)
return terms
class TermsView(DetailView):
"""
View Terms and Conditions View
url: /terms/view
"""
template_name = "termsandconditions/tc_view_terms.html"
context_object_name = 'terms'
def get_object(self, queryset=None):
"""Override of DetailView method, queries for which T&C to return"""
LOGGER.debug('termsandconditions.views.TermsView.get_object')
return get_terms(self.kwargs)
class AcceptTermsView(CreateView):
"""
Terms and Conditions Acceptance view
url: /terms/accept
"""
model = UserTermsAndConditions
form_class = UserTermsAndConditionsModelForm
template_name = "termsandconditions/tc_accept_terms.html"
def get_initial(self):
"""Override of CreateView method, queries for which T&C to accept and catches returnTo from URL"""
LOGGER.debug('termsandconditions.views.AcceptTermsView.get_initial')
terms = get_terms(self.kwargs)
returnTo = self.request.GET.get('returnTo', '/')
return {'terms': terms, 'returnTo': returnTo}
def form_valid(self, form):
"""Override of CreateView method, assigns default values based on user situation"""
if self.request.user.is_authenticated():
form.instance.user = self.request.user
else: #Get user out of saved pipeline from django-socialauth
if self.request.session.has_key('partial_pipeline'):
user_pk = self.request.session['partial_pipeline']['kwargs']['user']['pk']
form.instance.user = User.objects.get(id=user_pk)
else:
return HttpResponseRedirect('/')
store_ip_address = getattr(settings, 'TERMS_STORE_IP_ADDRESS', True)
if store_ip_address:
form.instance.ip_address = self.request.META['REMOTE_ADDR']
self.success_url = form.cleaned_data.get('returnTo', '/') or '/'
return super(AcceptTermsView, self).form_valid(form)
class EmailTermsView(FormView):
"""
Email Terms and Conditions View
url: /terms/email
"""
template_name = "termsandconditions/tc_email_terms_form.html"
form_class = EmailTermsForm
def get_initial(self):
"""Override of CreateView method, queries for which T&C send, catches returnTo from URL"""
LOGGER.debug('termsandconditions.views.EmailTermsView.get_initial')
terms = get_terms(self.kwargs)
returnTo = self.request.GET.get('returnTo', '/')
return {'terms': terms, 'returnTo': returnTo}
def form_valid(self, form):
"""Override of CreateView method, sends the email."""
LOGGER.debug('termsandconditions.views.EmailTermsView.form_valid')
template = get_template("termsandconditions/tc_email_terms.html")
template_rendered = template.render({"terms": form.cleaned_data.get('terms')})
LOGGER.debug("Email Terms Body:")
LOGGER.debug(template_rendered)
try:
send_mail(form.cleaned_data.get('email_subject', 'Terms'),
template_rendered,
settings.DEFAULT_FROM_EMAIL,
[form.cleaned_data.get('email_address')],
fail_silently=False)
messages.add_message(self.request, messages.INFO, "Terms and Conditions Sent.")
except SMTPException: # pragma: no cover
messages.add_message(self.request, messages.ERROR, "An Error Occurred Sending Your Message.")
self.success_url = form.cleaned_data.get('returnTo', '/') or '/'
return super(EmailTermsView, self).form_valid(form)
def form_invalid(self, form):
"""Override of CreateView method, logs invalid email form submissions."""
LOGGER.debug("Invalid Email Form Submitted")
messages.add_message(self.request, messages.ERROR, "Invalid Email Address.")
return super(EmailTermsView, self).form_invalid(form)