Compare commits

...

13 Commits

Author SHA1 Message Date
9c1c88a8d6 Specify version of reportlab
Should fix CI - looks like I went a bit too ham-handed in my requirements.txt purge last time...
2020-04-13 15:35:05 +01:00
2a403d9940 Fix migration 2020-04-13 01:36:59 +01:00
e2c495b037 Merge branch 'master' into assets_cabletypes 2020-04-13 01:23:20 +01:00
02d40d1b39 FIX: Patch for choices being none
Honestly no idea if this is going to work, I can't reproduce the issue locally...
2020-03-07 16:47:30 +00:00
8568c591a9 Update Python Dependencies (#404)
* [requires.io] dependency update

* Server starts...

Various things are broken, but it runs!

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* FIX: Broken migrations

* FIX: Update auth framework

* FIX: Correct static use in templates

* FIX: Fix supplier sort

* FIX: Remaining tests

* Revert "Disable password reset as temporary fix to vulnerability (#396)"

This reverts commit e0c6a56263.

# Conflicts:
#	RIGS/urls.py

* FIX: Fix broken newlining in PDFs

Introduced by a change in Django 2.1 'HTML rendered by form widgets no longer includes a closing slash on void elements, e.g. <br>. This is incompatible within XHTML, although some widgets already used aspects of HTML5 such as boolean attributes.'

* FIX: Fix some Django4 deprecation warnings

Why not...

* Refactor dependency file

Should now only include dependencies we actually use, not dependencies of dependencies and unused things

* Add newlines to the paperwork print test event

This will catch the error encountered in 79ec9214f9

* Swap to pycodestyle rather than pep8 in Travis

And eliminate W605 errors

* Bit too heavy handed with the dep purge there...

* Whoops, helps if one installs pycodestyle...

* FIX: Re-add overridden login view

* Better fix for previous commit

* FIX: Bloody smartquotes

Co-authored-by: requires.io <support@requires.io>
2020-03-07 16:21:48 +00:00
David Taylor
797ad778a9 Improve search logic and allow search of event archive (#248)
* Added search to person, venue, organisation and event archive

* Added search to invoice archive

* Added event search to homepage

* Tidy up event search logic and optimise

* Fixed merge issues

* Stopped 404 on failed search

* Set default ordering of people, organisations & venues to alphabetical (rather than order of addition to database)

* Added invoice search to home page (if you have permissions)

* Made invoice archive sort by reverse invoice date (rather than order added to database)

* Added search help page (very pretty)

* Made single search box for all search types

* FIX: Missing date field breaking archive view

* FEAT: Add omnisearch to header

Tis a bit broken on mobile at the moment...

* CHORE: Conform old code to pep8

* FIX: Select the event form, not the search one in tests!

* Revert "FEAT: Add omnisearch to header"

This reverts commit 6bcb242d6b because it caused MANY more problems than anticipated...

* FIX: Stop 404 on failed search, again

* FEAT: Basic testing of search

* Use a tooltip to help explain the UX

Obviously since it needs a tooltip it isn't brilliant UX but the best I can think of for now...

Co-authored-by: Tom Price <tom@codedinternet.com>
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: Arona Jones <aj@aronajones.com>
2020-02-29 11:57:33 +00:00
9cd90d0d52 Move cabletype menu links into 'Assets' dropdown 2020-02-27 10:39:25 +00:00
a707fe847e FIX: Update tests to match new UX 2020-02-27 10:20:50 +00:00
09d1b42728 FIX: Meta migrations... :> 2020-02-18 18:40:27 +00:00
e7324e2d10 FIX: pep8/remove debug print 2020-02-18 18:37:54 +00:00
d1ccb561f5 FIX: Prevent creating duplicate cable types 2020-02-18 18:35:39 +00:00
c60771e613 CRUD and ordering 2020-02-18 18:02:04 +00:00
386fec1f01 Move relevant fields and create migration to autogen cable types 2020-02-18 17:00:46 +00:00
61 changed files with 938 additions and 380 deletions

View File

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

View File

@@ -50,7 +50,6 @@ if DEBUG:
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
@@ -169,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
NOCAPTCHA = True
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
# Email
EMAILER_TEST = False
if not DEBUG or EMAILER_TEST:

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
from django.contrib import admin
from RIGS import models, forms
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 django.contrib.admin import helpers

View File

@@ -11,6 +11,7 @@ from django.template.loader import get_template
from django.views import generic
from django.db.models import Q
from z3c.rml import rml2pdf
from django.db.models import Q
from RIGS import models
@@ -76,7 +77,7 @@ class InvoicePrint(generic.View):
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['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'
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):
model = models.Event

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.utils import timezone
from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
from reversion import revisions as reversion
from reversion.models import Version
import string
@@ -22,7 +21,6 @@ from django.urls import reverse_lazy
# Create your models here.
@python_2_unicode_compatible
class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True)
@@ -66,11 +64,6 @@ class Profile(AbstractUser):
def __str__(self):
return self.name
class Meta:
permissions = (
('view_profile', 'Can view Profile'),
)
class RevisionMixin(object):
@property
@@ -101,7 +94,6 @@ class RevisionMixin(object):
@reversion.register
@python_2_unicode_compatible
class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True)
@@ -137,14 +129,8 @@ class Person(models.Model, RevisionMixin):
def get_absolute_url(self):
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
class Meta:
permissions = (
('view_person', 'Can view Persons'),
)
@reversion.register
@python_2_unicode_compatible
class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True)
@@ -181,11 +167,6 @@ class Organisation(models.Model, RevisionMixin):
def get_absolute_url(self):
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
class Meta:
permissions = (
('view_organisation', 'Can view Organisations'),
)
class VatManager(models.Manager):
def current_rate(self):
@@ -202,7 +183,6 @@ class VatManager(models.Manager):
@reversion.register
@python_2_unicode_compatible
class VatRate(models.Model, RevisionMixin):
start_at = models.DateField()
rate = models.DecimalField(max_digits=6, decimal_places=6)
@@ -223,7 +203,6 @@ class VatRate(models.Model, RevisionMixin):
@reversion.register
@python_2_unicode_compatible
class Venue(models.Model, RevisionMixin):
name = models.CharField(max_length=255)
phone = models.CharField(max_length=15, blank=True, null=True)
@@ -246,11 +225,6 @@ class Venue(models.Model, RevisionMixin):
def get_absolute_url(self):
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
class Meta:
permissions = (
('view_venue', 'Can view Venues'),
)
class EventManager(models.Manager):
def current_events(self):
@@ -297,7 +271,6 @@ class EventManager(models.Manager):
@reversion.register(follow=['items'])
@python_2_unicode_compatible
class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database
PROVISIONAL = 0
@@ -491,11 +464,6 @@ class Event(models.Model, RevisionMixin):
self.full_clean()
super(Event, self).save(*args, **kwargs)
class Meta:
permissions = (
('view_event', 'Can view Events'),
)
class EventItem(models.Model):
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
@@ -533,7 +501,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
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', on_delete=models.CASCADE)
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
def get_absolute_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
@@ -543,7 +511,6 @@ class EventAuthorisation(models.Model, RevisionMixin):
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
@python_2_unicode_compatible
class Invoice(models.Model):
event = models.OneToOneField('Event', on_delete=models.CASCADE)
invoice_date = models.DateField(auto_now_add=True)
@@ -576,13 +543,9 @@ class Invoice(models.Model):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
class Meta:
permissions = (
('view_invoice', 'Can view Invoices'),
)
ordering = ['-invoice_date']
@python_2_unicode_compatible
class Payment(models.Model):
CASH = 'C'
INTERNAL = 'I'

