Merge branch 'master' into assets_cabletypes

This commit is contained in:
2020-04-13 01:23:20 +01:00
committed by GitHub
55 changed files with 783 additions and 317 deletions

View File

@@ -12,14 +12,14 @@ install:
- export PATH=$PATH:$(pwd) - export PATH=$PATH:$(pwd)
- chmod +x chromedriver - chmod +x chromedriver
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install coveralls codeclimate-test-reporter pep8 - pip install coveralls codeclimate-test-reporter pycodestyle
before_script: before_script:
- export PATH=$PATH:/usr/lib/chromium-browser/ - export PATH=$PATH:/usr/lib/chromium-browser/
- python manage.py collectstatic --noinput - python manage.py collectstatic --noinput
script: script:
- pep8 . --exclude=migrations,importer* - pycodestyle . --exclude=migrations,importer*
- python manage.py check - python manage.py check
- python manage.py makemigrations --check --dry-run - python manage.py makemigrations --check --dry-run
- coverage run manage.py test --verbosity=2 - coverage run manage.py test --verbosity=2

View File

@@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
import os import os
import raven import raven
import secrets import secrets
import datetime
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@@ -44,12 +45,11 @@ if not DEBUG:
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
ADMINS = ( ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'), ('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
('Tom Price', 'tomtom5152@gmail.com') if DEBUG:
) ADMINS.append(('Testing Superuser', 'superuser@example.com'))
# Application definition # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@@ -168,6 +168,8 @@ RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', "6LeIxAcTAAAAAJcZV
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
NOCAPTCHA = True NOCAPTCHA = True
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
# Email # Email
EMAILER_TEST = False EMAILER_TEST = False
if not DEBUG or EMAILER_TEST: if not DEBUG or EMAILER_TEST:
@@ -182,6 +184,8 @@ if not DEBUG or EMAILER_TEST:
else: else:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/ # https://docs.djangoproject.com/en/1.7/topics/i18n/

View File

@@ -11,7 +11,7 @@ def create_browser():
if os.environ.get('CI', False): if os.environ.get('CI', False):
options.add_argument("--headless") options.add_argument("--headless")
options.add_argument("--no-sandbox") options.add_argument("--no-sandbox")
driver = webdriver.Chrome(chrome_options=options) driver = webdriver.Chrome(options=options)
return driver return driver

View File

@@ -1,3 +1,4 @@
from django.urls import path
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.staticfiles.urls import staticfiles_urlpatterns
@@ -15,8 +16,8 @@ urlpatterns = [
url('^assets/', include('assets.urls')), url('^assets/', include('assets.urls')),
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail), url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
name="registration_register"), name="registration_register"),
url('^user/', include('django.contrib.auth.urls')), path('user/', include('django.contrib.auth.urls')),
url('^user/', include('registration.backends.default.urls')), path('user/', include('registration.backends.default.urls')),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
] ]

View File

@@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from RIGS import models, forms from RIGS import models, forms
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from django.contrib.admin import helpers from django.contrib.admin import helpers
@@ -22,13 +22,22 @@ admin.site.register(models.Invoice)
admin.site.register(models.Payment) admin.site.register(models.Payment)
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
approve_user.short_description = "Approve selected users"
@admin.register(models.Profile) @admin.register(models.Profile)
class ProfileAdmin(UserAdmin): class ProfileAdmin(UserAdmin):
# Don't know how to add 'is_approved' whilst preserving the default list...
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), { (_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}), 'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', (_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}), 'groups', 'user_permissions')}),
(_('Important dates'), { (_('Important dates'), {
'fields': ('last_login', 'date_joined')}), 'fields': ('last_login', 'date_joined')}),
@@ -41,6 +50,7 @@ class ProfileAdmin(UserAdmin):
) )
form = forms.ProfileChangeForm form = forms.ProfileChangeForm
add_form = forms.ProfileCreationForm add_form = forms.ProfileCreationForm
actions = [approve_user]
class AssociateAdmin(VersionAdmin): class AssociateAdmin(VersionAdmin):

View File

@@ -11,6 +11,7 @@ from django.template.loader import get_template
from django.views import generic from django.views import generic
from django.db.models import Q from django.db.models import Q
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from django.db.models import Q
from RIGS import models from RIGS import models
@@ -76,7 +77,7 @@ class InvoicePrint(generic.View):
pdfData = buffer.read() pdfData = buffer.read()
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name) escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
response = HttpResponse(content_type='application/pdf') response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName) response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName)
@@ -122,6 +123,34 @@ class InvoiceArchive(generic.ListView):
template_name = 'RIGS/invoice_list_archive.html' template_name = 'RIGS/invoice_list_archive.html'
paginate_by = 25 paginate_by = 25
def get_queryset(self):
q = self.request.GET.get('q', "")
filter = Q(event__name__icontains=q)
# try and parse an int
try:
val = int(q)
filter = filter | Q(pk=val)
filter = filter | Q(event__pk=val)
except: # noqa
# not an integer
pass
try:
if q[0] == "N":
val = int(q[1:])
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
elif q[0] == "#":
val = int(q[1:])
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
except: # noqa
pass
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
return object_list
class InvoiceWaiting(generic.ListView): class InvoiceWaiting(generic.ListView):
model = models.Event model = models.Event

View File

@@ -2,8 +2,10 @@ from django import forms
from django.utils import formats from django.utils import formats
from django.conf import settings from django.conf import settings
from django.core import serializers from django.core import serializers
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
from registration.forms import RegistrationFormUniqueEmail from registration.forms import RegistrationFormUniqueEmail
from django.contrib.auth.forms import AuthenticationForm
from captcha.fields import ReCaptchaField from captcha.fields import ReCaptchaField
import simplejson import simplejson
@@ -33,8 +35,16 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
return self.cleaned_data['initials'] return self.cleaned_data['initials']
class CheckApprovedForm(AuthenticationForm):
def confirm_login_allowed(self, user):
if user.is_approved or user.is_superuser:
return AuthenticationForm.confirm_login_allowed(self, user)
else:
raise forms.ValidationError("Your account hasn't been approved by an administrator yet. Please check back in a few minutes!")
# Embedded Login form - remove the autofocus # Embedded Login form - remove the autofocus
class EmbeddedAuthenticationForm(AuthenticationForm): class EmbeddedAuthenticationForm(CheckApprovedForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop('autofocus', None) self.fields['username'].widget.attrs.pop('autofocus', None)

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.0.13 on 2020-01-10 14:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0035_auto_20191124_1319'),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_approved',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='profile',
name='last_emailed',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.0.13 on 2020-01-11 18:29
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
from django.db import migrations
def approve_legacy(apps, schema_editor):
Profile = apps.get_model('RIGS', 'Profile')
for person in Profile.objects.all():
person.is_approved = True
person.save()
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0036_profile_is_approved'),
]
operations = [
migrations.RunPython(approve_legacy)
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 2.0.13 on 2020-03-06 20:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0037_approve_legacy'),
]
operations = [
migrations.AlterModelOptions(
name='event',
options={},
),
migrations.AlterModelOptions(
name='invoice',
options={'ordering': ['-invoice_date']},
),
migrations.AlterModelOptions(
name='organisation',
options={},
),
migrations.AlterModelOptions(
name='person',
options={},
),
migrations.AlterModelOptions(
name='profile',
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
),
migrations.AlterModelOptions(
name='venue',
options={},
),
]

View File

