Compare commits

..

44 Commits

Author SHA1 Message Date
b949770838 pep eiiiiiight 2020-04-14 21:01:02 +01:00
e78a474290 Modify audit exclusions to properly prevent data loss 2020-04-14 20:54:41 +01:00
c291a211b0 Fix for my fix... 2020-04-13 16:57:10 +01:00
cd54ad944b Fix migrations again 2020-04-13 16:50:16 +01:00
8e62a5bfeb Merge branch 'master' into assets_audit
# Conflicts:
#	assets/models.py
# Migrations
2020-04-13 16:49:59 +01:00
0e648329f7 Use aware time in audit 2020-04-13 16:47:54 +01:00
0fe7d55eab Fix for existing invalid cable types
Also hotfix against more in the future. Proper rework needed...This is why I should have waited for review...! Lesson learnt?
2020-04-13 16:33:57 +01:00
be4a7baf8e Remove obsolete 'next_scheduled_maint' from asset model
Should fix production data...
2020-04-13 16:14:06 +01:00
c33ddaee37 Fix README badges to point to right branch
While I'm here eh :P
2020-04-13 16:00:44 +01:00
c11cbaccfb Merge branch 'master' into assets_audit
# Conflicts:
#	assets/models.py
2020-04-13 15:59:08 +01:00
a0491891e9 Add 'CableTypes' (#406)
* Move relevant fields and create migration to autogen cable types

* CRUD and ordering

* FIX: Prevent creating duplicate cable types

* FIX: pep8/remove debug print

* FIX: Meta migrations... :>

* FIX: Update tests to match new UX

* Move cabletype menu links into 'Assets' dropdown

* Fix migration

* 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:54:43 +01:00
afeee3b552 Remake migrations 2020-04-13 01:07:07 +01:00
39b1ff7c2f Merge branch 'master' into assets_audit 2020-04-13 01:00:01 +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
2bfde0510c Merge branch 'master' into assets_audit 2020-03-03 19:36:25 +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
a05a5d5ccc Merge branch 'master' into assets_audit 2020-02-29 11:43:38 +00:00
4a4d4a5cf3 Add authorisation process for sign ups and allow access to EventDetail for basic users (#399)
* CHANGE: First pass at opening up RIGS #233

Whilst it makes it something of a misnomer, the intent is to make the 'view_event' perm a permission to view event details like client/price. I don't see the point in giving everyone 'view_event' and adding a new 'view_event_detail'...Open to arguments the other way.

* CHANGE: New user signups now require admin approval

Given that I intend to reveal much more data to new users this seems necessary...

* CHORE: Fix CI

* FIX: Legacy Profiles are now auto-approved correctly

* Add testing of approval mechanism

This fixes the other functional tests failing because the user cannot login without being approved.

* Superusers bypass approval check

This should fix the remainder of the tests

* Prevent unapproved users logging in through embeds

Test suite doing its job...!

* FIX: Require login on events and event embeds again

Little too far to the open side there Arona... Whooooooops!

* FIX: Use has_oembed decorator for events

* FIX: Re-prevent basic seeing reversion

This is to prevent financials/client data leaking when changed. Hopefully can show them a filtered version in future.

* FIX: Remove mitigation for #264

Someone quietly fixed it, it appears

* FEAT: Add admin email notif when an account is activated and awaiting approval

No async or time-since shenanigans yet!

* FIX: Whoops, undo accidental whitespace change

* FEAT: Add a fifteen min cooldown between emails to admins

Probably not the right way to go about it...but it does work!

TODO: How to handle cooldown-emailing shared mailbox addresses?

* FIX: Remove event modal history deadlink for basic users

Also removes some links on the RIGS homepage that will deadlink for them

* FIX: Wrong perms syntax for history pages

* CHORE: Squash migrations

* FIX: Use a setting for cooldown

* FIX: Minor code improvements
2020-02-29 11:34:50 +00:00
24284f9d55 FIX?: What about this way... 2020-02-27 09:55:27 +00:00
1f663c8919 FIX?: Up WebDriverWait timeout for modal tests 2020-02-27 09:30:38 +00:00
aa4977edb5 Potentially make modal tests more consistent 2020-02-25 16:44:56 +00:00
742e90fa13 Remember to test the tests Arona 2020-02-24 17:40:00 +00:00
94412da545 Improve asset audit testing 2020-02-24 17:36:04 +00:00
db7440e9da Some deduplication for testing code 2020-02-23 23:42:18 +00:00
da0c9ba87b Start tests for audit 2020-02-23 23:31:12 +00:00
e5a1830b00 FIX: Migrations 2020-02-18 16:04:03 +00:00
da48a75073 FIX: Hide asset detail buttons for basic users 2020-02-18 16:00:33 +00:00
b9434dc576 FIX: Stop quickbuttons being tab-selected
If someone's tabbing through, they won't be needing the buttons...
2020-02-17 21:27:26 +00:00
75660644eb FEAT: More handy buttons 2020-02-17 21:25:25 +00:00
6e15f12fbf FIX: Fix asset sample data command when run alone 2020-02-17 16:29:25 +00:00
68891dccd2 FEAT: Add buttons for some common defaults on audit form
TODO: Partialise those fragments and add them to the edit/create forms too.
2020-02-17 13:10:51 +00:00
759faf30f1 FIX: Gracefully handle 404s in audit search 2020-02-17 13:00:35 +00:00
0c12a3efdb Improve sample data generator
Does reversion properly and sets colours for asset statuses
2020-02-16 18:46:12 +00:00
20d9a71a9e FIX: Remove assets from to-be-audited table when audited
Previously required a page reload
2020-02-16 15:49:53 +00:00
54ec38f7e1 Various UX Improvements
Also rearranged asset detail/edit to be more space efficient
2020-02-16 15:37:09 +00:00
ed5339925e FIX: Revert partialising of asset search 2020-02-16 02:41:36 +00:00
0b2fc6d57c Added cable functionality to audit form
Also improved styling
2020-02-16 02:34:33 +00:00
1ec277978e Filter asset audit list by never-audited 2020-02-15 12:54:31 +00:00
e656b90a22 Improve audit search bar
Optimise for APM!
2020-02-15 12:54:12 +00:00
7c42ad853c WIP: Javascript shenanigans for asset audit search
It's not clean but it works..
2020-02-14 17:20:50 +00:00
e7fcaa36bb WIP: Audit modal works
Need to get the ID search working.
2020-02-14 11:47:05 +00:00
e9a9250027 WIP: Basic work on audit 2020-02-10 00:09:46 +00:00
ae151ed45e Add assets test suite (#400)
* Started POM and assets test

* FEAT: Adapt unit tests from RIGS to assets

* CHORE: pep8...

* Added Asset Create and Edit forms

* Add non-cable asset creation test

* CHORE: Frickin pep8...

* Add cable asset creation test

* Basic asset create validation testing

* Asset edit tests are here

A bit dodgy in places but par for the course for me :P

* Add access level tests

* Delete unused code

Much less effort way to increase coverage stats :D

* Add delete sample data test for completeness

Chasing that sweet 100% coverage...

* Add supplier list page + tests

Also fix the supplier page not being ordered alphabetically

* Helps if I add the migration...

* Add supplier create/edit tests

* Asset duplicate tests

Also fixed some random bugs

* Asset search tests

* 404 tests and test that everything requires authentication

* Test visibility of form errors

And fix supplier form not displaying errors correctly!

* Fix broken search test


Co-authored-by: Matthew Smith <mattysmith22@googlemail.com>
2020-02-08 13:52:07 +00:00
82 changed files with 1781 additions and 502 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

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

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

@@ -2,6 +2,7 @@ from pypom import Page, Region
from selenium.webdriver.common.by import By
from selenium.webdriver import Chrome
from selenium.common.exceptions import NoSuchElementException
from PyRIGS.tests import regions
class BasePage(Page):
@@ -34,37 +35,19 @@ class FormPage(BasePage):
self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
def submit(self):
previous_errors = self.errors
self.find_element(*self._submit_locator).click()
self.wait.until(lambda x: self.errors != previous_errors or self.success)
@property
def errors(self):
try:
error_page = self.ErrorPage(self, self.find_element(*self._errors_selector))
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
return error_page.errors
except NoSuchElementException:
return None
class ErrorPage(Region):
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
class ErrorItem(Region):
_field_selector = (By.CSS_SELECTOR, "dt")
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
@property
def field_name(self):
return self.find_element(*self._field_selector).text
@property
def errors(self):
return [x.text for x in self.find_elements(*self._error_selector)]
@property
def errors(self):
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
errors = {}
for error in error_items:
errors[error.field_name] = error.errors
return errors
class LoginPage(BasePage):
URL_TEMPLATE = '/user/login'

View File

@@ -131,3 +131,27 @@ class SingleSelectPicker(Region):
def set_value(self, value):
picker = Select(self.root)
picker.select_by_visible_text(value)
class ErrorPage(Region):
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
class ErrorItem(Region):
_field_selector = (By.CSS_SELECTOR, "dt")
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
@property
def field_name(self):
return self.find_element(*self._field_selector).text
@property
def errors(self):
return [x.text for x in self.find_elements(*self._error_selector)]
@property
def errors(self):
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
errors = {}
for error in error_items:
errors[error.field_name] = error.errors
return errors

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,6 +1,6 @@
# TEC PA & Lighting - PyRIGS #
[![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg?branch=develop)](https://travis-ci.org/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg)](https://travis-ci.org/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.

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
@@ -22,13 +22,22 @@ admin.site.register(models.Invoice)
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)
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 = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'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')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
@@ -41,6 +50,7 @@ class ProfileAdmin(UserAdmin):
)
form = forms.ProfileChangeForm
add_form = forms.ProfileCreationForm
actions = [approve_user]
class AssociateAdmin(VersionAdmin):

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

@@ -2,8 +2,10 @@ from django import forms
from django.utils import formats
from django.conf import settings
from django.core import serializers
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
from registration.forms import RegistrationFormUniqueEmail
from django.contrib.auth.forms import AuthenticationForm
from captcha.fields import ReCaptchaField
import simplejson
@@ -33,8 +35,16 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
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
class EmbeddedAuthenticationForm(AuthenticationForm):
class EmbeddedAuthenticationForm(CheckApprovedForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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.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,11 +21,12 @@ 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)
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
def make_api_key(cls):
@@ -53,14 +53,17 @@ class Profile(AbstractUser):
def latest_events(self):
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):
return self.name
class Meta:
permissions = (
('view_profile', 'Can view Profile'),
)
class RevisionMixin(object):
@property
@@ -91,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)
@@ -127,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)
@@ -171,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):
@@ -192,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)
@@ -213,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)
@@ -236,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):
@@ -287,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
@@ -481,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)
@@ -523,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})
@@ -533,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)
@@ -566,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

