From 41f9a3950c576c1a28823c7bd36a2e5d3e5bbdd5 Mon Sep 17 00:00:00 2001 From: zhengxie Date: Fri, 15 Jul 2016 10:47:32 +0800 Subject: [PATCH] Add ToC --- thirdpart/termsandconditions/__init__.py | 2 + thirdpart/termsandconditions/admin.py | 25 ++ thirdpart/termsandconditions/apps.py | 8 + thirdpart/termsandconditions/decorators.py | 31 ++ thirdpart/termsandconditions/forms.py | 26 ++ thirdpart/termsandconditions/middleware.py | 55 +++ .../migrations/0001_initial.py | 57 +++ .../0002_termsandconditions_info.py | 22 + .../termsandconditions/migrations/__init__.py | 0 thirdpart/termsandconditions/models.py | 122 ++++++ thirdpart/termsandconditions/pipeline.py | 40 ++ .../static/termsandconditions/css/modal.css | 65 +++ .../static/termsandconditions/js/modal.js | 7 + .../snippets/termsandconditions.html | 29 ++ .../termsandconditions/tc_accept_terms.html | 24 ++ .../termsandconditions/tc_email_terms.html | 5 + .../tc_email_terms_form.html | 16 + .../termsandconditions/tc_print_terms.html | 24 ++ .../termsandconditions/tc_view_terms.html | 11 + .../templatetags/__init__.py | 1 + .../templatetags/terms_tags.py | 42 ++ thirdpart/termsandconditions/tests.py | 392 ++++++++++++++++++ thirdpart/termsandconditions/urls.py | 43 ++ thirdpart/termsandconditions/views.py | 133 ++++++ 24 files changed, 1180 insertions(+) create mode 100644 thirdpart/termsandconditions/__init__.py create mode 100644 thirdpart/termsandconditions/admin.py create mode 100644 thirdpart/termsandconditions/apps.py create mode 100644 thirdpart/termsandconditions/decorators.py create mode 100644 thirdpart/termsandconditions/forms.py create mode 100644 thirdpart/termsandconditions/middleware.py create mode 100644 thirdpart/termsandconditions/migrations/0001_initial.py create mode 100644 thirdpart/termsandconditions/migrations/0002_termsandconditions_info.py create mode 100644 thirdpart/termsandconditions/migrations/__init__.py create mode 100644 thirdpart/termsandconditions/models.py create mode 100644 thirdpart/termsandconditions/pipeline.py create mode 100644 thirdpart/termsandconditions/static/termsandconditions/css/modal.css create mode 100644 thirdpart/termsandconditions/static/termsandconditions/js/modal.js create mode 100644 thirdpart/termsandconditions/templates/termsandconditions/snippets/termsandconditions.html create mode 100644 thirdpart/termsandconditions/templates/termsandconditions/tc_accept_terms.html create mode 100644 thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms.html create mode 100644 thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms_form.html create mode 100644 thirdpart/termsandconditions/templates/termsandconditions/tc_print_terms.html create mode 100644 thirdpart/termsandconditions/templates/termsandconditions/tc_view_terms.html create mode 100644 thirdpart/termsandconditions/templatetags/__init__.py create mode 100644 thirdpart/termsandconditions/templatetags/terms_tags.py create mode 100644 thirdpart/termsandconditions/tests.py create mode 100644 thirdpart/termsandconditions/urls.py create mode 100644 thirdpart/termsandconditions/views.py diff --git a/thirdpart/termsandconditions/__init__.py b/thirdpart/termsandconditions/__init__.py new file mode 100644 index 0000000000..d84400e5be --- /dev/null +++ b/thirdpart/termsandconditions/__init__.py @@ -0,0 +1,2 @@ +"""Django Terms and Conditions Module""" +from __future__ import unicode_literals diff --git a/thirdpart/termsandconditions/admin.py b/thirdpart/termsandconditions/admin.py new file mode 100644 index 0000000000..96bae518ee --- /dev/null +++ b/thirdpart/termsandconditions/admin.py @@ -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) diff --git a/thirdpart/termsandconditions/apps.py b/thirdpart/termsandconditions/apps.py new file mode 100644 index 0000000000..6abe3c73a2 --- /dev/null +++ b/thirdpart/termsandconditions/apps.py @@ -0,0 +1,8 @@ +"""Django Apps Config""" + +from django.apps import AppConfig + + +class TermsAndConditionsConfig(AppConfig): + name = 'termsandconditions' + verbose_name = "Terms and Conditions" diff --git a/thirdpart/termsandconditions/decorators.py b/thirdpart/termsandconditions/decorators.py new file mode 100644 index 0000000000..569c4d0ed7 --- /dev/null +++ b/thirdpart/termsandconditions/decorators.py @@ -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 diff --git a/thirdpart/termsandconditions/forms.py b/thirdpart/termsandconditions/forms.py new file mode 100644 index 0000000000..2bb60c264c --- /dev/null +++ b/thirdpart/termsandconditions/forms.py @@ -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()) diff --git a/thirdpart/termsandconditions/middleware.py b/thirdpart/termsandconditions/middleware.py new file mode 100644 index 0000000000..9c0ebac80b --- /dev/null +++ b/thirdpart/termsandconditions/middleware.py @@ -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 diff --git a/thirdpart/termsandconditions/migrations/0001_initial.py b/thirdpart/termsandconditions/migrations/0001_initial.py new file mode 100644 index 0000000000..f3e2bf1344 --- /dev/null +++ b/thirdpart/termsandconditions/migrations/0001_initial.py @@ -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')]), + ), + ] diff --git a/thirdpart/termsandconditions/migrations/0002_termsandconditions_info.py b/thirdpart/termsandconditions/migrations/0002_termsandconditions_info.py new file mode 100644 index 0000000000..50bacfc1be --- /dev/null +++ b/thirdpart/termsandconditions/migrations/0002_termsandconditions_info.py @@ -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, + ), + ] diff --git a/thirdpart/termsandconditions/migrations/__init__.py b/thirdpart/termsandconditions/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/thirdpart/termsandconditions/models.py b/thirdpart/termsandconditions/models.py new file mode 100644 index 0000000000..2b636bbd83 --- /dev/null +++ b/thirdpart/termsandconditions/models.py @@ -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 diff --git a/thirdpart/termsandconditions/pipeline.py b/thirdpart/termsandconditions/pipeline.py new file mode 100644 index 0000000000..0ade7d5d69 --- /dev/null +++ b/thirdpart/termsandconditions/pipeline.py @@ -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)) diff --git a/thirdpart/termsandconditions/static/termsandconditions/css/modal.css b/thirdpart/termsandconditions/static/termsandconditions/css/modal.css new file mode 100644 index 0000000000..2e7cc8da98 --- /dev/null +++ b/thirdpart/termsandconditions/static/termsandconditions/css/modal.css @@ -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%; +} diff --git a/thirdpart/termsandconditions/static/termsandconditions/js/modal.js b/thirdpart/termsandconditions/static/termsandconditions/js/modal.js new file mode 100644 index 0000000000..bef33e0615 --- /dev/null +++ b/thirdpart/termsandconditions/static/termsandconditions/js/modal.js @@ -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"; + }; +}; diff --git a/thirdpart/termsandconditions/templates/termsandconditions/snippets/termsandconditions.html b/thirdpart/termsandconditions/templates/termsandconditions/snippets/termsandconditions.html new file mode 100644 index 0000000000..2611fbf2cc --- /dev/null +++ b/thirdpart/termsandconditions/templates/termsandconditions/snippets/termsandconditions.html @@ -0,0 +1,29 @@ +{% load staticfiles %} +{% load i18n %} + +{% if terms %} + + + + +
+
+ X +