@@ -8,7 +8,6 @@ from django.contrib.auth.models import AbstractUser
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
import string import string
@@ -22,11 +21,12 @@ from django.urls import reverse_lazy
# Create your models here. # Create your models here.
@python_2_unicode_compatible
class Profile(AbstractUser): class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False) initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True) phone = models.CharField(max_length=13, null=True, blank=True)
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True) api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
is_approved = models.BooleanField(default=False)
last_emailed = models.DateTimeField(blank=True, null=True) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
@classmethod @classmethod
def make_api_key(cls): def make_api_key(cls):
@@ -53,14 +53,17 @@ class Profile(AbstractUser):
def latest_events(self): def latest_events(self):
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
@classmethod
def admins(cls):
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
@classmethod
def users_awaiting_approval_count(cls):
return Profile.objects.filter(models.Q(is_approved=False)).count()
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
permissions = (
('view_profile', 'Can view Profile'),
)
class RevisionMixin(object): class RevisionMixin(object):
@property @property
@@ -91,7 +94,6 @@ class RevisionMixin(object):
@reversion.register @reversion.register
@python_2_unicode_compatible
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True) phone = models.CharField(max_length=15, blank=True, null=True)
@@ -127,14 +129,8 @@ class Person(models.Model, RevisionMixin):
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('person_detail', kwargs={'pk': self.pk}) return reverse_lazy('person_detail', kwargs={'pk': self.pk})
class Meta:
permissions = (
('view_person', 'Can view Persons'),
)
@reversion.register @reversion.register
@python_2_unicode_compatible
class Organisation(models.Model, RevisionMixin): class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True) phone = models.CharField(max_length=15, blank=True, null=True)
@@ -171,11 +167,6 @@ class Organisation(models.Model, RevisionMixin):
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk}) return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
class Meta:
permissions = (
('view_organisation', 'Can view Organisations'),
)
class VatManager(models.Manager): class VatManager(models.Manager):
def current_rate(self): def current_rate(self):
@@ -192,7 +183,6 @@ class VatManager(models.Manager):
@reversion.register @reversion.register
@python_2_unicode_compatible
class VatRate(models.Model, RevisionMixin): class VatRate(models.Model, RevisionMixin):
start_at = models.DateField() start_at = models.DateField()
rate = models.DecimalField(max_digits=6, decimal_places=6) rate = models.DecimalField(max_digits=6, decimal_places=6)
@@ -213,7 +203,6 @@ class VatRate(models.Model, RevisionMixin):
@reversion.register @reversion.register
@python_2_unicode_compatible
class Venue(models.Model, RevisionMixin): class Venue(models.Model, RevisionMixin):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
phone = models.CharField(max_length=15, blank=True, null=True) phone = models.CharField(max_length=15, blank=True, null=True)
@@ -236,11 +225,6 @@ class Venue(models.Model, RevisionMixin):
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('venue_detail', kwargs={'pk': self.pk}) return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
class Meta:
permissions = (
('view_venue', 'Can view Venues'),
)
class EventManager(models.Manager): class EventManager(models.Manager):
def current_events(self): def current_events(self):
@@ -287,7 +271,6 @@ class EventManager(models.Manager):
@reversion.register(follow=['items']) @reversion.register(follow=['items'])
@python_2_unicode_compatible
class Event(models.Model, RevisionMixin): class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database # Done to make it much nicer on the database
PROVISIONAL = 0 PROVISIONAL = 0
@@ -481,11 +464,6 @@ class Event(models.Model, RevisionMixin):
self.full_clean() self.full_clean()
super(Event, self).save(*args, **kwargs) super(Event, self).save(*args, **kwargs)
class Meta:
permissions = (
('view_event', 'Can view Events'),
)
class EventItem(models.Model): class EventItem(models.Model):
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE) event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
@@ -523,7 +501,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID") 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) account_code = models.CharField(max_length=50, blank=True, null=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount") amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
sent_by = models.ForeignKey('RIGS.Profile', on_delete=models.CASCADE) sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
@@ -533,7 +511,6 @@ class EventAuthorisation(models.Model, RevisionMixin):
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')') return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
@python_2_unicode_compatible
class Invoice(models.Model): class Invoice(models.Model):
event = models.OneToOneField('Event', on_delete=models.CASCADE) event = models.OneToOneField('Event', on_delete=models.CASCADE)
invoice_date = models.DateField(auto_now_add=True) invoice_date = models.DateField(auto_now_add=True)
@@ -566,13 +543,9 @@ class Invoice(models.Model):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
class Meta: class Meta:
permissions = (
('view_invoice', 'Can view Invoices'),
)
ordering = ['-invoice_date'] ordering = ['-invoice_date']
@python_2_unicode_compatible
class Payment(models.Model): class Payment(models.Model):
CASH = 'C' CASH = 'C'
INTERNAL = 'I' INTERNAL = 'I'

View File

@@ -110,7 +110,7 @@ class EventCreate(generic.CreateView):
context['currentVAT'] = models.VatRate.objects.current_rate() context['currentVAT'] = models.VatRate.objects.current_rate()
form = context['form'] form = context['form']
if re.search('"-\d+"', form['items_json'].value()): if re.search(r'"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.") 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. # Get some other objects to include in the form. Used when there are errors but also nice and quick.
@@ -206,7 +206,6 @@ class EventPrint(generic.View):
} }
rml = template.render(context) rml = template.render(context)
buffer = rml2pdf.parseString(rml) buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer)) merger.append(PdfFileReader(buffer))
buffer.close() buffer.close()
@@ -219,17 +218,25 @@ class EventPrint(generic.View):
response = HttpResponse(content_type='application/pdf') response = HttpResponse(content_type='application/pdf')
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name) escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName) response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
response.write(merged.getvalue()) response.write(merged.getvalue())
return response return response
class EventArchive(generic.ArchiveIndexView): class EventArchive(generic.ListView):
model = models.Event model = models.Event
date_field = "start_date"
paginate_by = 25 paginate_by = 25
template_name = "RIGS/event_archive.html"
def get_context_data(self, **kwargs):
# get super context
context = super(EventArchive, self).get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
return context
def get_queryset(self): def get_queryset(self):
start = self.request.GET.get('start', None) start = self.request.GET.get('start', None)
@@ -241,19 +248,34 @@ class EventArchive(generic.ArchiveIndexView):
"Muppet! Check the dates, it has been fixed for you.") "Muppet! Check the dates, it has been fixed for you.")
start, end = end, start # Stop the impending fail start, end = end, start # Stop the impending fail
filter = False filter = Q()
if end != "": if end != "":
filter = Q(start_date__lte=end) filter &= Q(start_date__lte=end)
if start: if start:
if filter: filter &= Q(start_date__gte=start)
filter = filter & Q(start_date__gte=start)
else:
filter = Q(start_date__gte=start)
if filter: q = self.request.GET.get('q', "")
qs = self.model.objects.filter(filter).order_by('-start_date')
else: if q is not "":
qs = self.model.objects.all().order_by('-start_date') qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
# try and parse an int
try:
val = int(q)
qfilter = qfilter | Q(pk=val)
except: # noqa not an integer
pass
try:
if q[0] == "N":
val = int(q[1:])
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
except: # noqa
pass
filter &= qfilter
qs = self.model.objects.filter(filter).order_by('-start_date')
# Preselect related for efficiency # Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic') qs.select_related('person', 'organisation', 'venue', 'mic')

View File

@@ -1,3 +1,4 @@
import datetime
import re import re
import urllib.request import urllib.request
import urllib.error import urllib.error
@@ -10,6 +11,9 @@ from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse
from django.utils import timezone
from registration.signals import user_activated
from premailer import Premailer from premailer import Premailer
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
@@ -69,7 +73,7 @@ def send_eventauthorisation_success_email(instance):
external_styles=css).transform() external_styles=css).transform()
client_email.attach_alternative(html, 'text/html') client_email.attach_alternative(html, 'text/html')
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', instance.event.name) escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName), client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
merged.getvalue(), merged.getvalue(),
@@ -102,3 +106,35 @@ def on_revision_commit(sender, instance, created, **kwargs):
post_save.connect(on_revision_commit, sender=models.EventAuthorisation) post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
def send_admin_awaiting_approval_email(user, request, **kwargs):
# Bit more controlled than just emailing all superusers
for admin in models.Profile.admins():
# Check we've ever emailed them before and if so, if cooldown has passed.
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
context = {
'request': request,
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
'number_of_users': models.Profile.users_awaiting_approval_count(),
'to_name': admin.first_name
}
email = EmailMultiAlternatives(
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
get_template("RIGS/admin_awaiting_approval.txt").render(context),
to=[admin.email],
reply_to=[user.email],
)
css = staticfiles_storage.path('css/email.css')
html = Premailer(get_template("RIGS/admin_awaiting_approval.html").render(context),
external_styles=css).transform()
email.attach_alternative(html, 'text/html')
email.send()
# Update last sent
admin.last_emailed = timezone.now()
admin.save()
user_activated.connect(send_admin_awaiting_approval_email)

View File