@@ -1,3 +1,4 @@
import datetime
import re
import urllib.request
import urllib.error
@@ -10,6 +11,9 @@ from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.template.loader import get_template
from django.urls import reverse
from django.utils import timezone
from registration.signals import user_activated
from premailer import Premailer
from z3c.rml import rml2pdf
@@ -69,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(),
@@ -102,3 +106,35 @@ def on_revision_commit(sender, instance, created, **kwargs):
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 %}
<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

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

View File

@@ -1,12 +1,11 @@
{% extends 'base_embed.html' %}
{% load static from staticfiles %}
{% load static %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<a href="/">
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
</a>
</div>
@@ -20,9 +19,9 @@
<span class="glyphicon glyphicon-exclamation-sign"></span>
{% endif %}
</span>
<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 %}
| {{ object.name }} </a>
{% if object.venue %}
@@ -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

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

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>
@@ -11,7 +19,7 @@
</div>
<div class="row">
<div class="col-sm-{% if perms.RIGS.view_event %}6{% else %}12{% endif %}">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="list-group-item-heading">Quick Links</h4>
@@ -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="//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="//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>
{% endif %}
</div>
</div>
<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>
@@ -73,7 +88,7 @@
</div>
{% if perms.RIGS.view_event %}
<div class="col-sm-6">
{% include 'RIGS/activity_feed.html' %}
{% include 'RIGS/activity_feed.html' %}
</div>
{% endif %}
</div>

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