+ {% if terms.name %} + {{ terms.name|safe }} + {% else %} + {% trans 'Terms and Conditions' %} + {% endif %} +

+ {% if terms.info %}{{ terms.info|safe }}{% endif %} + {% trans 'To accept new Terms, visit' %} + {% url 'tc_accept_specific_page' terms.slug %} +
+
+{% endif %} + diff --git a/thirdpart/termsandconditions/templates/termsandconditions/tc_accept_terms.html b/thirdpart/termsandconditions/templates/termsandconditions/tc_accept_terms.html new file mode 100644 index 0000000000..618762681b --- /dev/null +++ b/thirdpart/termsandconditions/templates/termsandconditions/tc_accept_terms.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block headtitle %}Accept Terms and Conditions{% endblock %} + +{% block header %}{% endblock %} + +{% block content %} +
+

Please Accept {{ form.initial.terms.name|safe }}

+ {{ form.errors }} +
+ {{ form.initial.terms.text|safe }} +
+
+ {% csrf_token %} + {{ form.terms }} + {{ form.returnTo }} +

+
+

Print Terms & Conditions

+
+{% endblock %} +{% block footer %}{% endblock %} \ No newline at end of file diff --git a/thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms.html b/thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms.html new file mode 100644 index 0000000000..316bf53222 --- /dev/null +++ b/thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms.html @@ -0,0 +1,5 @@ +{{ terms.name|safe }} + +Version {{ terms.version_number|safe }} + +{{ terms.text|safe }} \ No newline at end of file diff --git a/thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms_form.html b/thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms_form.html new file mode 100644 index 0000000000..02b0bb2993 --- /dev/null +++ b/thirdpart/termsandconditions/templates/termsandconditions/tc_email_terms_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block headtitle %}Email Terms and Conditions{% endblock %} + +{% block content %} +
+