@@ -0,0 +1,9 @@
{% extends 'base_client_email.html' %}
{% block content %}
<p>Hi {{ to_name|default_if_none:"Administrator" }},</p>
<p>{{ number_of_users|default_if_none:"Some" }} new users are awaiting administrator approval on RIGS. Click <a href="{{ request.scheme }}://{{ request.get_host }}{{ link_suffix }}">here</a> to approve them.</p>
<p>TEC PA &amp; Lighting</p>
{% endblock %}

View File

@@ -0,0 +1,5 @@
Hi {{ to_name|default_if_none:"Administrator" }},
{{ number_of_users|default_if_none:"Some" }} new users are awaiting administrator approval on RIGS. Use this link to approve them: {{ request.scheme }}://{{ request.get_host }}/{{ link_suffix }}
TEC PA & Lighting

View File

@@ -5,34 +5,49 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<h2>Event Archive</h2> <div class="col-sm-12">
<h2>Event Archive</h2>
<div class="col-sm-12 col-md-6 pagination"> </div>
<div class="col-sm-12">
<form class="form-inline"> <form class="form-inline">
<div class="form-group">
<label for="start">Start</label> <div class="input-group">
<input type="date" name="start" id="start" value="{{ request.GET.start }}" placeholder="Start" class="form-control" /> <div class="input-group-addon">Start</div>
<input type="date" name="start" id="start" value="{{ start|default_if_none:"" }}" placeholder="Start" class="form-control" />
</div> </div>
<div class="form-group">
<label for="end">End</label> <div class="input-group">
<input type="date" name="end" id="end" value="{% if request.GET.end %}{{ request.GET.end }}{% else %}{% now "Y-m-d" %}{% endif %}" placeholder="End" class="form-control" /> <div class="input-group-addon">End</div>
<input type="date" name="end" id="end" value="{{ end|default_if_none:"" }}" placeholder="End" class="form-control" />
</div> </div>
<div class="form-group">
<input type="submit" class="btn btn-primary" /> <div class="input-group">
<div class="input-group-addon">Keyword</div>
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}"
class="form-control"/>
</div> </div>
<div class="input-group">
<input type="submit" class="btn btn-primary" value="Search"/>
</div>
</form> </form>
</div> </div>
{% if is_paginated %} <div class="col-sm-12">
<div class="col-md-6 text-right"> {% if is_paginated %}
{% paginator %} <div class="pull-right">
</div> {% paginator %}
{% endif %} </div>
{% endif %}
</div> </div>
<div class="row"> <div class="row">
{% with latest as events %} <div class="col-sm-12">
{% include 'RIGS/event_table.html' %} {% with object_list as events %}
{% endwith %} {% include 'RIGS/event_table.html' %}
{% endwith %}
</div>
</div> </div>
{% if is_paginated %} {% if is_paginated %}

View File