@@ -6,17 +6,21 @@
<em class="description">{{item.description|linebreaksbr}}</em>
</div>
</td>
{% if perms.RIGS.view_event %}
<td>£&nbsp;<span class="cost">{{item.cost|floatformat:2}}</span></td>
{% endif %}
<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>
{% endif %}
{% if edit %}
<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"
data-pk="{{item.pk}}"
data-toggle="modal" data-target="#itemModal">
<span class="glyphicon glyphicon-edit"></span>
</button>
<button type="button" class="item-delete btn btn-xs btn-danger"
<button type="button" class="item-delete btn btn-xs btn-danger"
data-pk="{{item.pk}}">
<span class="glyphicon glyphicon-remove"></span>
</button>

View File

@@ -3,13 +3,17 @@
<thead>
<tr>
<td>Item</td>
{% if perms.RIGS.view_event %}
<td>Price</td>
{% endif %}
<td>Quantity</td>
{% if perms.RIGS.view_event %}
<td>Sub-total</td>
{% endif %}
{% 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>
@@ -22,6 +26,7 @@
{% include 'RIGS/item_row.html' %}
{% endfor %}
</tbody>
{% if perms.RIGS.view_event %}
<tfoot>
<tr>
<td rowspan="3" colspan="2"></td>
@@ -43,6 +48,7 @@
<td colspan="2">£ <span id="total">{{object.total|default:0|floatformat:2}}</span></td>
</tr>
</tfoot>
{% endif %}
</table>
</div>
<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.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
@@ -141,18 +142,41 @@ class UserRegistrationTest(LiveServerTestCase):
self.assertEqual(password.get_attribute('placeholder'), 'Password')
self.assertEqual(password.get_attribute('type'), 'password')
# Expected to fail as not approved
username.send_keys('TestUsername')
password.send_keys('correcthorsebatterystaple')
self.browser.execute_script(
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
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
udd = self.browser.find_element_by_class_name('navbar').text
self.assertIn('Hi John', udd)
# Check all the data actually got saved
profileObject = models.Profile.objects.all()[0]
self.assertEqual(profileObject.username, 'TestUsername')
self.assertEqual(profileObject.first_name, 'John')
self.assertEqual(profileObject.last_name, 'Smith')
@@ -231,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)
@@ -469,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
@@ -481,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")
@@ -551,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
@@ -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)
# 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'")
@@ -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)
# 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'")
@@ -613,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'")
@@ -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)
# 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'")
@@ -665,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
@@ -1199,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,12 +1,14 @@
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
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
urlpatterns = [
@@ -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()),
@@ -87,8 +89,7 @@ urlpatterns = [
permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()),
name='activity_feed'),
url(r'^event/(?P<pk>\d+)/$',
permission_required_with_403('RIGS.view_event', oembed_view="event_oembed")(
url(r'^event/(?P<pk>\d+)/$', has_oembed(oembed_view="event_oembed")(
rigboard.EventDetail.as_view()),
name='event_detail'),
url(r'^event/(?P<pk>\d+)/embed/$',

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)
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):
@@ -12,7 +13,7 @@ class AssetForm(forms.ModelForm):
class Meta:
model = models.Asset
fields = '__all__'
exclude = ['asset_id_prefix', 'asset_id_number']
exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -20,6 +21,13 @@ class AssetForm(forms.ModelForm):
self.fields['date_acquired'].widget.format = '%Y-%m-%d'
class AssetAuditForm(AssetForm):
class Meta(AssetForm.Meta):
# Prevents assets losing existing data that isn't included in the audit form
exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by',
'parent', 'purchased_from', 'purchase_price', 'comments']
class AssetSearchForm(forms.Form):
query = forms.CharField(required=False)
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
@@ -34,3 +42,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

