mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 05:22:16 +00:00
Merge pull request #283 from nottinghamtec/feature/online-auth
Add online RIGS authorisation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = 'RIGS.apps.RIGSAppConfig'
|
||||
|
||||
8
RIGS/apps.py
Normal file
8
RIGS/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RIGSAppConfig(AppConfig):
|
||||
name = 'RIGS'
|
||||
|
||||
def ready(self):
|
||||
import RIGS.signals
|
||||
@@ -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')
|
||||
|
||||
27
RIGS/migrations/0025_eventauthorisation.py
Normal file
27
RIGS/migrations/0025_eventauthorisation.py
Normal file
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0026_remove_eventauthorisation_created_at.py
Normal file
18
RIGS/migrations/0026_remove_eventauthorisation_created_at.py
Normal file
@@ -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',
|
||||
),
|
||||
]
|
||||
19
RIGS/migrations/0027_eventauthorisation_event_singular.py
Normal file
19
RIGS/migrations/0027_eventauthorisation_event_singular.py
Normal file
@@ -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'),
|
||||
),
|
||||
]
|
||||
21
RIGS/migrations/0029_eventauthorisation_sent_by.py
Normal file
21
RIGS/migrations/0029_eventauthorisation_sent_by.py
Normal file
@@ -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,
|
||||
),
|
||||
]
|
||||
30
RIGS/migrations/0030_auth_request_sending.py
Normal file
30
RIGS/migrations/0030_auth_request_sending.py
Normal file
@@ -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),
|
||||
),
|
||||
]
|
||||
16
RIGS/migrations/0031_merge_20170512_2102.py
Normal file
16
RIGS/migrations/0031_merge_20170512_2102.py
Normal file
@@ -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 = [
|
||||
]
|
||||
@@ -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')
|
||||
|
||||
225
RIGS/rigboard.py
225
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
|
||||
|
||||
98
RIGS/signals.py
Normal file
98
RIGS/signals.py
Normal file
@@ -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)
|
||||
1
RIGS/static/css/email.css
Normal file
1
RIGS/static/css/email.css
Normal file
@@ -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}
|
||||
@@ -136,4 +136,4 @@ $("#item-table tbody").sortable({
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
64
RIGS/static/scss/email.scss
Normal file
64
RIGS/static/scss/email.scss
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
78
RIGS/templates/RIGS/client_eventdetails.html
Normal file
78
RIGS/templates/RIGS/client_eventdetails.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-6 col-lg-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Contact Details</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Person</dt>
|
||||
<dd>
|
||||
{% if event.person %}
|
||||
{{ event.person.name }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Email</dt>
|
||||
<dd>
|
||||
<span class="overflow-ellipsis">{{ event.person.email }}</span>
|
||||
</dd>
|
||||
|
||||
<dt>Phone Number</dt>
|
||||
<dd>{{ event.person.phone }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% if event.organisation %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Organisation</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Organisation</dt>
|
||||
<dd>
|
||||
{{ event.organisation.name }}
|
||||
</dd>
|
||||
|
||||
<dt>Phone Number</dt>
|
||||
<dd>{{ object.organisation.phone }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-6 col-lg-7">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">Event Info</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Event Venue</dt>
|
||||
<dd>
|
||||
{% if object.venue %}
|
||||
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
|
||||
{{ object.venue }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd>{{ event.get_status_display }}</dd>
|
||||
|
||||
<dd> </dd>
|
||||
|
||||
<dt>Access From</dt>
|
||||
<dd>{{ event.access_at|date:"D d M Y H:i"|default:"" }}</dd>
|
||||
|
||||
<dt>Event Starts</dt>
|
||||
<dd>{{ event.start_date|date:"D d M Y" }} {{ event.start_time|date:"H:i" }}</dd>
|
||||
|
||||
<dt>Event Ends</dt>
|
||||
<dd>{{ event.end_date|date:"D d M Y" }} {{ event.end_time|date:"H:i" }}</dd>
|
||||
|
||||
<dd> </dd>
|
||||
|
||||
<dt>Event Description</dt>
|
||||
<dd>{{ event.description|linebreaksbr }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,36 +11,9 @@
|
||||
</h1>
|
||||
</div>
|
||||
<div class="col-sm-12 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-edit"></span> <span
|
||||
class="hidden-xs">Edit</span></a>
|
||||
{% if event.is_rig %}
|
||||
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-print"></span> <span
|
||||
class="hidden-xs">Print</span></a>
|
||||
{% endif %}
|
||||
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
|
||||
class="glyphicon glyphicon-duplicate"></span> <span
|
||||
class="hidden-xs">Duplicate</span></a>
|
||||
{% if event.is_rig %}
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
|
||||
{% if event.invoice and event.invoice.is_closed %}
|
||||
btn-success
|
||||
{% elif event.invoice %}
|
||||
btn-warning
|
||||
{% else %}
|
||||
btn-danger
|
||||
{% endif %}
|
||||
" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span>
|
||||
<span class="hidden-xs">Invoice</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'RIGS/event_detail_buttons.html' %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% if object.is_rig %}
|
||||
{# only need contact details for a rig #}
|
||||
@@ -97,6 +70,39 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if event.is_rig and event.internal %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Client Authorisation</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Authorised</dt>
|
||||
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>
|
||||
|
||||
<dt>Authorised by</dt>
|
||||
<dd>
|
||||
{% if object.authorisation %}
|
||||
{{ object.authorisation.name }}
|
||||
(<a href="mailto:{{ object.authorisation.email }}">{{ object.authorisation.email }}</a>)
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Authorised at</dt>
|
||||
<dd>{{ object.authorisation.last_edited_at }}</dd>
|
||||
|
||||
<dt>Authorised amount</dt>
|
||||
<dd>
|
||||
{% if object.authorisation %}
|
||||
£ {{ object.authorisation.amount|floatformat:"2" }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Requested by</dt>
|
||||
<dd>{{ object.authorisation.sent_by }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-12 {% if event.is_rig %}col-md-6 col-lg-7{% endif %}">
|
||||
@@ -175,8 +181,25 @@
|
||||
{% endif %}
|
||||
|
||||
{% if event.is_rig %}
|
||||
<dt>PO</dt>
|
||||
<dd>{{ object.purchase_order }}</dd>
|
||||
<dd> </dd>
|
||||
|
||||
{% if object.internal %}
|
||||
<dt>Authorisation Request</dt>
|
||||
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
|
||||
|
||||
<dt>By</dt>
|
||||
<dd>{{ object.auth_request_by }}</dd>
|
||||
|
||||
<dt>At</dt>
|
||||
<dd>{{ object.auth_request_at|date:"D d M Y H:i"|default:"" }}</dd>
|
||||
|
||||
<dt>To</dt>
|
||||
<dd>{{ object.auth_request_to }}</dd>
|
||||
|
||||
{% else %}
|
||||
<dt>PO</dt>
|
||||
<dd>{{ object.purchase_order }}</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
@@ -184,34 +207,7 @@
|
||||
</div>
|
||||
{% if not request.is_ajax %}
|
||||
<div class="col-sm-12 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-edit"></span> <span
|
||||
class="hidden-xs">Edit</span></a>
|
||||
{% if event.is_rig %}
|
||||
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-print"></span> <span
|
||||
class="hidden-xs">Print</span></a>
|
||||
{% endif %}
|
||||
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
|
||||
class="glyphicon glyphicon-duplicate"></span> <span
|
||||
class="hidden-xs">Duplicate</span></a>
|
||||
{% if event.is_rig %}
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
|
||||
{% if event.invoice and event.invoice.is_closed %}
|
||||
btn-success
|
||||
{% elif event.invoice %}
|
||||
btn-warning
|
||||
{% else %}
|
||||
btn-danger
|
||||
{% endif %}
|
||||
" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span>
|
||||
<span class="hidden-xs">Invoice</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'RIGS/event_detail_buttons.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.is_rig %}
|
||||
@@ -229,34 +225,7 @@
|
||||
</div>
|
||||
{% if not request.is_ajax %}
|
||||
<div class="col-sm-12 text-right">
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-edit"></span> <span
|
||||
class="hidden-xs">Edit</span></a>
|
||||
{% if event.is_rig %}
|
||||
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-print"></span> <span
|
||||
class="hidden-xs">Print</span></a>
|
||||
{% endif %}
|
||||
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
|
||||
class="glyphicon glyphicon-duplicate"></span> <span
|
||||
class="hidden-xs">Duplicate</span></a>
|
||||
{% if event.is_rig %}
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
|
||||
{% if event.invoice and event.invoice.is_closed %}
|
||||
btn-success
|
||||
{% elif event.invoice %}
|
||||
btn-warning
|
||||
{% else %}
|
||||
btn-danger
|
||||
{% endif %}
|
||||
" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span>
|
||||
<span class="hidden-xs">Invoice</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'RIGS/event_detail_buttons.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
54
RIGS/templates/RIGS/event_detail_buttons.html
Normal file
54
RIGS/templates/RIGS/event_detail_buttons.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="btn-group btn-page">
|
||||
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-edit"></span> <span
|
||||
class="hidden-xs">Edit</span></a>
|
||||
{% if event.is_rig %}
|
||||
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-print"></span> <span
|
||||
class="hidden-xs">Print</span></a>
|
||||
{% endif %}
|
||||
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
|
||||
class="glyphicon glyphicon-duplicate"></span> <span
|
||||
class="hidden-xs">Duplicate</span></a>
|
||||
{% if event.is_rig %}
|
||||
{% if event.internal %}
|
||||
<a class="btn btn-default item-add modal-href event-authorise-request
|
||||
{% if event.authorised %}
|
||||
btn-success
|
||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||
btn-warning
|
||||
{% elif event.auth_request_to %}
|
||||
btn-info
|
||||
{% endif %}
|
||||
"
|
||||
href="{% url 'event_authorise_request' object.pk %}">
|
||||
<span class="glyphicon glyphicon-send"></span>
|
||||
<span class="hidden-xs">
|
||||
{% if event.authorised %}
|
||||
Authorised
|
||||
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
|
||||
Authorisation Issue
|
||||
{% elif event.auth_request_to %}
|
||||
Awaiting Authorisation
|
||||
{% else %}
|
||||
Request Authorisation
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
|
||||
{% if event.invoice and event.invoice.is_closed %}
|
||||
btn-success
|
||||
{% elif event.invoice %}
|
||||
btn-warning
|
||||
{% else %}
|
||||
btn-danger
|
||||
{% endif %}
|
||||
" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span>
|
||||
<span class="hidden-xs">Invoice</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -398,6 +398,7 @@
|
||||
{% render_field form.collector class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
||||
<label for="{{ form.purchase_order.id_for_label }}"
|
||||
class="col-sm-4 control-label">{{ form.purchase_order.label }}</label>
|
||||
|
||||
@@ -54,20 +54,22 @@
|
||||
<td><a href="{% url 'event_detail' object.pk %}">N{{ object.pk|stringformat:"05d" }}</a><br>
|
||||
<span class="text-muted">{{ object.get_status_display }}</span></td>
|
||||
<td>{{ object.start_date }}</td>
|
||||
<td>{{ object.name }}</td>
|
||||
<td>
|
||||
{% if object.organisation %}
|
||||
{{ object.organisation.name }}
|
||||
<br>
|
||||
<span class="text-muted">{{ object.organisation.union_account|yesno:'Internal,External' }}</span>
|
||||
{% else %}
|
||||
{{ object.person.name }}
|
||||
<br>
|
||||
<span class="text-muted">External</span>
|
||||
{{ object.name }}
|
||||
{% if object.is_rig and perms.RIGS.view_event and object.authorised %}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>{{ object.sum_total|floatformat:2 }}</td>
|
||||
<td>
|
||||
{{ object.organisation.name }}
|
||||
<br>
|
||||
<span class="text-muted">{{ object.internal|yesno:'Internal,External' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ object.sum_total|floatformat:2 }}
|
||||
<br />
|
||||
<span class="text-muted">{% if not object.internal %}{{ object.purchase_order }}{% endif %}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if object.mic %}
|
||||
{{ object.mic.initials }}<br>
|
||||
@@ -77,7 +79,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'invoice_event' object.pk %}" class="btn btn-default" data-toggle="tooltip" title="'Invoice' this event - click this when paperwork has been sent to treasury">
|
||||
<a href="{% url 'invoice_event' object.pk %}"
|
||||
class="btn btn-default"
|
||||
data-toggle="tooltip"
|
||||
title="'Invoice' this event - click this when paperwork has been sent to treasury">
|
||||
<span class="glyphicon glyphicon-gbp"></span>
|
||||
</a>
|
||||
</td>
|
||||
@@ -92,4 +97,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -22,21 +22,21 @@
|
||||
<paraStyle name="style.Heading2" fontName="OpenSans-Bold" fontSize="10" spaceAfter="2"/>
|
||||
<paraStyle name="style.Heading3" fontName="OpenSans" fontSize="10" spaceAfter="0"/>
|
||||
<paraStyle name="center" alignment="center"/>
|
||||
<paraStyle name="invoice-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
|
||||
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
|
||||
|
||||
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
|
||||
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
|
||||
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
|
||||
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
|
||||
<paraStyle name="style.invoice_titles" fontName="OpenSans-Bold" fontSize="10" />
|
||||
<paraStyle name="style.invoice_numbers" fontName="OpenSans" fontSize="10" />
|
||||
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
|
||||
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
|
||||
|
||||
<blockTableStyle id="eventSpecifics">
|
||||
<blockValign value="top"/>
|
||||
<lineStyle kind="LINEAFTER" colorName="LightGrey" start="0,0" stop="1,0" thickness="1"/>
|
||||
</blockTableStyle>
|
||||
|
||||
<blockTableStyle id="invoiceLayout">
|
||||
<blockTableStyle id="headLayout">
|
||||
<blockValign value="top"/>
|
||||
|
||||
</blockTableStyle>
|
||||
@@ -100,10 +100,11 @@
|
||||
|
||||
|
||||
<setFont name="OpenSans" size="10" />
|
||||
{% if not invoice %}<drawCenteredString x="302.5" y="50">[{{ copy }} Copy]</drawCenteredString>{% endif %}
|
||||
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
|
||||
<setFont name="OpenSans" size="7" />
|
||||
<drawCenteredString x="302.5" y="26">[Paperwork generated by {{current_user.name}} | {% now "d/m/Y H:i" %} | {{object.current_version_id}}]</drawCenteredString>
|
||||
<drawCenteredString x="302.5" y="26">
|
||||
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
|
||||
</drawCenteredString>
|
||||
</pageGraphics>
|
||||
|
||||
<frame id="main" x1="50" y1="65" width="495" height="645"/>
|
||||
@@ -115,10 +116,11 @@
|
||||
<image file="RIGS/static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
|
||||
|
||||
<setFont name="OpenSans" size="10"/>
|
||||
{% if not invoice %}<drawCenteredString x="302.5" y="50">[{{ copy }} Copy]</drawCenteredString>{% endif %}
|
||||
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
|
||||
<setFont name="OpenSans" size="7" />
|
||||
<drawCenteredString x="302.5" y="26">[Paperwork generated by {{current_user.name}} | {% now "d/m/Y H:i" %} | {{object.current_version_id}}]</drawCenteredString>
|
||||
<drawCenteredString x="302.5" y="26">
|
||||
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
|
||||
</drawCenteredString>
|
||||
</pageGraphics>
|
||||
<frame id="main" x1="50" y1="65" width="495" height="727"/>
|
||||
</pageTemplate>
|
||||
@@ -128,4 +130,4 @@
|
||||
{% include "RIGS/event_print_page.xml" %}
|
||||
</story>
|
||||
|
||||
</document>
|
||||
</document>
|
||||
|
||||
@@ -1,59 +1,70 @@
|
||||
<setNextFrame name="main"/>
|
||||
<nextFrame/>
|
||||
|
||||
{% if invoice %}
|
||||
|
||||
<blockTable style="invoiceLayout" colWidths="330,165">
|
||||
<blockTable style="headLayout" colWidths="330,165">
|
||||
<tr>
|
||||
<td>
|
||||
{% endif %}
|
||||
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></h1>
|
||||
|
||||
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></h1>
|
||||
|
||||
<para style="style.event_description">
|
||||
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
||||
</para>
|
||||
|
||||
<keepInFrame>
|
||||
<para style="style.event_description">
|
||||
{{ object.description|default_if_none:""|linebreaksbr }}
|
||||
<para style="style.event_description">
|
||||
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
||||
</para>
|
||||
</keepInFrame>
|
||||
|
||||
{% if invoice %}
|
||||
|
||||
<keepInFrame>
|
||||
<para style="style.event_description">
|
||||
{{ object.description|default_if_none:""|linebreaksbr }}
|
||||
</para>
|
||||
</keepInFrame>
|
||||
</td>
|
||||
<td>
|
||||
<para style="invoice-head">INVOICE</para>
|
||||
<spacer length="10"/>
|
||||
<blockTable style="eventDetails" colWidths="100,175">
|
||||
<tr>
|
||||
<td><para style="invoice_titles">Invoice Number</para></td>
|
||||
<td>
|
||||
<para style="invoice_numbers">{{ invoice.pk|stringformat:"05d" }}</para>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><para style="invoice_titles">Invoice Date</para></td>
|
||||
<td>
|
||||
<para style="invoice_numbers">{{ invoice.invoice_date|date:"d/m/Y" }}</para>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><para style="invoice_titles">PO Number</para></td>
|
||||
<td>
|
||||
<para style="invoice_numbers">{{ object.purchase_order|default_if_none:"" }}</para>
|
||||
</td>
|
||||
</tr>
|
||||
{% if invoice %}
|
||||
<para style="page-head">INVOICE</para>
|
||||
<spacer length="10"/>
|
||||
<blockTable style="eventDetails" colWidths="100,175">
|
||||
<tr>
|
||||
<td><para style="head_titles">Invoice Number</para></td>
|
||||
<td>
|
||||
<para style="head_numbers">{{ invoice.pk|stringformat:"05d" }}</para>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><para style="head_titles">Invoice Date</para></td>
|
||||
<td>
|
||||
<para style="head_numbers">{{ invoice.invoice_date|date:"d/m/Y" }}</para>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</blockTable>
|
||||
{% if not object.internal %}
|
||||
<tr>
|
||||
<td><para style="head_titles">PO</para></td>
|
||||
<td><para style="head_numbers">{{ object.purchase_order }}</para></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</blockTable>
|
||||
|
||||
{% elif quote %}
|
||||
|
||||
<para style="page-head">QUOTE</para>
|
||||
<spacer length="10"/>
|
||||
<blockTable style="eventDetails" colWidths="100,175">
|
||||
<tr>
|
||||
<td><para style="head_titles">Quote Date</para></td>
|
||||
<td>
|
||||
<para style="head_numbers">{% now "d/m/Y" %}</para>
|
||||
</td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
|
||||
{% elif receipt %}
|
||||
|
||||
<para style="page-head">CONFIRMATION</para>
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
<spacer length="15"/>
|
||||
<blockTable style="eventSpecifics" colWidths="165,165,165">
|
||||
<tr>
|
||||
@@ -189,16 +200,16 @@
|
||||
<keepTogether>
|
||||
<blockTable style="totalTable" colWidths="300,115,80">
|
||||
<tr>
|
||||
<td>{% if not invoice %}VAT Registration Number: 170734807{% endif %}</td>
|
||||
<td>{% if quote %}VAT Registration Number: 170734807{% endif %}</td>
|
||||
<td>Total (ex. VAT)</td>
|
||||
<td>£ {{ object.sum_total|floatformat:2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{% if not invoice %}
|
||||
<para>
|
||||
<b>The full hire fee is payable at least 10 days before the event.</b>
|
||||
</para>
|
||||
{% if quote %}
|
||||
<para>
|
||||
This quote is valid for 30 days unless otherwise arranged.
|
||||
</para>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td>
|
||||
@@ -206,113 +217,140 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<para>
|
||||
{% if invoice %}
|
||||
VAT Registration Number: 170734807
|
||||
{% if quote %}
|
||||
<para>
|
||||
<b>The full hire fee is payable at least 10 days before the event.</b>
|
||||
</para>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if invoice %}
|
||||
<td>Total</td>
|
||||
<td>£ {{ object.total|floatformat:2 }}</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<para>
|
||||
<b>Total</b>
|
||||
</para>
|
||||
</td>
|
||||
<td>
|
||||
<para>
|
||||
<b>£ {{ object.total|floatformat:2 }}</b>
|
||||
</para>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</blockTable>
|
||||
</keepTogether>
|
||||
|
||||
{% if invoice %}
|
||||
<spacer length="15"/>
|
||||
<keepTogether>
|
||||
<h2>Payments</h2>
|
||||
<blockTable style="itemTable" colWidths="300,115,80">
|
||||
<tr>
|
||||
<td>
|
||||
<para>
|
||||
<b>Method</b>
|
||||
</para>
|
||||
</td>
|
||||
<td>
|
||||
<para>
|
||||
<b>Date</b>
|
||||
</para>
|
||||
</td>
|
||||
<td>
|
||||
<para>
|
||||
<b>Amount</b>
|
||||
</para>
|
||||
</td>
|
||||
</tr>
|
||||
{% for payment in object.invoice.payment_set.all %}
|
||||
<tr>
|
||||
<td>{{ payment.get_method_display }}</td>
|
||||
<td>{{ payment.date }}</td>
|
||||
<td>£ {{ payment.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</blockTable>
|
||||
<blockTable style="totalTable" colWidths="300,115,80">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>Payment Total</td>
|
||||
<td>£ {{ object.invoice.payment_total|floatformat:2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<para>
|
||||
<b>Balance</b> (ex. VAT)
|
||||
</para>
|
||||
</td>
|
||||
<td>
|
||||
<para>
|
||||
<b>£ {{ object.invoice.balance|floatformat:2 }}</b>
|
||||
</para>
|
||||
</td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
</keepTogether>
|
||||
{% endif %}
|
||||
|
||||
<keepTogether>
|
||||
<blockTable style="infoTable">>
|
||||
{% if quote %}
|
||||
<tr><td><spacer length="15" /></td></tr>
|
||||
<tr>
|
||||
<td>
|
||||
{% if object.internal %}
|
||||
<para>Bookings will
|
||||
<b>not</b>
|
||||
be confirmed until the event is authorised online.
|
||||
</para>
|
||||
{% else %}
|
||||
<b>This contract is not an invoice.</b>
|
||||
<para>Bookings will
|
||||
<b>not</b>
|
||||
be confirmed until we have received written confirmation and a Purchase Order.
|
||||
</para>
|
||||
{% endif %}
|
||||
</para>
|
||||
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>24 Hour Emergency Contacts: 07825 065681 and 07825 065678</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>
|
||||
<para>
|
||||
<b>Total</b>
|
||||
</para>
|
||||
<para>VAT Registration Number: 170734807</para>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr><td><spacer length="15" /></td></tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<para>
|
||||
<b>£ {{ object.total|floatformat:2 }}</b>
|
||||
</para>
|
||||
{% if object.internal and object.authorised %}
|
||||
<para>
|
||||
Event authorised online by {{ object.authorisation.name }} ({{ object.authorisation.email }}) at
|
||||
{{ object.authorisation.last_edited_at }}.
|
||||
</para>
|
||||
|
||||
<blockTable colWidths="165,165,165">
|
||||
<tr>
|
||||
<td><para><b>University ID</b></para></td>
|
||||
<td><para><b>Account Code</b></para></td>
|
||||
<td><para><b>Authorised Amount</b></para></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ object.authorisation.uni_id }}</td>
|
||||
<td>{{ object.authorisation.account_code }}</td>
|
||||
<td>£ {{ object.authorisation.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
</keepTogether>
|
||||
{% if not invoice %}
|
||||
<keepTogether>
|
||||
<blockTable style="infoTable">
|
||||
<tr>
|
||||
<td>
|
||||
<para>Bookings will
|
||||
<b>not</b>
|
||||
be confirmed until payment is received and the contract is signed.
|
||||
</para>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>24 Hour Emergency Contacts: 07825 065681 and 07825 065678</td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
</keepTogether>
|
||||
<spacer length="15"/>
|
||||
<keepTogether>
|
||||
|
||||
<para style="blockPara">
|
||||
<b>To be signed on booking:</b>
|
||||
</para>
|
||||
{% if object.organisation.union_account %}
|
||||
<para style="blockPara">
|
||||
<i>
|
||||
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>
|
||||
</para>
|
||||
<para style="blockPara">
|
||||
<i>
|
||||
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.
|
||||
</i>
|
||||
</para>
|
||||
|
||||
<para style="blockPara">
|
||||
<b>
|
||||
Conditions of hire attached and available on the TEC PA & Lighting website. E&OE
|
||||
</b>
|
||||
</para>
|
||||
|
||||
<para style="blockPara">
|
||||
Please return this form directly to TEC PA & Lighting and not the Students' Union Finance Department.
|
||||
</para>
|
||||
|
||||
<blockTable style="signatureTable" colWidths="70,100,325">
|
||||
<tr>
|
||||
<td>Account Code</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
|
||||
{% else %}
|
||||
<para style="blockPara">
|
||||
<i>
|
||||
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.
|
||||
</i>
|
||||
</para>
|
||||
|
||||
<para style="blockPara">
|
||||
<b>
|
||||
Conditions of hire attached and available on the TEC PA & Lighting website. E&OE
|
||||
</b>
|
||||
</para>
|
||||
|
||||
{% include "RIGS/event_print_signature.xml" %}
|
||||
<spacer length="10"/>
|
||||
<para style="blockPara">
|
||||
<b>To be signed on the day of the event/hire:</b>
|
||||
</para>
|
||||
<para style="blockPara">
|
||||
<i>
|
||||
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.
|
||||
</i>
|
||||
</para>
|
||||
{% endif %}
|
||||
|
||||
{% include "RIGS/event_print_signature.xml" %}
|
||||
</keepTogether>
|
||||
{% endif %}
|
||||
<namedString id="lastPage"><pageNumber/></namedString>
|
||||
<namedString id="lastPage"><pageNumber/></namedString>
|
||||
|
||||
@@ -33,13 +33,18 @@
|
||||
</td>
|
||||
<td>
|
||||
<h4>
|
||||
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' event.pk %}" {% endif %}>{{ event.name }}</a>
|
||||
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' event.pk %}" {% endif %}>
|
||||
{{ event.name }}
|
||||
</a>
|
||||
{% if event.venue %}
|
||||
<small>at {{ event.venue }}</small>
|
||||
{% endif %}
|
||||
{% if event.dry_hire %}
|
||||
<span class="label label-default">Dry Hire</span>
|
||||
{% endif %}
|
||||
{% if event.is_rig and perms.RIGS.view_event and event.authorised %}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
{% endif %}
|
||||
</h4>
|
||||
{% if event.is_rig and not event.cancelled %}
|
||||
<h5>
|
||||
@@ -99,4 +104,4 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
41
RIGS/templates/RIGS/eventauthorisation_client_request.html
Normal file
41
RIGS/templates/RIGS/eventauthorisation_client_request.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends 'base_client_email.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>Hi {{ to_name|default:"there" }},</p>
|
||||
|
||||
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>N{{ object.pk|stringformat:"05d" }}
|
||||
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{{ object.person.name }}</b>{% endif %}.</p>
|
||||
|
||||
<p>
|
||||
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 %}
|
||||
</p>
|
||||
|
||||
|
||||
<table class="button-container" width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="button" align="center">
|
||||
<a href="{{ request.scheme }}://{{ request.get_host }}{% url 'event_authorise' object.pk hmac %}">
|
||||
Complete Authorisation Form
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<p>Your event will not be booked until you complete this form.</p>
|
||||
|
||||
<p>TEC PA & Lighting<br/>
|
||||
|
||||
{% endblock %}
|
||||
16
RIGS/templates/RIGS/eventauthorisation_client_request.txt
Normal file
16
RIGS/templates/RIGS/eventauthorisation_client_request.txt
Normal file
@@ -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
|
||||
21
RIGS/templates/RIGS/eventauthorisation_client_success.html
Normal file
21
RIGS/templates/RIGS/eventauthorisation_client_success.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'base_client_email.html' %}
|
||||
|
||||
{% block content %}
|
||||
<p>Hi {{ to_name|default:"there" }},</p>
|
||||
|
||||
<p>
|
||||
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
|
||||
for <b>£{{ object.amount }}</b>
|
||||
by <b>{{ object.name }}</b> as of <b>{{ object.last_edited_at }}</b>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% 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 %}
|
||||
</p>
|
||||
|
||||
<p>TEC PA & Lighting</p>
|
||||
{% endblock %}
|
||||
11
RIGS/templates/RIGS/eventauthorisation_client_success.txt
Normal file
11
RIGS/templates/RIGS/eventauthorisation_client_success.txt
Normal file
@@ -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
|
||||
133
RIGS/templates/RIGS/eventauthorisation_form.html
Normal file
133
RIGS/templates/RIGS/eventauthorisation_form.html
Normal file
@@ -0,0 +1,133 @@
|
||||
{% extends 'base_client.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
|
||||
$('form').on('submit', function () {
|
||||
$('#loading-modal').modal({
|
||||
backdrop: 'static',
|
||||
show: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %} | {{ event.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>
|
||||
{% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %}
|
||||
| {{ event.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% include 'RIGS/client_eventdetails.html' %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% with object=event %}
|
||||
{% include 'RIGS/item_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" id="eventauth">Event Authorisation</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal itemised_form" role="form" method="POST" action="#eventauth">
|
||||
{% csrf_token %}
|
||||
{% include 'form_errors.html' %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
I agree that I am authorised to approve this event. I agree that I am the
|
||||
<strong>President/Treasurer or account holder</strong> of the hirer, or that I
|
||||
have the written permission of the
|
||||
<strong>President/Treasurer or account holder</strong> of the hirer stating that
|
||||
I can authorise this event.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<div class="col-sm-12 form-group" data-toggle="tooltip"
|
||||
title="Your name as the person authorising the event.">
|
||||
<label for="{{ form.name.id_for_label }}"
|
||||
class="col-sm-4 control-label">{{ form.name.label }}</label>
|
||||
|
||||
<div class="col-sm-8">
|
||||
{% render_field form.name class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-group" data-toggle="tooltip"
|
||||
title="Your Student ID or Staff username as the person authorising the event.">
|
||||
<label for="{{ form.uni_id.id_for_label }}"
|
||||
class="col-sm-4 control-label">{{ form.uni_id.label }}</label>
|
||||
<div class="col-sm-8">
|
||||
{% render_field form.uni_id class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<div class="col-sm-12 form-group" data-toggle="tooltip"
|
||||
title="The Students' Union account code you wish this event to be charged to.">
|
||||
<label for="{{ form.account_code.id_for_label }}"
|
||||
class="col-sm-4 control-label">{{ form.account_code.label }}</label>
|
||||
<div class="col-sm-8">
|
||||
{% render_field form.account_code class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-group" data-toggle="tooltip"
|
||||
title="The full amount chargable for this event as displayed above, including VAT.">
|
||||
<label for="{{ form.amount.id_for_label }}"
|
||||
class="col-sm-4 control-label">{{ form.amount.label }}</label>
|
||||
<div class="col-sm-8">
|
||||
{% render_field form.amount class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12 col-md-8 form-group">
|
||||
<div class="col-sm-offset-4 col-sm-8 col-md-offset-3" data-toggle="tooltip"
|
||||
title="In order to book and event you must agree to the TEC Terms of Hire.">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
{% render_field form.tos %} I have read and agree to the TEC
|
||||
<a href="{{ tos_url }}">Terms of Hire</a>. E&OE.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-4 text-right">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" type="submit">Authorise</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
5
RIGS/templates/RIGS/eventauthorisation_mic_success.txt
Normal file
5
RIGS/templates/RIGS/eventauthorisation_mic_success.txt
Normal file
@@ -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
|
||||
59
RIGS/templates/RIGS/eventauthorisation_request.html
Normal file
59
RIGS/templates/RIGS/eventauthorisation_request.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Request Authorisation{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="alert alert-warning">
|
||||
<h1>Send authorisation request email.</h1>
|
||||
<p>Pressing send will email the address provided. Please triple check everything before continuing.</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Person Email</dt>
|
||||
<dd>{{ object.person.email }}</dd>
|
||||
|
||||
<dt>Organisation Email</dt>
|
||||
<dd>{{ object.organisation.email }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-10 col-md-offset-1">
|
||||
<form action="{{ form.action|default:request.path }}" method="post" class="form-horizontal"
|
||||
id="auth-request-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
{% include 'form_errors.html' %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label"
|
||||
for="{{ form.email.id_for_label }}">{{ form.email.label }}</label>
|
||||
|
||||
<div class="col-sm-9">
|
||||
{% render_field form.email type="email" class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right col-sm-3 col-sm-offset-9">
|
||||
<div class="form-group">
|
||||
<button type="submit" class="form-control btn btn-primary">
|
||||
<span class="glyphicon glyphicon-send"></span>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#auth-request-form').on('submit', function () {
|
||||
$('#auth-request-form button').attr('disabled', true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
15
RIGS/templates/RIGS/eventauthorisation_request_error.html
Normal file
15
RIGS/templates/RIGS/eventauthorisation_request_error.html
Normal file
@@ -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 %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="alert alert-warning">
|
||||
<h1>An error occured.</h1>
|
||||
<p>Your RIGS account must have an @nottinghamtec.co.uk email address before you can send emails to clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
64
RIGS/templates/RIGS/eventauthorisation_success.html
Normal file
64
RIGS/templates/RIGS/eventauthorisation_success.html
Normal file
@@ -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 %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>
|
||||
{% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %}
|
||||
| {{ event.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'RIGS/client_eventdetails.html' %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% with object=event %}
|
||||
{% include 'RIGS/item_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Event Authorisation</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Name</dt>
|
||||
<dd>{{ object.name }}</dd>
|
||||
|
||||
<dt>Email</dt>
|
||||
<dd>{{ object.email }}</dd>
|
||||
|
||||
{% if internal %}
|
||||
<dt>University ID</dt>
|
||||
<dd>{{ object.uni_id }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Account code</dt>
|
||||
<dd>{{ object.account_code }}</dd>
|
||||
|
||||
<dt>Authorised amount</dt>
|
||||
<dd>£ {{ object.amount|floatformat:2 }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -76,8 +76,43 @@
|
||||
<dd>{{ object.checked_in_by.name }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>PO</dt>
|
||||
<dd>{{ object.event.purchase_order }}</dd>
|
||||
<dd> </dd>
|
||||
|
||||
<dt>Authorised</dt>
|
||||
<dd>{{ object.event.authorised|yesno:"Yes,No" }}</dd>
|
||||
|
||||
<dt>Authorised by</dt>
|
||||
<dd>
|
||||
{% if object.event.authorised %}
|
||||
{{ object.event.authorisation.name }}
|
||||
(<a href="mailto:{{ object.event.authorisation.email }}">{{ object.event.authorisation.email }}</a>)
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
{% if object.event.internal %}
|
||||
{# internal #}
|
||||
<dt>Uni ID</dt>
|
||||
<dd>{{ object.event.authorisation.uni_id }}</dd>
|
||||
|
||||
<dt>Account code</dt>
|
||||
<dd>{{ object.event.authorisation.account_code }}</dd>
|
||||
{% else %}
|
||||
<dt>PO</dt>
|
||||
<dd>{{ object.event.purchase_order }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>Authorised at</dt>
|
||||
<dd>{{ object.event.authorisation.last_edited_at }}</dd>
|
||||
|
||||
<dt>Authorised amount</dt>
|
||||
<dd>
|
||||
{% if object.event.authorised %}
|
||||
£ {{ object.event.authorisation.amount|floatformat:"2" }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Authorsation request sent by</dt>
|
||||
<dd>{{ object.authorisation.sent_by }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,4 +174,4 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<td>{% if object.event.organisation %}
|
||||
{{ object.event.organisation.name }}
|
||||
<br>
|
||||
<span class="text-muted">{{ object.event.organisation.union_account|yesno:'Internal,External' }}</span>
|
||||
<span class="text-muted">{{ object.event.internal|yesno:'Internal,External' }}</span>
|
||||
{% else %}
|
||||
{{ object.event.person.name }}
|
||||
<br>
|
||||
@@ -59,7 +59,11 @@
|
||||
</td>
|
||||
<td>{{ object.event.start_date }}</td>
|
||||
<td>{{ object.invoice_date }}</td>
|
||||
<td>{{ object.balance|floatformat:2 }}</td>
|
||||
<td>
|
||||
{{ object.balance|floatformat:2 }}
|
||||
<br />
|
||||
<span class="text-muted">{{ object.event.purchase_order }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
@@ -76,4 +80,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
47
RIGS/urls.py
47
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<view>(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\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<view>(month|week|day))/$',
|
||||
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\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<pk>\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<pk>\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<pk>\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<pk>\d+)/auth/$',
|
||||
permission_required_with_403('RIGS.change_event')(
|
||||
rigboard.EventAuthorisationRequest.as_view()
|
||||
),
|
||||
name='event_authorise_request'),
|
||||
url(r'^event/(?P<pk>\d+)/auth/preview/$',
|
||||
permission_required_with_403('RIGS.change_event')(
|
||||
rigboard.EventAuthoriseRequestEmailPreview.as_view()
|
||||
),
|
||||
name='event_authorise_preview'),
|
||||
url(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\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<pk>\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<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
|
||||
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
|
||||
name="ics_calendar"),
|
||||
|
||||
# API
|
||||
url(r'^api/(?P<model>\w+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||
url(r'^api/(?P<model>\w+)/(?P<pk>\d+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||
url(r'^api/(?P<model>\w+)/$', login_required(views.SecureAPIRequest.as_view()),
|
||||
name="api_secure"),
|
||||
url(r'^api/(?P<model>\w+)/(?P<pk>\d+)/$', login_required(views.SecureAPIRequest.as_view()),
|
||||
name="api_secure"),
|
||||
|
||||
# Legacy URL's
|
||||
url(r'^rig/show/(?P<pk>\d+)/$', RedirectView.as_view(permanent=True, pattern_name='event_detail')),
|
||||
url(r'^rig/show/(?P<pk>\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')),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,4 +22,4 @@
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
125
templates/base_client.html
Normal file
125
templates/base_client.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load raven %}
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}"
|
||||
xml:lang="{% firstof LANGUAGE_CODE 'en' %}"
|
||||
lang="{% firstof LANGUAGE_CODE 'en' %}">
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %} | Rig Information Gathering System</title>
|
||||
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
|
||||
<link rel="icon" type="image/png" href="{% static "imgs/pyrigs-avatar.png" %}">
|
||||
<link rel="apple-touch-icon" href="{% static "imgs/pyrigs-avatar.png" %}">
|
||||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet'
|
||||
type='text/css'>
|
||||
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static "css/screen.css" %}">
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.8.3.min.js"
|
||||
integrity="sha256-YcbK69I5IXQftf/mYD8WY0/KmEDCv1asggHpJk1trM8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.ravenjs.com/1.3.0/jquery,native/raven.min.js"></script>
|
||||
<script>Raven.config('{% sentry_public_dsn %}').install()</script>
|
||||
{% block preload_js %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra-head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% include "analytics.html" %}
|
||||
<div class="navbar navbar-fixed-top navbar-inverse hidden-print" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<span class="navbar-brand">RIGS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="content" class="row">
|
||||
{% block content-header %}
|
||||
{% if error %}
|
||||
<div class="error">{{ error }}</div>{% endif %}
|
||||
{% if info %}
|
||||
<div class="info">{{ info }}</div>{% endif %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.level_tag }} alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div id="sidebar" class="column">
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modal" role="dialog" tabindex=-1></div>
|
||||
|
||||
<div class="modal fade" id="loading-modal" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Please wait</h2>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="loading-animation">
|
||||
<div class="circle"></div>
|
||||
<div class="circle1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
Date.prototype.getISOString = function () {
|
||||
var yyyy = this.getFullYear().toString();
|
||||
var mm = (this.getMonth() + 1).toString(); // getMonth() is zero-based
|
||||
var dd = this.getDate().toString();
|
||||
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
|
||||
};
|
||||
</script>
|
||||
<script src="{% static "js/jquery.cookie.js" %}"></script>
|
||||
<script src="{% static "js/alert.js" %}"></script>
|
||||
<script src="{% static "js/collapse.js" %}"></script>
|
||||
<script>
|
||||
$('.navbar-collapse').addClass('collapse')
|
||||
</script>
|
||||
<script src="{% static "js/dropdown.js" %}"></script>
|
||||
<script src="{% static "js/modal.js" %}"></script>
|
||||
<script>
|
||||
jQuery(document).ready(function () {
|
||||
jQuery(document).on('click', '.modal-href', function (e) {
|
||||
$link = jQuery(this);
|
||||
// Anti modal inception
|
||||
if ($link.parents('#modal').length == 0) {
|
||||
e.preventDefault();
|
||||
modaltarget = $link.data('target');
|
||||
modalobject = "";
|
||||
jQuery('#modal').load($link.attr('href'), function (e) {
|
||||
jQuery('#modal').modal();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block js %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
58
templates/base_client_email.html
Normal file
58
templates/base_client_email.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load raven %}
|
||||
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<table class="main-table">
|
||||
<tr class="client-header">
|
||||
<td align="center">
|
||||
<!--[if mso]>
|
||||
<center>
|
||||
<table><tr><td width="640">
|
||||
<![endif]-->
|
||||
<table class="logos" cellpadding=20>
|
||||
<tr>
|
||||
<td align="left">
|
||||
<img src="https://www.nottinghamtec.co.uk/imgs/tec_logo_white.png"/>
|
||||
</td>
|
||||
<td align="right">
|
||||
<img src="https://www.nottinghamtec.co.uk/imgs/UoNSU.png"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso]>
|
||||
</td></tr></table>
|
||||
</center>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" class="content-container">
|
||||
<!--[if mso]>
|
||||
<center>
|
||||
<table><tr><td width="600">
|
||||
<![endif]-->
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<!--[if mso]>
|
||||
</td></tr></table>
|
||||
</center>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user