@@ -10,12 +10,14 @@
| {{ object.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %} | {{ object.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %}
</h1> </h1>
</div> </div>
{% if perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'RIGS/event_detail_buttons.html' %} {% include 'RIGS/event_detail_buttons.html' %}
</div> </div>
{% endif %}
{% endif %} {% endif %}
{% if object.is_rig %} {% if object.is_rig and perms.RIGS.view_event %}
{# only need contact details for a rig #} {# only need contact details for a rig #}
<div class="col-sm-12 col-md-6 col-lg-5"> <div class="col-sm-12 col-md-6 col-lg-5">
<div class="panel panel-default"> <div class="panel panel-default">
@@ -72,7 +74,7 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="col-sm-12 {% if event.is_rig %}col-md-6 col-lg-7{% endif %}"> <div class="col-sm-12 {% if event.is_rig and perms.RIGS.view_event %}col-md-6 col-lg-7{% endif %}">
<div class="panel panel-info"> <div class="panel panel-info">
<div class="panel-heading">Event Info</div> <div class="panel-heading">Event Info</div>
<div class="panel-body"> <div class="panel-body">
@@ -147,7 +149,7 @@
<dd>{{ object.collector }}</dd> <dd>{{ object.collector }}</dd>
{% endif %} {% endif %}
{% if event.is_rig and not event.internal %} {% if event.is_rig and not event.internal and perms.RIGS.view_event %}
<dd>&nbsp;</dd> <dd>&nbsp;</dd>
<dt>PO</dt> <dt>PO</dt>
<dd>{{ object.purchase_order }}</dd> <dd>{{ object.purchase_order }}</dd>
@@ -156,7 +158,7 @@
</div> </div>
</div> </div>
</div> </div>
{% if event.is_rig and event.internal %} {% if event.is_rig and event.internal and perms.RIGS.view_event %}
<div class="col-sm-12"> <div class="col-sm-12">
<div class="panel panel-default <div class="panel panel-default
{% if object.authorised %} {% if object.authorised %}
@@ -212,7 +214,7 @@
</div> </div>
<div> <div>
{% endif %} {% endif %}
{% if not request.is_ajax %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'RIGS/event_detail_buttons.html' %} {% include 'RIGS/event_detail_buttons.html' %}
</div> </div>
@@ -222,21 +224,23 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Event Details</div> <div class="panel-heading">Event Details</div>
<div class="panel-body"> <div class="panel-body">
{% if perms.RIGS.view_event %}
<div class="well well-sm"> <div class="well well-sm">
<h4>Notes</h4> <h4>Notes</h4>
<div class="dont-break-out">{{ event.notes|linebreaksbr }}</div> <div class="dont-break-out">{{ event.notes|linebreaksbr }}</div>
</div> </div>
{% endif %}
{% include 'RIGS/item_table.html' %} {% include 'RIGS/item_table.html' %}
</div> </div>
</div> </div>
</div> </div>
{% if not request.is_ajax %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'RIGS/event_detail_buttons.html' %} {% include 'RIGS/event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if not request.is_ajax %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
<div> <div>
<a href="{% url 'event_history' object.pk %}" title="View Revision History"> <a href="{% url 'event_history' object.pk %}" title="View Revision History">
@@ -251,12 +255,16 @@
{% if request.is_ajax %} {% if request.is_ajax %}
{% block footer %} {% block footer %}
<div class="row"> <div class="row">
{% if perms.RIGS.view_event %}
<div class="col-sm-10 align-left"> <div class="col-sm-10 align-left">
<a href="{% url 'event_history' object.pk %}" title="View Revision History"> <a href="{% url 'event_history' object.pk %}" title="View Revision History">
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }} Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
</a> </a>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
{% else %}
<div class="col-sm-12">
{% endif %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'event_detail' object.pk %}" class="btn btn-primary">Open Event Page <span <a href="{% url 'event_detail' object.pk %}" class="btn btn-primary">Open Event Page <span
class="glyphicon glyphicon-eye"></span></a> class="glyphicon glyphicon-eye"></span></a>

View File

@@ -1,8 +1,7 @@
{% extends 'base_embed.html' %} {% extends 'base_embed.html' %}
{% load static from staticfiles %} {% load static %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<a href="/"> <a href="/">
@@ -22,7 +21,7 @@
</span> </span>
<h3> <h3>
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' object.pk %}"{% endif %}> <a href="{% url 'event_detail' object.pk %}">
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %} {% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
| {{ object.name }} </a> | {{ object.name }} </a>
{% if object.venue %} {% if object.venue %}
@@ -72,7 +71,6 @@
</p> </p>
</div> </div>
<div class="col-xs-6"> <div class="col-xs-6">
{% if object.meet_at %} {% if object.meet_at %}
<p> <p>
<strong>Crew meet:</strong> <strong>Crew meet:</strong>
@@ -97,10 +95,7 @@
{{ object.description|linebreaksbr }} {{ object.description|linebreaksbr }}
</p> </p>
{% endif %} {% endif %}
</table> </table>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,7 +1,6 @@
{% load filters %}
<setNextFrame name="main"/> <setNextFrame name="main"/>
<nextFrame/> <nextFrame/>
<blockTable style="headLayout" colWidths="330,165"> <blockTable style="headLayout" colWidths="330,165">
<tr> <tr>
<td> <td>
@@ -13,7 +12,7 @@
<keepInFrame> <keepInFrame>
<para style="style.event_description"> <para style="style.event_description">
{{ object.description|default_if_none:""|linebreaksbr }} {{ object.description|default_if_none:""|linebreaksxml }}
</para> </para>
</keepInFrame> </keepInFrame>
</td> </td>
@@ -75,9 +74,9 @@
{% if invoice %} {% if invoice %}
<keepInFrame> <keepInFrame>
{% if object.organisation.address %} {% if object.organisation.address %}
<para style="specific_description">{{ object.organisation.address|default_if_none:""|linebreaksbr }}</para> <para style="specific_description">{{ object.organisation.address|default_if_none:""|linebreaksxml }}</para>
{% elif object.person.address %} {% elif object.person.address %}
<para style="specific_description">{{ object.person.address|default_if_none:""|linebreaksbr }}</para> <para style="specific_description">{{ object.person.address|default_if_none:""|linebreaksxml }}</para>
{% endif %} {% endif %}
</keepInFrame> </keepInFrame>
{% endif %} {% endif %}
@@ -109,7 +108,7 @@
<h3>{{ object.venue.name }}</h3> <h3>{{ object.venue.name }}</h3>
{% if not invoice %} {% if not invoice %}
<keepInFrame> <keepInFrame>
<para style="specific_description">{{ object.venue.address|default_if_none:""|linebreaksbr }}</para> <para style="specific_description">{{ object.venue.address|default_if_none:""|linebreaksxml }}</para>
</keepInFrame> </keepInFrame>
{% endif %} {% endif %}
</td> </td>
@@ -185,7 +184,7 @@
{% if item.description %} {% if item.description %}
</para> </para>
<para style="item_description"> <para style="item_description">
<em>{{ item.description|linebreaksbr }}</em> <em>{{ item.description|linebreaksxml }}</em>
</para> </para>
<para> <para>
{% endif %} {% endif %}

View File

@@ -33,7 +33,7 @@
</td> </td>
<td> <td>
<h4> <h4>
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' event.pk %}" {% endif %}> <a href="{% url 'event_detail' event.pk %}">
{{ event.name }} {{ event.name }}
</a> </a>
{% if event.venue %} {% if event.venue %}

View File

@@ -1,6 +1,14 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% block title %}RIGS{% endblock %} {% block title %}RIGS{% endblock %}
{% block js %}
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
</script>
{% endblock %}
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1> <h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
@@ -26,44 +34,51 @@
<a class="list-group-item" href="https://forum.nottinghamtec.co.uk" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a> <a class="list-group-item" href="https://forum.nottinghamtec.co.uk" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a> <a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
{% if perms.RIGS.view_event %}
<a class="list-group-item" href="http://members.nottinghamtec.co.uk/wiki/images/2/22/Event_Risk_Assesment.pdf" target="_blank"><span class="glyphicon glyphicon-link"></span> Pre-Event Risk Assessment</a> <a class="list-group-item" href="http://members.nottinghamtec.co.uk/wiki/images/2/22/Event_Risk_Assesment.pdf" target="_blank"><span class="glyphicon glyphicon-link"></span> Pre-Event Risk Assessment</a>
<a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a> <a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a>
<a class="list-group-item" href="https://goo.gl/forms/jdPWov8PCNPoXtbn2" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a> <a class="list-group-item" href="https://goo.gl/forms/jdPWov8PCNPoXtbn2" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a>
{% endif %}
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title">Search Rigboard</h4> <h4 class="panel-title">Search Rigboard
<a href="{% url 'search_help' %}" class="pull-right modal-href"><span class="glyphicon glyphicon-question-sign"</span></a></h4>
</div> </div>
<script>
$(document).ready(function(){
$('#search-options li a').click(function(){
$('#searchForm').attr('action', $(this).data('action')).submit();
});
$('#id_search_input').keypress(function (e) {
if (e.which == 13) {
$('#searchForm').attr('action', $('#search-options li a').first().data('action')).submit();
return false;
}
});
});
</script>
<div class="list-group"> <div class="list-group">
<div class="list-group-item"> <div class="list-group-item">
<form class="form" role="form" action="{% url 'person_list' %}" method="GET"> <form id="searchForm" class="form" role="form" method="GET">
<div class="input-group"> <div class="input-group" data-toggle="tooltip" title="Use the dropdown button to select what to search. The default is Event Archive.">
<input type="search" name="q" class="form-control" placeholder="Search People" /> <input id="id_search_input" type="search" name="q" class="form-control" placeholder="Search..." />
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button> <div class="btn-group" role="group">
</span> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-search"></span> Search <span class="caret"></span></button>
</div> <ul id="search-options" class="dropdown-menu">
</form> <li><a data-action="{% url 'event_archive' %}" href="#">Events</a></li>
</div> <li><a data-action="{% url 'person_list' %}" href="#">People</a></li>
<div class="list-group-item"> <li><a data-action="{% url 'organisation_list' %}" href="#">Organisations</a></li>
<form class="form" role="form" action="{% url 'organisation_list' %}" method="GET"> <li><a data-action="{% url 'venue_list' %}" href="#">Venues</a></li>
<div class="input-group"> {% if perms.RIGS.view_invoice %}
<input type="search" name="q" class="form-control" placeholder="Search Organisations" /> <li><a data-action="{% url 'invoice_archive' %}" href="#">Invoices</a></li>
<span class="input-group-btn"> {% endif %}
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button> </ul>
</span> </div>
</div>
</form>
</div>
<div class="list-group-item">
<form class="form" role="form" action="{% url 'venue_list' %}" method="GET">
<div class="input-group">
<input type="search" name="q" class="form-control" placeholder="Search Venues" />
<span class="input-group-btn">
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</span> </span>
</div> </div>
</form> </form>

View File

@@ -111,7 +111,7 @@
{% endif %} {% endif %}
</dd> </dd>
<dt>Authorsation request sent by</dt> <dt>Authorisation request sent by</dt>
<dd>{{ object.authorisation.sent_by }}</dd> <dd>{{ object.authorisation.sent_by }}</dd>
</dl> </dl>
</div> </div>

View File

@@ -12,6 +12,7 @@
{% paginator %} {% paginator %}
</div> </div>
{% endif %} {% endif %}
{% block search %}{% endblock %}
<div class="table-responsive col-sm-12"> <div class="table-responsive col-sm-12">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>

View File

@@ -11,3 +11,14 @@ All Invoices
{% block description %} {% block description %}
<p>This page displays all invoices: outstanding, paid, and void</p> <p>This page displays all invoices: outstanding, paid, and void</p>
{% endblock %} {% endblock %}
{% block search %}
<div class="col-sm-3 col-sm-offset-9">
<form class="form form-horizontal col-sm-12">
<div class="form-group">
<input type="search" name="q" placeholder="Search" value="{{ request.GET.q }}"
class="form-control"/>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,4 +1,4 @@
<div class="modal fade" id="itemModal" role="dialog" aria-labelledby="itemModal" aria-hidded="true"> <div class="modal fade" id="itemModal" role="dialog" aria-labelledby="itemModal" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -33,7 +33,6 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<div class="form-group"> <div class="form-group">
<label for="item_quantity" class="col-sm-4 control-label">Quantity</label> <label for="item_quantity" class="col-sm-4 control-label">Quantity</label>

View File

@@ -6,9 +6,13 @@
<em class="description">{{item.description|linebreaksbr}}</em> <em class="description">{{item.description|linebreaksbr}}</em>
</div> </div>
</td> </td>
{% if perms.RIGS.view_event %}
<td>£&nbsp;<span class="cost">{{item.cost|floatformat:2}}</span></td> <td>£&nbsp;<span class="cost">{{item.cost|floatformat:2}}</span></td>
{% endif %}
<td class="quantity">{{item.quantity}}</td> <td class="quantity">{{item.quantity}}</td>
{% if perms.RIGS.view_event %}
<td>£&nbsp;<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td> <td>£&nbsp;<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
{% endif %}
{% if edit %} {% if edit %}
<td class="vert-align text-right"> <td class="vert-align text-right">
<button type="button" class="item-edit btn btn-xs btn-default" <button type="button" class="item-edit btn btn-xs btn-default"

View File

@@ -3,13 +3,17 @@
<thead> <thead>
<tr> <tr>
<td>Item</td> <td>Item</td>
{% if perms.RIGS.view_event %}
<td>Price</td> <td>Price</td>
{% endif %}
<td>Quantity</td> <td>Quantity</td>
{% if perms.RIGS.view_event %}
<td>Sub-total</td> <td>Sub-total</td>
{% endif %}
{% if edit %} {% if edit %}
<td class="text-right"> <td class="text-right">
<button type="button" class="btn btn-default btn-xs item-add" <button type="button" class="btn btn-default btn-xs item-add"
data-url="{#% url eventitem_add object.pk %#}" data-toggle="modal" data-toggle="modal"
data-target="#itemModal"> data-target="#itemModal">
<span class="glyphicon glyphicon-plus"></span> <span class="glyphicon glyphicon-plus"></span>
</button> </button>
@@ -22,6 +26,7 @@
{% include 'RIGS/item_row.html' %} {% include 'RIGS/item_row.html' %}
{% endfor %} {% endfor %}
</tbody> </tbody>
{% if perms.RIGS.view_event %}
<tfoot> <tfoot>
<tr> <tr>
<td rowspan="3" colspan="2"></td> <td rowspan="3" colspan="2"></td>
@@ -43,6 +48,7 @@
<td colspan="2">£ <span id="total">{{object.total|default:0|floatformat:2}}</span></td> <td colspan="2">£ <span id="total">{{object.total|default:0|floatformat:2}}</span></td>
</tr> </tr>
</tfoot> </tfoot>
{% endif %}
</table> </table>
</div> </div>
<table class="hidden invisible"> <table class="hidden invisible">

View File

@@ -1,9 +0,0 @@
{% extends 'base_rigs.html' %}
{% block title %}Password Reset Disabled{% endblock %}
{% block content %}
<h1>Password reset is disabled</h1>
<p> We are very sorry for the inconvenience, but due to a security vulnerability, password reset is currently disabled until the vulnerability can be patched.</p>
<p> If you are locked out of your account, please contact an administrator and we can manually perform a reset</p>
{% endblock %}

View File

@@ -0,0 +1,70 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% block title %}Search Help{% endblock %}
{% block content %}
<div class="row">
{% if not request.is_ajax %}
<div class="col-sm-12">
<h1>Search Help</h1>
</div>
{% endif %}
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Searching Events</h3>
</div>
<div class="panel-body">
<p>
Searches for entire query in:
<button type="button" class="btn btn-default btn-xs">name</button>
<button type="button" class="btn btn-default btn-xs">description</button> and
<button type="button" class="btn btn-default btn-xs">notes</button>
</p>
<p>You can search for an event by <button type="button" class="btn btn-default btn-xs">event_id</button> by entering an integer, or using the format <code>N01234</code></p>
<p>On the search results page you can also specify the date range for the <button type="button" class="btn btn-default btn-xs">start_date</button> of the event</p>
<p>Events are sorted in reverse <button type="button" class="btn btn-default btn-xs">start_date</button> order (most recent events at the top)</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Searching People/Organisations/Venues</h3>
</div>
<div class="panel-body">
<p>
Searches for entire search phrase in:
<button type="button" class="btn btn-default btn-xs">name</button>
<button type="button" class="btn btn-default btn-xs">email</button>
<button type="button" class="btn btn-default btn-xs">address</button>
<button type="button" class="btn btn-default btn-xs">notes</button> and
<button type="button" class="btn btn-default btn-xs">phone</button>
</p>
<p>You can search for an entry by <button type="button" class="btn btn-default btn-xs">id</button> by entering an integer</p>
<p>Entries are sorted in alphabetical order by <button type="button" class="btn btn-default btn-xs">name</button></p>
</div>
</div>
{% if perms.RIGS.view_invoice %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Searching Invoices</h3>
</div>
<div class="panel-body">
<p>
Searches for entire search phrase in:
<button type="button" class="btn btn-default btn-xs">event__name</button>
</p>
<p>You can search for an event's invoice by entering the <button type="button" class="btn btn-default btn-xs">event_id</button> using the format <code>N01234</code></p>
<p>You can search for an invoice by <button type="button" class="btn btn-default btn-xs">invoice_id</button> using the format <code>#01234</code></p>
<p>Entering a raw integer will search by both <button type="button" class="btn btn-default btn-xs">invoice_id</button> and <button type="button" class="btn btn-default btn-xs">event_id</button></p>
<p>Entries are sorted in reverse <button type="button" class="btn btn-default btn-xs">invoice_date</button> order</p>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -2,10 +2,24 @@ from django import template
from django import forms from django import forms
from django.forms.forms import NON_FIELD_ERRORS from django.forms.forms import NON_FIELD_ERRORS
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.utils.text import normalize_newlines
from django.template.defaultfilters import stringfilter
from django.utils.safestring import SafeData, mark_safe
from django.utils.html import escape
register = template.Library() register = template.Library()
@register.filter(is_safe=True, needs_autoescape=True)
@stringfilter
def linebreaksxml(value, autoescape=True):
autoescape = autoescape and not isinstance(value, SafeData)
value = normalize_newlines(value)
if autoescape:
value = escape(value)
return mark_safe(value.replace('\n', '<br />'))
@register.filter @register.filter
def multiply(value, arg): def multiply(value, arg):
return value * arg return value * arg

View File

@@ -94,7 +94,8 @@ class UserRegistrationTest(LiveServerTestCase):
# Read what the error is # Read what the error is
alert = self.browser.find_element_by_css_selector( alert = self.browser.find_element_by_css_selector(
'div.alert-danger').text 'div.alert-danger').text
self.assertIn("password fields didn't match", alert) # TODO Use regex matching to handle smart/unsmart quotes...
self.assertIn("password fields didn", alert)
# Passwords should be empty # Passwords should be empty
self.assertEqual(password1.get_attribute('value'), '') self.assertEqual(password1.get_attribute('value'), '')
@@ -121,7 +122,7 @@ class UserRegistrationTest(LiveServerTestCase):
email = mail.outbox[0] email = mail.outbox[0]
self.assertIn('John Smith "JS" activation required', email.subject) self.assertIn('John Smith "JS" activation required', email.subject)
urls = re.findall( urls = re.findall(
'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body) r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
self.assertEqual(len(urls), 1) self.assertEqual(len(urls), 1)
mail.outbox = [] # empty this for later mail.outbox = [] # empty this for later
@@ -141,18 +142,41 @@ class UserRegistrationTest(LiveServerTestCase):
self.assertEqual(password.get_attribute('placeholder'), 'Password') self.assertEqual(password.get_attribute('placeholder'), 'Password')
self.assertEqual(password.get_attribute('type'), 'password') self.assertEqual(password.get_attribute('type'), 'password')
# Expected to fail as not approved
username.send_keys('TestUsername') username.send_keys('TestUsername')
password.send_keys('correcthorsebatterystaple') password.send_keys('correcthorsebatterystaple')
self.browser.execute_script( self.browser.execute_script(
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()") "return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
password.send_keys(Keys.ENTER) password.send_keys(Keys.ENTER)
# Test approval
profileObject = models.Profile.objects.all()[0]
self.assertFalse(profileObject.is_approved)
# Read what the error is
alert = self.browser.find_element_by_css_selector(
'div.alert-danger').text
self.assertIn("approved", alert)
# Approve the user so we can proceed
profileObject.is_approved = True
profileObject.save()
# Retry login
self.browser.get(self.live_server_url + '/user/login')
username = self.browser.find_element_by_id('id_username')
username.send_keys('TestUsername')
password = self.browser.find_element_by_id('id_password')
password.send_keys('correcthorsebatterystaple')
self.browser.execute_script(
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
password.send_keys(Keys.ENTER)
# Check we are logged in # Check we are logged in
udd = self.browser.find_element_by_class_name('navbar').text udd = self.browser.find_element_by_class_name('navbar').text
self.assertIn('Hi John', udd) self.assertIn('Hi John', udd)
# Check all the data actually got saved # Check all the data actually got saved
profileObject = models.Profile.objects.all()[0]
self.assertEqual(profileObject.username, 'TestUsername') self.assertEqual(profileObject.username, 'TestUsername')
self.assertEqual(profileObject.first_name, 'John') self.assertEqual(profileObject.first_name, 'John')
self.assertEqual(profileObject.last_name, 'Smith') self.assertEqual(profileObject.last_name, 'Smith')
@@ -231,7 +255,7 @@ class EventTest(LiveServerTestCase):
# Slider expands and save button visible # Slider expands and save button visible
self.assertTrue(save.is_displayed()) self.assertTrue(save.is_displayed())
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
# For now, just check that HTML5 Client validation is in place TODO Test needs rewriting to properly test all levels of validation. # For now, just check that HTML5 Client validation is in place TODO Test needs rewriting to properly test all levels of validation.
self.assertTrue(self.browser.find_element_by_id('id_name').get_attribute('required') is not None) self.assertTrue(self.browser.find_element_by_id('id_name').get_attribute('required') is not None)
@@ -469,7 +493,7 @@ class EventTest(LiveServerTestCase):
save = self.browser.find_element_by_xpath( save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]') '(//button[@type="submit"])[3]')
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
# Check the items are visible # 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
@@ -481,8 +505,10 @@ class EventTest(LiveServerTestCase):
# Add item # Add item
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click() 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 = self.browser.find_element_by_id("itemModal")
wait.until(animation_is_finished())
# See modal has opened
self.assertTrue(modal.is_displayed())
modal.find_element_by_id("item_name").send_keys("Test Item 3") modal.find_element_by_id("item_name").send_keys("Test Item 3")
modal.find_element_by_id("item_description").send_keys( modal.find_element_by_id("item_description").send_keys(
"This is an item description\nthat for reasons unknown spans two lines") "This is an item description\nthat for reasons unknown spans two lines")
@@ -551,7 +577,7 @@ class EventTest(LiveServerTestCase):
# Click Rig button # Click Rig button
self.browser.find_element_by_xpath('//button[.="Rig"]').click() self.browser.find_element_by_xpath('//button[.="Rig"]').click()
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('//*[@id="content"]/form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
# Set title # Set title
@@ -581,7 +607,7 @@ class EventTest(LiveServerTestCase):
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text) 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 # Same date, end time before start time
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') 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_start_date').value='3015-04-24'")
@@ -600,7 +626,7 @@ class EventTest(LiveServerTestCase):
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text) 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 # Same date, end time before start time
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') 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_start_date').value='3015-04-24'")
@@ -613,7 +639,7 @@ class EventTest(LiveServerTestCase):
form.find_element_by_id('id_end_time').send_keys('06:00') form.find_element_by_id('id_end_time').send_keys('06:00')
# No end date, end time before start time # No end date, end time before start time
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') 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_start_date').value='3015-04-24'")
@@ -632,7 +658,7 @@ class EventTest(LiveServerTestCase):
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text) self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
# 2 dates, end after start # 2 dates, end after start
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') 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_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_end_date').value='3015-04-26'")
@@ -665,7 +691,7 @@ class EventTest(LiveServerTestCase):
# Click Rig button # Click Rig button
self.browser.find_element_by_xpath('//button[.="Rig"]').click() self.browser.find_element_by_xpath('//button[.="Rig"]').click()
form = self.browser.find_element_by_tag_name('form') form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
# Set title # Set title
@@ -1199,3 +1225,47 @@ class TECEventAuthorisationTest(TestCase):
self.assertEqual(self.event.auth_request_by, self.profile) self.assertEqual(self.event.auth_request_by, self.profile)
self.assertEqual(self.event.auth_request_to, 'client@functional.test') self.assertEqual(self.event.auth_request_to, 'client@functional.test')
self.assertIsNotNone(self.event.auth_request_at) self.assertIsNotNone(self.event.auth_request_at)
class SearchTest(LiveServerTestCase):
def setUp(self):
self.profile = models.Profile(
username="SearchTest", first_name="Search", last_name="Test", initials="STU", is_superuser=True)
self.profile.set_password("SearchTestPassword")
self.profile.save()
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.browser = create_browser()
self.browser.implicitly_wait(10) # Set implicit wait session wide
os.environ['RECAPTCHA_TESTING'] = 'True'
models.Event.objects.create(name="Right Event", status=models.Event.PROVISIONAL,
start_date=date.today(), description="This event is searched for endlessly over and over")
models.Event.objects.create(name="Wrong Event", status=models.Event.PROVISIONAL, start_date=date.today(),
description="This one should never be found.")
def tearDown(self):
self.browser.quit()
os.environ['RECAPTCHA_TESTING'] = 'False'
def test_search(self):
self.browser.get(self.live_server_url)
username = self.browser.find_element_by_id('id_username')
password = self.browser.find_element_by_id('id_password')
submit = self.browser.find_element_by_css_selector(
'input[type=submit]')
username.send_keys("SearchTest")
password.send_keys("SearchTestPassword")
submit.click()
form = self.browser.find_element_by_id('searchForm')
search_box = form.find_element_by_id('id_search_input')
search_box.send_keys('Right')
search_box.send_keys(Keys.ENTER)
event_name = self.browser.find_element_by_xpath('//*[@id="content"]/div[1]/div[4]/div/div/table/tbody/tr[1]/td[3]/h4').text
self.assertIn('Right', event_name)
self.assertNotIn('Wrong', event_name)

View File

@@ -424,7 +424,7 @@ class RIGSVersionTestCase(TestCase):
def test_find_parent_version(self): def test_find_parent_version(self):
# Find the most recent version # Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
self.assertEqual(currentVersion._object_version.object.notes, "A new note on the event") self.assertEqual(currentVersion._object_version.object.notes, "A new note on the event")
# Check the prev version is loaded correctly # Check the prev version is loaded correctly
@@ -436,7 +436,7 @@ class RIGSVersionTestCase(TestCase):
def test_changes_since(self): def test_changes_since(self):
# Find the most recent version # Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
changes = currentVersion.changes changes = currentVersion.changes
self.assertEqual(len(changes.field_changes), 1) self.assertEqual(len(changes.field_changes), 1)
@@ -453,7 +453,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save() self.event.save()
# Find the most recent version # Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
diff = currentVersion.changes diff = currentVersion.changes
# There are two changes # There are two changes
@@ -475,7 +475,7 @@ class RIGSVersionTestCase(TestCase):
self.person.save() self.person.save()
# Find the most recent version # Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.person).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.person).latest('revision__date_created')
diff = currentVersion.changes diff = currentVersion.changes
# Should be declared as long # Should be declared as long
@@ -488,7 +488,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save() self.event.save()
# Find the most recent version # Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
# Check the diff is correct # Check the diff is correct
self.assertEqual(currentVersion.changes.field_changes[0].diff, self.assertEqual(currentVersion.changes.field_changes[0].diff,
@@ -504,12 +504,12 @@ class RIGSVersionTestCase(TestCase):
self.event.status = models.Event.CONFIRMED self.event.status = models.Event.CONFIRMED
self.event.save() self.event.save()
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
self.assertEqual(currentVersion.changes.field_changes[0].old, 'Provisional') self.assertEqual(currentVersion.changes.field_changes[0].old, 'Provisional')
self.assertEqual(currentVersion.changes.field_changes[0].new, 'Confirmed') self.assertEqual(currentVersion.changes.field_changes[0].new, 'Confirmed')
def test_creation_behaviour(self): def test_creation_behaviour(self):
firstVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created').parent firstVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created').parent
diff = firstVersion.changes diff = firstVersion.changes
# Mainly to check for exceptions: # Mainly to check for exceptions:
@@ -522,7 +522,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save() self.event.save()
# Find the most recent version # Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
diffs = currentVersion.changes.item_changes diffs = currentVersion.changes.item_changes
@@ -541,7 +541,7 @@ class RIGSVersionTestCase(TestCase):
item1.save() item1.save()
self.event.save() self.event.save()
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
diffs = currentVersion.changes.item_changes diffs = currentVersion.changes.item_changes
@@ -563,7 +563,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save() self.event.save()
# Find the most recent version # Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created') currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created')
diffs = currentVersion.changes.item_changes diffs = currentVersion.changes.item_changes

View File

@@ -226,7 +226,7 @@ class TestPrintPaperwork(TestCase):
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.events = { cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today()), 1: models.Event.objects.create(name="TE E1", start_date=date.today(), description="This is an event description\nthat for a very specific reason spans two lines."),
} }
cls.invoices = { cls.invoices = {
@@ -423,3 +423,107 @@ class TestSampleDataGenerator(TestCase):
from django.core.management.base import CommandError from django.core.management.base import CommandError
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData') self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
class TestSearchLogic(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True)
cls.persons = {
1: models.Person.objects.create(name="Right Person", phone="1234"),
2: models.Person.objects.create(name="Wrong Person", phone="5678"),
}
cls.organisations = {
1: models.Organisation.objects.create(name="Right Organisation", email="test@example.com"),
2: models.Organisation.objects.create(name="Wrong Organisation", email="check@fake.co.uk"),
}
cls.venues = {
1: models.Venue.objects.create(name="Right Venue", address="1 Test Street, EX1"),
2: models.Venue.objects.create(name="Wrong Venue", address="2 Check Way, TS2"),
}
cls.events = {
1: models.Event.objects.create(name="Right Event", start_date=date.today(), person=cls.persons[1],
organisation=cls.organisations[1], venue=cls.venues[1]),
2: models.Event.objects.create(name="Wrong Event", start_date=date.today(), person=cls.persons[2],
organisation=cls.organisations[2], venue=cls.venues[2]),
}
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
def test_event_search(self):
# Test search by name
request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].name)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.events[1].name)
self.assertNotContains(response, self.events[2].name)
# Test search by ID
request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].pk)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.events[1].name)
self.assertNotContains(response, self.events[2].name)
def test_people_search(self):
# Test search by name
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].name)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.persons[1].name)
self.assertNotContains(response, self.persons[2].name)
# Test search by ID
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].pk)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.persons[1].name)
self.assertNotContains(response, self.persons[2].name)
# Test search by phone
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].phone)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.persons[1].name)
self.assertNotContains(response, self.persons[2].name)
def test_organisation_search(self):
# Test search by name
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].name)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.organisations[1].name)
self.assertNotContains(response, self.organisations[2].name)
# Test search by ID
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].pk)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.organisations[1].name)
self.assertNotContains(response, self.organisations[2].name)
# Test search by email
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].email)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.organisations[1].email)
self.assertNotContains(response, self.organisations[2].email)
def test_venue_search(self):
# Test search by name
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].name)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.venues[1].name)
self.assertNotContains(response, self.venues[2].name)
# Test search by ID
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].pk)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.venues[1].name)
self.assertNotContains(response, self.venues[2].name)
# Test search by address
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].address)
response = self.client.get(request_url, follow=True)
self.assertContains(response, self.venues[1].address)
self.assertNotContains(response, self.venues[2].address)