Email {{ form.initial.terms.name|safe }}

+
+ {% csrf_token %} + {{ form.email_address }} + {{ form.terms }} + +

+
+
+{% endblock %} \ No newline at end of file diff --git a/thirdpart/termsandconditions/templates/termsandconditions/tc_print_terms.html b/thirdpart/termsandconditions/templates/termsandconditions/tc_print_terms.html new file mode 100644 index 0000000000..22af9fc8d0 --- /dev/null +++ b/thirdpart/termsandconditions/templates/termsandconditions/tc_print_terms.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block headtitle %}Print Terms and Conditions{% endblock %} + +{% block body_class %}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block header %}{% endblock %} + +{% block content %} +

{{ terms.name|safe }}

+

Version {{ terms.version_number|safe }}

+ +
+ {{ terms.text|safe }} +
+{% endblock %} + +{% block footer %}{% endblock %} \ No newline at end of file diff --git a/thirdpart/termsandconditions/templates/termsandconditions/tc_view_terms.html b/thirdpart/termsandconditions/templates/termsandconditions/tc_view_terms.html new file mode 100644 index 0000000000..c19588a156 --- /dev/null +++ b/thirdpart/termsandconditions/templates/termsandconditions/tc_view_terms.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} +
+

{{ terms.name|safe }}

+ +
+ {{ terms.text|safe }} +
+
+{% endblock %} \ No newline at end of file diff --git a/thirdpart/termsandconditions/templatetags/__init__.py b/thirdpart/termsandconditions/templatetags/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/thirdpart/termsandconditions/templatetags/__init__.py @@ -0,0 +1 @@ + diff --git a/thirdpart/termsandconditions/templatetags/terms_tags.py b/thirdpart/termsandconditions/templatetags/terms_tags.py new file mode 100644 index 0000000000..e8208f86a1 --- /dev/null +++ b/thirdpart/termsandconditions/templatetags/terms_tags.py @@ -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 {} diff --git a/thirdpart/termsandconditions/tests.py b/thirdpart/termsandconditions/tests.py new file mode 100644 index 0000000000..c8b8d1c738 --- /dev/null +++ b/thirdpart/termsandconditions/tests.py @@ -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) diff --git a/thirdpart/termsandconditions/urls.py b/thirdpart/termsandconditions/urls.py new file mode 100644 index 0000000000..ca2ccc7654 --- /dev/null +++ b/thirdpart/termsandconditions/urls.py @@ -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[a-zA-Z0-9_.-]+)/$', TermsView.as_view(), name="tc_view_specific_page"), + + # View Specific Version of Terms + url(r'^view/(?P[a-zA-Z0-9_.-]+)/(?P[0-9.]+)/$', TermsView.as_view(), name="tc_view_specific_version_page"), + + # Print Specific Version of Terms + url(r'^print/(?P[a-zA-Z0-9_.-]+)/(?P[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[a-zA-Z0-9_.-]+)$', AcceptTermsView.as_view(), name="tc_accept_specific_page"), + + # Accept Specific Terms Version + url(r'^accept/(?P[a-zA-Z0-9_.-]+)/(?P[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[a-zA-Z0-9_.-]+)/(?P[0-9\.]+)/$', EmailTermsView.as_view(), name="tc_specific_version_page"), + +) diff --git a/thirdpart/termsandconditions/views.py b/thirdpart/termsandconditions/views.py new file mode 100644 index 0000000000..ecb5d614fb --- /dev/null +++ b/thirdpart/termsandconditions/views.py @@ -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)