diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py
index 68549449..f9bb4c20 100644
--- a/PyRIGS/decorators.py
+++ b/PyRIGS/decorators.py
@@ -1,6 +1,5 @@
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import render
-from django.template import RequestContext
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
@@ -79,3 +78,17 @@ def api_key_required(function):
return error_resp
return function(request, *args, **kwargs)
return wrap
+
+
+def nottinghamtec_address_required(function):
+ """
+ Checks that the current user has an email address ending @nottinghamtec.co.uk
+ """
+ def wrap(request, *args, **kwargs):
+ # Fail if current user's email address isn't @nottinghamtec.co.uk
+ if not request.user.email.endswith('@nottinghamtec.co.uk'):
+ error_resp = render(request, 'RIGS/eventauthorisation_request_error.html')
+ return error_resp
+
+ return function(request, *args, **kwargs)
+ return wrap
diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py
index 5c010a23..21f36848 100644
--- a/PyRIGS/settings.py
+++ b/PyRIGS/settings.py
@@ -233,3 +233,4 @@ TEMPLATES = [
USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
+AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
diff --git a/RIGS/__init__.py b/RIGS/__init__.py
index e69de29b..f6f847cc 100644
--- a/RIGS/__init__.py
+++ b/RIGS/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'RIGS.apps.RIGSAppConfig'
diff --git a/RIGS/apps.py b/RIGS/apps.py
new file mode 100644
index 00000000..a0dc3724
--- /dev/null
+++ b/RIGS/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class RIGSAppConfig(AppConfig):
+ name = 'RIGS'
+
+ def ready(self):
+ import RIGS.signals
diff --git a/RIGS/forms.py b/RIGS/forms.py
index 85fd0394..95576ca3 100644
--- a/RIGS/forms.py
+++ b/RIGS/forms.py
@@ -144,4 +144,32 @@ class EventForm(forms.ModelForm):
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
- 'collector', 'purchase_order']
+ 'purchase_order', 'collector']
+
+
+class BaseClientEventAuthorisationForm(forms.ModelForm):
+ tos = forms.BooleanField(required=True, label="Terms of hire")
+ name = forms.CharField(label="Your Name")
+
+ def clean(self):
+ if self.cleaned_data.get('amount') != self.instance.event.total:
+ self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
+ return super(BaseClientEventAuthorisationForm, self).clean()
+
+ class Meta:
+ abstract = True
+
+
+class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
+ def __init__(self, **kwargs):
+ super(InternalClientEventAuthorisationForm, self).__init__(**kwargs)
+ self.fields['uni_id'].required = True
+ self.fields['account_code'].required = True
+
+ class Meta:
+ model = models.EventAuthorisation
+ fields = ('tos', 'name', 'amount', 'uni_id', 'account_code')
+
+
+class EventAuthorisationRequestForm(forms.Form):
+ email = forms.EmailField(required=True, label='Authoriser Email')
diff --git a/RIGS/migrations/0025_eventauthorisation.py b/RIGS/migrations/0025_eventauthorisation.py
new file mode 100644
index 00000000..2065c11d
--- /dev/null
+++ b/RIGS/migrations/0025_eventauthorisation.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0024_auto_20160229_2042'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='EventAuthorisation',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('email', models.EmailField(max_length=254)),
+ ('name', models.CharField(max_length=255)),
+ ('uni_id', models.CharField(max_length=10, null=True, verbose_name=b'University ID', blank=True)),
+ ('account_code', models.CharField(max_length=50, null=True, blank=True)),
+ ('amount', models.DecimalField(verbose_name=b'authorisation amount', max_digits=10, decimal_places=2)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('event', models.ForeignKey(related_name='authroisations', to='RIGS.Event')),
+ ],
+ ),
+ ]
diff --git a/RIGS/migrations/0026_remove_eventauthorisation_created_at.py b/RIGS/migrations/0026_remove_eventauthorisation_created_at.py
new file mode 100644
index 00000000..c5ddc143
--- /dev/null
+++ b/RIGS/migrations/0026_remove_eventauthorisation_created_at.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0025_eventauthorisation'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='eventauthorisation',
+ name='created_at',
+ ),
+ ]
diff --git a/RIGS/migrations/0027_eventauthorisation_event_singular.py b/RIGS/migrations/0027_eventauthorisation_event_singular.py
new file mode 100644
index 00000000..d7796895
--- /dev/null
+++ b/RIGS/migrations/0027_eventauthorisation_event_singular.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0026_remove_eventauthorisation_created_at'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='eventauthorisation',
+ name='event',
+ field=models.OneToOneField(related_name='authorisation', to='RIGS.Event'),
+ ),
+ ]
diff --git a/RIGS/migrations/0029_eventauthorisation_sent_by.py b/RIGS/migrations/0029_eventauthorisation_sent_by.py
new file mode 100644
index 00000000..592bc968
--- /dev/null
+++ b/RIGS/migrations/0029_eventauthorisation_sent_by.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0027_eventauthorisation_event_singular'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='eventauthorisation',
+ name='sent_by',
+ field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL),
+ preserve_default=False,
+ ),
+ ]
diff --git a/RIGS/migrations/0030_auth_request_sending.py b/RIGS/migrations/0030_auth_request_sending.py
new file mode 100644
index 00000000..7243e9a7
--- /dev/null
+++ b/RIGS/migrations/0030_auth_request_sending.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0029_eventauthorisation_sent_by'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='event',
+ name='auth_request_at',
+ field=models.DateTimeField(null=True, blank=True),
+ ),
+ migrations.AddField(
+ model_name='event',
+ name='auth_request_by',
+ field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True),
+ ),
+ migrations.AddField(
+ model_name='event',
+ name='auth_request_to',
+ field=models.EmailField(max_length=254, null=True, blank=True),
+ ),
+ ]
diff --git a/RIGS/migrations/0031_merge_20170512_2102.py b/RIGS/migrations/0031_merge_20170512_2102.py
new file mode 100644
index 00000000..7056c46b
--- /dev/null
+++ b/RIGS/migrations/0031_merge_20170512_2102.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.1 on 2017-05-12 20:02
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0030_auth_request_sending'),
+ ('RIGS', '0026_auto_20170510_1846'),
+ ]
+
+ operations = [
+ ]
diff --git a/RIGS/models.py b/RIGS/models.py
index b245f543..c1a33eb8 100644
--- a/RIGS/models.py
+++ b/RIGS/models.py
@@ -332,6 +332,11 @@ class Event(models.Model, RevisionMixin):
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
+ # Authorisation request details
+ auth_request_by = models.ForeignKey('Profile', null=True, blank=True)
+ auth_request_at = models.DateTimeField(null=True, blank=True)
+ auth_request_to = models.EmailField(null=True, blank=True)
+
# Calculated values
"""
EX Vat
@@ -364,7 +369,7 @@ class Event(models.Model, RevisionMixin):
@property
def vat(self):
- return self.sum_total * self.vat_rate.rate
+ return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
"""
Inc VAT
@@ -372,7 +377,7 @@ class Event(models.Model, RevisionMixin):
@property
def total(self):
- return self.sum_total + self.vat
+ return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
@property
def cancelled(self):
@@ -382,6 +387,10 @@ class Event(models.Model, RevisionMixin):
def confirmed(self):
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
+ @property
+ def authorised(self):
+ return not self.internal and self.purchase_order or self.authorisation.amount == self.total
+
@property
def has_start_time(self):
return self.start_time is not None
@@ -442,6 +451,10 @@ class Event(models.Model, RevisionMixin):
else:
return endDate
+ @property
+ def internal(self):
+ return self.organisation and self.organisation.union_account
+
objects = EventManager()
def get_absolute_url(self):
@@ -498,6 +511,17 @@ class EventCrew(models.Model):
notes = models.TextField(blank=True, null=True)
+@reversion.register
+class EventAuthorisation(models.Model, RevisionMixin):
+ event = models.OneToOneField('Event', related_name='authorisation')
+ email = models.EmailField()
+ name = models.CharField(max_length=255)
+ uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
+ account_code = models.CharField(max_length=50, blank=True, null=True)
+ amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
+ sent_by = models.ForeignKey('RIGS.Profile')
+
+
@python_2_unicode_compatible
class Invoice(models.Model):
event = models.OneToOneField('Event')
diff --git a/RIGS/rigboard.py b/RIGS/rigboard.py
index 0d2c6ec8..4546b502 100644
--- a/RIGS/rigboard.py
+++ b/RIGS/rigboard.py
@@ -1,8 +1,9 @@
-import os
import cStringIO as StringIO
from io import BytesIO
import urllib2
+from django.contrib.staticfiles.storage import staticfiles_storage
+from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.views import generic
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import get_object_or_404
@@ -10,14 +11,19 @@ from django.template import RequestContext
from django.template.loader import get_template
from django.conf import settings
from django.core.urlresolvers import reverse
+from django.core import signing
from django.http import HttpResponse
+from django.core.exceptions import SuspiciousOperation
from django.db.models import Q
from django.contrib import messages
+from django.utils.decorators import method_decorator
from z3c.rml import rml2pdf
from PyPDF2 import PdfFileMerger, PdfFileReader
import simplejson
+import premailer
from RIGS import models, forms
+from PyRIGS import decorators
import datetime
import re
import copy
@@ -36,15 +42,17 @@ class RigboardIndex(generic.TemplateView):
context['events'] = models.Event.objects.current_events()
return context
+
class WebCalendar(generic.TemplateView):
template_name = 'RIGS/calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
- context['view'] = kwargs.get('view','')
- context['date'] = kwargs.get('date','')
+ context['view'] = kwargs.get('view', '')
+ context['date'] = kwargs.get('date', '')
return context
+
class EventDetail(generic.DetailView):
model = models.Event
@@ -53,7 +61,6 @@ class EventOembed(generic.View):
model = models.Event
def get(self, request, pk=None):
-
embed_url = reverse('event_embed', args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
@@ -85,7 +92,6 @@ class EventCreate(generic.CreateView):
if re.search('"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
-
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.iteritems():
value = form[field].value()
@@ -112,20 +118,35 @@ class EventUpdate(generic.UpdateView):
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
+
+ # If this event has already been emailed to a client, show a warning
+ if self.object.auth_request_at is not None:
+ messages.info(self.request, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
+
+ if hasattr(self.object, 'authorised'):
+ messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
+
return context
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
+
class EventDuplicate(EventUpdate):
def get_object(self, queryset=None):
- old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
- new = copy.copy(old) # Make a copy of the object in memory
- new.based_on = old # Make the new event based on the old event
+ old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
+ new = copy.copy(old) # Make a copy of the object in memory
+ new.based_on = old # Make the new event based on the old event
new.purchase_order = None
- if self.request.method in ('POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
- new.pk = None # This means a new event will be created on save, and all items will be re-created
+ # Remove all the authorisation information from the new event
+ new.auth_request_to = None
+ new.auth_request_by = None
+ new.auth_request_at = None
+
+ if self.request.method in (
+ 'POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
+ new.pk = None # This means a new event will be created on save, and all items will be re-created
else:
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
@@ -141,34 +162,26 @@ class EventPrint(generic.View):
def get(self, request, pk):
object = get_object_or_404(models.Event, pk=pk)
template = get_template('RIGS/event_print.xml')
- copies = ('TEC', 'Client')
merger = PdfFileMerger()
- for thisCopy in copies:
+ context = {
+ 'object': object,
+ 'fonts': {
+ 'opensans': {
+ 'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
+ 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
+ }
+ },
+ 'quote': True,
+ 'current_user': request.user,
+ }
- context = { # this should be outside the loop, but bug in 1.8.2 prevents this
- 'object': object,
- 'fonts': {
- 'opensans': {
- 'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
- 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
- }
- },
- 'copy': thisCopy,
- 'current_user': request.user,
- }
+ rml = template.render(context)
- # context['copy'] = copy # this is the way to do it once we upgrade to Django 1.8.3
-
- rml = template.render(context)
- buffer = StringIO.StringIO()
-
- buffer = rml2pdf.parseString(rml)
-
- merger.append(PdfFileReader(buffer))
-
- buffer.close()
+ buffer = rml2pdf.parseString(rml)
+ merger.append(PdfFileReader(buffer))
+ buffer.close()
terms = urllib2.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(StringIO.StringIO(terms.read()))
@@ -184,6 +197,7 @@ class EventPrint(generic.View):
response.write(merged.getvalue())
return response
+
class EventArchive(generic.ArchiveIndexView):
model = models.Event
date_field = "start_date"
@@ -220,3 +234,148 @@ class EventArchive(generic.ArchiveIndexView):
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs
+
+
+class EventAuthorise(generic.UpdateView):
+ template_name = 'RIGS/eventauthorisation_form.html'
+ success_template = 'RIGS/eventauthorisation_success.html'
+
+ def form_valid(self, form):
+ self.object = form.save()
+
+ self.template_name = self.success_template
+ messages.add_message(self.request, messages.SUCCESS,
+ 'Success! Your event has been authorised. ' +
+ 'You will also receive email confirmation to %s.' % (self.object.email))
+ return self.render_to_response(self.get_context_data())
+
+ @property
+ def event(self):
+ return models.Event.objects.select_related('organisation', 'person', 'venue').get(pk=self.kwargs['pk'])
+
+ def get_object(self, queryset=None):
+ return getattr(self.event, 'authorisation', None)
+
+ def get_form_class(self):
+ return forms.InternalClientEventAuthorisationForm
+
+ def get_context_data(self, **kwargs):
+ context = super(EventAuthorise, self).get_context_data(**kwargs)
+ context['event'] = self.event
+
+ context['tos_url'] = settings.TERMS_OF_HIRE_URL
+ return context
+
+ def get(self, request, *args, **kwargs):
+ if self.get_object() is not None and self.get_object().pk is not None:
+ if self.event.authorised:
+ messages.add_message(self.request, messages.WARNING,
+ "This event has already been authorised. "
+ "Reauthorising is not necessary at this time.")
+ else:
+ messages.add_message(self.request, messages.WARNING,
+ "This event has already been authorised, but the amount has changed. " +
+ "Please check the amount and reauthorise.")
+ return super(EventAuthorise, self).get(request, *args, **kwargs)
+
+ def get_form(self, **kwargs):
+ form = super(EventAuthorise, self).get_form(**kwargs)
+ form.instance.event = self.event
+ form.instance.email = self.request.email
+ form.instance.sent_by = self.request.sent_by
+ return form
+
+ def dispatch(self, request, *args, **kwargs):
+ # Verify our signature matches up and all is well with the integrity of the URL
+ try:
+ data = signing.loads(kwargs.get('hmac'))
+ assert int(kwargs.get('pk')) == int(data.get('pk'))
+ request.email = data['email']
+ request.sent_by = models.Profile.objects.get(pk=data['sent_by'])
+ except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
+ raise SuspiciousOperation(
+ "This URL is invalid. Please ask your TEC contact for a new URL")
+ return super(EventAuthorise, self).dispatch(request, *args, **kwargs)
+
+
+class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
+ model = models.Event
+ form_class = forms.EventAuthorisationRequestForm
+ template_name = 'RIGS/eventauthorisation_request.html'
+
+ @method_decorator(decorators.nottinghamtec_address_required)
+ def dispatch(self, *args, **kwargs):
+ return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
+
+ @property
+ def object(self):
+ return self.get_object()
+
+ def get_success_url(self):
+ if self.request.is_ajax():
+ url = reverse_lazy('closemodal')
+ messages.info(self.request, "location.reload()")
+ else:
+ url = reverse_lazy('event_detail', kwargs={
+ 'pk': self.object.pk,
+ })
+ messages.add_message(self.request, messages.SUCCESS, "Authorisation request successfully sent.")
+ return url
+
+ def form_valid(self, form):
+ email = form.cleaned_data['email']
+ event = self.object
+ event.auth_request_by = self.request.user
+ event.auth_request_at = datetime.datetime.now()
+ event.auth_request_to = email
+ event.save()
+
+ context = {
+ 'object': self.object,
+ 'request': self.request,
+ 'hmac': signing.dumps({
+ 'pk': self.object.pk,
+ 'email': email,
+ 'sent_by': self.request.user.pk,
+ }),
+ }
+ if email == event.person.email:
+ context['to_name'] = event.person.name
+
+ msg = EmailMultiAlternatives(
+ "N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
+ get_template("RIGS/eventauthorisation_client_request.txt").render(context),
+ to=[email],
+ reply_to=[self.request.user.email],
+ )
+ css = staticfiles_storage.path('css/email.css')
+ html = premailer.Premailer(get_template("RIGS/eventauthorisation_client_request.html").render(context),
+ external_styles=css).transform()
+ msg.attach_alternative(html, 'text/html')
+
+ msg.send()
+
+ return super(EventAuthorisationRequest, self).form_valid(form)
+
+
+class EventAuthoriseRequestEmailPreview(generic.DetailView):
+ template_name = "RIGS/eventauthorisation_client_request.html"
+ model = models.Event
+
+ def render_to_response(self, context, **response_kwargs):
+ from django.contrib.staticfiles.storage import staticfiles_storage
+ css = staticfiles_storage.path('css/email.css')
+ response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
+ assert isinstance(response, HttpResponse)
+ response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
+ return response
+
+ def get_context_data(self, **kwargs):
+ context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs)
+ context['hmac'] = signing.dumps({
+ 'pk': self.object.pk,
+ 'email': self.request.GET.get('email', 'hello@world.test'),
+ 'sent_by': self.request.user.pk,
+ })
+ context['to_name'] = self.request.GET.get('to_name', None)
+ return context
diff --git a/RIGS/signals.py b/RIGS/signals.py
new file mode 100644
index 00000000..faf0f873
--- /dev/null
+++ b/RIGS/signals.py
@@ -0,0 +1,98 @@
+import cStringIO as StringIO
+import re
+import urllib2
+from io import BytesIO
+
+import reversion
+from PyPDF2 import PdfFileReader, PdfFileMerger
+from django.conf import settings
+from django.contrib.staticfiles.storage import staticfiles_storage
+from django.core.mail import EmailMessage, EmailMultiAlternatives
+from django.template.loader import get_template
+from premailer import Premailer
+from z3c.rml import rml2pdf
+
+from RIGS import models
+
+
+def send_eventauthorisation_success_email(instance):
+ # Generate PDF first to prevent context conflicts
+ context = {
+ 'object': instance.event,
+ 'fonts': {
+ 'opensans': {
+ 'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
+ 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
+ }
+ },
+ 'receipt': True,
+ 'current_user': False,
+ }
+
+ template = get_template('RIGS/event_print.xml')
+ merger = PdfFileMerger()
+
+ rml = template.render(context)
+
+ buffer = rml2pdf.parseString(rml)
+ merger.append(PdfFileReader(buffer))
+ buffer.close()
+
+ terms = urllib2.urlopen(settings.TERMS_OF_HIRE_URL)
+ merger.append(StringIO.StringIO(terms.read()))
+
+ merged = BytesIO()
+ merger.write(merged)
+
+ # Produce email content
+ context = {
+ 'object': instance,
+ }
+
+ if instance.email == instance.event.person.email:
+ context['to_name'] = instance.event.person.name
+
+ subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
+
+ client_email = EmailMultiAlternatives(
+ subject,
+ get_template("RIGS/eventauthorisation_client_success.txt").render(context),
+ to=[instance.email],
+ reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
+ )
+
+ css = staticfiles_storage.path('css/email.css')
+ html = Premailer(get_template("RIGS/eventauthorisation_client_success.html").render(context),
+ external_styles=css).transform()
+ client_email.attach_alternative(html, 'text/html')
+
+ escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', instance.event.name)
+
+ client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
+ merged.getvalue(),
+ 'application/pdf'
+ )
+
+ if instance.event.mic:
+ mic_email_address = instance.event.mic.email
+ else:
+ mic_email_address = settings.AUTHORISATION_NOTIFICATION_ADDRESS
+
+ mic_email = EmailMessage(
+ subject,
+ get_template("RIGS/eventauthorisation_mic_success.txt").render(context),
+ to=[mic_email_address]
+ )
+
+ # Now we have both emails successfully generated, send them out
+ client_email.send(fail_silently=True)
+ mic_email.send(fail_silently=True)
+
+
+def on_revision_commit(instances, **kwargs):
+ for instance in instances:
+ if isinstance(instance, models.EventAuthorisation):
+ send_eventauthorisation_success_email(instance)
+
+
+reversion.revisions.post_revision_commit.connect(on_revision_commit)
diff --git a/RIGS/static/css/email.css b/RIGS/static/css/email.css
new file mode 100644
index 00000000..915b52f3
--- /dev/null
+++ b/RIGS/static/css/email.css
@@ -0,0 +1 @@
+body{margin:0px}.main-table{width:100%;border-collapse:collapse}.client-header{background-image:url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");background-color:#222;background-repeat:no-repeat;background-position:center;width:100%;margin-bottom:28px}.client-header .logos{width:100%;max-width:640px}.client-header img{height:110px}.content-container{width:100%}.content-container .content{font-family:"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;width:100%;max-width:600px;padding:10px;text-align:left}.content-container .content .button-container{width:100%}.content-container .content .button-container .button{padding:6px 12px;background-color:#357ebf;border-radius:4px}.content-container .content .button-container .button a{color:#fff;text-decoration:none}
diff --git a/RIGS/static/js/interaction.js b/RIGS/static/js/interaction.js
index e7d023e1..56f04dec 100644
--- a/RIGS/static/js/interaction.js
+++ b/RIGS/static/js/interaction.js
@@ -136,4 +136,4 @@ $("#item-table tbody").sortable({
});
}
-});
\ No newline at end of file
+});
diff --git a/RIGS/static/scss/email.scss b/RIGS/static/scss/email.scss
new file mode 100644
index 00000000..73ffe478
--- /dev/null
+++ b/RIGS/static/scss/email.scss
@@ -0,0 +1,64 @@
+$button_color: #357ebf;
+
+body{
+ margin: 0px;
+}
+
+.main-table{
+ width: 100%;
+ border-collapse: collapse;
+
+}
+
+.client-header {
+ background-image: url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");
+ background-color: #222;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ width: 100%;
+
+ margin-bottom: 28px;
+
+ .logos{
+ width: 100%;
+ max-width: 640px;
+ }
+
+ img {
+ height: 110px;
+ }
+}
+
+.content-container{
+ width: 100%;
+
+ .content {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+ width: 100%;
+ max-width: 600px;
+ padding: 10px;
+ text-align: left;
+
+ .button-container{
+ width: 100%;
+
+ .button {
+ padding: 6px 12px;
+ background-color: $button_color;
+ border-radius: 4px;
+
+ a {
+ color: #fff;
+ text-decoration: none;
+ }
+
+ }
+
+ }
+
+ }
+}
+
+
diff --git a/RIGS/templates/RIGS/client_eventdetails.html b/RIGS/templates/RIGS/client_eventdetails.html
new file mode 100644
index 00000000..4f3810f8
--- /dev/null
+++ b/RIGS/templates/RIGS/client_eventdetails.html
@@ -0,0 +1,78 @@
+
+
+
+
Contact Details
+
+
+ Person
+
+ {% if event.person %}
+ {{ event.person.name }}
+ {% endif %}
+
+
+ Email
+
+ {{ event.person.email }}
+
+
+ Phone Number
+ {{ event.person.phone }}
+
+
+
+ {% if event.organisation %}
+
+
Organisation
+
+
+ Organisation
+
+ {{ event.organisation.name }}
+
+
+ Phone Number
+ {{ object.organisation.phone }}
+
+
+
+ {% endif %}
+
+
+
+
+
Event Info
+
+
+ Event Venue
+
+ {% if object.venue %}
+
+ {{ object.venue }}
+
+ {% endif %}
+
+
+ Status
+ {{ event.get_status_display }}
+
+
+
+ Access From
+ {{ event.access_at|date:"D d M Y H:i"|default:"" }}
+
+ Event Starts
+ {{ event.start_date|date:"D d M Y" }} {{ event.start_time|date:"H:i" }}
+
+ Event Ends
+ {{ event.end_date|date:"D d M Y" }} {{ event.end_time|date:"H:i" }}
+
+
+
+ Event Description
+ {{ event.description|linebreaksbr }}
+
+
+
+
+
diff --git a/RIGS/templates/RIGS/event_detail.html b/RIGS/templates/RIGS/event_detail.html
index d4b089a9..5f2885b5 100644
--- a/RIGS/templates/RIGS/event_detail.html
+++ b/RIGS/templates/RIGS/event_detail.html
@@ -11,36 +11,9 @@
-
-
Edit
- {% if event.is_rig %}
-
Print
- {% endif %}
-
Duplicate
- {% if event.is_rig %}
- {% if perms.RIGS.add_invoice %}
-
- Invoice
- {% endif %}
- {% endif %}
-
+ {% include 'RIGS/event_detail_buttons.html' %}
-
+
{% endif %}
{% if object.is_rig %}
{# only need contact details for a rig #}
@@ -97,6 +70,39 @@
{% endif %}
+
+ {% if event.is_rig and event.internal %}
+
+
Client Authorisation
+
+
+ Authorised
+ {{ object.authorised|yesno:"Yes,No" }}
+
+ Authorised by
+
+ {% if object.authorisation %}
+ {{ object.authorisation.name }}
+ ({{ object.authorisation.email }} )
+ {% endif %}
+
+
+ Authorised at
+ {{ object.authorisation.last_edited_at }}
+
+ Authorised amount
+
+ {% if object.authorisation %}
+ £ {{ object.authorisation.amount|floatformat:"2" }}
+ {% endif %}
+
+
+ Requested by
+ {{ object.authorisation.sent_by }}
+
+
+
+ {% endif %}
{% endif %}
@@ -175,8 +181,25 @@
{% endif %}
{% if event.is_rig %}
-
PO
- {{ object.purchase_order }}
+
+
+ {% if object.internal %}
+ Authorisation Request
+ {{ object.auth_request_to|yesno:"Yes,No" }}
+
+ By
+ {{ object.auth_request_by }}
+
+ At
+ {{ object.auth_request_at|date:"D d M Y H:i"|default:"" }}
+
+ To
+ {{ object.auth_request_to }}
+
+ {% else %}
+ PO
+ {{ object.purchase_order }}
+ {% endif %}
{% endif %}
@@ -184,34 +207,7 @@
{% if not request.is_ajax %}
-
-
Edit
- {% if event.is_rig %}
-
Print
- {% endif %}
-
Duplicate
- {% if event.is_rig %}
- {% if perms.RIGS.add_invoice %}
-
- Invoice
- {% endif %}
- {% endif %}
-
+ {% include 'RIGS/event_detail_buttons.html' %}
{% endif %}
{% if event.is_rig %}
@@ -229,34 +225,7 @@
{% if not request.is_ajax %}
-
-
Edit
- {% if event.is_rig %}
-
Print
- {% endif %}
-
Duplicate
- {% if event.is_rig %}
- {% if perms.RIGS.add_invoice %}
-
- Invoice
- {% endif %}
- {% endif %}
-
+ {% include 'RIGS/event_detail_buttons.html' %}
{% endif %}
{% endif %}
diff --git a/RIGS/templates/RIGS/event_detail_buttons.html b/RIGS/templates/RIGS/event_detail_buttons.html
new file mode 100644
index 00000000..878684cc
--- /dev/null
+++ b/RIGS/templates/RIGS/event_detail_buttons.html
@@ -0,0 +1,54 @@
+
diff --git a/RIGS/templates/RIGS/event_form.html b/RIGS/templates/RIGS/event_form.html
index 6832d571..d7ba9591 100644
--- a/RIGS/templates/RIGS/event_form.html
+++ b/RIGS/templates/RIGS/event_form.html
@@ -398,6 +398,7 @@
{% render_field form.collector class+="form-control" %}
+
{% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/RIGS/templates/RIGS/event_print.xml b/RIGS/templates/RIGS/event_print.xml
index 6d693117..f91409ac 100644
--- a/RIGS/templates/RIGS/event_print.xml
+++ b/RIGS/templates/RIGS/event_print.xml
@@ -22,21 +22,21 @@
-
+
-
-
+
+
-
+
@@ -100,10 +100,11 @@
- {% if not invoice %}[{{ copy }} Copy] {% endif %}
[Page of ]
- [Paperwork generated by {{current_user.name}} | {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
+
+ [Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
+
@@ -115,10 +116,11 @@
- {% if not invoice %}[{{ copy }} Copy] {% endif %}
[Page of ]
- [Paperwork generated by {{current_user.name}} | {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
+
+ [Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
+
@@ -128,4 +130,4 @@
{% include "RIGS/event_print_page.xml" %}
-
\ No newline at end of file
+
diff --git a/RIGS/templates/RIGS/event_print_page.xml b/RIGS/templates/RIGS/event_print_page.xml
index a7049459..4b1ddfdf 100644
--- a/RIGS/templates/RIGS/event_print_page.xml
+++ b/RIGS/templates/RIGS/event_print_page.xml
@@ -1,59 +1,70 @@
-{% if invoice %}
-
+
- {% endif %}
+ N{{ object.pk|stringformat:"05d" }}: '{{ object.name }}'
-N{{ object.pk|stringformat:"05d" }}: '{{ object.name }}'
-
-
-{{object.start_date|date:"D jS N Y"}}
-
-
-
-
- {{ object.description|default_if_none:""|linebreaksbr }}
+
+ {{object.start_date|date:"D jS N Y"}}
-
-
-{% if invoice %}
+
+
+ {{ object.description|default_if_none:""|linebreaksbr }}
+
+
- INVOICE
-
-
-
- Invoice Number
-
- {{ invoice.pk|stringformat:"05d" }}
-
-
-
- Invoice Date
-
- {{ invoice.invoice_date|date:"d/m/Y" }}
-
-
-
- PO Number
-
- {{ object.purchase_order|default_if_none:"" }}
-
-
+ {% if invoice %}
+ INVOICE
+
+
+
+ Invoice Number
+
+ {{ invoice.pk|stringformat:"05d" }}
+
+
+
+ Invoice Date
+
+ {{ invoice.invoice_date|date:"d/m/Y" }}
+
+
-
+ {% if not object.internal %}
+
+ PO
+ {{ object.purchase_order }}
+
+ {% endif %}
+
+
+ {% elif quote %}
+
+ QUOTE
+
+
+
+ Quote Date
+
+ {% now "d/m/Y" %}
+
+
+
+
+ {% elif receipt %}
+
+ CONFIRMATION
+
+ {% endif %}
-
-{% endif %}
-
@@ -189,16 +200,16 @@
- {% if not invoice %}VAT Registration Number: 170734807{% endif %}
+ {% if quote %}VAT Registration Number: 170734807{% endif %}
Total (ex. VAT)
£ {{ object.sum_total|floatformat:2 }}
- {% if not invoice %}
-
- The full hire fee is payable at least 10 days before the event.
-
+ {% if quote %}
+
+ This quote is valid for 30 days unless otherwise arranged.
+
{% endif %}
VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%
@@ -206,113 +217,140 @@
-
-
- {% if invoice %}
- VAT Registration Number: 170734807
+ {% if quote %}
+
+ The full hire fee is payable at least 10 days before the event.
+
+ {% endif %}
+
+ {% if invoice %}
+ Total
+ £ {{ object.total|floatformat:2 }}
+ {% else %}
+
+
+ Total
+
+
+
+
+ £ {{ object.total|floatformat:2 }}
+
+
+ {% endif %}
+
+
+
+
+{% if invoice %}
+
+
+ Payments
+
+
+
+
+ Method
+
+
+
+
+ Date
+
+
+
+
+ Amount
+
+
+
+ {% for payment in object.invoice.payment_set.all %}
+
+ {{ payment.get_method_display }}
+ {{ payment.date }}
+ £ {{ payment.amount|floatformat:2 }}
+
+ {% endfor %}
+
+
+
+
+ Payment Total
+ £ {{ object.invoice.payment_total|floatformat:2 }}
+
+
+
+
+
+ Balance (ex. VAT)
+
+
+
+
+ £ {{ object.invoice.balance|floatformat:2 }}
+
+
+
+
+
+{% endif %}
+
+
+ >
+ {% if quote %}
+
+
+
+ {% if object.internal %}
+ Bookings will
+ not
+ be confirmed until the event is authorised online.
+
{% else %}
- This contract is not an invoice.
+ Bookings will
+ not
+ be confirmed until we have received written confirmation and a Purchase Order.
+
{% endif %}
-
-
-
+
+
+
+ 24 Hour Emergency Contacts: 07825 065681 and 07825 065678
+
+ {% else %}
+
-
- Total
-
+ VAT Registration Number: 170734807
+
+ {% endif %}
+
+
+
+
-
- £ {{ object.total|floatformat:2 }}
-
+ {% if object.internal and object.authorised %}
+
+ Event authorised online by {{ object.authorisation.name }} ({{ object.authorisation.email }}) at
+ {{ object.authorisation.last_edited_at }}.
+
+
+
+
+ University ID
+ Account Code
+ Authorised Amount
+
+
+ {{ object.authorisation.uni_id }}
+ {{ object.authorisation.account_code }}
+ £ {{ object.authorisation.amount|floatformat:2 }}
+
+
+ {% endif %}
-{% if not invoice %}
-
-
-
-
- Bookings will
- not
- be confirmed until payment is received and the contract is signed.
-
-
-
-
- 24 Hour Emergency Contacts: 07825 065681 and 07825 065678
-
-
-
-
-
-
- To be signed on booking:
-
- {% if object.organisation.union_account %}
-
-
- I agree that am authorised to sign this invoice. I agree that I am the President/Treasurer of the hirer, or
- that I have provided written permission from either the President or Treasurer of the hirer stating that I can
- sign for this invoice.
-
-
-
-
- I have read, understood and fully accepted the current conditions of hire. I agree to return any dry hire
- items to TEC PA & Lighting in the same condition at the end of the hire period.
-
-
-
-
-
- Conditions of hire attached and available on the TEC PA & Lighting website. E&OE
-
-
-
-
- Please return this form directly to TEC PA & Lighting and not the Students' Union Finance Department.
-
-
-
-
- Account Code
-
-
-
-
-
- {% else %}
-
-
- I, the hirer, have read, understand and fully accept the current conditions of hire. This document forms a
- binding contract between TEC PA & Lighting and the hirer, the aforementioned conditions of hire forming
- an integral part of it.
-
-
-
-
-
- Conditions of hire attached and available on the TEC PA & Lighting website. E&OE
-
-
-
- {% include "RIGS/event_print_signature.xml" %}
-
-
- To be signed on the day of the event/hire:
-
-
-
- I, the hirer, have received the goods/services as requested and in good order. I agree to return any dry hire
- items to TEC PA & Lighting in a similar condition at the end of the hire period.
-
-
- {% endif %}
-
- {% include "RIGS/event_print_signature.xml" %}
-
- {% endif %}
-
\ No newline at end of file
+
diff --git a/RIGS/templates/RIGS/event_table.html b/RIGS/templates/RIGS/event_table.html
index 949c710a..c84b6624 100644
--- a/RIGS/templates/RIGS/event_table.html
+++ b/RIGS/templates/RIGS/event_table.html
@@ -33,13 +33,18 @@
- {{ event.name }}
+
+ {{ event.name }}
+
{% if event.venue %}
at {{ event.venue }}
{% endif %}
{% if event.dry_hire %}
Dry Hire
{% endif %}
+ {% if event.is_rig and perms.RIGS.view_event and event.authorised %}
+
+ {% endif %}
{% if event.is_rig and not event.cancelled %}
@@ -99,4 +104,4 @@
{% endfor %}
-
\ No newline at end of file
+
diff --git a/RIGS/templates/RIGS/eventauthorisation_client_request.html b/RIGS/templates/RIGS/eventauthorisation_client_request.html
new file mode 100644
index 00000000..a6bb7bb9
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_client_request.html
@@ -0,0 +1,41 @@
+{% extends 'base_client_email.html' %}
+
+{% block content %}
+
+ Hi {{ to_name|default:"there" }},
+
+ {{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}
+ | {{ object.name }} {% if not to_name %} on behalf of {{ object.person.name }} {% endif %}.
+
+
+ Please find the link below to complete the event booking process.
+ {% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
+ Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
+ this
+ email on.
+ {% endif %}
+
+
+
+
+
+
+ Your event will not be booked until you complete this form.
+
+ TEC PA & Lighting
+
+{% endblock %}
diff --git a/RIGS/templates/RIGS/eventauthorisation_client_request.txt b/RIGS/templates/RIGS/eventauthorisation_client_request.txt
new file mode 100644
index 00000000..7b1297b1
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_client_request.txt
@@ -0,0 +1,16 @@
+Hi {{ to_name|default:"there" }},
+
+{{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {{ object.person.name }}{% endif %}.
+
+ Please find the link below to complete the event booking process.
+ {% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
+ Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
+ this
+ email on.
+ {% endif %}
+
+{{ request.scheme }}://{{ request.get_host }}{% url 'event_authorise' object.pk hmac %}
+
+Please note you event will not be booked until you complete this form.
+
+TEC PA & Lighting
diff --git a/RIGS/templates/RIGS/eventauthorisation_client_success.html b/RIGS/templates/RIGS/eventauthorisation_client_success.html
new file mode 100644
index 00000000..dc6d0485
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_client_success.html
@@ -0,0 +1,21 @@
+{% extends 'base_client_email.html' %}
+
+{% block content %}
+
Hi {{ to_name|default:"there" }},
+
+
+ Your event N{{ object.event.pk|stringformat:"05d" }} has been successfully authorised
+ for £{{ object.amount }}
+ by {{ object.name }} as of {{ object.last_edited_at }} .
+
+
+
+ {% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
+ Your event is now fully booked and payment will be processed by the finance department automatically.
+ {% else %}{# external #}
+ Your event is now fully booked and our finance department will be contact to arrange payment.
+ {% endif %}
+
+
+ TEC PA & Lighting
+{% endblock %}
diff --git a/RIGS/templates/RIGS/eventauthorisation_client_success.txt b/RIGS/templates/RIGS/eventauthorisation_client_success.txt
new file mode 100644
index 00000000..ff934c4d
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_client_success.txt
@@ -0,0 +1,11 @@
+Hi {{ to_name|default:"there" }},
+
+Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
+
+{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
+Your event is now fully booked and payment will be processed by the finance department automatically.
+{% else %}{# external #}
+Your event is now fully booked and our finance department will be contact to arrange payment.
+{% endif %}
+
+TEC PA & Lighting
diff --git a/RIGS/templates/RIGS/eventauthorisation_form.html b/RIGS/templates/RIGS/eventauthorisation_form.html
new file mode 100644
index 00000000..06df386d
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_form.html
@@ -0,0 +1,133 @@
+{% extends 'base_client.html' %}
+{% load widget_tweaks %}
+{% load static %}
+
+{% block js %}
+
+
+{% endblock %}
+
+{% block title %}
+ {% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %} | {{ event.name }}
+{% endblock %}
+
+{% block content %}
+
+
+
+ {% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %}
+ | {{ event.name }} {% if event.dry_hire %}Dry Hire {% endif %}
+
+
+
+
+
+ {% include 'RIGS/client_eventdetails.html' %}
+
+
+
+
+ {% with object=event %}
+ {% include 'RIGS/item_table.html' %}
+ {% endwith %}
+
+
+
+
+
+
+
Event Authorisation
+
+
+
+
+
+{% endblock %}
diff --git a/RIGS/templates/RIGS/eventauthorisation_mic_success.txt b/RIGS/templates/RIGS/eventauthorisation_mic_success.txt
new file mode 100644
index 00000000..7548cad4
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_mic_success.txt
@@ -0,0 +1,5 @@
+Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
+
+Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
+
+The TEC Rig Information Gathering System
diff --git a/RIGS/templates/RIGS/eventauthorisation_request.html b/RIGS/templates/RIGS/eventauthorisation_request.html
new file mode 100644
index 00000000..6067b3fe
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_request.html
@@ -0,0 +1,59 @@
+{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
+{% load widget_tweaks %}
+
+{% block title %}Request Authorisation{% endblock %}
+
+{% block content %}
+
+
+
+
Send authorisation request email.
+
Pressing send will email the address provided. Please triple check everything before continuing.
+
+
+
+
+ Person Email
+ {{ object.person.email }}
+
+ Organisation Email
+ {{ object.organisation.email }}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/RIGS/templates/RIGS/eventauthorisation_request_error.html b/RIGS/templates/RIGS/eventauthorisation_request_error.html
new file mode 100644
index 00000000..b366622d
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_request_error.html
@@ -0,0 +1,15 @@
+{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
+{% load widget_tweaks %}
+
+{% block title %}NottinghamTEC Email Address Required{% endblock %}
+
+{% block content %}
+
+
+
+
An error occured.
+
Your RIGS account must have an @nottinghamtec.co.uk email address before you can send emails to clients.
+
+
+
+{% endblock %}
diff --git a/RIGS/templates/RIGS/eventauthorisation_success.html b/RIGS/templates/RIGS/eventauthorisation_success.html
new file mode 100644
index 00000000..cb24738a
--- /dev/null
+++ b/RIGS/templates/RIGS/eventauthorisation_success.html
@@ -0,0 +1,64 @@
+{% extends 'base_client.html' %}
+{% load widget_tweaks %}
+
+{% block title %}
+ {% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %} | {{ event.name }}
+{% endblock %}
+
+{% block content %}
+
+
+
+ {% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %}
+ | {{ event.name }} {% if event.dry_hire %}Dry Hire {% endif %}
+
+
+
+
+ {% include 'RIGS/client_eventdetails.html' %}
+
+
+
+ {% with object=event %}
+ {% include 'RIGS/item_table.html' %}
+ {% endwith %}
+
+
+
+
+
+
+
Event Authorisation
+
+
+
+
+
+ Name
+ {{ object.name }}
+
+ Email
+ {{ object.email }}
+
+ {% if internal %}
+ University ID
+ {{ object.uni_id }}
+ {% endif %}
+
+
+
+
+
+ Account code
+ {{ object.account_code }}
+
+ Authorised amount
+ £ {{ object.amount|floatformat:2 }}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html
index cfcfcfe9..b305a04f 100644
--- a/RIGS/templates/RIGS/invoice_detail.html
+++ b/RIGS/templates/RIGS/invoice_detail.html
@@ -76,8 +76,43 @@
{{ object.checked_in_by.name }}
{% endif %}
- PO
- {{ object.event.purchase_order }}
+
+
+ Authorised
+ {{ object.event.authorised|yesno:"Yes,No" }}
+
+ Authorised by
+
+ {% if object.event.authorised %}
+ {{ object.event.authorisation.name }}
+ ({{ object.event.authorisation.email }} )
+ {% endif %}
+
+
+ {% if object.event.internal %}
+ {# internal #}
+ Uni ID
+ {{ object.event.authorisation.uni_id }}
+
+ Account code
+ {{ object.event.authorisation.account_code }}
+ {% else %}
+ PO
+ {{ object.event.purchase_order }}
+ {% endif %}
+
+ Authorised at
+ {{ object.event.authorisation.last_edited_at }}
+
+ Authorised amount
+
+ {% if object.event.authorised %}
+ £ {{ object.event.authorisation.amount|floatformat:"2" }}
+ {% endif %}
+
+
+ Authorsation request sent by
+ {{ object.authorisation.sent_by }}
@@ -139,4 +174,4 @@
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/RIGS/templates/RIGS/invoice_list.html b/RIGS/templates/RIGS/invoice_list.html
index 2f34b6c9..63e61e11 100644
--- a/RIGS/templates/RIGS/invoice_list.html
+++ b/RIGS/templates/RIGS/invoice_list.html
@@ -50,7 +50,7 @@
{% if object.event.organisation %}
{{ object.event.organisation.name }}
- {{ object.event.organisation.union_account|yesno:'Internal,External' }}
+ {{ object.event.internal|yesno:'Internal,External' }}
{% else %}
{{ object.event.person.name }}
@@ -59,7 +59,11 @@
{{ object.event.start_date }}
{{ object.invoice_date }}
- {{ object.balance|floatformat:2 }}
+
+ {{ object.balance|floatformat:2 }}
+
+ {{ object.event.purchase_order }}
+
@@ -76,4 +80,4 @@
{% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py
index 7e9595cb..27675442 100644
--- a/RIGS/test_functional.py
+++ b/RIGS/test_functional.py
@@ -4,9 +4,11 @@ import re
import pytz
from datetime import date, time, datetime, timedelta
+
from django.core import mail
from django.db import transaction
-from django.test import LiveServerTestCase
+from django.http import HttpResponseBadRequest
+from django.test import LiveServerTestCase, TestCase
from django.test.client import Client
from selenium import webdriver
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
@@ -16,6 +18,9 @@ from selenium.webdriver.support.ui import WebDriverWait
from RIGS import models
from reversion import revisions as reversion
+from django.core.urlresolvers import reverse
+from django.core import mail, signing
+
from django.conf import settings
@@ -63,9 +68,9 @@ def create_browser(test_name, desired_capabilities):
@on_platforms(browsers)
class UserRegistrationTest(LiveServerTestCase):
-
def setUp(self):
self.browser = create_browser(self.id(), self.desired_capabilities)
+
self.browser.implicitly_wait(3) # Set implicit wait session wide
os.environ['RECAPTCHA_TESTING'] = 'True'
@@ -195,18 +200,19 @@ class UserRegistrationTest(LiveServerTestCase):
@on_platforms(browsers)
class EventTest(LiveServerTestCase):
-
def setUp(self):
self.profile = models.Profile(
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword")
self.profile.save()
+
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1')
self.browser = create_browser(self.id(), self.desired_capabilities)
self.browser.implicitly_wait(10) # Set implicit wait session wide
# self.browser.maximize_window()
+
os.environ['RECAPTCHA_TESTING'] = 'True'
def tearDown(self):
@@ -416,26 +422,28 @@ class EventTest(LiveServerTestCase):
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 1")
- modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
+ modal.find_element_by_id("item_description").send_keys(
+ "This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
- e.send_keys(Keys.ENTER) # enter submit
+ e.send_keys(Keys.ENTER) # enter submit
# Confirm item has been saved to json field
objectitems = self.browser.execute_script("return objectitems;")
self.assertEqual(1, len(objectitems))
- testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
+ testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
self.assertEqual("Test Item 1", testitem['name'])
- self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
+ self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
# See new item appear in table
- row = self.browser.find_element_by_id('item--1') # ID number is known, see above
+ row = self.browser.find_element_by_id('item--1') # ID number is known, see above
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
- self.assertIn("This is an item description", row.find_element_by_xpath('//div[@class="item-description"]').text)
+ self.assertIn("This is an item description",
+ row.find_element_by_xpath('//div[@class="item-description"]').text)
self.assertEqual(u'£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
self.assertEqual(u'£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
@@ -477,6 +485,7 @@ class EventTest(LiveServerTestCase):
# See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name')
+
self.assertIn("N%05d | Test Event Name"%event.pk, successTitle)
except WebDriverException:
# This is a dirty workaround for wercker being a bit funny and not running it correctly.
@@ -484,91 +493,105 @@ class EventTest(LiveServerTestCase):
pass
def testEventDuplicate(self):
- testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end", purchase_order="TESTPO")
+ testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
+ start_date=date.today() + timedelta(days=6),
+ description="start future no end",
+ purchase_order='TESTPO',
+ auth_request_by=self.profile,
+ auth_request_at=self.create_datetime(2015, 06, 04, 10, 00),
+ auth_request_to="some@email.address")
item1 = models.EventItem(
- event=testEvent,
- name="Test Item 1",
- cost="10.00",
- quantity="1",
- order=1
- ).save()
+ event=testEvent,
+ name="Test Item 1",
+ cost="10.00",
+ quantity="1",
+ order=1
+ ).save()
item2 = models.EventItem(
- event=testEvent,
- name="Test Item 2",
- description="Foo",
- cost="9.72",
- quantity="3",
- order=2,
- ).save()
+ event=testEvent,
+ name="Test Item 2",
+ description="Foo",
+ cost="9.72",
+ quantity="3",
+ order=2,
+ ).save()
self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk) + '/duplicate/')
self.authenticate('/event/' + str(testEvent.pk) + '/duplicate/')
+
wait = WebDriverWait(self.browser, 3) #setup WebDriverWait to use later (to wait for animations)
save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
form = self.browser.find_element_by_tag_name('form')
-
# Check the items are visible
- table = self.browser.find_element_by_id('item-table') # ID number is known, see above
+ table = self.browser.find_element_by_id('item-table') # ID number is known, see above
self.assertIn("Test Item 1", table.text)
self.assertIn("Test Item 2", table.text)
# Check the info message is visible
- self.assertIn("Event data duplicated but not yet saved",self.browser.find_element_by_id('content').text)
+ self.assertIn("Event data duplicated but not yet saved", self.browser.find_element_by_id('content').text)
# Add item
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 3")
- modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
+ modal.find_element_by_id("item_description").send_keys(
+ "This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
- e.send_keys(Keys.ENTER) # enter submit
+ e.send_keys(Keys.ENTER) # enter submit
# Attempt to save
save.click()
+ newEvent = models.Event.objects.latest('pk')
+
+ self.assertEqual(newEvent.auth_request_to, None)
+ self.assertEqual(newEvent.auth_request_by, None)
+ self.assertEqual(newEvent.auth_request_at, None)
+
+ self.assertFalse(hasattr(newEvent, 'authorised'))
+
self.assertNotIn("N%05d"%testEvent.pk, self.browser.find_element_by_xpath('//h1').text)
self.assertNotIn("Event data duplicated but not yet saved", self.browser.find_element_by_id('content').text) # Check info message not visible
# Check the new items are visible
- table = self.browser.find_element_by_id('item-table') # ID number is known, see above
+ table = self.browser.find_element_by_id('item-table') # ID number is known, see above
self.assertIn("Test Item 1", table.text)
self.assertIn("Test Item 2", table.text)
self.assertIn("Test Item 3", table.text)
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
-
- self.assertIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
-
+ self.assertIn("N0000%d" % testEvent.pk,
+ infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the PO hasn't carried through
self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
-
+ self.assertIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk)) #Go back to the old event
#Check that based-on hasn't crept into the old event
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
-
- self.assertNotIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
-
+ self.assertNotIn("N0000%d" % testEvent.pk,
+ infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the PO remains on the old event
self.assertIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
+ self.assertNotIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the items are as they were
- table = self.browser.find_element_by_id('item-table') # ID number is known, see above
+ table = self.browser.find_element_by_id('item-table') # ID number is known, see above
self.assertIn("Test Item 1", table.text)
self.assertIn("Test Item 2", table.text)
self.assertNotIn("Test Item 3", table.text)
@@ -578,6 +601,7 @@ class EventTest(LiveServerTestCase):
# Gets redirected to login and back
self.authenticate('/event/create/')
+
wait = WebDriverWait(self.browser, 3) #setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
@@ -604,7 +628,6 @@ class EventTest(LiveServerTestCase):
self.assertTrue(error.is_displayed())
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
-
# Same date, end time before start time
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
@@ -624,7 +647,6 @@ class EventTest(LiveServerTestCase):
self.assertTrue(error.is_displayed())
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
-
# Same date, end time before start time
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
@@ -638,7 +660,6 @@ class EventTest(LiveServerTestCase):
form.find_element_by_id('id_end_time').send_keys(Keys.DELETE)
form.find_element_by_id('id_end_time').send_keys('06:00')
-
# No end date, end time before start time
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
@@ -658,22 +679,23 @@ class EventTest(LiveServerTestCase):
self.assertTrue(error.is_displayed())
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
-
# 2 dates, end after start
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-26'")
+
self.browser.execute_script("document.getElementById('id_start_time').value=''")
self.browser.execute_script("document.getElementById('id_end_time').value=''")
# Attempt to save - should succeed
save.click()
-
+
# See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name')
+
self.assertIn("N%05d | Test Event Name"%event.pk, successTitle)
def testRigNonRig(self):
@@ -681,6 +703,7 @@ class EventTest(LiveServerTestCase):
# Gets redirected to login and back
self.authenticate('/event/create/')
+
wait = WebDriverWait(self.browser, 3) #setup WebDriverWait to use later (to wait for animations)
self.browser.implicitly_wait(3) #Set session-long wait (only works for non-existant DOM objects)
@@ -713,7 +736,8 @@ class EventTest(LiveServerTestCase):
person = models.Person(name="Event Detail Person", email="eventdetail@person.tests.rigs", phone="123 123")
person.save()
with transaction.atomic(), reversion.create_revision():
- organisation = models.Organisation(name="Event Detail Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
+ organisation = models.Organisation(name="Event Detail Organisation",
+ email="eventdetail@organisation.tests.rigs", phone="123 456").save()
with transaction.atomic(), reversion.create_revision():
venue = models.Venue(name="Event Detail Venue").save()
with transaction.atomic(), reversion.create_revision():
@@ -743,18 +767,21 @@ class EventTest(LiveServerTestCase):
order=2,
).save()
-
- self.browser.get(self.live_server_url + '/event/%d'%event.pk)
- self.authenticate('/event/%d/'%event.pk)
- self.assertIn("N%05d | %s"%(event.pk, event.name), self.browser.find_element_by_xpath('//h1').text)
+ self.browser.get(self.live_server_url + '/event/%d' % event.pk)
+ self.authenticate('/event/%d/' % event.pk)
+ self.assertIn("N%05d | %s" % (event.pk, event.name), self.browser.find_element_by_xpath('//h1').text)
personPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
- self.assertEqual(person.name, personPanel.find_element_by_xpath('//dt[text()="Person"]/following-sibling::dd[1]').text)
- self.assertEqual(person.email, personPanel.find_element_by_xpath('//dt[text()="Email"]/following-sibling::dd[1]').text)
- self.assertEqual(person.phone, personPanel.find_element_by_xpath('//dt[text()="Phone Number"]/following-sibling::dd[1]').text)
+ self.assertEqual(person.name,
+ personPanel.find_element_by_xpath('//dt[text()="Person"]/following-sibling::dd[1]').text)
+ self.assertEqual(person.email,
+ personPanel.find_element_by_xpath('//dt[text()="Email"]/following-sibling::dd[1]').text)
+ self.assertEqual(person.phone,
+ personPanel.find_element_by_xpath('//dt[text()="Phone Number"]/following-sibling::dd[1]').text)
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
+
def testEventEdit(self):
person = models.Person(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123").save()
organisation = models.Organisation(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
@@ -816,43 +843,66 @@ class EventTest(LiveServerTestCase):
@on_platforms(browsers)
class IcalTest(LiveServerTestCase):
-
def setUp(self):
self.all_events = set(range(1, 18))
self.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
self.not_current_events = set(self.all_events) - set(self.current_events)
- self.vatrate = models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1')
+ self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.profile = models.Profile(
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword")
self.profile.save()
# produce 7 normal events - 5 current - 1 last week - 1 two years ago - 2 provisional - 2 confirmed - 3 booked
- models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end")
- models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL, start_date=date.today(), description="start today no end")
- models.Event.objects.create(name="TE E3", status=models.Event.CONFIRMED, start_date=date.today(), end_date=date.today(), description="start today with end today")
- models.Event.objects.create(name="TE E4", status=models.Event.CONFIRMED, start_date=date.today()-timedelta(weeks=104), description="start past 2 years no end")
- models.Event.objects.create(name="TE E5", status=models.Event.BOOKED, start_date=date.today()-timedelta(days=7), end_date=date.today()-timedelta(days=1), description="start past 1 week with end past")
- models.Event.objects.create(name="TE E6", status=models.Event.BOOKED, start_date=date.today()-timedelta(days=2), end_date=date.today()+timedelta(days=2), description="start past, end future")
- models.Event.objects.create(name="TE E7", status=models.Event.BOOKED, start_date=date.today()+timedelta(days=2), end_date=date.today()+timedelta(days=2), description="start + end in future")
+ models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
+ start_date=date.today() + timedelta(days=6), description="start future no end")
+ models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL, start_date=date.today(),
+ description="start today no end")
+ models.Event.objects.create(name="TE E3", status=models.Event.CONFIRMED, start_date=date.today(),
+ end_date=date.today(), description="start today with end today")
+ models.Event.objects.create(name="TE E4", status=models.Event.CONFIRMED,
+ start_date=date.today() - timedelta(weeks=104),
+ description="start past 2 years no end")
+ models.Event.objects.create(name="TE E5", status=models.Event.BOOKED,
+ start_date=date.today() - timedelta(days=7),
+ end_date=date.today() - timedelta(days=1),
+ description="start past 1 week with end past")
+ models.Event.objects.create(name="TE E6", status=models.Event.BOOKED,
+ start_date=date.today() - timedelta(days=2),
+ end_date=date.today() + timedelta(days=2), description="start past, end future")
+ models.Event.objects.create(name="TE E7", status=models.Event.BOOKED,
+ start_date=date.today() + timedelta(days=2),
+ end_date=date.today() + timedelta(days=2), description="start + end in future")
# 2 cancelled - 1 current
- models.Event.objects.create(name="TE E8", start_date=date.today()+timedelta(days=2), end_date=date.today()+timedelta(days=2), status=models.Event.CANCELLED, description="cancelled in future")
- models.Event.objects.create(name="TE E9", start_date=date.today()-timedelta(days=1), end_date=date.today()+timedelta(days=2), status=models.Event.CANCELLED, description="cancelled and started")
+ models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
+ end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
+ description="cancelled in future")
+ models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
+ end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
+ description="cancelled and started")
# 5 dry hire - 3 current - 1 cancelled
models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, description="dryhire today")
- models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile, description="dryhire today, checked in")
- models.Event.objects.create(name="TE E12", start_date=date.today()-timedelta(days=1), dry_hire=True, status=models.Event.BOOKED, description="dryhire past")
- models.Event.objects.create(name="TE E13", start_date=date.today()-timedelta(days=2), dry_hire=True, checked_in_by=self.profile, description="dryhire past checked in")
- models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True, status=models.Event.CANCELLED, description="dryhire today cancelled")
+ models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile,
+ description="dryhire today, checked in")
+ models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
+ status=models.Event.BOOKED, description="dryhire past")
+ models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
+ checked_in_by=self.profile, description="dryhire past checked in")
+ models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
+ status=models.Event.CANCELLED, description="dryhire today cancelled")
# 4 non rig - 3 current
models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, description="non rig today")
- models.Event.objects.create(name="TE E16", start_date=date.today()+timedelta(days=1), is_rig=False, description="non rig tomorrow")
- models.Event.objects.create(name="TE E17", start_date=date.today()-timedelta(days=1), is_rig=False, description="non rig yesterday")
- models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED, description="non rig today cancelled")
+ models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
+ description="non rig tomorrow")
+ models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
+ description="non rig yesterday")
+ models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED,
+ description="non rig today cancelled")
+
self.browser = create_browser(self.id(), self.desired_capabilities)
self.browser.implicitly_wait(3) # Set implicit wait session wide
@@ -886,14 +936,15 @@ class IcalTest(LiveServerTestCase):
# Completes and comes back to /user/
# Checks that no api key is displayed
- self.assertEqual("No API Key Generated", self.browser.find_element_by_xpath("//div[@id='content']/div/div/div[3]/dl[2]/dd").text)
+ self.assertEqual("No API Key Generated",
+ self.browser.find_element_by_xpath("//div[@id='content']/div/div/div[3]/dl[2]/dd").text)
self.assertEqual("No API Key Generated", self.browser.find_element_by_css_selector("pre").text)
-
+
# Now creates an API key, and check a URL is displayed one
self.browser.find_element_by_link_text("Generate API Key").click()
self.assertIn("rigs.ics", self.browser.find_element_by_id("cal-url").text)
self.assertNotIn("?", self.browser.find_element_by_id("cal-url").text)
-
+
# Lets change everything so it's not the default value
self.browser.find_element_by_xpath("//input[@value='rig']").click()
self.browser.find_element_by_xpath("//input[@value='non-rig']").click()
@@ -903,7 +954,9 @@ class IcalTest(LiveServerTestCase):
self.browser.find_element_by_xpath("//input[@value='confirmed']").click()
# and then check the url is correct
- self.assertIn("rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false", self.browser.find_element_by_id("cal-url").text)
+ self.assertIn(
+ "rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false",
+ self.browser.find_element_by_id("cal-url").text)
# Awesome - all seems to work
@@ -916,27 +969,24 @@ class IcalTest(LiveServerTestCase):
# Now creates an API key, and check a URL is displayed one
self.browser.find_element_by_link_text("Generate API Key").click()
-
-
c = Client()
-
+
# Default settings - should have all non-cancelled events
# Get the ical file (can't do this in selanium because reasons)
icalUrl = self.browser.find_element_by_id("cal-url").text
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
- #Check has entire file
+ # Check has entire file
self.assertIn("BEGIN:VCALENDAR", response.content)
self.assertIn("END:VCALENDAR", response.content)
- expectedIn= [1,2,3,5,6,7,10,11,12,13,15,16,17]
- for test in range(1,18):
+ expectedIn = [1, 2, 3, 5, 6, 7, 10, 11, 12, 13, 15, 16, 17]
+ for test in range(1, 18):
if test in expectedIn:
- self.assertIn("TE E"+str(test)+" ", response.content)
+ self.assertIn("TE E" + str(test) + " ", response.content)
else:
- self.assertNotIn("TE E"+str(test)+" ", response.content)
-
+ self.assertNotIn("TE E" + str(test) + " ", response.content)
# Only dry hires
self.browser.find_element_by_xpath("//input[@value='rig']").click()
@@ -946,13 +996,12 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
- expectedIn= [10,11,12,13]
- for test in range(1,18):
+ expectedIn = [10, 11, 12, 13]
+ for test in range(1, 18):
if test in expectedIn:
- self.assertIn("TE E"+str(test)+" ", response.content)
+ self.assertIn("TE E" + str(test) + " ", response.content)
else:
- self.assertNotIn("TE E"+str(test)+" ", response.content)
-
+ self.assertNotIn("TE E" + str(test) + " ", response.content)
# Only provisional rigs
self.browser.find_element_by_xpath("//input[@value='rig']").click()
@@ -963,12 +1012,12 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
- expectedIn= [1,2]
- for test in range(1,18):
+ expectedIn = [1, 2]
+ for test in range(1, 18):
if test in expectedIn:
- self.assertIn("TE E"+str(test)+" ", response.content)
+ self.assertIn("TE E" + str(test) + " ", response.content)
else:
- self.assertNotIn("TE E"+str(test)+" ", response.content)
+ self.assertNotIn("TE E" + str(test) + " ", response.content)
# Only cancelled non-rigs
self.browser.find_element_by_xpath("//input[@value='rig']").click()
@@ -980,12 +1029,12 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
- expectedIn= [18]
- for test in range(1,18):
+ expectedIn = [18]
+ for test in range(1, 18):
if test in expectedIn:
- self.assertIn("TE E"+str(test)+" ", response.content)
+ self.assertIn("TE E" + str(test) + " ", response.content)
else:
- self.assertNotIn("TE E"+str(test)+" ", response.content)
+ self.assertNotIn("TE E" + str(test) + " ", response.content)
# Nothing selected
self.browser.find_element_by_xpath("//input[@value='non-rig']").click()
@@ -995,17 +1044,19 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
- expectedIn= []
- for test in range(1,18):
+ expectedIn = []
+ for test in range(1, 18):
if test in expectedIn:
- self.assertIn("TE E"+str(test)+" ", response.content)
+ self.assertIn("TE E" + str(test) + " ", response.content)
else:
- self.assertNotIn("TE E"+str(test)+" ", response.content)
-
- # Wow - that was a lot of tests
+ self.assertNotIn("TE E" + str(test) + " ", response.content)
+
+ # Wow - that was a lot of tests
+
class animation_is_finished(object):
""" Checks if animation is done """
+
def __init__(self):
pass
@@ -1016,3 +1067,167 @@ class animation_is_finished(object):
import time
time.sleep(0.1)
return finished
+
+
+class ClientEventAuthorisationTest(TestCase):
+ auth_data = {
+ 'name': 'Test ABC',
+ 'po': '1234ABCZXY',
+ 'account_code': 'ABC TEST 12345',
+ 'uni_id': 1234567890,
+ 'tos': True
+ }
+
+ def setUp(self):
+ self.profile = models.Profile.objects.get_or_create(
+ first_name='Test',
+ last_name='TEC User',
+ username='eventauthtest',
+ email='teccie@functional.test',
+ is_superuser=True # lazily grant all permissions
+ )[0]
+ self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
+ venue = models.Venue.objects.create(name='Authorisation Test Venue')
+ client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
+ organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=False)
+ self.event = models.Event.objects.create(
+ name='Authorisation Test',
+ start_date=date.today(),
+ venue=venue,
+ person=client,
+ organisation=organisation,
+ )
+ self.hmac = signing.dumps({'pk': self.event.pk, 'email': 'authemail@function.test',
+ 'sent_by': self.profile.pk})
+ self.url = reverse('event_authorise', kwargs={'pk': self.event.pk, 'hmac': self.hmac})
+
+ def test_requires_valid_hmac(self):
+ bad_hmac = self.hmac[:-1]
+ url = reverse('event_authorise', kwargs={'pk': self.event.pk, 'hmac': bad_hmac})
+ response = self.client.get(url)
+ self.assertIsInstance(response, HttpResponseBadRequest)
+ # TODO: Add some form of sensbile user facing error
+ # self.assertIn(response.content, "new URL") # check there is some level of sane instruction
+
+ response = self.client.get(self.url)
+ self.assertContains(response, self.event.organisation.name)
+
+ def test_generic_validation(self):
+ response = self.client.get(self.url)
+ self.assertContains(response, "Terms of Hire")
+
+ response = self.client.post(self.url)
+ self.assertContains(response, "This field is required.", 5)
+
+ data = self.auth_data
+ data['amount'] = self.event.total + 1
+
+ response = self.client.post(self.url, data)
+ self.assertContains(response, "The amount authorised must equal the total for the event")
+ self.assertNotContains(response, "This field is required.")
+
+ data['amount'] = self.event.total
+ response = self.client.post(self.url, data)
+ self.assertContains(response, "Your event has been authorised")
+
+ self.event.refresh_from_db()
+ self.assertTrue(self.event.authorised)
+ self.assertEqual(self.event.authorisation.email, "authemail@function.test")
+
+ def test_internal_validation(self):
+ self.event.organisation.union_account = True
+ self.event.organisation.save()
+
+ response = self.client.get(self.url)
+ self.assertContains(response, "Account code")
+ self.assertContains(response, "University ID")
+
+ response = self.client.post(self.url)
+ self.assertContains(response, "This field is required.", 5)
+
+ data = self.auth_data
+ response = self.client.post(self.url, data)
+ self.assertContains(response, "Your event has been authorised.")
+
+ def test_duplicate_warning(self):
+ auth = models.EventAuthorisation.objects.create(event=self.event, name='Test ABC', email='dupe@functional.test',
+ amount=self.event.total, sent_by=self.profile)
+ response = self.client.get(self.url)
+ self.assertContains(response, 'This event has already been authorised.')
+
+ auth.amount += 1
+ auth.save()
+
+ response = self.client.get(self.url)
+ self.assertContains(response, 'amount has changed')
+
+ def test_email_sent(self):
+ mail.outbox = []
+
+ data = self.auth_data
+ data['amount'] = self.event.total
+
+ response = self.client.post(self.url, data)
+ self.assertContains(response, "Your event has been authorised.")
+ self.assertEqual(len(mail.outbox), 2)
+
+ self.assertEqual(mail.outbox[0].to, ['authemail@function.test'])
+ self.assertEqual(mail.outbox[1].to, [settings.AUTHORISATION_NOTIFICATION_ADDRESS])
+
+
+class TECEventAuthorisationTest(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.profile = models.Profile.objects.get_or_create(
+ first_name='Test',
+ last_name='TEC User',
+ username='eventauthtest',
+ email='teccie@nottinghamtec.co.uk',
+ is_superuser=True # lazily grant all permissions
+ )[0]
+ cls.profile.set_password('eventauthtest123')
+ cls.profile.save()
+
+ def setUp(self):
+ self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
+ venue = models.Venue.objects.create(name='Authorisation Test Venue')
+ client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
+ organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=False)
+ self.event = models.Event.objects.create(
+ name='Authorisation Test',
+ start_date=date.today(),
+ venue=venue,
+ person=client,
+ organisation=organisation,
+ )
+ self.url = reverse('event_authorise_request', kwargs={'pk': self.event.pk})
+
+ def test_email_check(self):
+ self.profile.email = 'teccie@someotherdomain.com'
+ self.profile.save()
+
+ self.assertTrue(self.client.login(username=self.profile.username, password='eventauthtest123'))
+
+ response = self.client.post(self.url)
+
+ self.assertContains(response, 'must have an @nottinghamtec.co.uk email address')
+
+ def test_request_send(self):
+ self.assertTrue(self.client.login(username=self.profile.username, password='eventauthtest123'))
+ response = self.client.post(self.url)
+ self.assertContains(response, 'This field is required.')
+
+ mail.outbox = []
+
+ response = self.client.post(self.url, {'email': 'client@functional.test'})
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(len(mail.outbox), 1)
+ email = mail.outbox[0]
+ self.assertIn('client@functional.test', email.to)
+ self.assertIn('/event/%d/' % (self.event.pk), email.body)
+
+ # Check sent by details are populated
+ self.event.refresh_from_db()
+ self.assertEqual(self.event.auth_request_by, self.profile)
+ self.assertEqual(self.event.auth_request_to, 'client@functional.test')
+ self.assertIsNotNone(self.event.auth_request_at)
diff --git a/RIGS/test_models.py b/RIGS/test_models.py
index 1809c73b..b5dfd1e3 100644
--- a/RIGS/test_models.py
+++ b/RIGS/test_models.py
@@ -1,5 +1,7 @@
import pytz
+import reversion
from django.conf import settings
+from django.core.exceptions import ValidationError
from django.test import TestCase
from RIGS import models
from datetime import date, timedelta, datetime, time
@@ -354,3 +356,37 @@ class EventPricingTestCase(TestCase):
def test_grand_total(self):
self.assertEqual(self.e1.total, Decimal('84.48'))
self.assertEqual(self.e2.total, Decimal('419.32'))
+
+
+class EventAuthorisationTestCase(TestCase):
+ def setUp(self):
+ models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
+ self.profile = models.Profile.objects.get_or_create(
+ first_name='Test',
+ last_name='TEC User',
+ username='eventauthtest',
+ email='teccie@functional.test',
+ is_superuser=True # lazily grant all permissions
+ )[0]
+ self.person = models.Person.objects.create(name='Authorisation Test Person')
+ self.organisation = models.Organisation.objects.create(name='Authorisation Test Organisation')
+ self.event = models.Event.objects.create(name="AuthorisationTestCase", person=self.person,
+ start_date=date.today())
+ # Add some items
+ models.EventItem.objects.create(event=self.event, name="Authorisation test item", quantity=2, cost=123.45,
+ order=1)
+
+ def test_event_property(self):
+ auth1 = models.EventAuthorisation.objects.create(event=self.event, email="authroisation@model.test.case",
+ name="Test Auth 1", amount=self.event.total - 1,
+ sent_by=self.profile)
+ self.assertFalse(self.event.authorised)
+ auth1.amount = self.event.total
+ auth1.save()
+ self.assertTrue(self.event.authorised)
+
+ def test_last_edited(self):
+ with reversion.revisions.create_revision():
+ auth = models.EventAuthorisation.objects.create(event=self.event, email="authroisation@model.test.case",
+ name="Test Auth", amount=self.event.total, sent_by=self.profile)
+ self.assertIsNotNone(auth.last_edited_at)
diff --git a/RIGS/urls.py b/RIGS/urls.py
index 5af3fd9a..4d1c67ec 100644
--- a/RIGS/urls.py
+++ b/RIGS/urls.py
@@ -18,6 +18,7 @@ urlpatterns = [
url('^user/login/$', views.login, name='login'),
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
+
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}),
# People
@@ -72,9 +73,12 @@ urlpatterns = [
# Rigboard
url(r'^rigboard/$', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
- url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
- url(r'^rigboard/calendar/(?P(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
- url(r'^rigboard/calendar/(?P(month|week|day))/(?P(\d{4}-\d{2}-\d{2}))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
+ url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()),
+ name='web_calendar'),
+ url(r'^rigboard/calendar/(?P(month|week|day))/$',
+ login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
+ url(r'^rigboard/calendar/(?P(month|week|day))/(?P(\d{4}-\d{2}-\d{2}))/$',
+ login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
url(r'^rigboard/activity/$',
permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()),
@@ -84,10 +88,12 @@ urlpatterns = [
name='activity_feed'),
url(r'^event/(?P\d+)/$',
- permission_required_with_403('RIGS.view_event', oembed_view="event_oembed")(rigboard.EventDetail.as_view()),
+ permission_required_with_403('RIGS.view_event', oembed_view="event_oembed")(
+ rigboard.EventDetail.as_view()),
name='event_detail'),
url(r'^event/(?P\d+)/embed/$',
- xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
+ xframe_options_exempt(
+ login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
name='event_embed'),
url(r'^event/(?P\d+)/oembed_json/$',
rigboard.EventOembed.as_view(),
@@ -111,8 +117,6 @@ urlpatterns = [
permission_required_with_403('RIGS.view_event')(versioning.VersionHistory.as_view()),
name='event_history', kwargs={'model': models.Event}),
-
-
# Finance
url(r'^invoice/$',
permission_required_with_403('RIGS.view_invoice')(finance.InvoiceIndex.as_view()),
@@ -147,6 +151,20 @@ urlpatterns = [
permission_required_with_403('RIGS.add_payment')(finance.PaymentDelete.as_view()),
name='payment_delete'),
+ # Client event authorisation
+ url(r'^event/(?P\d+)/auth/$',
+ permission_required_with_403('RIGS.change_event')(
+ rigboard.EventAuthorisationRequest.as_view()
+ ),
+ name='event_authorise_request'),
+ url(r'^event/(?P\d+)/auth/preview/$',
+ permission_required_with_403('RIGS.change_event')(
+ rigboard.EventAuthoriseRequestEmailPreview.as_view()
+ ),
+ name='event_authorise_preview'),
+ url(r'^event/(?P\d+)/(?P[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
+ name='event_authorise'),
+
# User editing
url(r'^user/$', login_required(views.ProfileDetail.as_view()), name='profile_detail'),
url(r'^user/(?P\d+)/$',
@@ -154,17 +172,22 @@ urlpatterns = [
name='profile_detail'),
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
name='profile_update_self'),
- url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
+ url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)),
+ name='reset_api_key'),
# ICS Calendar - API key authentication
- url(r'^ical/(?P\d+)/(?P\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
+ url(r'^ical/(?P\d+)/(?P\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
+ name="ics_calendar"),
# API
- url(r'^api/(?P\w+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
- url(r'^api/(?P\w+)/(?P\d+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
+ url(r'^api/(?P\w+)/$', login_required(views.SecureAPIRequest.as_view()),
+ name="api_secure"),
+ url(r'^api/(?P\w+)/(?P\d+)/$', login_required(views.SecureAPIRequest.as_view()),
+ name="api_secure"),
# Legacy URL's
- url(r'^rig/show/(?P\d+)/$', RedirectView.as_view(permanent=True, pattern_name='event_detail')),
+ url(r'^rig/show/(?P\d+)/$',
+ RedirectView.as_view(permanent=True, pattern_name='event_detail')),
url(r'^bookings/$', RedirectView.as_view(permanent=True, pattern_name='rigboard')),
url(r'^bookings/past/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
]
diff --git a/requirements.txt b/requirements.txt
index 3e7d8d17..1c38b09c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,6 +10,7 @@ django-recaptcha==1.3.0
django-registration-redux==1.6
django-reversion==1.10.2
django-toolbelt==0.0.1
+premailer==3.0.1
django-widget-tweaks==1.4.1
gunicorn==19.7.1
icalendar==3.11.4
diff --git a/templates/base_ajax.html b/templates/base_ajax.html
index 298ca314..79b934d3 100644
--- a/templates/base_ajax.html
+++ b/templates/base_ajax.html
@@ -22,4 +22,4 @@
});
-
\ No newline at end of file
+
diff --git a/templates/base_client.html b/templates/base_client.html
new file mode 100644
index 00000000..824e9f88
--- /dev/null
+++ b/templates/base_client.html
@@ -0,0 +1,125 @@
+{% load static from staticfiles %}
+{% load raven %}
+
+
+
+
+
+ {% block title %}{% endblock %} | Rig Information Gathering System
+
+
+
+
+
+
+
+
+
+ {% block css %}
+ {% endblock %}
+
+
+
+
+ {% block preload_js %}
+ {% endblock %}
+
+ {% block extra-head %}{% endblock %}
+
+
+
+{% include "analytics.html" %}
+
+
+
+
+ {% block content-header %}
+ {% if error %}
+
{{ error }}
{% endif %}
+ {% if info %}
+
{{ info }}
{% endif %}
+ {% if messages %}
+ {% for message in messages %}
+
+ ×
+ {{ message }}
+
+ {% endfor %}
+ {% endif %}
+ {% endblock %}
+
+ {% block content %}{% endblock %}
+
+
+
+ {% block sidebar %}
+ {% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% block js %}
+{% endblock %}
+
+
diff --git a/templates/base_client_email.html b/templates/base_client_email.html
new file mode 100644
index 00000000..f6f5e255
--- /dev/null
+++ b/templates/base_client_email.html
@@ -0,0 +1,58 @@
+{% load static from staticfiles %}
+{% load raven %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+
+
+
+
+
+
+