View File

@@ -1,12 +1,14 @@
from django.urls import path
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth.views import password_reset from django.contrib.auth.views import PasswordResetView
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import LoginView
from RIGS import models, views, rigboard, finance, ical, versioning, forms from RIGS import models, views, rigboard, finance, ical, versioning, forms
from django.views.generic import RedirectView from django.views.generic import RedirectView
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from PyRIGS.decorators import permission_required_with_403 from PyRIGS.decorators import permission_required_with_403, has_oembed
from PyRIGS.decorators import api_key_required from PyRIGS.decorators import api_key_required
urlpatterns = [ urlpatterns = [
@@ -16,10 +18,10 @@ urlpatterns = [
url('^$', login_required(views.Index.as_view()), name='index'), url('^$', login_required(views.Index.as_view()), name='index'),
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'), url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
url('^user/login/$', views.login, name='login'), path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'),
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'), path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'),
url(r'^user/password_reset/$', views.PasswordResetDisabled.as_view()), url(r'^search_help/$', views.SearchHelp.as_view(), name='search_help'),
# People # People
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()), url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
@@ -87,8 +89,7 @@ urlpatterns = [
permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()), permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()),
name='activity_feed'), name='activity_feed'),
url(r'^event/(?P<pk>\d+)/$', url(r'^event/(?P<pk>\d+)/$', has_oembed(oembed_view="event_oembed")(
permission_required_with_403('RIGS.view_event', oembed_view="event_oembed")(
rigboard.EventDetail.as_view()), rigboard.EventDetail.as_view()),
name='event_detail'), name='event_detail'),
url(r'^event/(?P<pk>\d+)/embed/$', url(r'^event/(?P<pk>\d+)/embed/$',

View File

@@ -25,7 +25,7 @@ class FieldComparison(object):
self._new = new self._new = new
def display_value(self, value): def display_value(self, value):
if isinstance(self.field, IntegerField) and len(self.field.choices) > 0: if isinstance(self.field, IntegerField) and self.field.choices is not None and len(self.field.choices) > 0:
return [x[1] for x in self.field.choices if x[0] == value][0] return [x[1] for x in self.field.choices if x[0] == value][0]
if self.field.name == "risk_assessment_edit_url": if self.field.name == "risk_assessment_edit_url":
return "completed" if value else "" return "completed" if value else ""
@@ -184,8 +184,7 @@ class RIGSVersion(Version):
versions = RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId).select_related("revision", "revision__user").all() versions = RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId).select_related("revision", "revision__user").all()
try: try:
previousVersion = versions.filter(revision_id__lt=self.revision_id).latest( previousVersion = versions.filter(revision_id__lt=self.revision_id).latest('revision__date_created')
field_name='revision__date_created')
except ObjectDoesNotExist: except ObjectDoesNotExist:
return False return False