View File

@@ -110,7 +110,7 @@ class EventCreate(generic.CreateView):
context['currentVAT'] = models.VatRate.objects.current_rate()
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.")
# 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)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
@@ -219,17 +218,25 @@ class EventPrint(generic.View):
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.write(merged.getvalue())
return response
class EventArchive(generic.ArchiveIndexView):
class EventArchive(generic.ListView):
model = models.Event
date_field = "start_date"
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):
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.")
start, end = end, start # Stop the impending fail
filter = False
filter = Q()
if end != "":
filter = Q(start_date__lte=end)
filter &= Q(start_date__lte=end)
if start:
if filter:
filter = filter & Q(start_date__gte=start)
else:
filter = Q(start_date__gte=start)
filter &= Q(start_date__gte=start)
if filter:
qs = self.model.objects.filter(filter).order_by('-start_date')
else:
qs = self.model.objects.all().order_by('-start_date')
q = self.request.GET.get('q', "")
if q is not "":
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
qs.select_related('person', 'organisation', 'venue', 'mic')

View File

@@ -73,7 +73,7 @@ def send_eventauthorisation_success_email(instance):
external_styles=css).transform()
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),
merged.getvalue(),

View File

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

View File

@@ -1,8 +1,7 @@
{% extends 'base_embed.html' %}
{% load static from staticfiles %}
{% load static %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<a href="/">
@@ -72,7 +71,6 @@
</p>
</div>
<div class="col-xs-6">
{% if object.meet_at %}
<p>
<strong>Crew meet:</strong>
@@ -97,10 +95,7 @@
{{ object.description|linebreaksbr }}
</p>
{% endif %}
</table>
</div>
</div>
{% endblock %}

View File

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

View File

@@ -1,6 +1,14 @@
{% extends 'base_rigs.html' %}
{% block title %}RIGS{% endblock %}
{% block js %}
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
</script>
{% endblock %}
{% block content %}
<div class="col-sm-12">
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
@@ -36,35 +44,41 @@
<div class="panel panel-default">
<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>
<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-item">
<form class="form" role="form" action="{% url 'person_list' %}" method="GET">
<div class="input-group">
<input type="search" name="q" class="form-control" placeholder="Search People" />
<form id="searchForm" class="form" role="form" method="GET">
<div class="input-group" data-toggle="tooltip" title="Use the dropdown button to select what to search. The default is Event Archive.">
<input id="id_search_input" type="search" name="q" class="form-control" placeholder="Search..." />
<span class="input-group-btn">
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</form>
</div>
<div class="list-group-item">
<form class="form" role="form" action="{% url 'organisation_list' %}" method="GET">
<div class="input-group">
<input type="search" name="q" class="form-control" placeholder="Search Organisations" />
<span class="input-group-btn">
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</span>
</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>
<div class="btn-group" role="group">
<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>
<ul id="search-options" class="dropdown-menu">
<li><a data-action="{% url 'event_archive' %}" href="#">Events</a></li>
<li><a data-action="{% url 'person_list' %}" href="#">People</a></li>
<li><a data-action="{% url 'organisation_list' %}" href="#">Organisations</a></li>
<li><a data-action="{% url 'venue_list' %}" href="#">Venues</a></li>
{% if perms.RIGS.view_invoice %}
<li><a data-action="{% url 'invoice_archive' %}" href="#">Invoices</a></li>
{% endif %}
</ul>
</div>
</span>
</div>
</form>

View File

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

View File

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

View File

@@ -10,4 +10,15 @@ All Invoices
{% block description %}
<p>This page displays all invoices: outstanding, paid, and void</p>
{% 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-content">
<div class="modal-header">
@@ -33,7 +33,6 @@
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label for="item_quantity" class="col-sm-4 control-label">Quantity</label>
@@ -71,4 +70,4 @@
</form>
</div>
</div>
</div>
</div>

View File

@@ -13,7 +13,7 @@
{% if edit %}
<td class="text-right">
<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">
<span class="glyphicon glyphicon-plus"></span>
</button>

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.forms.forms import NON_FIELD_ERRORS
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.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
def multiply(value, arg):
return value * arg

View File

@@ -94,7 +94,8 @@ class UserRegistrationTest(LiveServerTestCase):
# Read what the error is
alert = self.browser.find_element_by_css_selector(
'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
self.assertEqual(password1.get_attribute('value'), '')
@@ -121,7 +122,7 @@ class UserRegistrationTest(LiveServerTestCase):
email = mail.outbox[0]
self.assertIn('John Smith "JS" activation required', email.subject)
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)
mail.outbox = [] # empty this for later
@@ -254,7 +255,7 @@ class EventTest(LiveServerTestCase):
# Slider expands and save button visible
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.
self.assertTrue(self.browser.find_element_by_id('id_name').get_attribute('required') is not None)
@@ -492,7 +493,7 @@ class EventTest(LiveServerTestCase):
save = self.browser.find_element_by_xpath(
'(//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
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
@@ -504,8 +505,10 @@ class EventTest(LiveServerTestCase):
# 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")
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_description").send_keys(
"This is an item description\nthat for reasons unknown spans two lines")
@@ -574,7 +577,7 @@ class EventTest(LiveServerTestCase):
# Click Rig button
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]')
# Set title
@@ -604,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)
# 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]')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
@@ -623,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)
# 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]')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
@@ -636,7 +639,7 @@ class EventTest(LiveServerTestCase):
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')
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]')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
@@ -655,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)
# 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]')
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'")
@@ -688,7 +691,7 @@ class EventTest(LiveServerTestCase):
# Click Rig button
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]')
# Set title
@@ -1222,3 +1225,47 @@ class TECEventAuthorisationTest(TestCase):
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)
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):
# 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")
# Check the prev version is loaded correctly
@@ -436,7 +436,7 @@ class RIGSVersionTestCase(TestCase):
def test_changes_since(self):
# 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
self.assertEqual(len(changes.field_changes), 1)
@@ -453,7 +453,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save()
# 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
# There are two changes
@@ -475,7 +475,7 @@ class RIGSVersionTestCase(TestCase):
self.person.save()
# 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
# Should be declared as long
@@ -488,7 +488,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save()
# 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
self.assertEqual(currentVersion.changes.field_changes[0].diff,
@@ -504,12 +504,12 @@ class RIGSVersionTestCase(TestCase):
self.event.status = models.Event.CONFIRMED
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].new, 'Confirmed')
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
# Mainly to check for exceptions:
@@ -522,7 +522,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save()
# 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
@@ -541,7 +541,7 @@ class RIGSVersionTestCase(TestCase):
item1.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
@@ -563,7 +563,7 @@ class RIGSVersionTestCase(TestCase):
self.event.save()
# 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

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.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 = {
@@ -423,3 +423,107 @@ class TestSampleDataGenerator(TestCase):
from django.core.management.base import CommandError
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,7 +1,9 @@
from django.urls import path
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.views import LoginView
from RIGS import models, views, rigboard, finance, ical, versioning, forms
from django.views.generic import RedirectView
from django.views.decorators.clickjacking import xframe_options_exempt
@@ -16,10 +18,10 @@ urlpatterns = [
url('^$', login_required(views.Index.as_view()), name='index'),
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
url('^user/login/$', views.login, name='login'),
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'),
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
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),

View File

@@ -25,7 +25,7 @@ class FieldComparison(object):
self._new = new
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]
if self.field.name == "risk_assessment_edit_url":
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()
try:
previousVersion = versions.filter(revision_id__lt=self.revision_id).latest(
field_name='revision__date_created')
previousVersion = versions.filter(revision_id__lt=self.revision_id).latest('revision__date_created')
except ObjectDoesNotExist:
return False