@@ -1,7 +1,9 @@
import random
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from reversion import revisions as reversion
from assets import models
from RIGS import models as rigsmodels
class Command(BaseCommand):
@@ -15,6 +17,7 @@ class Command(BaseCommand):
random.seed('Some object to see the random number generator')
self.create_profile()
self.create_categories()
self.create_statuses()
self.create_suppliers()
@@ -22,6 +25,13 @@ class Command(BaseCommand):
self.create_connectors()
self.create_cables()
# Make sure that there's at least one profile if this command is run standalone
def create_profile(self):
name = "Fred Johnson"
models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
email=name.replace(" ", "") + "@example.com",
initials="".join([j[0].upper() for j in name.split()]))
def create_categories(self):
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
@@ -29,17 +39,19 @@ class Command(BaseCommand):
models.AssetCategory.objects.create(name=cat)
def create_statuses(self):
statuses = [('In Service', True), ('Lost', False), ('Binned', False), ('Sold', False), ('Broken', False)]
statuses = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')]
for stat in statuses:
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1])
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2])
def create_suppliers(self):
suppliers = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
for supplier in suppliers:
models.Supplier.objects.create(name=supplier)
with reversion.create_revision():
for supplier in suppliers:
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
models.Supplier.objects.create(name=supplier)
def create_assets(self):
asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink']
@@ -48,22 +60,24 @@ class Command(BaseCommand):
statuses = models.AssetStatus.objects.all()
suppliers = models.Supplier.objects.all()
for i in range(100):
asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()),
description=random.choice(asset_description),
category=random.choice(categories),
status=random.choice(statuses),
date_acquired=timezone.now().date()
)
with reversion.create_revision():
for i in range(100):
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()),
description=random.choice(asset_description),
category=random.choice(categories),
status=random.choice(statuses),
date_acquired=timezone.now().date()
)
if i % 4 == 0:
asset.parent = models.Asset.objects.order_by('?').first()
if i % 4 == 0:
asset.parent = models.Asset.objects.order_by('?').first()
if i % 3 == 0:
asset.purchased_from = random.choice(suppliers)
asset.clean()
asset.save()
if i % 3 == 0:
asset.purchased_from = random.choice(suppliers)
asset.clean()
asset.save()
def create_cables(self):
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
@@ -78,6 +92,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 +104,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

@@ -0,0 +1,17 @@
# Generated by Django 3.0.3 on 2020-04-13 15:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0014_auto_20200218_1840'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='next_sched_maint',
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 3.0.3 on 2020-04-13 15:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0015_remove_asset_next_sched_maint'),
]
operations = [
migrations.AlterField(
model_name='cabletype',
name='circuits',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='cabletype',
name='cores',
field=models.IntegerField(default=3),
),
migrations.AlterField(
model_name='cabletype',
name='plug',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plug', to='assets.Connector'),
),
migrations.AlterField(
model_name='cabletype',
name='socket',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='socket', to='assets.Connector'),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.0.3 on 2020-04-13 00:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0016_auto_20200413_1632'),
]
operations = [
migrations.AddField(
model_name='asset',
name='last_audited_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='asset',
name='last_audited_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audited_by', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='asset',
name='csa',
field=models.DecimalField(blank=True, decimal_places=2, help_text='mm²', max_digits=10, null=True),
),
]

View File

@@ -9,7 +9,7 @@ from django.dispatch.dispatcher import receiver
from reversion import revisions as reversion
from reversion.models import Version
from RIGS.models import RevisionMixin
from RIGS.models import RevisionMixin, Profile
class AssetCategory(models.Model):
@@ -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,32 @@ class Connector(models.Model):
return self.description
# Things are nullable that shouldn't be because I didn't properly fix the data structure when moving this to its own model...
class CableType(models.Model):
class Meta:
ordering = ['plug', 'socket', '-circuits']
circuits = models.IntegerField(default=1)
cores = models.IntegerField(default=3)
plug = models.ForeignKey(Connector, on_delete=models.CASCADE,
related_name='plug', null=True)
socket = models.ForeignKey(Connector, on_delete=models.CASCADE,
related_name='socket', null=True)
def __str__(self):
if self.plug and self.socket:
return "%s%s" % (self.plug.description, self.socket.description)
else:
return "Unknown"
@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)
@@ -88,20 +103,18 @@ class Asset(models.Model, RevisionMixin):
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
comments = models.TextField(blank=True)
next_sched_maint = models.DateField(blank=True, null=True)
# Audit
last_audited_at = models.DateTimeField(blank=True, null=True)
last_audited_by = models.ForeignKey(Profile, on_delete=models.SET_NULL, related_name='audited_by', blank=True, null=True)
# 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)
blank=True, null=True, help_text='mm²')
# Hidden asset_id components
# For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001"
@@ -131,7 +144,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 +169,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