View File

@@ -3,6 +3,7 @@ from django.http.response import HttpResponseRedirect
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse_lazy, reverse, NoReverseMatch from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic from django.views import generic
from django.contrib.auth.views import LoginView
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.core import serializers from django.core import serializers
@@ -34,28 +35,19 @@ class Index(generic.TemplateView):
return context return context
def login(request, **kwargs): class SearchHelp(generic.TemplateView):
if request.user.is_authenticated: template_name = 'RIGS/search_help.html'
next = request.GET.get('next', '/')
return HttpResponseRedirect(next)
else:
from django.contrib.auth.views import login
return login(request)
# This view should be exempt from requiring CSRF token. # This view should be exempt from requiring CSRF token.
# Then we can check for it and show a nice error # Then we can check for it and show a nice error
# Don't worry, django.contrib.auth.views.login will # Don't worry, django.contrib.auth.views.login will
# check for it before logging the user in # check for it before logging the user in
@csrf_exempt class LoginEmbed(LoginView):
def login_embed(request, **kwargs): template_name = 'registration/login_embed.html'
if request.user.is_authenticated:
next = request.GET.get('next', '/')
return HttpResponseRedirect(next)
else:
from django.contrib.auth.views import login
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
if request.method == "POST": if request.method == "POST":
csrf_cookie = request.COOKIES.get('csrftoken', None) csrf_cookie = request.COOKIES.get('csrftoken', None)
@@ -63,7 +55,7 @@ def login_embed(request, **kwargs):
messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.') messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.')
request.method = 'GET' # Render the page without trying to login request.method = 'GET' # Render the page without trying to login
return login(request, template_name="registration/login_embed.html", authentication_form=forms.EmbeddedAuthenticationForm) return super().dispatch(request, *args, **kwargs)
""" """
@@ -86,11 +78,20 @@ class PersonList(generic.ListView):
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
if len(q) >= 3:
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(email__icontains=q)) filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q)
else:
object_list = self.model.objects.all() # try and parse an int
orderBy = self.request.GET.get('orderBy', None) try:
val = int(q)
filter = filter | Q(pk=val)
except: # noqa
# not an integer
pass
object_list = self.model.objects.filter(filter)
orderBy = self.request.GET.get('orderBy', 'name')
if orderBy is not None: if orderBy is not None:
object_list = object_list.order_by(orderBy) object_list = object_list.order_by(orderBy)
return object_list return object_list
@@ -140,11 +141,20 @@ class OrganisationList(generic.ListView):
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
if len(q) >= 3:
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q)) filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q)
else:
object_list = self.model.objects.all() # try and parse an int
orderBy = self.request.GET.get('orderBy', "") try:
val = int(q)
filter = filter | Q(pk=val)
except: # noqa
# not an integer
pass
object_list = self.model.objects.filter(filter)
orderBy = self.request.GET.get('orderBy', "name")
if orderBy is not "": if orderBy is not "":
object_list = object_list.order_by(orderBy) object_list = object_list.order_by(orderBy)
return object_list return object_list
@@ -194,11 +204,20 @@ class VenueList(generic.ListView):
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
if len(q) >= 3:
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q)) filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q)
else:
object_list = self.model.objects.all() # try and parse an int
orderBy = self.request.GET.get('orderBy', "") try:
val = int(q)
filter = filter | Q(pk=val)
except: # noqa
# not an integer
pass
object_list = self.model.objects.filter(filter)
orderBy = self.request.GET.get('orderBy', "name")
if orderBy is not "": if orderBy is not "":
object_list = object_list.order_by(orderBy) object_list = object_list.order_by(orderBy)
return object_list return object_list
@@ -392,7 +411,3 @@ class ResetApiKey(generic.RedirectView):
self.request.user.save() self.request.user.save()
return reverse_lazy('profile_detail') return reverse_lazy('profile_detail')
class PasswordResetDisabled(generic.TemplateView):
template_name = "RIGS/password_reset_disable.html"