View File

@@ -3,6 +3,7 @@ from django.http.response import HttpResponseRedirect
from django.http import HttpResponse
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
from django.contrib.auth.views import LoginView
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.core import serializers
@@ -34,28 +35,19 @@ class Index(generic.TemplateView):
return context
def login(request, **kwargs):
if request.user.is_authenticated:
next = request.GET.get('next', '/')
return HttpResponseRedirect(next)
else:
from django.contrib.auth.views import login
return login(request, authentication_form=forms.CheckApprovedForm)
class SearchHelp(generic.TemplateView):
template_name = 'RIGS/search_help.html'
# This view should be exempt from requiring CSRF token.
# Then we can check for it and show a nice error
# Don't worry, django.contrib.auth.views.login will
# check for it before logging the user in
@csrf_exempt
def login_embed(request, **kwargs):
if request.user.is_authenticated:
next = request.GET.get('next', '/')
return HttpResponseRedirect(next)
else:
from django.contrib.auth.views import login
class LoginEmbed(LoginView):
template_name = 'registration/login_embed.html'
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
if request.method == "POST":
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.')
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):
q = self.request.GET.get('q', "")
if len(q) >= 3:
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(email__icontains=q))
else:
object_list = self.model.objects.all()
orderBy = self.request.GET.get('orderBy', None)
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)
# try and parse an int
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:
object_list = object_list.order_by(orderBy)
return object_list
@@ -140,11 +141,20 @@ class OrganisationList(generic.ListView):
def get_queryset(self):
q = self.request.GET.get('q', "")
if len(q) >= 3:
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q))
else:
object_list = self.model.objects.all()
orderBy = self.request.GET.get('orderBy', "")
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)
# try and parse an int
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 "":
object_list = object_list.order_by(orderBy)
return object_list
@@ -194,11 +204,20 @@ class VenueList(generic.ListView):
def get_queryset(self):
q = self.request.GET.get('q', "")
if len(q) >= 3:
object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q))
else:
object_list = self.model.objects.all()
orderBy = self.request.GET.get('orderBy', "")
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)
# try and parse an int
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 "":
object_list = object_list.order_by(orderBy)
return object_list
@@ -392,7 +411,3 @@ class ResetApiKey(generic.RedirectView):
self.request.user.save()
return reverse_lazy('profile_detail')
class PasswordResetDisabled(generic.TemplateView):
template_name = "RIGS/password_reset_disable.html"