@@ -0,0 +1,142 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% load widget_tweaks %}
{% block title %}Audit Asset {{ object.asset_id }}{% endblock %}
{% block content %}
<script>
function setAcquired(today) {
var date = new Date(1970, 0, 1);
if(today) {
date = new Date();
}
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
}
function setLength(length) {
$('#id_length').val(length);
}
function setCSA(CSA) {
$('#id_csa').val(CSA);
}
function checkIfCableHidden() {
if (document.getElementById("id_is_cable").checked) {
document.getElementById("cable-table").hidden = false;
} else {
document.getElementById("cable-table").hidden = true;
}
}
checkIfCableHidden();
</script>
<form class="form-horizontal" method="POST" id="asset_audit_form" action="{{ form.action|default:request.path }}">
{% include 'form_errors.html' %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
<div class="form-group">
<label for="{{ form.asset_id.id_for_label }}" class="col-sm-2 control-label">Asset ID</label>
<div class="col-sm-10">
{% render_field form.asset_id|add_class:'form-control' value=object.asset_idz %}
</div>
</div>
<div class="form-group">
<label for="{{ form.description.id_for_label }}" class="col-sm-2 control-label">Description</label>
<div class="col-sm-10">
{% render_field form.description|add_class:'form-control' value=object.description %}
</div>
</div>
<div class="form-group">
<label for="{{ form.category.id_for_label }}" class="col-sm-2 control-label">Category</label>
<div class="col-sm-10">
{% render_field form.category|add_class:'form-control'%}
</div>
</div>
<div class="form-group">
<label for="{{ form.status.id_for_label }}" class="col-sm-2 control-label">Status</label>
<div class="col-sm-10">
{% render_field form.status|add_class:'form-control'%}
</div>
</div>
<div class="form-group">
<label for="{{ form.serial_number.id_for_label }}" class="col-sm-2 control-label">Serial Number</label>
<div class="col-sm-10">
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
</div>
</div>
<div class="form-group">
<label for="{{ form.date_acquired.id_for_label }}" class="col-sm-2 control-label">Date Acquired</label>
<div class="col-sm-6">
{% render_field form.date_acquired|add_class:'form-control' value=object.date_acquired %}
</div>
<div class="col-sm-4">
<btn class="btn btn-default" onclick="setAcquired(true);" tabindex="-1">Today</btn>
<btn class="btn btn-default" onclick="setAcquired(false);" tabindex="-1">Unknown</btn>
</div>
</div>
<div class="form-group">
<label for="{{ form.date_sold.id_for_label }}" class="col-sm-2 control-label">Date Sold</label>
<div class="col-sm-6">
{% render_field form.date_sold|add_class:'form-control' value=object.date_sold %}
</div>
</div>
<div class="form-group">
<label for="{{ form.salvage_value.id_for_label }}" class="col-sm-2 control-label">Salvage Value</label>
<div class="col-sm-10">
<div class="input-group">
<span class="input-group-addon">£</span>
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
</div>
</div>
</div>
<hr>
<div class="form-group">
<label for="{{ form.is_cable.id_for_label }}" class="col-sm-2 control-label">Cable?</label>
<div class="col-sm-10">
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %}
</div>
</div>
<div id="cable-table">
<div class="form-group">
<label for="{{ form.cable_type.id_for_label }}" class="col-sm-2 control-label">Cable Type</label>
<div class="col-sm-10">
{% render_field form.cable_type|add_class:'form-control' %}
</div>
</div>
<div class="form-group">
<label for="{{ form.length.id_for_label }}" class="col-sm-2 control-label">Length</label>
<div class="col-sm-6">
<div class="input-group">
{% render_field form.length|add_class:'form-control' %}
<span class="input-group-addon">{{ form.length.help_text }}</span>
</div>
</div>
<div class="col-sm-4">
<btn class="btn btn-danger" onclick="setLength('5');" tabindex="-1">5{{ form.length.help_text }}</btn>
<btn class="btn btn-success" onclick="setLength('10');" tabindex="-1">10{{ form.length.help_text }}</btn>
<btn class="btn btn-info" onclick="setLength('20');" tabindex="-1">20{{ form.length.help_text }}</btn>
</div>
</div>
<div class="form-group">
<label for="{{ form.csa.id_for_label }}" class="col-sm-2 control-label">Cross Sectional Area</label>
<div class="col-sm-6">
<div class="input-group">
{% render_field form.csa|add_class:'form-control' value=object.csa %}
<span class="input-group-addon">{{ form.csa.help_text }}</span>
</div>
</div>
<div class="col-sm-4">
<btn class="btn btn-default" onclick="setCSA('1.5');" tabindex="-1">1.5{{ form.csa.help_text }}</btn>
<btn class="btn btn-default" onclick="setCSA('2.5');" tabindex="-1">2.5{{ form.csa.help_text }}</btn>
</div>
</div>
</div>
{% if not request.is_ajax %}
<div class="form-group pull-right">
<button class="btn btn-success" type="submit" form="asset_audit_form" id="id_mark_audited">Mark Audited</button>
</div>
{% endif %}
</form>
{% endblock %}
{% block footer %}
<div class="form-group">
<button class="btn btn-success pull-right" type="submit" form="asset_audit_form" onclick="onAuditClick({{form.asset_id.value}});" id="id_mark_audited">Mark Audited</button>
</div>
{% endblock %}