View File

@@ -57,7 +57,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='asset', model_name='asset',
name='parent', name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Asset'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Asset'),
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='asset', model_name='asset',
@@ -85,7 +85,7 @@ class Migration(migrations.Migration):
('circuits', models.IntegerField(blank=True, null=True)), ('circuits', models.IntegerField(blank=True, null=True)),
('cores', models.IntegerField(blank=True, null=True)), ('cores', models.IntegerField(blank=True, null=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetCategory')), ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetCategory')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Cable')), ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Cable')),
], ],
options={ options={
'abstract': False, 'abstract': False,

View File

@@ -1,17 +0,0 @@
# Generated by Django 2.0.13 on 2020-02-07 17:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20200103_2215'),
]
operations = [
migrations.AlterModelOptions(
name='supplier',
options={'ordering': ['name'], 'permissions': (('view_supplier', 'Can view a supplier'),)},
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.3 on 2020-02-19 14:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20200103_2215'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['asset_id_prefix', 'asset_id_number'], 'permissions': [('asset_finance', 'Can see financial data for assets')]},
),
migrations.AlterModelOptions(
name='supplier',
options={'ordering': ['name']},
),
]

View File

@@ -41,13 +41,10 @@ class AssetStatus(models.Model):
@reversion.register @reversion.register
class Supplier(models.Model, RevisionMixin): class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80)
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
permissions = (
('view_supplier', 'Can view a supplier'), name = models.CharField(max_length=80)
)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('supplier_list') return reverse('supplier_list')
@@ -85,10 +82,9 @@ class CableType(models.Model):
class Asset(models.Model, RevisionMixin): class Asset(models.Model, RevisionMixin):
class Meta: class Meta:
ordering = ['asset_id_prefix', 'asset_id_number'] ordering = ['asset_id_prefix', 'asset_id_number']
permissions = ( permissions = [
('asset_finance', 'Can see financial data for assets'), ('asset_finance', 'Can see financial data for assets')
('view_asset', 'Can view an asset') ]
)
parent = models.ForeignKey(to='self', related_name='asset_parent', parent = models.ForeignKey(to='self', related_name='asset_parent',
blank=True, null=True, on_delete=models.SET_NULL) blank=True, null=True, on_delete=models.SET_NULL)