View File

@@ -23,10 +23,15 @@ class SupplierAdmin(admin.ModelAdmin):
@admin.register(assets.Asset)
class AssetAdmin(admin.ModelAdmin):
list_display = ['id', 'asset_id', 'description', 'category', 'status']
list_filter = ['is_cable', 'category']
list_filter = ['is_cable', 'category', 'status']
search_fields = ['id', 'asset_id', 'description']
@admin.register(assets.CableType)
class CableTypeAdmin(admin.ModelAdmin):
list_display = ['id', '__str__', 'plug', 'socket', 'cores', 'circuits']
@admin.register(assets.Connector)
class ConnectorAdmin(admin.ModelAdmin):
list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins']

View File

@@ -1,6 +1,7 @@
from django import forms
from assets import models
from django.db.models import Q
class AssetForm(forms.ModelForm):
@@ -34,3 +35,17 @@ class SupplierForm(forms.ModelForm):
class SupplierSearchForm(forms.Form):
query = forms.CharField(required=False)
class CableTypeForm(forms.ModelForm):
class Meta:
model = models.CableType
fields = '__all__'
def clean(self):
form_data = self.cleaned_data
queryset = models.CableType.objects.filter(Q(plug=form_data['plug']) & Q(socket=form_data['socket']) & Q(circuits=form_data['circuits']) & Q(cores=form_data['cores']))
# Being identical to itself shouldn't count...
if queryset.exists() and self.instance.pk != queryset[0].pk:
raise forms.ValidationError("A cable type that exactly matches this one already exists, please use that instead.", code="notunique")
return form_data

