Commit 4bb19eda authored by Patrick Kimber's avatar Patrick Kimber

Merge branch '2485-gdpr' into 'master'

Preparing for GDPR

See merge request !3
parents 3907d32b ce763490
Pipeline #22594008 passed with stage
in 1 minute and 19 seconds
......@@ -39,8 +39,8 @@ nosetests.xml
# dev
.pytest_cache/
logfile
logfile.*
logger
logger.*
media
media-private
temp.db
......
......@@ -2,9 +2,11 @@
import logging
from captcha.fields import ReCaptchaField
from django import forms
from django.urls import reverse
from base.form_utils import RequiredFieldForm
from gdpr.models import UserConsent
from mail.models import Notify
from mail.service import queue_mail_message
from mail.tasks import process_mail
......@@ -18,19 +20,26 @@ class EnquiryForm(RequiredFieldForm):
"""user is not logged in... so we need a captcha."""
captcha = ReCaptchaField()
consent_checkbox = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
"""Don't use the captcha if the user is already logged in."""
user = kwargs.pop("user")
self.consent = kwargs.pop("consent")
self.req = kwargs.pop("request")
super().__init__(*args, **kwargs)
if user.is_authenticated:
del self.fields["captcha"]
for name in ("name", "description", "email", "phone"):
self.fields[name].widget.attrs.update(
{"class": "pure-input-1", "rows": 4}
)
self.fields["description"].help_text = "Please enter your message"
if self.req.user.is_authenticated:
del self.fields["captcha"]
# gdpr
if self.consent.show_checkbox:
f = self.fields["consent_checkbox"]
f.label = self.consent.message
else:
del self.fields["consent_checkbox"]
class Meta:
model = Enquiry
......@@ -53,8 +62,17 @@ class EnquiryForm(RequiredFieldForm):
def _email_subject(self, instance):
return "Enquiry from {}".format(instance.name)
def clean_consent_checkbox(self):
data = self.cleaned_data.get("consent_checkbox")
if not data:
raise forms.ValidationError(
self.consent.no_consent_message,
code="consent_checkbox__not_ticked",
)
return data
def save(self, commit=True):
instance = super(EnquiryForm, self).save(commit)
instance = super().save(commit)
if commit:
email_addresses = [n.email for n in Notify.objects.all()]
if email_addresses:
......@@ -70,4 +88,12 @@ class EnquiryForm(RequiredFieldForm):
"Enquiry app cannot send email notifications. "
"No email addresses set-up in 'mail.models.Notify'"
)
if self.req.user.is_authenticated:
UserConsent.objects.set_consent(
self.consent, True, user=self.req.user
)
else:
UserConsent.objects.set_consent(
self.consent, True, content_object=instance
)
return instance
# -*- encoding: utf-8 -*-
from django.core.management.base import BaseCommand
from enquiry.tests.scenario import default_scenario_enquiry
class Command(BaseCommand):
help = "Create demo data for 'enquiry'"
def handle(self, *args, **options):
default_scenario_enquiry()
print("Created 'enquiry' demo data...")
# -*- encoding: utf-8 -*-
from django.core.management.base import BaseCommand
from enquiry.models import Enquiry
from gdpr.models import Consent
class Command(BaseCommand):
help = "Initialise 'enquiry' application"
def handle(self, *args, **options):
Consent.objects.init_consent(
Enquiry.GDPR_CONTACT_SLUG,
"Enquiries",
"Enquiry (Contact) Form",
True,
)
print("Initialised 'enquiry' app...")
......@@ -11,6 +11,8 @@ from mail.models import Message
class Enquiry(TimeStampedModel):
GDPR_CONTACT_SLUG = "enquiry-app-contact"
name = models.CharField(max_length=100)
description = models.TextField()
email = models.EmailField(blank=True)
......
<div class="pure-u-1 pure-u-md-1-4">
<div class="pure-menu">
<span class="pure-menu-heading">Enquiries</span>
<ul class="pure-menu-list">
<li class="pure-menu-item">
<a href="{% url 'enquiry.list' %}" class="pure-menu-link">
<i class="fa fa-comments-o"></i>
Enquiries
</a>
</li>
</ul>
<div class="pure-g">
<div class="pure-u-1">
<h4>Enquiries</h4>
</div>
</div>
<div class="r-box">
<a href="{% url 'enquiry.list' %}" class="pure-menu-link">
<i class="fa fa-comments-o"></i>
Enquiries
</a>
</div>
</div>
......@@ -21,7 +21,7 @@
</div>
<div class="pure-g">
<div class="pure-u-1">
<table class="pure-table pure-table-bordered">
<table class="pure-table pure-table-bordered" width="100%">
<thead>
<tr>
<th>Date</th>
......
......@@ -8,3 +8,15 @@ class EnquiryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Enquiry
@factory.sequence
def email(n):
return "{:02d}@test.com".format(n)
@factory.sequence
def name(n):
return "name_{:02d}".format(n)
@factory.sequence
def phone(n):
return "{:02d}".format(n)
# -*- encoding: utf-8 -*-
from enquiry.models import Enquiry
from enquiry.tests.model_maker import make_enquiry
from mail.models import Notify
def get_enquiry_buy_some_hay():
return Enquiry.objects.get(description="Can I buy some hay?")
def default_scenario_enquiry():
Notify.objects.create_notify("test1@pkimber.net")
Notify.objects.create_notify("test2@pkimber.net")
make_enquiry("Rick", "Can I buy some hay?", "", "07840 538 357")
make_enquiry(
"Ryan",
(
"Can I see some of the fencing you have done?\n"
"I would like to see some of your standard agricultural "
"fencing on a local dairy farm. "
"I like this fencing: http://en.wikipedia.org/wiki/Fencing"
),
"test@pkimber.net",
"01234 567 890",
)
# -*- encoding: utf-8 -*-
from django.core.exceptions import ValidationError
from django.test import TestCase
import pytest
from .model_maker import make_enquiry
from enquiry.tests.factories import EnquiryFactory
class TestEnquiry(TestCase):
def test_enquiry(self):
"""A simple enquiry."""
make_enquiry(
"Rachel", "Can I buy some hay?", "web@pkimber.net", "07840 538 357"
)
def test_enquiry_no_contact(self):
"""This enquiry has no contact details."""
self.assertRaises(
ValidationError,
make_enquiry,
"Ruth",
"Can I buy some straw?",
"",
"",
)
def test_enquiry_no_description(self):
"""This enquiry has no description."""
self.assertRaises(
ValidationError,
make_enquiry,
"Ruth",
"",
"web@pkimber.net",
"07840 538 357",
)
@pytest.mark.django_db
def test_str():
enquiry = EnquiryFactory(
name="Patrick", email="test@pkimber.net", phone="01837"
)
assert "Patrick: test@pkimber.net, 01837" == str(enquiry)
# -*- encoding: utf-8 -*-
from django.test import TestCase
import pytest
from login.management.commands import demo_data_login
from django.core.management import call_command
from enquiry.management.commands import demo_data_enquiry, init_app_enquiry
class TestCommand(TestCase):
def test_demo_data(self):
"""Test the management command."""
pre_command = demo_data_login.Command()
pre_command.handle()
command = demo_data_enquiry.Command()
command.handle()
def test_init_app(self):
"""Test the management command."""
command = init_app_enquiry.Command()
command.handle()
@pytest.mark.django_db
def test_init_app_enquiry():
call_command("init_app_enquiry")
# -*- encoding: utf-8 -*-
from django.test import TestCase
from django.urls import reverse
from enquiry.tests.scenario import default_scenario_enquiry
from login.tests.factories import TEST_PASSWORD
from login.tests.scenario import default_scenario_login, get_user_staff
import pytest
from django.urls import reverse
from http import HTTPStatus
class TestView(TestCase):
from enquiry.tests.factories import EnquiryFactory
from login.tests.factories import TEST_PASSWORD, UserFactory
def setUp(self):
default_scenario_login()
default_scenario_enquiry()
staff = get_user_staff()
self.assertTrue(
self.client.login(username=staff.username, password=TEST_PASSWORD)
)
def test_list(self):
response = self.client.get(reverse("enquiry.list"))
self.assertEqual(response.status_code, 200)
@pytest.mark.django_db
def test_list(client):
u = UserFactory(is_staff=True)
assert client.login(username=u.username, password=TEST_PASSWORD) is True
EnquiryFactory(name="a")
EnquiryFactory(name="b")
response = client.get(reverse("enquiry.list"))
assert HTTPStatus.OK == response.status_code
assert "enquiry_list" in response.context
# order by created
assert ["b", "a"] == [x.name for x in response.context["enquiry_list"]]
......@@ -2,17 +2,41 @@
from django.views.generic import ListView
from braces.views import LoginRequiredMixin, StaffuserRequiredMixin
from django.db import transaction
from base.view_utils import BaseMixin
from gdpr.models import Consent
from .forms import EnquiryForm
from .models import Enquiry
class EnquiryCreateMixin:
form_class = EnquiryForm
model = Enquiry
def _consent(self):
return Consent.objects.get(slug=Enquiry.GDPR_CONTACT_SLUG)
def form_valid(self, form):
"""Do the form save within a transaction."""
with transaction.atomic():
result = super().form_valid(form)
return result
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
consent = self._consent()
# if we are not displaying the check box, then display the message
consent_message = ""
if not consent.show_checkbox:
consent_message = consent.message
context.update(dict(consent_message=consent_message))
return context
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update(dict(request=self.request, user=self.request.user))
kwargs.update(dict(consent=self._consent(), request=self.request))
return kwargs
......
......@@ -15,7 +15,7 @@ def get_env_variable(key):
return os.environ[key]
except KeyError:
error_msg = "Set the {} env variable".format(key)
print('ImproperlyConfigured: {}'.format(error_msg))
print("ImproperlyConfigured: {}".format(error_msg))
raise ImproperlyConfigured(error_msg)
......@@ -26,9 +26,7 @@ THUMBNAIL_DEBUG = DEBUG
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
ADMINS = (
('admin', 'code@pkimber.net'),
)
ADMINS = (("admin", "code@pkimber.net"),)
MANAGERS = ADMINS
......@@ -39,11 +37,11 @@ MANAGERS = ADMINS
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'Europe/London'
TIME_ZONE = "Europe/London"
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-gb'
LANGUAGE_CODE = "en-gb"
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
......@@ -58,22 +56,22 @@ USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = 'media'
MEDIA_ROOT = "media"
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = '/media/'
MEDIA_URL = "/media/"
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = 'web_static/'
STATIC_ROOT = "web_static/"
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'
STATIC_URL = "/static/"
# Additional locations of static files
STATICFILES_DIRS = (
......@@ -85,73 +83,64 @@ STATICFILES_DIRS = (
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'w@t8%tdwyi-n$u_s#4_+cwnq&6)1n)l3p-qe(ziala0j^vo12d'
SECRET_KEY = "w@t8%tdwyi-n$u_s#4_+cwnq&6)1n)l3p-qe(ziala0j^vo12d"
MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'reversion.middleware.RevisionMiddleware',
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"reversion.middleware.RevisionMiddleware",
)
ROOT_URLCONF = 'example_enquiry.urls'
ROOT_URLCONF = "example_enquiry.urls"
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'example_enquiry.wsgi.application'
WSGI_APPLICATION = "example_enquiry.wsgi.application"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
],
'string_if_invalid': '**** INVALID EXPRESSION: %s ****',
"string_if_invalid": "**** INVALID EXPRESSION: %s ****",
},
},
}
]
DJANGO_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
# admin after login, so we prefer login templates
'django.contrib.admin',
"django.contrib.admin",
)
THIRD_PARTY_APPS = (
'captcha',
'reversion',
)
THIRD_PARTY_APPS = ("captcha", "reversion")
LOCAL_APPS = (
'base',
'enquiry',
'example_enquiry',
'login',
'mail',
)
LOCAL_APPS = ("base", "enquiry", "example_enquiry", "gdpr", "login", "mail")
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
......@@ -161,41 +150,37 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
"version": 1,
"disable_existing_loggers": False,
"filters": {
"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}
},
"handlers": {
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
"loggers": {
"django.request": {
"handlers": ["mail_admins"], "level": "ERROR", "propagate": True
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
# django-compressor
COMPRESS_ENABLED = False # defaults to the opposite of DEBUG
COMPRESS_ENABLED = False # defaults to the opposite of DEBUG
# to send the emails, run 'django-admin.py mail_send'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
FTP_STATIC_DIR = None
FTP_STATIC_URL = None
# URL where requests are redirected after login when the contrib.auth.login
# view gets no next parameter.
LOGIN_REDIRECT_URL = reverse_lazy('project.dash')
LOGIN_REDIRECT_URL = reverse_lazy("project.dash")
# See the list of constants at the top of 'mail.models'
MAIL_TEMPLATE_TYPE = get_env_variable("MAIL_TEMPLATE_TYPE")
......@@ -205,9 +190,9 @@ MAILGUN_SERVER_NAME = get_env_variable("MAILGUN_SERVER_NAME")
# https://github.com/praekelt/django-recaptcha
NOCAPTCHA = True
RECAPTCHA_PUBLIC_KEY = get_env_variable('NORECAPTCHA_SITE_KEY')
RECAPTCHA_PRIVATE_KEY = get_env_variable('NORECAPTCHA_SECRET_KEY')
RECAPTCHA_PUBLIC_KEY = get_env_variable("NORECAPTCHA_SITE_KEY")
RECAPTCHA_PRIVATE_KEY = get_env_variable("NORECAPTCHA_SECRET_KEY")
# https://github.com/johnsensible/django-sendfile
SENDFILE_BACKEND = 'sendfile.backends.development'
SENDFILE_ROOT = 'media-private'
SENDFILE_BACKEND = "sendfile.backends.development"
SENDFILE_ROOT = "media-private"
# -*- encoding: utf-8 -*-
from celery import Celery
#from celery.schedules import crontab
# from celery.schedules import crontab
from django.conf import settings
# Working through:
# http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html#using-celery-with-django
app = Celery('example')
app = Celery("example")
# Config in one of three ways:
# 1) settings on the app
......@@ -16,6 +17,6 @@ app = Celery('example')
# 2) dedicated config module ('project/celeryconfig.py')
# app.config_from_object('project.celeryconfig')
# 3) read from the django settings
app.config_from_object('django.conf:settings')
app.config_from_object("django.conf:settings")
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
......@@ -2,13 +2,13 @@
from .base import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'temp.db', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
"default": {
"ENGINE": "django.db.backends.sqlite3", # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
"NAME": "temp.db", # Or path to database file if using sqlite3.
"USER": "", # Not used with sqlite3.
"PASSWORD": "", # Not used with sqlite3.
"HOST": "", # Set to empty string for localhost. Not used with sqlite3.
"PORT": "", # Set to empty string for default. Not used with sqlite3.
}
}
......
......@@ -2,13 +2,13 @@
from .base import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'temp.db', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
"default": {
"ENGINE": "django.db.backends.sqlite3", # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
"NAME": "temp.db", # Or path to database file if using sqlite3.
"USER": "", # Not used with sqlite3.
"PASSWORD": "", # Not used with sqlite3.
"HOST": "", # Set to empty string for localhost. Not used with sqlite3.
"PORT": "", # Set to empty string for default. Not used with sqlite3.
}
}
......
......@@ -3,13 +3,13 @@ from .base import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'temp.db',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "temp.db",
"USER": "",
"PASSWORD": "",
"HOST": "",
"PORT": "",
}
}
......
......@@ -11,9 +11,7 @@
{% block content %}
<div class="pure-g">
<div class="pure-u-1">
<div class="l-box">
{% include '_form.html' with legend='Enquiry' %}
</div>
{% include '_form.html' with legend='Enquiry' help_text=consent_message inline_checkbox=True %}
</div>
</div>
{% endblock content %}
......@@ -12,4 +12,5 @@
{% block content %}
{% include 'enquiry/_settings.html' %}
{% include 'gdpr/_settings.html' %}
{% endblock content %}
# -*- encoding: utf-8 -*-
import os
import pytest
from django.test import TestCase
from django.urls import reverse
from http import HTTPStatus
from enquiry.models import Enquiry
from enquiry.tests.scenario import default_scenario_enquiry
from login.tests.factories import TEST_PASSWORD
from login.tests.scenario import default_scenario_login, get_user_staff
from gdpr.models import UserConsent