View File

@@ -1,8 +1,7 @@
{% extends 'base_embed.html' %} {% extends 'base_embed.html' %}
{% load static from staticfiles %} {% load static %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<a href="/assets"> <a href="/assets">
@@ -35,6 +34,4 @@
</table> </table>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -248,7 +248,7 @@ class TestSupplierCreateAndEdit(AutoLoginTest):
def test_supplier_edit(self): def test_supplier_edit(self):
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open() self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
self.assertEquals("Fullmetal Heavy Industry", self.page.name) self.assertEqual("Fullmetal Heavy Industry", self.page.name)
new_name = "Cyberdyne Systems" new_name = "Cyberdyne Systems"
self.page.name = new_name self.page.name = new_name
self.page.submit() self.page.submit()

View File

@@ -1,41 +1,24 @@
beautifulsoup4==4.6.0 diff-match-patch==20181111
contextlib2==0.5.5
diff-match-patch==20121119
dj-database-url==0.5.0 dj-database-url==0.5.0
dj-static==0.0.6 dj-static==0.0.6
Django==2.0.13 Django==3.0.3
django-filter==2.0.0 django-debug-toolbar==2.2
django-widget-tweaks==1.4.3 django-ical==1.7.0
django-debug-toolbar==1.9.1 django-recaptcha==2.0.6
django-ical==1.4 django-registration-redux==2.7
django-recaptcha==1.4.0 django-reversion==3.0.7
django-registration-redux==2.4 django-widget-tweaks==1.4.5
django-reversion==2.0.13 gunicorn==20.0.4
django-toolbelt==0.0.1 icalendar==4.0.4
premailer==3.2.0 lxml==4.5.0
git+git://github.com/jazzband/django-widget-tweaks.git@1.4.2 premailer==3.6.1
gunicorn==19.8.1 psycopg2==2.8.4
icalendar==4.0.1
lxml==4.2.1
Markdown==2.6.11
Pillow==6.2.0
psycopg2==2.7.4
Pygments==2.2.0
PyPDF2==1.26.0 PyPDF2==1.26.0
python-dateutil==2.7.3 PyPOM==2.2.0
pytz==2018.4 pytz==2019.3
raven==6.8.0 raven==6.10.0
reportlab==3.4.0 requests==2.23.0
selenium==3.12.0 selenium==3.141.0
simplejson==3.15.0 simplejson==3.17.0
six==1.11.0 whitenoise==5.0.1
sqlparse==0.2.4 z3c.rml==3.9.1
static3==0.7.0
svg2rlg==0.3
yolk==0.4.3
whitenoise==4.1.2
z3c.rml==3.5.0
zope.event==4.3.0
zope.interface==4.5.0
zope.schema==4.5.0
pypom==2.2.0

View File

@@ -1,5 +1,5 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load staticfiles %} {% load static %}
{% block title %}Bad Request{% endblock %} {% block title %}Bad Request{% endblock %}
{% block content %} {% block content %}

View File

@@ -1,5 +1,5 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load staticfiles %} {% load static %}
{% block title %}Unauthorized{% endblock %} {% block title %}Unauthorized{% endblock %}
{% block content %} {% block content %}

View File

@@ -1,5 +1,5 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load staticfiles %} {% load static %}
{% block title %}Forbidden{% endblock %} {% block title %}Forbidden{% endblock %}
{% block content %} {% block content %}

View File

@@ -1,5 +1,5 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load staticfiles %} {% load static %}
{% block title %}Page Not Found{% endblock %} {% block title %}Page Not Found{% endblock %}
{% block content %} {% block content %}

View File

@@ -1,5 +1,5 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load staticfiles %} {% load static %}
{% block title %}Server error{% endblock %} {% block title %}Server error{% endblock %}
{% block content %} {% block content %}

View File

@@ -1,7 +1,6 @@
{% load static from staticfiles %} {% load static %}
{% load raven %} {% load raven %}
<!DOCTYPE html> <!DOCTYPE html>
<html <html
dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}" dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}"

View File

@@ -1,4 +1,4 @@
{% load static from staticfiles %} {% load static %}
{% load raven %} {% load raven %}

View File

@@ -1,17 +1,14 @@
{% load static from staticfiles %} {% load static %}
{% load raven %} {% load raven %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!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"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
</head> </head>
<body> <body>
<table class="main-table"> <table class="main-table">
<tr class="client-header"> <tr class="client-header">
<td align="center"> <td align="center">
@@ -51,8 +48,5 @@
</td> </td>
</tr> </tr>
</table> </table>
</body> </body>
</html> </html>

View File

@@ -1,4 +1,4 @@
{% load static from staticfiles %} {% load static %}
{% load raven %} {% load raven %}
<!DOCTYPE html> <!DOCTYPE html>

View File

@@ -1,5 +1,5 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load staticfiles %} {% load static %}
{% block title %}Login Required{% endblock %} {% block title %}Login Required{% endblock %}
{% block js %} {% block js %}

View File

@@ -5,6 +5,6 @@
{% block content %} {% block content %}
<div class="alert alert-success"> <div class="alert alert-success">
<h2>Activation Complete</h2> <h2>Activation Complete</h2>
<p>You user account is now fully registered. Enjoy RIGS</p> <p>Your user account is now awaiting administrator approval. Won't be long!</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,8 +1,6 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% include 'form_errors.html' %} {% include 'form_errors.html' %}
<div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4"> <div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4">
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %} <form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="id_username">{{ form.username.label }}</label> <label for="id_username">{{ form.username.label }}</label>