View File

@@ -78,6 +78,9 @@ class Command(BaseCommand):
suppliers = models.Supplier.objects.all()
connectors = models.Connector.objects.all()
for i in range(len(connectors)):
models.CableType.objects.create(plug=random.choice(connectors), socket=random.choice(connectors), circuits=random.choice(circuits), cores=random.choice(cores))
for i in range(100):
asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()),
@@ -87,12 +90,9 @@ class Command(BaseCommand):
date_acquired=timezone.now().date(),
is_cable=True,
plug=random.choice(connectors),
socket=random.choice(connectors),
cable_type=random.choice(models.CableType.objects.all()),
csa=random.choice(csas),
length=random.choice(lengths),
circuits=random.choice(circuits),
cores=random.choice(circuits)
)
if i % 5 == 0:

View File

@@ -57,7 +57,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='asset',
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(
model_name='asset',
@@ -85,7 +85,7 @@ class Migration(migrations.Migration):
('circuits', 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')),
('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={
'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

@@ -0,0 +1,29 @@
# Generated by Django 2.0.13 on 2020-02-18 16:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_auto_20200219_1444'),
]
operations = [
migrations.CreateModel(
name='CableType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('circuits', models.IntegerField(blank=True, null=True)),
('cores', models.IntegerField(blank=True, null=True)),
('plug', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector')),
('socket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector')),
],
),
migrations.AddField(
model_name='asset',
name='cable_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.CableType'),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 2.0.13 on 2020-02-18 16:27
from django.db import migrations
from django.db.models import Q
def move_cable_type_data(apps, schema_editor):
Asset = apps.get_model('assets', 'Asset')
CableType = apps.get_model('assets', 'CableType')
for asset in Asset.objects.filter(is_cable=True):
# Only create one type per...well...type
if(not CableType.objects.filter(Q(plug=asset.plug) & Q(socket=asset.socket))):
cabletype = CableType.objects.create(plug=asset.plug, socket=asset.socket, circuits=asset.circuits, cores=asset.cores)
asset.save()
cabletype.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0011_auto_20200218_1617'),
]
operations = [
migrations.RunPython(move_cable_type_data)
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 2.0.13 on 2020-02-18 16:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0012_auto_20200218_1627'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='circuits',
),
migrations.RemoveField(
model_name='asset',
name='cores',
),
migrations.RemoveField(
model_name='asset',
name='plug',
),
migrations.RemoveField(
model_name='asset',
name='socket',
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.0.13 on 2020-02-18 18:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0013_auto_20200218_1639'),
]
operations = [
migrations.AlterModelOptions(
name='cabletype',
options={'ordering': ['plug', 'socket', '-circuits']},
),
]

View File

@@ -41,13 +41,10 @@ class AssetStatus(models.Model):
@reversion.register
class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80)
class Meta:
ordering = ['name']
permissions = (
('view_supplier', 'Can view a supplier'),
)
name = models.CharField(max_length=80)
def get_absolute_url(self):
return reverse('supplier_list')
@@ -66,14 +63,28 @@ class Connector(models.Model):
return self.description
class CableType(models.Model):
class Meta:
ordering = ['plug', 'socket', '-circuits']
circuits = models.IntegerField(blank=True, null=True)
cores = models.IntegerField(blank=True, null=True)
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL,
related_name='plug', blank=True, null=True)
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL,
related_name='socket', blank=True, null=True)
def __str__(self):
return "%s%s" % (self.plug.description, self.socket.description)
@reversion.register
class Asset(models.Model, RevisionMixin):
class Meta:
ordering = ['asset_id_prefix', 'asset_id_number']
permissions = (
('asset_finance', 'Can see financial data for assets'),
('view_asset', 'Can view an asset')
)
permissions = [
('asset_finance', 'Can see financial data for assets')
]
parent = models.ForeignKey(to='self', related_name='asset_parent',
blank=True, null=True, on_delete=models.SET_NULL)
@@ -92,16 +103,11 @@ class Asset(models.Model, RevisionMixin):
# Cable assets
is_cable = models.BooleanField(default=False)
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL,
related_name='plug', blank=True, null=True)
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL,
related_name='socket', blank=True, null=True)
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
length = models.DecimalField(decimal_places=1, max_digits=10,
blank=True, null=True, help_text='m')
csa = models.DecimalField(decimal_places=2, max_digits=10,
blank=True, null=True, help_text='mm^2')
circuits = models.IntegerField(blank=True, null=True)
cores = models.IntegerField(blank=True, null=True)
# Hidden asset_id components
# For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001"
@@ -131,7 +137,7 @@ class Asset(models.Model, RevisionMixin):
def __str__(self):
out = str(self.asset_id) + ' - ' + self.description
if self.is_cable:
out += '{} - {}m - {}'.format(self.plug, self.length, self.socket)
out += '{} - {}m - {}'.format(self.cable_type.plug, self.length, self.cable_type.socket)
return out
def clean(self):
@@ -156,14 +162,16 @@ class Asset(models.Model, RevisionMixin):
errdict["length"] = ["The length of a cable must be more than 0"]
if not self.csa or self.csa <= 0:
errdict["csa"] = ["The CSA of a cable must be more than 0"]
if not self.circuits or self.circuits <= 0:
errdict["circuits"] = ["There must be at least one circuit in a cable"]
if not self.cores or self.cores <= 0:
errdict["cores"] = ["There must be at least one core in a cable"]
if self.socket is None:
errdict["socket"] = ["A cable must have a socket"]
if self.plug is None:
errdict["plug"] = ["A cable must have a plug"]
if not self.cable_type:
errdict["cable_type"] = ["A cable must have a type"]
# if not self.circuits or self.circuits <= 0:
# errdict["circuits"] = ["There must be at least one circuit in a cable"]
# if not self.cores or self.cores <= 0:
# errdict["cores"] = ["There must be at least one core in a cable"]
# if self.socket is None:
# errdict["socket"] = ["A cable must have a socket"]
# if self.plug is None:
# errdict["plug"] = ["A cable must have a plug"]
if errdict != {}: # If there was an error when validation
raise ValidationError(errdict)