View File

@@ -0,0 +1,87 @@
{% extends 'base_assets.html' %}
{% block title %}Asset Audit List{% endblock %}
{% load static %}
{% load paginator from filters %}
{% load widget_tweaks %}
{% block js %}
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script src="{% static "js/interaction.js" %}"></script>
<script src="{% static "js/modal.js" %}"></script>
<script>
$('document').ready(function(){
$('#asset-search-form').submit(function () {
$('#searchButton').focus().click();
return false;
});
$('#searchButton').click(function (e) {
e.preventDefault();
var url = "{% url 'asset_audit' None %}";
var id = $("#{{form.query.id_for_label}}").val();
url = url.replace('None', id);
$.ajax({
url: url,
success: function(){
$link = $(this);
// Anti modal inception
if ($link.parents('#modal').length == 0) {
modaltarget = $link.data('target');
modalobject = "";
$('#modal').load(url, function (e) {
$('#modal').modal();
});
}
},
error:function(){
$("#error404").attr("hidden", false);
}
});
});
});
function onAuditClick(assetID) {
$('#' + assetID).remove();
}
</script>
{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="text-center">Asset Audit List</h1>
</div>
<div id="error404" class="alert alert-danger alert-dismissable" hidden=true>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<span>Asset with that ID does not exist!</span>
</div>
<h3>Audit Asset:</h3>
<form id="asset-search-form" class="form-horizontal" method="POST">
<div class="input-group input-group-lg" style="width: auto;">
{% render_field form.query|add_class:'form-control' placeholder='Enter Asset ID' autofocus="true" %}
<label for="query" class="sr-only">Asset ID:</label>
<span class="input-group-btn"><a id="searchButton" class="btn btn-default" class="submit" type="submit">Search</a></span>
</div>
</form>
<h3>Assets Requiring Audit:</h3>
<table class="table">
<thead>
<tr>
<th>Asset ID</th>
<th>Description</th>
<th>Category</th>
<th>Status</th>
<th class="hidden-xs"></th>
</tr>
</thead>
<tbody id="asset_table_body">
{% include 'partials/asset_list_table_body.html' with audit="true" %}
</tbody>
</table>
{% if is_paginated %}
<div class="text-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}

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

@@ -1,4 +1,4 @@
{% extends 'base_assets.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% load widget_tweaks %}
{% block title %}Asset {{ object.asset_id }}{% endblock %}
@@ -23,18 +23,23 @@
</div>
</div>
<div class="row">
{% if perms.assets.asset_finance %}
<div class="col-md-6">
{% include 'partials/purchasedetails_form.html' %}
</div>
{%endif%}
<div class="col-md-6"
<div class="col-md-4"
{% if not object.is_cable %} hidden="true" {% endif %} id="cable-table">
{% include 'partials/cable_form.html' %}
</div>
{% if perms.assets.asset_finance %}
<div class="col-md-4">
{% include 'partials/purchasedetails_form.html' %}
</div>
{%endif%}
<div class="col-md-4">
{% include 'partials/parent_form.html' %}
</div>
{% if not edit %}
<div class="col-md-4">
{% include 'partials/audit_details.html' %}
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-12">

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

@@ -1,25 +1,28 @@
{% if edit and object %}
<!--edit-->
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
{% elif duplicate %}
<!--duplicate-->
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
{% elif create %}
<!--create-->
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
{% else %}
<!--detail view-->
<div class="btn-group">
<a href="{% url 'asset_update' object.asset_id %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
<a class="btn btn-default" href="{% url 'asset_duplicate' object.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
</div>
{% endif %}
{% if create or edit or duplicate %}
<br>
<button type="reset" class="btn btn-link" onclick="
{%if duplicate%}
{% url 'asset_detail' previous_asset_id %}
{%else%}
history.back(){%endif%}">Cancel</button>
{% if perms.assets.change_asset %}
{% if edit and object %}
<!--edit-->
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
{% elif duplicate %}
<!--duplicate-->
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
{% elif create %}
<!--create-->
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
{% else %}
<!--detail view-->
<div class="btn-group">
<a href="{% url 'asset_update' object.asset_id %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
<a class="btn btn-default" href="{% url 'asset_duplicate' object.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><i class="glyphicon glyphicon-object-align-left"></i> Audit</a>
</div>
{% endif %}
{% if create or edit or duplicate %}
<br>
<button type="reset" class="btn btn-link" onclick="
{%if duplicate%}
{% url 'asset_detail' previous_asset_id %}
{%else%}
history.back(){%endif%}">Cancel</button>
{% endif %}
{% endif %}

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

@@ -1,19 +1,21 @@
{% for item in object_list %}
{# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for-->
<tr class="{{ item.status.display_class|default:'' }} assetRow">
<tr class="{{ item.status.display_class|default:'' }} assetRow" id="{{item.asset_id}}">
<td style="vertical-align: middle;"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
<td class="assetDesc" style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
<td class="assetCategory" style="vertical-align: middle;">{{ item.category }}</td>
<td class="assetStatus" style="vertical-align: middle;">{{ item.status }}</td>
<td class="hidden-xs">
<div class="btn-group" role="group">
{% if audit %}
<a type="button" class="btn btn-info modal-href" href="{% url 'asset_audit' item.asset_id %}"><i class="glyphicon glyphicon-object-align-left"></i> Audit</a>
{% else %}
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_detail' item.asset_id %}"><i class="glyphicon glyphicon-eye-open"></i> View</a>
{% if perms.assets.change_asset %}
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_update' item.asset_id %}"><i class="glyphicon glyphicon-edit"></i> Edit</a>
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_duplicate' item.asset_id %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
{% endif %}
</div>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -0,0 +1,8 @@
<div class="panel {% if object.last_audited_at is not None %} panel-success {% else %} panel-warning {% endif %}">
<div class="panel-heading">
Audit Details
</div>
<div class="panel-body">
<p>Audited at <span class="label label-default">{{ object.last_audited_at|default_if_none:'-' }}</span> by <span class="label label-info">{{ object.last_audited_by|default_if_none:'-' }}</span></p>
</div>
</div>

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

@@ -6,7 +6,7 @@ from selenium.webdriver import Chrome
from django.urls import reverse
from PyRIGS.tests import regions
from PyRIGS.tests.pages import BasePage, FormPage
import pdb
from selenium.common.exceptions import NoSuchElementException
class AssetList(BasePage):
@@ -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
@@ -98,11 +95,6 @@ class AssetForm(FormPage):
def parent_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._parent_select_locator))
def submit(self):
previous_errors = self.errors
self.find_element(*self._submit_locator).click()
self.wait.until(lambda x: self.errors != previous_errors or self.success)
class AssetEdit(AssetForm):
URL_TEMPLATE = '/assets/asset/id/{asset_id}/edit/'
@@ -165,11 +157,6 @@ class SupplierForm(FormPage):
'name': (regions.TextBox, (By.ID, 'id_name')),
}
def submit(self):
previous_errors = self.errors
self.find_element(*self._submit_locator).click()
self.wait.until(lambda x: self.errors != previous_errors or self.success)
class SupplierCreate(SupplierForm):
URL_TEMPLATE = reverse('supplier_create')
@@ -186,3 +173,90 @@ class SupplierEdit(SupplierForm):
@property
def success(self):
return '/edit' not in self.driver.current_url
class AssetAuditList(AssetList):
URL_TEMPLATE = reverse('asset_audit_list')
_search_text_locator = (By.ID, 'id_query')
_go_button_locator = (By.ID, 'searchButton')
_modal_locator = (By.ID, 'modal')
_errors_selector = (By.CLASS_NAME, "alert-danger")
@property
def modal(self):
return self.AssetAuditModal(self, self.find_element(*self._modal_locator))
@property
def query(self):
return self.find_element(*self._search_text_locator).text
def set_query(self, queryString):
element = self.find_element(*self._search_text_locator)
element.clear()
element.send_keys(queryString)
def search(self):
self.find_element(*self._go_button_locator).click()
@property
def error(self):
try:
return self.find_element(*self._errors_selector)
except NoSuchElementException:
return None
class AssetAuditModal(Region):
_errors_selector = (By.CLASS_NAME, "alert-danger")
# Don't use the usual success selector - that tries and fails to hit the '10m long cable' helper button...
_submit_locator = (By.ID, "id_mark_audited")
form_items = {
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
'description': (regions.TextBox, (By.ID, 'id_description')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
'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')),
'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
def errors(self):
try:
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
return error_page.errors
except NoSuchElementException:
return None
def submit(self):
previous_errors = self.errors
self.root.find_element(*self._submit_locator).click()
# self.wait.until(lambda x: not self.is_displayed) TODO
def remove_all_required(self):
self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
def __getattr__(self, name):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
return form_element.value
else:
return super().__getattribute__(name)
def __setattr__(self, name, value):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
form_element.set_value(value)
else:
self.__dict__[name] = value

View File

@@ -9,8 +9,13 @@ from RIGS import models as rigsmodels
from PyRIGS.tests.base import BaseTest, AutoLoginTest
from assets import models, urls
from reversion import revisions as reversion
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from RIGS.test_functional import animation_is_finished
import datetime
from django.utils import timezone
class TestAssetList(AutoLoginTest):
@@ -98,6 +103,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 +160,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,13 +253,83 @@ 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()
self.assertTrue(self.page.success)
class TestAssetAudit(AutoLoginTest):
def setUp(self):
super().setUp()
self.category = models.AssetCategory.objects.create(name="Haulage")
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
self.supplier = models.Supplier.objects.create(name="The Bazaar")
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1, voltage_rating=40, num_pins=13)
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
self.wait = WebDriverWait(self.driver, 5)
def test_audit_process(self):
asset_id = "1111"
self.page.set_query(asset_id)
self.page.search()
mdl = self.page.modal
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
# Do it wrong on purpose to check error display
mdl.remove_all_required()
mdl.description = ""
mdl.submit()
# self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
self.wait.until(animation_is_finished())
# self.assertTrue(self.driver.find_element_by_id('modal').is_displayed())
self.assertIn("This field is required.", mdl.errors["Description"])
# Now do it properly
new_desc = "A BIG hammer"
mdl.description = new_desc
mdl.submit()
self.wait.until(animation_is_finished())
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
# Check data is correct
audited = models.Asset.objects.get(asset_id="1111")
self.assertEqual(audited.description, new_desc)
# Make sure audit 'log' was filled out
self.assertEqual(self.profile.initials, audited.last_audited_by.initials)
self.assertEqual(timezone.now().date(), audited.last_audited_at.date())
self.assertEqual(timezone.now().hour, audited.last_audited_at.hour)
self.assertEqual(timezone.now().minute, audited.last_audited_at.minute)
# Check we've removed it from the 'needing audit' list
self.assertNotIn(asset_id, self.page.assets)
def test_audit_list(self):
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
assetRow = self.page.assets[0]
assetRow.find_element(By.CSS_SELECTOR, "td:nth-child(5) > div:nth-child(1) > a:nth-child(1)").click()
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
self.assertEqual(self.page.modal.asset_id, assetRow.id)
# First close button is for the not found error
self.page.find_element(By.XPATH, '(//button[@class="close"])[2]').click()
self.wait.until(animation_is_finished())
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
# Make sure audit log was NOT filled out
audited = models.Asset.objects.get(asset_id=assetRow.id)
self.assertEqual(None, audited.last_audited_by)
# Check that a failed search works
self.page.set_query("NOTFOUND")
self.page.search()
self.wait.until(animation_is_finished())
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
self.assertIn("Asset with that ID does not exist!", self.page.error.text)
class TestSupplierValidation(TestCase):
@classmethod
def setUpTestData(cls):
@@ -375,7 +449,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 +474,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 +494,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(
@@ -31,6 +36,9 @@ urlpatterns = [
views.AssetOembed.as_view(),
name='asset_oembed'),
path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'),
path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'),
path('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
path('supplier/create', permission_required_with_403('assets.add_supplier')

View File

@@ -4,13 +4,17 @@ from django.http import HttpResponse, Http404
from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.urls import reverse
from django.urls import reverse_lazy, reverse
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.core import serializers
from django.contrib import messages
from assets import models, forms
from RIGS import versioning
import simplejson
import datetime
from django.utils import timezone
@method_decorator(csrf_exempt, name='dispatch')
@@ -109,7 +113,14 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
return context
def get_success_url(self):
return reverse("asset_detail", kwargs={"pk": self.object.asset_id})
if self.request.is_ajax():
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
else:
url = reverse_lazy('asset_detail', kwargs={'pk': self.object.asset_id, })
return url
class AssetCreate(LoginRequiredMixin, generic.CreateView):
@@ -173,6 +184,30 @@ class AssetEmbed(AssetDetail):
template_name = 'asset_embed.html'
@method_decorator(csrf_exempt, name='dispatch')
class AssetAuditList(AssetList):
template_name = 'asset_audit_list.html'
hide_hidden_status = False
# TODO Refresh this when the modal is submitted
def get_queryset(self):
self.form = forms.AssetSearchForm(data={})
return self.model.objects.filter(Q(last_audited_at__isnull=True))
class AssetAudit(AssetEdit):
template_name = 'asset_audit.html'
form_class = forms.AssetAuditForm
def get_success_url(self):
# TODO For some reason this doesn't stick when done in form_valid??
asset = self.get_object()
asset.last_audited_by = self.request.user
asset.last_audited_at = timezone.now()
asset.save()
return super().get_success_url()
class SupplierList(generic.ListView):
model = models.Supplier
template_name = 'supplier_list.html'
@@ -252,3 +287,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,30 +10,33 @@
{% 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>
<li><a href="{% url 'asset_audit_list' %}">Audit</a></li>
{% endif %}
{% endblock %}

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

@@ -5,6 +5,6 @@
{% block content %}
<div class="alert alert-success">
<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>
{% endblock %}
{% endblock %}

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>