View File

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

View File

@@ -0,0 +1,61 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% block title %}Cable Type{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% if create %}Create{% elif edit %}Edit{% endif %} Cable Type
</h1>
</div>
{% if create %}
<form method="POST" action="{% url 'cable_type_create'%}">
{% elif edit %}
<form method="POST" action="{% url 'cable_type_update' object.id %}">
{% endif %}
{% include 'form_errors.html' %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
<div class="row">
<div class="col-sm-12">
{% if create or edit %}
<div class="form-group">
<label for="{{ form.plug.id_for_label }}">Plug</label>
{% render_field form.plug|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.socket.id_for_label }}">Socket</label>
{% render_field form.socket|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
</div>
<div class="form-group">
<label for="{{ form.cores.id_for_label }}">Cores</label>
{% render_field form.cores|add_class:'form-control' value=object.cores %}
</div>
<div class="pull-left">
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
<br>
<button type="reset" class="btn btn-link">Cancel</button>
</div>
{% else %}
<dl>
<dt>Socket</dt>
<dd>{{ object.socket|default_if_none:'-' }}</dd>
<dt>Plug</dt>
<dd>{{ object.plug|default_if_none:'-' }}</dd>
<dt>Circuits</dt>
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
<dt>Cores</dt>
<dd>{{ object.cores|default_if_none:'-' }}</dd>
</dl>
{% endif %}
</div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends 'base_assets.html' %}
{% block title %}Supplier List{% endblock %}
{% load paginator from filters %}
{% load widget_tweaks %}
{% block content %}
<div class="page-header">
<h1>Cable Type List</h1>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Cable Type</th>
<th>Circuits</th>
<th>Cores</th>
<th>Quick Links</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr>
<td>{{ item }}</td>
<td>{{ item.circuits }}</td>
<td>{{ item.cores }}</td>
<td>
<a href="{% url 'cable_type_detail' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-eye-open"></i> View</a>
<a href="{% url 'cable_type_update' item.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<div class="text-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}

View File

@@ -23,7 +23,6 @@
<label for="{{ form.category.id_for_label }}" >Category</label>
{% render_field form.category|add_class:'form-control'%}
</div>
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %} <label for="{{ form.is_cable.id_for_label }}">Cable?</label>
<div class="form-group">
<label for="{{ form.status.id_for_label }}" >Status</label>
{% render_field form.status|add_class:'form-control'%}
@@ -32,6 +31,10 @@
<label for="{{ form.serial_number.id_for_label }}">Serial Number</label>
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
</div>
<div class="form-group">
<label for="{{ form.is_cable.id_for_label }}">Cable?</label>
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %}
</div>
<!---TODO: Lower default number of lines in comments box-->
<div class="form-group">
<label for="{{ form.comments.id_for_label }}">Comments</label>

View File

@@ -6,12 +6,10 @@
<div class="panel-body">
{% if create or edit or duplicate %}
<div class="form-group">
<label for="{{ form.plug.id_for_label }}">Plug</label>
{% render_field form.plug|add_class:'form-control'%}
</div>
<div class="form-group">
<label for="{{ form.socket.id_for_label }}">Socket</label>
{% render_field form.socket|add_class:'form-control'%}
<label for="{{ form.cable_type.id_for_label }}">Cable Type</label>
<div class="input-group">
{% render_field form.cable_type|add_class:'form-control' %}
</div>
</div>
<div class="form-group">
<label for="{{ form.length.id_for_label }}">Length</label>
@@ -27,33 +25,16 @@
<span class="input-group-addon">{{ form.csa.help_text }}</span>
</div>
</div>
<div class="form-group">
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
</div>
<div class="form-group">
<label for="{{ form.cores.id_for_label }}">Cores</label>
{% render_field form.cores|add_class:'form-control' value=object.cores %}
</div>
{% else %}
<dl>
<dt>Socket</dt>
<dd>{{ object.socket|default_if_none:'-' }}</dd>
<dt>Plug</dt>
<dd>{{ object.plug|default_if_none:'-' }}</dd>
<dt>Cable Type</dt>
<dd>{{ object.cable_type|default_if_none:'-' }}</dd>
<dt>Length</dt>
<dd>{{ object.length|default_if_none:'-' }}m</dd>
<dt>Cross Sectional Area</dt>
<dd>{{ object.csa|default_if_none:'-' }}m^2</dd>
<dt>Circuits</dt>
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
<dt>Cores</dt>
<dd>{{ object.cores|default_if_none:'-' }}</dd>
<dd>{{ object.csa|default_if_none:'-' }}m</dd>
</dl>
{% endif %}
</div>

View File

@@ -82,12 +82,9 @@ class AssetForm(FormPage):
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
'plug': (regions.SingleSelectPicker, (By.ID, 'id_plug')),
'socket': (regions.SingleSelectPicker, (By.ID, 'id_socket')),
'cable_type': (regions.SingleSelectPicker, (By.ID, 'id_cable_type')),
'length': (regions.TextBox, (By.ID, 'id_length')),
'csa': (regions.TextBox, (By.ID, 'id_csa')),
'circuits': (regions.TextBox, (By.ID, 'id_circuits')),
'cores': (regions.TextBox, (By.ID, 'id_cores'))
}
@property

View File

@@ -98,6 +98,7 @@ class TestAssetForm(AutoLoginTest):
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
self.parent = models.Asset.objects.create(asset_id="9000", description="Shelf", status=self.status, category=self.category, date_acquired=datetime.date(2000, 1, 1))
self.connector = models.Connector.objects.create(description="IEC", current_rating=10, voltage_rating=240, num_pins=3)
self.cable_type = models.CableType.objects.create(plug=self.connector, socket=self.connector, circuits=1, cores=3)
self.page = pages.AssetCreate(self.driver, self.live_server_url).open()
def test_asset_create(self):
@@ -154,12 +155,10 @@ class TestAssetForm(AutoLoginTest):
self.page.is_cable = True
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
self.page.plug = "IEC"
self.page.cable_type = "IEC → IEC"
self.page.socket = "IEC"
self.page.length = 10
self.page.csa = "1.5"
self.page.circuits = 1
self.page.cores = 3
self.page.submit()
self.assertTrue(self.page.success)
@@ -249,7 +248,7 @@ class TestSupplierCreateAndEdit(AutoLoginTest):
def test_supplier_edit(self):
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"
self.page.name = new_name
self.page.submit()
@@ -375,7 +374,8 @@ class TestFormValidation(TestCase):
cls.status = models.AssetStatus.objects.create(name="Broken", should_show=True)
cls.asset = models.Asset.objects.create(asset_id="9999", description="The Office", status=cls.status, category=cls.category, date_acquired=datetime.date(2018, 6, 15))
cls.connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, plug=cls.connector, socket=cls.connector, length=10, csa="1.5", circuits=1, cores=3)
cls.cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=cls.connector, socket=cls.connector)
cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cls.cable_type, length=10, csa="1.5")
def setUp(self):
self.profile.set_password('testuser')
@@ -399,12 +399,9 @@ class TestFormValidation(TestCase):
response = self.client.post(url, {'asset_id': 'X$%A', 'is_cable': True})
self.assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
self.assertFormError(response, 'form', 'plug', 'A cable must have a plug')
self.assertFormError(response, 'form', 'socket', 'A cable must have a socket')
self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway!
def test_asset_edit(self):
@@ -422,24 +419,19 @@ class TestFormValidation(TestCase):
def test_cable_edit(self):
url = reverse('asset_update', kwargs={'pk': self.cable_asset.asset_id})
# TODO Why do I have to send is_cable=True here?
response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3, 'circuits': -4, 'cores': -8})
response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
# Can't figure out how to select the 'none' option...
# self.assertFormError(response, 'form', 'plug', 'A cable must have a plug')
# self.assertFormError(response, 'form', 'socket', 'A cable must have a socket')
# TODO Can't figure out how to select the 'none' option...
# self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
def test_asset_duplicate(self):
url = reverse('asset_duplicate', kwargs={'pk': self.cable_asset.asset_id})
response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0, 'circuits': 0, 'cores': 0})
response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
self.assertFormError(response, 'form', 'circuits', 'There must be at least one circuit in a cable')
self.assertFormError(response, 'form', 'cores', 'There must be at least one core in a cable')
class TestSampleDataGenerator(TestCase):

View File

@@ -22,6 +22,11 @@ urlpatterns = [
path('activity', permission_required_with_403('assets.view_asset')
(views.ActivityTable.as_view()), name='asset_activity_table'),
path('cabletype/list/', permission_required_with_403('assets.view_cable_type')(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
path('cabletype/<int:pk>/detail/', permission_required_with_403('assets.view_cable_type')(views.CableTypeDetail.as_view()), name='cable_type_detail'),
path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'),
path('asset/id/<str:pk>/embed/',
xframe_options_exempt(

View File

@@ -252,3 +252,45 @@ class ActivityTable(versioning.ActivityTable):
versions = versioning.RIGSVersion.objects.get_for_multiple_models(
[models.Asset, models.Supplier])
return versions
class CableTypeList(generic.ListView):
model = models.CableType
template_name = 'cable_type_list.html'
paginate_by = 40
# ordering = ['__str__']
class CableTypeDetail(generic.DetailView):
model = models.CableType
template_name = 'cable_type_form.html'
class CableTypeCreate(generic.CreateView):
model = models.CableType
template_name = "cable_type_form.html"
form_class = forms.CableTypeForm
def get_context_data(self, **kwargs):
context = super(CableTypeCreate, self).get_context_data(**kwargs)
context["create"] = True
return context
def get_success_url(self):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
class CableTypeUpdate(generic.UpdateView):
model = models.CableType
template_name = "cable_type_form.html"
form_class = forms.CableTypeForm
def get_context_data(self, **kwargs):
context = super(CableTypeUpdate, self).get_context_data(**kwargs)
context["edit"] = True
return context
def get_success_url(self):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,29 +10,31 @@
{% endblock %}
{% block titleelements %}
{# % if perms.assets.view_asset % #}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'asset_list' %}"><span class="glyphicon glyphicon-list"></span> List Assets</a></li>
{% if perms.assets.add_asset %}
<li><a href="{% url 'asset_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Asset</a></li>
{% endif %}
</ul>
</li>
{# % endif % #}
{# % if perms.assets.view_supplier % #}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
List Suppliers</a></li>
{% if perms.assets.add_supplier %}
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
{% endif %}
</ul>
</li>
{# % endif % #}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'asset_list' %}"><span class="glyphicon glyphicon-list"></span> List Assets</a></li>
{% if perms.assets.add_asset %}
<li><a href="{% url 'asset_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Asset</a></li>
{% endif %}
<li role="separator" class="divider"></li>
<li><a href="{% url 'cable_type_list' %}"><span class="glyphicon glyphicon-list"></span>
List Cable Types</a></li>
{% if perms.assets.add_cable_type %}
<li><a href="{% url 'cable_type_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Cable Type</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
List Suppliers</a></li>
{% if perms.assets.add_supplier %}
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
{% endif %}
</ul>
</li>
{% if perms.assets.view_asset %}
<li><a href="{% url 'asset_activity_table' %}">Recent Changes</a></li>
{% endif %}

View File

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

View File

@@ -1,17 +1,14 @@
{% load static from staticfiles %}
{% load static %}
{% 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">
@@ -32,7 +29,7 @@
<!--[if mso]>
</td></tr></table>
</center>
<![endif]-->
<![endif]-->
</td>
</tr>
<tr>
@@ -47,12 +44,9 @@
<!--[if mso]>
</td></tr></table>
</center>
<![endif]-->
<![endif]-->
</td>
</tr>
</table>
</body>
</html>

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
{% load widget_tweaks %}
{% include 'form_errors.html' %}
<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 %}
<div class="form-group">
<label for="id_username">{{ form.username.label }}</label>