Compare commits

..

42 Commits

Author SHA1 Message Date
Matthew Smith
5696cf73ce Refactored away "assets" property on "Supplier" by using "related_name" instead 2020-01-03 21:34:53 +00:00
Matthew Smith
096ca24336 Fixed supplier not working on all the create asset template 2020-01-03 21:09:45 +00:00
c779c24d63 FIX: Squash migrations 2020-01-02 20:42:33 +00:00
dad9797f74 FEAT: Statuses can have a CSS class defined in the admin panel
This replaces the hardcoding of colours in the asset list.
2020-01-02 19:43:44 +00:00
69facd5dc6 FIX: Order asset categories/statuses alphabetically
Instead of by pk because that's silly.
2020-01-02 19:43:44 +00:00
406a9dad36 FEAT #386: Asset search searches serial number.
Pending addition of advanced search.
2020-01-02 18:34:38 +00:00
81c32baa8e Potential fix for #380
No idea if it works because I can't reproduce locally. S/O Reckons it should... :P
2019-12-31 20:01:16 +00:00
eef0682e4f FIX: Tweak supplier list to make detail view accessible 2019-12-31 19:20:57 +00:00
2465390889 FEAT: Add associated assets to supplier detail view 2019-12-31 19:20:57 +00:00
fc9ce9f01c FIX #388: Prevent assets losing supplier data on edit 2019-12-31 18:50:36 +00:00
David Taylor
97c0dffbd3 Order revisions by date created (#389) 2019-12-31 16:42:56 +00:00
David Taylor
3b28eafc82 Order RIGSVersions by date 2019-12-31 16:33:43 +00:00
ca8253894a FIX #321: Authorisation time shown as 'None' in emails (#378)
* FIX #321: Authorisation Success emails dated 'None'

* FIX: Additionally fix datestamp on HTML client emails (#321)
2019-12-31 12:45:38 +00:00
01a87e0e0b FEAT: Add revision history to assets and suppliers (#387)
* FEAT: Initial work on revision history for assets

The revision history for individual items mostly works, though it shows database ID where it should show asset ID. Recent changes feed isn't yet done.

* FEAT: Initial implementation of asset activity stream

* CHORE: Fix pep8

* FIX: Asset history table 'branding'

* FIX: Individual asset version history is now correctly filtered

* FEAT: Make revision history for suppliers accessible

* CHORE: *sings* And a pep8 in a broken tree...

* Refactored out duplicated code from `AssetVersionHistory

* CHORE: pep8

And another random bit of wierd whitespace I found

Co-authored-by: Matthew Smith <mattysmith22@googlemail.com>

Closes #358
2019-12-31 12:25:42 +00:00
Matthew Smith
7c876348d7 Asset fixes (#383) 2019-12-10 22:50:28 +00:00
ddc23ce4e5 FIX: Prefix field still too limited for legacy data
Fingers crossed this works I don't have the actual data locally... I know I'm making a mess but needs must.

I genuinely hate whoever decided prefixes were a good idea now.
2019-12-06 00:58:39 +00:00
602ccc15ea FIX: Fix missing import
Presumably caused by Matt's IDE being overzealous again. I know I shouldn't be pushing to master but...one line fix...
2019-12-06 00:40:56 +00:00
Matthew Smith
b77615b9b9 Fix handling of prefixed Asset IDs and sorting of the asset list (#382)
* FIX: Remove misleading admin site title

* Moved across assets_id sorting to use proper numeric values. Also mofifies SQL command to find free asset IDs so that it works on postgres.

* Changed generateSampleAssetsData.py to include prefices on some cables.

* Fixed pep8

* Fixed missed migration

* Ensured hidden asset fields are completed on every database write

* CMULTI is a thing, and therefore a max prefix length of 5 cannot be a thing
2019-12-06 00:28:54 +00:00
David Taylor
228d72b7b2 Automatically run migrations on deploy
Because running them via Heroku CLI is easy to forget
2019-12-05 17:26:02 +00:00
62541194ee CHORE: Fix pep8
mutter mutter mutter, grumble
2019-12-05 13:10:08 +00:00
0d8fd99d92 FIX: Permission errors
This fixes keyholders being unable to see financials information or create assets. (Well, the latter needs anyone to be able to create assets before it is fully fixed)
2019-12-05 13:00:47 +00:00
9d51a82f31 FIX: Fix asset sample data generation 2019-12-05 12:56:22 +00:00
c059227d5d Revert "CHANGE: Restrict viewing asset DB to keyholders."
This reverts commit 2c334196d5.
2019-12-05 12:42:05 +00:00
2c334196d5 CHANGE: Restrict viewing asset DB to keyholders.
This is in line with what it was when it was on the Shared Drive.
2019-12-04 23:59:39 +00:00
4f036af85a Create the Asset Database (#363) 2019-12-04 23:14:27 +00:00
5210afc772 Combine client authorisation information in rig detail (#373)
* Combine client authorisation information in rig detail

* Fix stuff for CI

pep8 compliance
migration
2019-11-26 17:26:32 +00:00
David Taylor
4da8040351 Only display embedded scrollbars when required 2019-10-30 13:16:14 +00:00
David Taylor
1a49bb50e5 Further version history improvements 2019-07-28 23:40:35 +01:00
David Taylor
86b349f60e Tidy up version history for risk assessments 2019-07-28 23:32:54 +01:00
David Taylor
35997aa882 Add API hook for logging risk assessment completion (#341) 2019-07-28 23:08:18 +01:00
David Taylor
faa4573f6d Add dash to date range 2019-07-14 23:15:13 +01:00
David Taylor
7babaee44c Add link to pre-filled risk assessment form 2019-07-14 23:09:44 +01:00
David Taylor
c0fe176495 Add docker dev instructions 2019-07-14 23:04:12 +01:00
David Taylor
b269069b6a Improve embedding style 2019-06-20 00:56:11 +01:00
David Taylor
7e06b5a162 Increase height of forum embeds 2019-06-20 00:29:10 +01:00
David Taylor
a18bb07d78 Update views.py 2019-06-20 00:15:16 +01:00
David Taylor
bd28d2054e Remove autofocus from form 2019-06-20 00:13:24 +01:00
David Taylor
14836f135c Update subhire form location
Requested by Jonny
2019-02-25 21:29:14 +00:00
David Taylor
5f8a77586a Revert "Add warning"
This reverts commit e5b7fdbae1.
2018-10-07 00:30:00 +01:00
David Taylor
e5b7fdbae1 Add warning 2018-09-13 14:01:44 +01:00
imgbot[bot]
f1c8dca8c4 [ImgBot] optimizes images (#339)
*Total -- 680.70kb -> 575.32kb (15.48%)

/RIGS/static/imgs/pyrigs-avatar.png -- 4.79kb -> 2.16kb (54.98%)
/RIGS/static/imgs/404.jpg -- 131.98kb -> 108.89kb (17.49%)
/RIGS/static/imgs/403.jpg -- 155.95kb -> 129.73kb (16.81%)
/RIGS/static/imgs/500.jpg -- 118.90kb -> 100.13kb (15.79%)
/RIGS/static/imgs/401.jpg -- 151.27kb -> 129.03kb (14.7%)
/RIGS/static/imgs/400.jpg -- 104.61kb -> 93.13kb (10.97%)
/RIGS/static/imgs/paperwork/tec-logo.jpg -- 13.21kb -> 12.26kb (7.19%)
2018-08-19 19:08:35 +01:00
David Taylor
843b76d8ea [requires.io] dependency update on master branch (#336)
* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update
2018-05-25 16:22:07 +01:00
133 changed files with 3275 additions and 523 deletions

2
.gitignore vendored
View File

@@ -44,6 +44,7 @@ htmlcov/
.tox/
.coverage
.cache
.pytest_cache
nosetests.xml
coverage.xml
@@ -107,3 +108,4 @@ atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
.vscode/

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.6
WORKDIR /app
ADD . /app
RUN pip install -r requirements.txt && \
python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

View File

@@ -1 +1,2 @@
release: python manage.py migrate
web: gunicorn PyRIGS.wsgi --log-file -

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import raven
import secrets
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@@ -57,6 +58,7 @@ INSTALLED_APPS = (
'django.contrib.messages',
'django.contrib.staticfiles',
'RIGS',
'assets',
'debug_toolbar',
'registration',
@@ -233,3 +235,7 @@ USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
RISK_ASSESSMENT_URL = os.environ.get('RISK_ASSESSMENT_URL') if os.environ.get(
'RISK_ASSESSMENT_URL') else "http://example.com"
RISK_ASSESSMENT_SECRET = os.environ.get('RISK_ASSESSMENT_SECRET') if os.environ.get(
'RISK_ASSESSMENT_SECRET') else secrets.token_hex(15)

View File

@@ -12,6 +12,7 @@ urlpatterns = [
# url(r'^blog/', include('blog.urls')),
url(r'^', include('RIGS.urls')),
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')),

View File

@@ -77,6 +77,13 @@ python manage.py runserver
```
Please refer to Django documentation for a full list of options available here.
### Development using docker
```
docker build . -t pyrigs
docker run -it --rm -p=8000:8000 -v $(pwd):/app pyrigs
```
### Sample Data ###
Sample data is available to aid local development and user acceptance testing. To load this data into your local database, first ensure the database is empty:
```

View File

@@ -1,24 +1,25 @@
from django.contrib import admin
from django.contrib import messages
from django.contrib.admin import helpers
from RIGS import models, forms
from django.contrib.auth.admin import UserAdmin
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Count
from django.forms import ModelForm
from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
from reversion import revisions as reversion
from reversion.admin import VersionAdmin
from RIGS import models, forms
from django.contrib.admin import helpers
from django.template.response import TemplateResponse
from django.contrib import messages
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from django.forms import ModelForm
from reversion import revisions as reversion
# Register your models here.
admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice, VersionAdmin)
admin.site.register(models.Payment, VersionAdmin)
admin.site.register(models.Invoice)
admin.site.register(models.Payment)
@admin.register(models.Profile)

View File

@@ -10,9 +10,8 @@ from django.template import RequestContext
from django.template.loader import get_template
from django.views import generic
from django.db.models import Q
from django.db import transaction
from z3c.rml import rml2pdf
import reversion
from RIGS import models
from django import forms
@@ -102,14 +101,14 @@ class InvoiceDelete(generic.DeleteView):
def get(self, request, pk):
obj = self.get_object()
if obj.payments.all().count() > 0:
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).get(pk)
def post(self, request, pk):
obj = self.get_object()
if obj.payments.all().count() > 0:
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).post(pk)
@@ -160,10 +159,7 @@ class InvoiceWaiting(generic.ListView):
class InvoiceEvent(generic.View):
@transaction.atomic()
@reversion.create_revision()
def get(self, *args, **kwargs):
reversion.set_user(self.request.user)
epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
invoice, created = models.Invoice.objects.get_or_create(event=event)
@@ -188,13 +184,6 @@ class PaymentCreate(generic.CreateView):
initial.update({'invoice': invoice})
return initial
@transaction.atomic()
@reversion.create_revision()
def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['invoice'])
reversion.set_comment("Payment removed")
return super().form_valid(form, *args, **kwargs)
def get_success_url(self):
messages.info(self.request, "location.reload()")
return reverse_lazy('closemodal')
@@ -203,12 +192,5 @@ class PaymentCreate(generic.CreateView):
class PaymentDelete(generic.DeleteView):
model = models.Payment
@transaction.atomic()
@reversion.create_revision()
def delete(self, *args, **kwargs):
reversion.add_to_revision(self.get_object().invoice)
reversion.set_comment("Payment removed")
return super().delete(*args, **kwargs)
def get_success_url(self):
return self.request.POST.get('next')

View File

@@ -33,7 +33,13 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
return self.cleaned_data['initials']
# Login form
# Embedded Login form - remove the autofocus
class EmbeddedAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop('autofocus', None)
class PasswordReset(PasswordResetForm):
captcha = ReCaptchaField(label='Captcha')

View File

@@ -1,252 +1,11 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission
from django.db import transaction
from reversion import revisions as reversion
import datetime
import random
from RIGS import models
from django.core.management import call_command
class Command(BaseCommand):
help = 'Adds sample data to use for testing'
can_import_settings = True
people = []
organisations = []
venues = []
profiles = []
keyholder_group = None
finance_group = None
def handle(self, *args, **options):
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.setupGenericProfiles()
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime", "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter", "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter", "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley", "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley", "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newPerson = models.Person.objects.create(name=name)
if i % 3 == 0:
newPerson.email = "address@person.com"
if i % 5 == 0:
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newPerson.phone = "01234 567894"
newPerson.save()
self.people.append(newPerson)
def setupOrganisations(self):
names = ["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 i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newOrganisation = models.Organisation.objects.create(name=name)
if i % 2 == 0:
newOrganisation.has_su_account = True
if i % 3 == 0:
newOrganisation.email = "address@organisation.com"
if i % 5 == 0:
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newOrganisation.phone = "01234 567894"
newOrganisation.save()
self.organisations.append(newOrganisation)
def setupVenues(self):
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark", "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye", "The Golden Tooth", # noqa
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown", "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep", "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai", "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth", "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis", "Yunkai"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newVenue = models.Venue.objects.create(name=name)
if i % 2 == 0:
newVenue.three_phase_available = True
if i % 3 == 0:
newVenue.email = "address@venue.com"
if i % 5 == 0:
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newVenue.phone = "01234 567894"
newVenue.save()
self.venues.append(newVenue)
def setupGroups(self):
self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance')
keyholderPerms = ["add_event", "change_event", "view_event", "add_eventitem", "change_eventitem", "delete_eventitem", "add_organisation", "change_organisation", "view_organisation", "add_person", "change_person", "view_person", "view_profile", "add_venue", "change_venue", "view_venue"]
financePerms = ["change_event", "view_event", "add_eventitem", "change_eventitem", "add_invoice", "change_invoice", "view_invoice", "add_organisation", "change_organisation", "view_organisation", "add_payment", "change_payment", "delete_payment", "add_person", "change_person", "view_person"]
for permId in keyholderPerms:
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
for permId in financePerms:
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
def setupGenericProfiles(self):
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble", "Jack Harkness", "Mickey Smith", "Rose Tyler"]
for i, name in enumerate(names):
newProfile = 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()]))
if i % 2 == 0:
newProfile.phone = "01234 567894"
newProfile.save()
self.profiles.append(newProfile)
def setupUsefulProfiles(self):
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
superUser.set_password('superuser')
superUser.save()
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
email="financeuser@example.com", is_active=True)
financeUser.groups.add(self.finance_group)
financeUser.groups.add(self.keyholder_group)
financeUser.set_password('finance')
financeUser.save()
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", initials="KU",
email="keyholderuser@example.com", is_active=True)
keyholderUser.groups.add(self.keyholder_group)
keyholderUser.set_password('keyholder')
keyholderUser.save()
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True)
basicUser.set_password('basic')
basicUser.save()
def setupEvents(self):
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection", "Choir Concert"]
descriptions = ["A brief desciption of the event", "This event is boring", "Probably wont happen", "Warning: this has lots of kit"]
notes = ["The client came into the office at some point", "Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
itemOptions = [{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, 'cost': 200.00},
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
dayDelta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
name = names[i % len(names)]
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
dayDelta = dayDelta + random.randint(0, 3)
newEvent = models.Event.objects.create(name=name, start_date=startDate)
if random.randint(0, 2) > 1: # 1 in 3 have a start time
newEvent.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
newEvent.end_time = datetime.time(random.randint(21, 23))
elif random.randint(0, 1) > 0: # half of the others finish early the next day
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
newEvent.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0, 6) > 0: # 5 in 6 have MIC
newEvent.mic = random.choice(self.profiles)
if random.randint(0, 6) > 0: # 5 in 6 have organisation
newEvent.organisation = random.choice(self.organisations)
if random.randint(0, 6) > 0: # 5 in 6 have person
newEvent.person = random.choice(self.people)
if random.randint(0, 6) > 0: # 5 in 6 have venue
newEvent.venue = random.choice(self.venues)
# Could have any status, equally weighted
newEvent.status = random.choice([models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
if random.randint(0, 1) > 0: # 1 in 2 have description
newEvent.description = random.choice(descriptions)
if random.randint(0, 1) > 0: # 1 in 2 have notes
newEvent.notes = random.choice(notes)
newEvent.save()
# Now add some items
for j in range(random.randint(1, 5)):
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
while newEvent.sum_total < 0:
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
newInvoice = models.Invoice.objects.create(event=newEvent)
if newEvent.status is models.Event.CANCELLED: # void cancelled events
newInvoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())
call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData')

View File

@@ -0,0 +1,260 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission
from django.db import transaction
from reversion import revisions as reversion
import datetime
import random
from RIGS import models
class Command(BaseCommand):
help = 'Adds sample data to use for testing'
can_import_settings = True
people = []
organisations = []
venues = []
profiles = []
keyholder_group = None
finance_group = None
def handle(self, *args, **options):
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.setupGenericProfiles()
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime", "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter", "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter", "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley", "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley", "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newPerson = models.Person.objects.create(name=name)
if i % 3 == 0:
newPerson.email = "address@person.com"
if i % 5 == 0:
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newPerson.phone = "01234 567894"
newPerson.save()
self.people.append(newPerson)
def setupOrganisations(self):
names = ["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 i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newOrganisation = models.Organisation.objects.create(name=name)
if i % 2 == 0:
newOrganisation.has_su_account = True
if i % 3 == 0:
newOrganisation.email = "address@organisation.com"
if i % 5 == 0:
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newOrganisation.phone = "01234 567894"
newOrganisation.save()
self.organisations.append(newOrganisation)
def setupVenues(self):
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark", "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye", "The Golden Tooth", # noqa
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown", "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep", "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai", "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth", "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis", "Yunkai"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newVenue = models.Venue.objects.create(name=name)
if i % 2 == 0:
newVenue.three_phase_available = True
if i % 3 == 0:
newVenue.email = "address@venue.com"
if i % 5 == 0:
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newVenue.phone = "01234 567894"
newVenue.save()
self.venues.append(newVenue)
def setupGroups(self):
self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance')
keyholderPerms = ["add_event", "change_event", "view_event",
"add_eventitem", "change_eventitem", "delete_eventitem",
"add_organisation", "change_organisation", "view_organisation",
"add_person", "change_person", "view_person", "view_profile",
"add_venue", "change_venue", "view_venue",
"add_asset", "change_asset", "delete_asset",
"asset_finance", "view_asset", "view_supplier", "asset_finance",
"add_supplier"]
financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice",
"add_payment", "change_payment", "delete_payment"]
for permId in keyholderPerms:
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
for permId in financePerms:
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
def setupGenericProfiles(self):
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble", "Jack Harkness", "Mickey Smith", "Rose Tyler"]
for i, name in enumerate(names):
newProfile = 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()]))
if i % 2 == 0:
newProfile.phone = "01234 567894"
newProfile.save()
self.profiles.append(newProfile)
def setupUsefulProfiles(self):
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
superUser.set_password('superuser')
superUser.save()
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
email="financeuser@example.com", is_active=True)
financeUser.groups.add(self.finance_group)
financeUser.groups.add(self.keyholder_group)
financeUser.set_password('finance')
financeUser.save()
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", initials="KU",
email="keyholderuser@example.com", is_active=True)
keyholderUser.groups.add(self.keyholder_group)
keyholderUser.set_password('keyholder')
keyholderUser.save()
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True)
basicUser.set_password('basic')
basicUser.save()
def setupEvents(self):
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection", "Choir Concert"]
descriptions = ["A brief desciption of the event", "This event is boring", "Probably wont happen", "Warning: this has lots of kit"]
notes = ["The client came into the office at some point", "Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
itemOptions = [{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, 'cost': 200.00},
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
dayDelta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
name = names[i % len(names)]
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
dayDelta = dayDelta + random.randint(0, 3)
newEvent = models.Event.objects.create(name=name, start_date=startDate)
if random.randint(0, 2) > 1: # 1 in 3 have a start time
newEvent.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
newEvent.end_time = datetime.time(random.randint(21, 23))
elif random.randint(0, 1) > 0: # half of the others finish early the next day
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
newEvent.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0, 6) > 0: # 5 in 6 have MIC
newEvent.mic = random.choice(self.profiles)
if random.randint(0, 6) > 0: # 5 in 6 have organisation
newEvent.organisation = random.choice(self.organisations)
if random.randint(0, 6) > 0: # 5 in 6 have person
newEvent.person = random.choice(self.people)
if random.randint(0, 6) > 0: # 5 in 6 have venue
newEvent.venue = random.choice(self.venues)
# Could have any status, equally weighted
newEvent.status = random.choice([models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
if random.randint(0, 1) > 0: # 1 in 2 have description
newEvent.description = random.choice(descriptions)
if random.randint(0, 1) > 0: # 1 in 2 have notes
newEvent.notes = random.choice(notes)
newEvent.save()
# Now add some items
for j in range(random.randint(1, 5)):
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
while newEvent.sum_total < 0:
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
newInvoice = models.Invoice.objects.create(event=newEvent)
if newEvent.status is models.Event.CANCELLED: # void cancelled events
newInvoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())

View File

@@ -1,19 +0,0 @@
# Generated by Django 2.0.13 on 2020-01-22 03:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0033_auto_20180325_0016'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='invoice',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='RIGS.Invoice'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.5 on 2019-07-28 21:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0033_auto_20180325_0016'),
]
operations = [
migrations.AddField(
model_name='event',
name='risk_assessment_edit_url',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.13 on 2019-11-24 13:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0034_event_risk_assessment_edit_url'),
]
operations = [
migrations.AlterField(
model_name='event',
name='risk_assessment_edit_url',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='risk assessment'),
),
]

View File

@@ -1,21 +1,24 @@
from collections import Counter
import datetime
import hashlib
import datetime
import pytz
import random
import string
from decimal import Decimal
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse_lazy
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
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
import random
from collections import Counter
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.urls import reverse_lazy
# Create your models here.
@@ -335,6 +338,9 @@ class Event(models.Model, RevisionMixin):
auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(null=True, blank=True)
# Risk assessment info
risk_assessment_edit_url = models.CharField(verbose_name="risk assessment", max_length=255, blank=True, null=True)
# Calculated values
"""
EX Vat
@@ -527,9 +533,8 @@ class EventAuthorisation(models.Model, RevisionMixin):
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
@reversion.register(follow=['payments'])
@python_2_unicode_compatible
class Invoice(models.Model, RevisionMixin):
class Invoice(models.Model):
event = models.OneToOneField('Event', on_delete=models.CASCADE)
invoice_date = models.DateField(auto_now_add=True)
void = models.BooleanField(default=False)
@@ -544,7 +549,7 @@ class Invoice(models.Model, RevisionMixin):
@property
def payment_total(self):
total = self.payments.aggregate(total=models.Sum('amount'))['total']
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
if total:
return total
return Decimal("0.00")
@@ -582,7 +587,7 @@ class Payment(models.Model):
(ADJUSTMENT, 'TEC Adjustment'),
)
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name="payments")
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
date = models.DateField()
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)

View File

@@ -18,6 +18,7 @@ from django.core.exceptions import SuspiciousOperation
from django.db.models import Q
from django.contrib import messages
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from z3c.rml import rml2pdf
from PyPDF2 import PdfFileMerger, PdfFileReader
import simplejson
@@ -80,6 +81,25 @@ class EventEmbed(EventDetail):
template_name = 'RIGS/event_embed.html'
class EventRA(generic.base.RedirectView):
permanent = False
def get_redirect_url(self, *args, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk'])
if event.risk_assessment_edit_url:
return event.risk_assessment_edit_url
params = {
'entry.708610078': f'N{event.pk:05}',
'entry.905899507': event.name,
'entry.139491562': event.venue.name if event.venue else '',
'entry.1689826056': event.start_date.strftime('%Y-%m-%d') + ((' - ' + event.end_date.strftime('%Y-%m-%d')) if event.end_date else ''),
'entry.902421165': event.mic.name if event.mic else ''
}
return settings.RISK_ASSESSMENT_URL + "?" + urllib.parse.urlencode(params)
class EventCreate(generic.CreateView):
model = models.Event
form_class = forms.EventForm
@@ -386,3 +406,27 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
})
context['to_name'] = self.request.GET.get('to_name', None)
return context
@method_decorator(csrf_exempt, name='dispatch')
class LogRiskAssessment(generic.View):
http_method_names = ["post"]
def post(self, request, **kwargs):
data = request.POST
shared_secret = data.get("secret")
edit_url = data.get("editUrl")
rig_number = data.get("rigNum")
if shared_secret is None or edit_url is None or rig_number is None:
return HttpResponse(status=422)
if shared_secret != settings.RISK_ASSESSMENT_SECRET:
return HttpResponse(status=403)
rig_number = int(re.sub("[^0-9]", "", rig_number))
event = get_object_or_404(models.Event, pk=rig_number)
event.risk_assessment_edit_url = edit_url
event.save()
return HttpResponse(status=200)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -149,16 +149,19 @@ ins {
}
html.embedded{
min-height:100%;
display: table;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
max-height: 100%;
overflow: hidden;
justify-content: center;
body{
padding:0;
display: table-cell;
vertical-align: middle;
width:100%;
background:none;
overflow: auto;
}
.embed_container{

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax_nomodal.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax_nomodal.html,base_rigs.html" %}
{% load static %}
{% load paginator from filters %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load static %}
{% load paginator from filters %}
{% load to_class_name from filters %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% load static %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% load paginator from filters %}
{% block title %}Event Archive{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% block title %}{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %} | {{object.name}}{% endblock %}
{% block content %}
@@ -70,39 +70,6 @@
</div>
</div>
{% endif %}
{% if event.is_rig and event.internal %}
<div class="panel panel-default">
<div class="panel-heading">Client Authorisation</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>Authorised</dt>
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>
<dt>Authorised by</dt>
<dd>
{% if object.authorisation %}
{{ object.authorisation.name }}
(<a href="mailto:{{ object.authorisation.email }}">{{ object.authorisation.email }}</a>)
{% endif %}
</dd>
<dt>Authorised at</dt>
<dd>{{ object.authorisation.last_edited_at }}</dd>
<dt>Authorised amount</dt>
<dd>
{% if object.authorisation %}
£ {{ object.authorisation.amount|floatformat:"2" }}
{% endif %}
</dd>
<dt>Requested by</dt>
<dd>{{ object.authorisation.sent_by }}</dd>
</dl>
</div>
</div>
{% endif %}
</div>
{% endif %}
<div class="col-sm-12 {% if event.is_rig %}col-md-6 col-lg-7{% endif %}">
@@ -180,31 +147,63 @@
<dd>{{ object.collector }}</dd>
{% endif %}
{% if event.is_rig %}
{% if event.is_rig and not event.internal %}
<dd>&nbsp;</dd>
{% if object.internal %}
<dt>Authorisation Request</dt>
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
<dt>By</dt>
<dd>{{ object.auth_request_by }}</dd>
<dt>At</dt>
<dd>{{ object.auth_request_at|date:"D d M Y H:i"|default:"" }}</dd>
<dt>To</dt>
<dd>{{ object.auth_request_to }}</dd>
{% else %}
<dt>PO</dt>
<dd>{{ object.purchase_order }}</dd>
{% endif %}
<dt>PO</dt>
<dd>{{ object.purchase_order }}</dd>
{% endif %}
</dl>
</div>
</div>
</div>
{% if event.is_rig and event.internal %}
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">Client Authorisation</div>
<div class="panel-body">
<dl class="dl-horizontal col-sm-6">
<dt>Authorisation Request</dt>
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
<dt>By</dt>
<dd>{{ object.auth_request_by }}</dd>
<dt>At</dt>
<dd>{{ object.auth_request_at|date:"D d M Y H:i"|default:"" }}</dd>
<dt>To</dt>
<dd>{{ object.auth_request_to }}</dd>
</dl>
<dd class="visible-xs">&nbsp;</dd>
<dl class="dl-horizontal col-sm-6">
<dt>Authorised</dt>
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>
<dt>Authorised by</dt>
<dd>
{% if object.authorisation %}
{{ object.authorisation.name }}
(<a href="mailto:{{ object.authorisation.email }}">{{ object.authorisation.email }}</a>)
{% endif %}
</dd>
<dt>Authorised at</dt>
<dd>{{ object.authorisation.last_edited_at }}</dd>
<dt>Authorised amount</dt>
<dd>
{% if object.authorisation %}
£ {{ object.authorisation.amount|floatformat:"2" }}
{% endif %}
</dd>
<dt>Requested by</dt>
<dd>{{ object.authorisation.sent_by }}</dd>
</dl>
</div>
</div>
<div>
{% endif %}
{% if not request.is_ajax %}
<div class="col-sm-12 text-right">
{% include 'RIGS/event_detail_buttons.html' %}
@@ -246,7 +245,7 @@
<div class="row">
<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 }} by {{ object.last_edited_by.name }}
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
</a>
</div>
<div class="col-sm-2">

View File

@@ -3,6 +3,17 @@
class="glyphicon glyphicon-edit"></span> <span
class="hidden-xs">Edit</span></a>
{% if event.is_rig %}
{% if not event.dry_hire %}
<a href="{% url 'event_ra' event.pk %}" class="btn btn-default
{% if event.risk_assessment_edit_url %}
btn-success
{% else %}
btn-warning
{% endif %}
"><span
class="glyphicon glyphicon-paperclip"></span> <span
class="hidden-xs">RA</span></a>
{% endif %}
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
class="glyphicon glyphicon-print"></span> <span
class="hidden-xs">Print</span></a>

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load multiply from filters %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% load paginator from filters %}
{% load static %}

View File

@@ -264,7 +264,7 @@
</para>
</td>
</tr>
{% for payment in object.invoice.payments.all %}
{% for payment in object.invoice.payment_set.all %}
<tr>
<td>{{ payment.get_method_display }}</td>
<td>{{ payment.date }}</td>

View File

@@ -6,7 +6,7 @@
<p>
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
for <b>&pound;{{ object.amount }}</b>
by <b>{{ object.name }}</b> as of <b>{{ object.last_edited_at }}</b>.
by <b>{{ object.name }}</b> as of <b>{{ object.event.last_edited_at }}</b>.
</p>
<p>

View File

@@ -1,6 +1,6 @@
Hi {{ to_name|default:"there" }},
Hi {{ to_name|default_if_none:"there" }},
Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
Your event is now fully booked and payment will be processed by the finance department automatically.

View File

@@ -1,5 +1,5 @@
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
The TEC Rig Information Gathering System

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}Request Authorisation{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}NottinghamTEC Email Address Required{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% block title %}RIGS{% endblock %}
{% block content %}
@@ -20,6 +20,7 @@
<a class="list-group-item" href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span> Rigboard</a>
<a class="list-group-item" href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span> Calendar</a>
{% if perms.RIGS.add_event %}<a class="list-group-item" href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span> New Event</a>{% endif %}
<a class="list-group-item" href="{% url 'asset_index' %}"><span class="glyphicon glyphicon-tag"></span> Asset Database </a>
<div class="list-group-item default"></div>
@@ -27,7 +28,7 @@
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
<a class="list-group-item" href="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://form.jotformeu.com/62203600438344" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a>
<a class="list-group-item" href="https://goo.gl/forms/jdPWov8PCNPoXtbn2" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a>
</div>
</div>
@@ -71,9 +72,8 @@
</div>
</div>
{% if perms.RIGS.view_event %}
<div class="col-sm-6" >
<div class="col-sm-6">
{% include 'RIGS/activity_feed.html' %}
</div>
{% endif %}
</div>

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% block title %}Invoice {{ object.pk }}{% endblock %}
@@ -6,21 +6,20 @@
<div class="col-sm-12">
<div class="row">
<div class="col-sm-8">
<h2>Invoice {{ object.pk }} ({{ object.invoice_date|date:"d/m/Y" }})</h2>
<h2>Invoice {{ object.pk }} ({{ object.invoice_date|date:"d/m/Y"}})</h2>
</div>
<div class="col-sm-4 text-right">
<div class="btn-group btn-page">
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-default" title="Delete Invoice">
<span class="glyphicon glyphicon-remove"></span> <span
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-default" title="Delete Invoice">
<span class="glyphicon glyphicon-remove"></span> <span
class="hidden-xs">Delete</span>
</a>
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-default" title="Void Invoice">
<span class="glyphicon glyphicon-ban-circle"></span> <span
</a>
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-default" title="Void Invoice">
<span class="glyphicon glyphicon-ban-circle"></span> <span
class="hidden-xs">Void</span>
</a>
<a href="{% url 'invoice_print' object.pk %}" target="_blank" title="Print Invoice"
class="btn btn-default"><span
</a>
<a href="{% url 'invoice_print' object.pk %}" target="_blank" title="Print Invoice" class="btn btn-default"><span
class="glyphicon glyphicon-print"></span> <span
class="hidden-xs">Print</span></a>
</div>
@@ -84,11 +83,9 @@
<dt>Authorised by</dt>
<dd>
{% if object.event.authorised %}
{% if object.event.authorised %}
{{ object.event.authorisation.name }}
(
<a href="mailto:{{ object.event.authorisation.email }}">{{ object.event.authorisation.email }}</a>
)
(<a href="mailto:{{ object.event.authorisation.email }}">{{ object.event.authorisation.email }}</a>)
{% endif %}
</dd>
@@ -143,7 +140,7 @@
</tr>
</thead>
<tbody>
{% for payment in object.payments.all %}
{% for payment in object.payment_set.all %}
<tr>
<td>{{ payment.date }}</td>
<td>{{ payment.amount|floatformat:2 }}</td>
@@ -175,13 +172,6 @@
</div>
</div>
</div>
</div>
<div class="col-sm-12 text-right">
<div>
<a href="{% url 'invoice_history' object.pk %}" title="View Revision History">
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% load paginator from filters %}
{% block title %}Invoices{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% block title %}Organisation | {{ object.name }}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}{% if object.pk %}Edit {{ object.name }}{% else %}Add Organisation{% endif %}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% load paginator from filters %}
{% load url_replace from filters %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% block title %}Person | {{ object.name }}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}{% if object.pk %}Edit {{ object.name }}{% else %}Add Person{% endif %}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% load paginator from filters %}
{% load url_replace from filters %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% block title %}RIGS Profile {{object.pk}}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}Update Profile {{object.name}}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base_rigs.html' %}
{% block title %}Rigboard{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% block title %}Venue | {{ object.name }}{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}{{ object.pk|yesno:"Edit,Add" }} Venue{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load widget_tweaks %}
{% load paginator from filters %}
{% load url_replace from filters %}

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load to_class_name from filters %}
{% load paginator from filters %}
{% load static %}

View File

@@ -88,7 +88,7 @@ class UserRegistrationTest(LiveServerTestCase):
initials.send_keys('JS')
phone.send_keys('0123456789')
self.browser.execute_script(
"return jQuery('#g-recaptcha-response').val('PASSED')")
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
# Submit incorrect form
submit = self.browser.find_element_by_xpath("//input[@type='submit']")
@@ -110,8 +110,9 @@ class UserRegistrationTest(LiveServerTestCase):
# Correct error
password1.send_keys('correcthorsebatterystaple')
password2.send_keys('correcthorsebatterystaple')
self.browser.execute_script("console.log('Hello, world!')")
self.browser.execute_script(
"return jQuery('#g-recaptcha-response').val('PASSED')")
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
# Submit again
password2.send_keys(Keys.ENTER)
@@ -150,7 +151,7 @@ class UserRegistrationTest(LiveServerTestCase):
username.send_keys('TestUsername')
password.send_keys('correcthorsebatterystaple')
self.browser.execute_script(
"return jQuery('#g-recaptcha-response').val('PASSED')")
"return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()")
password.send_keys(Keys.ENTER)
# Check we are logged in

View File

@@ -414,7 +414,7 @@ class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def test_generate_sample_data(self):
# Run the management command and check there are no exceptions
call_command('generateSampleData')
call_command('generateSampleRIGSData')
# Check there are lots of events
self.assertTrue(models.Event.objects.all().count() > 100)
@@ -422,4 +422,4 @@ class TestSampleDataGenerator(TestCase):
def test_production_exception(self):
from django.core.management.base import CommandError
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleData')
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')

View File

@@ -1,12 +1,13 @@
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import password_reset
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import RedirectView
from PyRIGS.decorators import api_key_required
from PyRIGS.decorators import permission_required_with_403
from django.contrib.auth.decorators import login_required
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 api_key_required
urlpatterns = [
# Examples:
@@ -100,6 +101,9 @@ urlpatterns = [
url(r'^event/(?P<pk>\d+)/print/$',
permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
name='event_print'),
url(r'^event/(?P<pk>\d+)/ra/$',
permission_required_with_403('RIGS.change_event')(rigboard.EventRA.as_view()),
name='event_ra'),
url(r'^event/create/$',
permission_required_with_403('RIGS.add_event')(rigboard.EventCreate.as_view()),
name='event_create'),
@@ -143,10 +147,6 @@ urlpatterns = [
url(r'^invoice/(?P<pk>\d+)/delete/$',
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceDelete.as_view()),
name='invoice_delete'),
url(r'^invoice/(?P<pk>\d+)/history/$',
permission_required_with_403('RIGS.view_invoice')(versioning.VersionHistory.as_view()),
name='invoice_history', kwargs={'model': models.Invoice}),
url(r'^payment/create/$',
permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
name='payment_create'),
@@ -188,6 +188,9 @@ urlpatterns = [
url(r'^api/(?P<model>\w+)/(?P<pk>\d+)/$', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"),
# Risk assessment API
url(r'^log_risk_assessment/$', rigboard.LogRiskAssessment.as_view(), name='log_risk_assessment'),
# Legacy URL's
url(r'^rig/show/(?P<pk>\d+)/$',
RedirectView.as_view(permanent=True, pattern_name='event_detail')),

View File

@@ -27,6 +27,8 @@ class FieldComparison(object):
def display_value(self, value):
if isinstance(self.field, IntegerField) 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 ""
return value
@property
@@ -166,7 +168,7 @@ class RIGSVersionManager(VersionQuerySet):
for model in model_array:
content_types.append(ContentType.objects.get_for_model(model))
return self.filter(content_type__in=content_types).select_related("revision").order_by("-pk")
return self.filter(content_type__in=content_types).select_related("revision").order_by("-revision__date_created")
class RIGSVersion(Version):
@@ -204,17 +206,14 @@ class VersionHistory(generic.ListView):
paginate_by = 25
def get_queryset(self, **kwargs):
thisModel = self.kwargs['model']
return RIGSVersion.objects.get_for_object(self.get_object()).select_related("revision", "revision__user").all().order_by("-revision__date_created")
versions = RIGSVersion.objects.get_for_object_reference(thisModel, self.kwargs['pk']).select_related("revision", "revision__user").all()
return versions
def get_object(self, **kwargs):
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
def get_context_data(self, **kwargs):
thisModel = self.kwargs['model']
context = super(VersionHistory, self).get_context_data(**kwargs)
thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk'])
context['object'] = thisObject
context['object'] = self.get_object()
return context
@@ -226,7 +225,7 @@ class ActivityTable(generic.ListView):
def get_queryset(self):
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
return versions
return versions.order_by("-revision__date_created")
class ActivityFeed(generic.ListView):
@@ -236,7 +235,7 @@ class ActivityFeed(generic.ListView):
def get_queryset(self):
versions = RIGSVersion.objects.get_for_multiple_models([models.Event, models.Venue, models.Person, models.Organisation, models.EventAuthorisation])
return versions
return versions.order_by("-revision__date_created")
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context

View File

@@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
from RIGS import models, forms
from assets import models as asset_models
from functools import reduce
"""
@@ -62,7 +63,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")
return login(request, template_name="registration/login_embed.html", authentication_form=forms.EmbeddedAuthenticationForm)
"""
@@ -248,6 +249,7 @@ class SecureAPIRequest(generic.View):
'organisation': models.Organisation,
'profile': models.Profile,
'event': models.Event,
'supplier': asset_models.Supplier
}
perms = {
@@ -256,6 +258,7 @@ class SecureAPIRequest(generic.View):
'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile',
'event': None,
'supplier': None
}
'''

0
assets/__init__.py Normal file
View File

32
assets/admin.py Normal file
View File

@@ -0,0 +1,32 @@
from django.contrib import admin
from assets import models as assets
@admin.register(assets.AssetCategory)
class AssetCategoryAdmin(admin.ModelAdmin):
list_display = ['id', 'name']
ordering = ['id']
@admin.register(assets.AssetStatus)
class AssetStatusAdmin(admin.ModelAdmin):
list_display = ['id', 'name']
ordering = ['id']
@admin.register(assets.Supplier)
class SupplierAdmin(admin.ModelAdmin):
list_display = ['id', 'name']
ordering = ['id']
@admin.register(assets.Asset)
class AssetAdmin(admin.ModelAdmin):
list_display = ['id', 'asset_id', 'description', 'category', 'status']
list_filter = ['is_cable', 'category']
search_fields = ['id', 'asset_id', 'description']
@admin.register(assets.Connector)
class ConnectorAdmin(admin.ModelAdmin):
list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins']

5
assets/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AssetsConfig(AppConfig):
name = 'assets'

9
assets/filters.py Normal file
View File

@@ -0,0 +1,9 @@
import django_filters
from assets import models
class AssetFilter(django_filters.FilterSet):
class Meta:
model = models.Asset
fields = ['asset_id', 'description', 'serial_number', 'category', 'status']

36
assets/forms.py Normal file
View File

@@ -0,0 +1,36 @@
from django import forms
from assets import models
class AssetForm(forms.ModelForm):
related_models = {
'asset': models.Asset,
'supplier': models.Supplier
}
class Meta:
model = models.Asset
fields = '__all__'
exclude = ['asset_id_prefix', 'asset_id_number']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date_sold'].widget.format = '%Y-%m-%d'
self.fields['date_acquired'].widget.format = '%Y-%m-%d'
class AssetSearchForm(forms.Form):
query = forms.CharField(required=False)
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
class SupplierForm(forms.ModelForm):
class Meta:
model = models.Supplier
fields = '__all__'
class SupplierSearchForm(forms.Form):
query = forms.CharField(required=False)

View File

View File

View File

@@ -0,0 +1,23 @@
from django.core.management.base import BaseCommand, CommandError
from assets import models
class Command(BaseCommand):
help = 'Deletes testing sample data'
def handle(self, *args, **kwargs):
from django.conf import settings
if not (settings.DEBUG):
raise CommandError('You cannot run this command in production')
self.delete_objects(models.AssetCategory)
self.delete_objects(models.AssetStatus)
self.delete_objects(models.Supplier)
self.delete_objects(models.Connector)
self.delete_objects(models.Asset)
def delete_objects(self, model):
for object in model.objects.all():
object.delete()

View File

@@ -0,0 +1,120 @@
import random
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from assets import models
class Command(BaseCommand):
help = 'Creates some sample data for testing'
def handle(self, *args, **kwargs):
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed('Some object to see the random number generator')
self.create_categories()
self.create_statuses()
self.create_suppliers()
self.create_assets()
self.create_connectors()
self.create_cables()
def create_categories(self):
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
for cat in categories:
models.AssetCategory.objects.create(name=cat)
def create_statuses(self):
statuses = [('In Service', True), ('Lost', False), ('Binned', False), ('Sold', False), ('Broken', False)]
for stat in statuses:
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1])
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)
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']
categories = models.AssetCategory.objects.all()
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()
)
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()
def create_cables(self):
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
csas = [0.75, 1.00, 1.25, 2.5, 4]
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
cores = [3, 5]
circuits = [1, 2, 3, 6]
categories = models.AssetCategory.objects.all()
statuses = models.AssetStatus.objects.all()
suppliers = models.Supplier.objects.all()
connectors = models.Connector.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(),
is_cable=True,
plug=random.choice(connectors),
socket=random.choice(connectors),
csa=random.choice(csas),
length=random.choice(lengths),
circuits=random.choice(circuits),
cores=random.choice(circuits)
)
if i % 5 == 0:
prefix = random.choice(asset_prefixes)
asset.asset_id = prefix + str(models.Asset.get_available_asset_id(wanted_prefix=prefix))
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()
def create_connectors(self):
connectors = [
{"description": "13A UK", "current_rating": 13, "voltage_rating": 230, "num_pins": 3},
{"description": "16A", "current_rating": 16, "voltage_rating": 230, "num_pins": 3},
{"description": "32/3", "current_rating": 32, "voltage_rating": 400, "num_pins": 5},
{"description": "Socapex", "current_rating": 23, "voltage_rating": 600, "num_pins": 19},
]
for connector in connectors:
conn = models.Connector.objects.create(** connector)
conn.save()

View File

@@ -0,0 +1,229 @@
import os
import datetime
import xml.etree.ElementTree as ET
from django.core.management.base import BaseCommand
from django.conf import settings
from assets import models
class Command(BaseCommand):
help = 'Imports old db from XML dump'
epoch = datetime.date(1970, 1, 1)
def handle(self, *args, **options):
self.import_categories()
self.import_statuses()
self.import_suppliers()
self.import_collections()
self.import_assets()
self.import_cables()
@staticmethod
def xml_path(file):
return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
@staticmethod
def parse_xml(file):
tree = ET.parse(file)
return tree.getroot()
def import_categories(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Asset_Categories.xml'))
for child in root:
obj, created = models.AssetCategory.objects.update_or_create(
pk=int(child.find('AssetCategoryID').text),
name=child.find('AssetCategory').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Categories - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_statuses(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Asset_Status_new.xml'))
for child in root:
obj, created = models.AssetStatus.objects.update_or_create(
pk=int(child.find('StatusID').text),
name=child.find('Status').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Statuses - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_suppliers(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Asset_Suppliers_new.xml'))
for child in root:
obj, created = models.Supplier.objects.update_or_create(
pk=int(child.find('Supplier_x0020_Id').text),
name=child.find('Supplier_x0020_Name').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Suppliers - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_assets(self):
# 0: updated, 1: created
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Assets.xml'))
for child in root:
defaults = dict()
# defaults['pk'] = int(child.find('ID').text)
defaults['asset_id'] = child.find('AssetID').text
try:
defaults['description'] = child.find('AssetDescription').text
except AttributeError:
defaults['description'] = 'None'
defaults['category'] = models.AssetCategory.objects.get(pk=int(child.find('AssetCategoryID').text))
defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('StatusID').text))
try:
defaults['serial_number'] = child.find('SerialNumber').text
except AttributeError:
pass
try:
defaults['purchased_from'] = models.Supplier.objects.get(pk=int(child.find('Supplier_x0020_Id').text))
except AttributeError:
pass
try:
defaults['date_acquired'] = datetime.datetime.strptime(child.find('DateAcquired').text, '%d/%m/%Y').date()
except AttributeError:
defaults['date_acquired'] = self.epoch
try:
defaults['date_sold'] = datetime.datetime.strptime(child.find('DateSold').text, '%d/%m/%Y').date()
except AttributeError:
pass
try:
defaults['purchase_price'] = float(child.find('Replacement_x0020_Value').text)
except AttributeError:
pass
try:
defaults['salvage_value'] = float(child.find('SalvageValue').text)
except AttributeError:
pass
try:
defaults['comments'] = child.find('Comments').text
except AttributeError:
pass
try:
date = child.find('NextSchedMaint').text.split('T')[0]
defaults['next_sched_maint'] = datetime.datetime.strptime(date, '%Y-%m-%d').date()
except AttributeError:
pass
print(defaults)
obj, created = models.Asset.objects.update_or_create(**defaults)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Assets - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_collections(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Cable_Collections.xml'))
for child in root:
defaults = dict()
defaults['pk'] = int(child.find('ID').text)
defaults['name'] = child.find('Cable_x0020_Trunk').text
obj, created = models.Collection.objects.update_or_create(**defaults)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_cables(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('TEC_Cables.xml'))
for child in root:
defaults = dict()
defaults['asset_id'] = child.find('Asset_x0020_Number').text
try:
defaults['description'] = child.find('Type_x0020_of_x0020_Cable').text
except AttributeError:
defaults['description'] = 'None'
defaults['is_cable'] = True
defaults['category'] = models.AssetCategory.objects.get(pk=9)
try:
defaults['length'] = child.find('Length_x0020__x0028_m_x0029_').text
except AttributeError:
pass
defaults['status'] = models.AssetStatus.objects.get(pk=int(child.find('Status').text))
try:
defaults['comments'] = child.find('Comments').text
except AttributeError:
pass
try:
collection_id = int(child.find('Collection').text)
if collection_id != 0:
defaults['collection'] = models.Collection.objects.get(pk=collection_id)
except AttributeError:
pass
try:
defaults['purchase_price'] = float(child.find('Purchase_x0020_Price').text)
except AttributeError:
pass
defaults['date_acquired'] = self.epoch
print(defaults)
obj, created = models.Asset.objects.update_or_create(**defaults)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Collections - Updated: {}, Created: {}'.format(tally[0], tally[1]))

View File

@@ -0,0 +1,110 @@
import os
import datetime
import xml.etree.ElementTree as ET
from django.core.management.base import BaseCommand
from django.conf import settings
class Command(BaseCommand):
help = 'Imports old db from XML dump'
epoch = datetime.date(1970, 1, 1)
def handle(self, *args, **options):
# self.update_statuses()
# self.update_suppliers()
self.update_cable_statuses()
@staticmethod
def xml_path(file):
return os.path.join(settings.BASE_DIR, 'data/DB_Dump/{}'.format(file))
@staticmethod
def parse_xml(file):
tree = ET.parse(file)
return tree.getroot()
def update_statuses(self):
file = self.xml_path('TEC_Assets.xml')
tree = ET.parse(file)
root = tree.getroot()
# map old status pk to new status pk
status_map = {
2: 2,
3: 4,
4: 3,
5: 5,
6: 1
}
for child in root:
status = int(child.find('StatusID').text)
child.find('StatusID').text = str(status_map[status])
tree.write(file)
def update_suppliers(self):
old_file = self.xml_path('TEC_Asset_Suppliers.xml')
old_tree = ET.parse(old_file)
old_root = old_tree.getroot()
new_file = self.xml_path('TEC_Asset_Suppliers_new.xml')
new_tree = ET.parse(new_file)
new_root = new_tree.getroot()
# map old supplier pk to new supplier pk
supplier_map = dict()
def find_in_old(name, root):
for child in root:
found_id = child.find('Supplier_x0020_Id').text
found_name = child.find('Supplier_x0020_Name').text
if found_name == name:
return found_id
for new_child in new_root:
new_id = new_child.find('Supplier_x0020_Id').text
new_name = new_child.find('Supplier_x0020_Name').text
old_id = find_in_old(new_name, old_root)
supplier_map[int(old_id)] = int(new_id)
file = self.xml_path('TEC_Assets.xml')
tree = ET.parse(file)
root = tree.getroot()
for child in root:
try:
supplier = int(child.find('Supplier_x0020_Id').text)
child.find('Supplier_x0020_Id').text = str(supplier_map[supplier])
except AttributeError:
pass
tree.write(file)
def update_cable_statuses(self):
file = self.xml_path('TEC_Cables.xml')
tree = ET.parse(file)
root = tree.getroot()
# map old status pk to new status pk
status_map = {
0: 7,
1: 3,
3: 2,
4: 5,
6: 6,
7: 1,
8: 4,
9: 2,
}
for child in root:
status = int(child.find('Status').text)
child.find('Status').text = str(status_map[status])
tree.write(file)

View File

@@ -0,0 +1,86 @@
# Generated by Django 2.0.2 on 2018-02-28 16:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Asset',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('asset_id', models.IntegerField()),
('description', models.CharField(max_length=120)),
('serial_number', models.CharField(blank=True, max_length=150, null=True)),
('date_acquired', models.DateField()),
('date_sold', models.DateField(blank=True, null=True)),
('purchase_price', models.IntegerField()),
('salvage_value', models.IntegerField(blank=True, null=True)),
('comments', models.TextField(blank=True, null=True)),
('next_sched_maint', models.DateField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='AssetCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=80)),
],
options={
'verbose_name': 'Asset Category',
'verbose_name_plural': 'Asset Categories',
},
),
migrations.CreateModel(
name='AssetStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=80)),
],
options={
'verbose_name': 'Asset Status',
'verbose_name_plural': 'Asset Statuses',
},
),
migrations.CreateModel(
name='Collection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=80)),
],
),
migrations.CreateModel(
name='Supplier',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=80)),
],
),
migrations.AddField(
model_name='asset',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetCategory'),
),
migrations.AddField(
model_name='asset',
name='collection',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Collection'),
),
migrations.AddField(
model_name='asset',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.Supplier'),
),
migrations.AddField(
model_name='asset',
name='status',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetStatus'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-03-01 16:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.IntegerField(blank=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-03-01 17:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0002_auto_20180301_1654'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='purchase_price',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.0.2 on 2018-03-01 17:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0003_auto_20180301_1700'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='collection',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.Collection'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-03-01 17:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0004_auto_20180301_1711'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,148 @@
# Generated by Django 2.1.5 on 2019-01-05 19:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
replaces = [('assets', '0006_auto_20180728_1451'), ('assets', '0007_auto_20181215_1447'), ('assets', '0008_auto_20181215_1448'), ('assets', '0009_auto_20181215_1640'), ('assets', '0010_auto_20181215_1640'), ('assets', '0011_auto_20181215_1749'), ('assets', '0012_auto_20181215_1813'), ('assets', '0013_asset_parent'), ('assets', '0014_auto_20190103_1615'), ('assets', '0015_auto_20190103_1617'), ('assets', '0016_remove_asset_collection'), ('assets', '0017_delete_collection'), ('assets', '0018_auto_20190103_1708'), ('assets', '0019_auto_20190103_1723'), ('assets', '0020_auto_20190103_1729'), ('assets', '0021_auto_20190105_1156')]
dependencies = [
('assets', '0005_auto_20180301_1725'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.IntegerField(blank=True, null=True, unique=True),
),
migrations.AlterField(
model_name='asset',
name='purchase_price',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='asset',
name='salvage_value',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='asset',
name='is_cable',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='asset',
name='length',
field=models.DecimalField(blank=True, decimal_places=1, max_digits=10, null=True),
),
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.CharField(default='', max_length=10),
preserve_default=False,
),
migrations.AddField(
model_name='asset',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=None, related_name='asset_parent', to='assets.Asset'),
),
migrations.RemoveField(
model_name='asset',
name='collection',
),
migrations.DeleteModel(
name='Collection',
),
migrations.CreateModel(
name='Cable',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('asset_id', models.CharField(max_length=10)),
('description', models.CharField(max_length=120)),
('serial_number', models.CharField(blank=True, max_length=150, null=True)),
('date_acquired', models.DateField()),
('date_sold', models.DateField(blank=True, null=True)),
('purchase_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('salvage_value', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('comments', models.TextField(blank=True, null=True)),
('next_sched_maint', models.DateField(blank=True, null=True)),
('is_cable', models.BooleanField(default=False)),
('length', models.DecimalField(blank=True, decimal_places=1, help_text='m', max_digits=10, null=True)),
('csa', models.DecimalField(blank=True, decimal_places=2, help_text='mm^2', max_digits=10, null=True)),
('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')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Connector',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.CharField(max_length=80)),
('current_rating', models.DecimalField(decimal_places=2, help_text='Amps', max_digits=10)),
('voltage_rating', models.IntegerField(default=0, help_text='Volts')),
('num_pins', models.IntegerField(blank=True, null=True)),
],
),
migrations.AddField(
model_name='cable',
name='plug',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'),
),
migrations.AddField(
model_name='cable',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.Supplier'),
),
migrations.AddField(
model_name='cable',
name='socket',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'),
),
migrations.AddField(
model_name='cable',
name='status',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.AssetStatus'),
),
migrations.AlterField(
model_name='asset',
name='comments',
field=models.TextField(blank=True, default=''),
preserve_default=False,
),
migrations.AlterField(
model_name='asset',
name='serial_number',
field=models.CharField(blank=True, default='', max_length=150),
preserve_default=False,
),
migrations.AlterField(
model_name='cable',
name='comments',
field=models.TextField(blank=True, default=''),
preserve_default=False,
),
migrations.AlterField(
model_name='cable',
name='serial_number',
field=models.CharField(blank=True, default='', max_length=150),
preserve_default=False,
),
]

View File

@@ -0,0 +1,176 @@
# Generated by Django 2.0.13 on 2019-12-04 17:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
replaces = [('assets', '0007_auto_20190108_0202'), ('assets', '0008_auto_20191002_1931'), ('assets', '0009_auto_20191008_2148'), ('assets', '0010_auto_20191013_2123'), ('assets', '0011_auto_20191013_2247'), ('assets', '0012_auto_20191014_0012'), ('assets', '0013_auto_20191016_1446'), ('assets', '0014_auto_20191017_2052')]
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('assets', '0006_auto_20180728_1451_squashed_0021_auto_20190105_1156'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Asset'),
),
migrations.AlterField(
model_name='cable',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_parent', to='assets.Cable'),
),
migrations.AlterField(
model_name='connector',
name='voltage_rating',
field=models.IntegerField(help_text='Volts'),
),
migrations.AlterModelOptions(
name='asset',
options={'base_manager_name': 'objects'},
),
migrations.AlterModelOptions(
name='cable',
options={'base_manager_name': 'objects'},
),
migrations.RemoveField(
model_name='asset',
name='length',
),
migrations.RemoveField(
model_name='cable',
name='asset_id',
),
migrations.RemoveField(
model_name='cable',
name='category',
),
migrations.RemoveField(
model_name='cable',
name='comments',
),
migrations.RemoveField(
model_name='cable',
name='date_acquired',
),
migrations.RemoveField(
model_name='cable',
name='date_sold',
),
migrations.RemoveField(
model_name='cable',
name='description',
),
migrations.RemoveField(
model_name='cable',
name='id',
),
migrations.RemoveField(
model_name='cable',
name='is_cable',
),
migrations.RemoveField(
model_name='cable',
name='next_sched_maint',
),
migrations.RemoveField(
model_name='cable',
name='parent',
),
migrations.RemoveField(
model_name='cable',
name='purchase_price',
),
migrations.RemoveField(
model_name='cable',
name='purchased_from',
),
migrations.RemoveField(
model_name='cable',
name='salvage_value',
),
migrations.RemoveField(
model_name='cable',
name='serial_number',
),
migrations.RemoveField(
model_name='cable',
name='status',
),
migrations.AddField(
model_name='asset',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_assets.asset_set+', to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.CharField(max_length=10, unique=True),
),
migrations.RemoveField(
model_name='cable',
name='plug',
),
migrations.RemoveField(
model_name='cable',
name='socket',
),
migrations.AlterModelOptions(
name='asset',
options={},
),
migrations.RemoveField(
model_name='asset',
name='polymorphic_ctype',
),
migrations.AddField(
model_name='asset',
name='circuits',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='asset',
name='cores',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='asset',
name='csa',
field=models.DecimalField(blank=True, decimal_places=2, help_text='mm^2', max_digits=10, null=True),
),
migrations.AddField(
model_name='asset',
name='length',
field=models.DecimalField(blank=True, decimal_places=1, help_text='m', max_digits=10, null=True),
),
migrations.AddField(
model_name='asset',
name='plug',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'),
),
migrations.AddField(
model_name='asset',
name='socket',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'),
),
migrations.DeleteModel(
name='Cable',
),
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['asset_id'], 'permissions': (('asset_finance', 'Can see financial data for assets'), ('view_asset', 'Can view an asset'))},
),
migrations.AddField(
model_name='assetstatus',
name='should_show',
field=models.BooleanField(default=True, help_text='Should this be shown by default in the asset list.'),
),
migrations.AlterModelOptions(
name='supplier',
options={'permissions': (('view_supplier', 'Can view a supplier'),)},
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 2.0.13 on 2019-12-06 21:24
from django.db import migrations, models, transaction
import re
def forwards(apps, schema_editor):
AssetModel = apps.get_model('assets', 'Asset')
with transaction.atomic():
for row in AssetModel.objects.all():
row.asset_id = row.asset_id.upper()
asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id)
if asset_search is None: # If the asset_id doesn't have a number at the end
row.asset_id += "1"
asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id)
row.asset_id_prefix = asset_search.group(1)
row.asset_id_number = int(asset_search.group(2))
row.save(update_fields=['asset_id', 'asset_id_prefix', 'asset_id_number'])
class Migration(migrations.Migration):
dependencies = [
('assets', '0007_auto_20190108_0202_squashed_0014_auto_20191017_2052'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['asset_id_prefix', 'asset_id_number'], 'permissions': (('asset_finance', 'Can see financial data for assets'), ('view_asset', 'Can view an asset'))},
),
migrations.AddField(
model_name='asset',
name='asset_id_number',
field=models.IntegerField(default=1),
),
migrations.AddField(
model_name='asset',
name='asset_id_prefix',
field=models.CharField(default='', max_length=8),
),
migrations.AlterField(
model_name='asset',
name='asset_id',
field=models.CharField(max_length=15, unique=True),
),
migrations.RunPython(
code=forwards,
reverse_code=migrations.operations.special.RunPython.noop,
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.0.13 on 2020-01-02 19:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0008_auto_20191206_2124'),
]
operations = [
migrations.AlterModelOptions(
name='assetcategory',
options={'ordering': ['name'], 'verbose_name': 'Asset Category', 'verbose_name_plural': 'Asset Categories'},
),
migrations.AlterModelOptions(
name='assetstatus',
options={'ordering': ['name'], 'verbose_name': 'Asset Status', 'verbose_name_plural': 'Asset Statuses'},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 2.0.13 on 2020-01-02 20:42
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('assets', '0009_auto_20200102_1933'), ('assets', '0010_assetstatus_display_class'), ('assets', '0011_auto_20200102_2040')]
dependencies = [
('assets', '0008_auto_20191206_2124'),
]
operations = [
migrations.AlterModelOptions(
name='assetcategory',
options={'ordering': ['name'], 'verbose_name': 'Asset Category', 'verbose_name_plural': 'Asset Categories'},
),
migrations.AlterModelOptions(
name='assetstatus',
options={'ordering': ['name'], 'verbose_name': 'Asset Status', 'verbose_name_plural': 'Asset Statuses'},
),
migrations.AddField(
model_name='assetstatus',
name='display_class',
field=models.CharField(help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.13 on 2020-01-02 19:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20200102_1933'),
]
operations = [
migrations.AddField(
model_name='assetstatus',
name='display_class',
field=models.TextField(default='', help_text='HTML class to be appended to alter display of assets with this status, such as in the list.'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.0.13 on 2020-01-03 21:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20200102_1933_squashed_0011_auto_20200102_2040'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Supplier'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.13 on 2020-01-02 20:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_assetstatus_display_class'),
]
operations = [
migrations.AlterField(
model_name='assetstatus',
name='display_class',
field=models.CharField(help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80),
),
]

View File

179
assets/models.py Normal file
View File

@@ -0,0 +1,179 @@
import re
from django.core.exceptions import ValidationError
from django.db import models, connection
from django.urls import reverse
from django.db.models.signals import pre_save
from django.dispatch.dispatcher import receiver
from reversion import revisions as reversion
from reversion.models import Version
from RIGS.models import RevisionMixin
class AssetCategory(models.Model):
class Meta:
verbose_name = 'Asset Category'
verbose_name_plural = 'Asset Categories'
ordering = ['name']
name = models.CharField(max_length=80)
def __str__(self):
return self.name
class AssetStatus(models.Model):
class Meta:
verbose_name = 'Asset Status'
verbose_name_plural = 'Asset Statuses'
ordering = ['name']
name = models.CharField(max_length=80)
should_show = models.BooleanField(
default=True, help_text="Should this be shown by default in the asset list.")
display_class = models.CharField(max_length=80, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
def __str__(self):
return self.name
@reversion.register
class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80)
class Meta:
permissions = (
('view_supplier', 'Can view a supplier'),
)
def get_absolute_url(self):
return reverse('supplier_list')
def __str__(self):
return self.name
class Connector(models.Model):
description = models.CharField(max_length=80)
current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text='Amps')
voltage_rating = models.IntegerField(help_text='Volts')
num_pins = models.IntegerField(blank=True, null=True)
def __str__(self):
return self.description
@reversion.register
class Asset(models.Model, RevisionMixin):
class Meta:
ordering = ['asset_id_prefix', 'asset_id_number']
permissions = (
('asset_finance', 'Can see financial data for assets'),
('view_asset', 'Can view an asset')
)
parent = models.ForeignKey(to='self', related_name='asset_parent',
blank=True, null=True, on_delete=models.SET_NULL)
asset_id = models.CharField(max_length=15, unique=True)
description = models.CharField(max_length=120)
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
serial_number = models.CharField(max_length=150, blank=True)
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True, related_name="assets")
date_acquired = models.DateField()
date_sold = models.DateField(blank=True, null=True)
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)
# 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)
length = models.DecimalField(decimal_places=1, max_digits=10,
blank=True, null=True, help_text='m')
csa = models.DecimalField(decimal_places=2, max_digits=10,
blank=True, null=True, help_text='mm^2')
circuits = models.IntegerField(blank=True, null=True)
cores = models.IntegerField(blank=True, null=True)
# Hidden asset_id components
# For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001"
asset_id_prefix = models.CharField(max_length=8, default="")
asset_id_number = models.IntegerField(default=1)
def get_available_asset_id(wanted_prefix=""):
sql = """
SELECT a.asset_id_number+1
FROM assets_asset a
LEFT OUTER JOIN assets_asset b ON
(a.asset_id_number + 1 = b.asset_id_number AND
a.asset_id_prefix = b.asset_id_prefix)
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [9000, wanted_prefix])
row = cursor.fetchone()
if row is None or row[0] is None:
return 9000
else:
return row[0]
def get_absolute_url(self):
return reverse('asset_detail', kwargs={'pk': self.asset_id})
def __str__(self):
out = str(self.asset_id) + ' - ' + self.description
if self.is_cable:
out += '{} - {}m - {}'.format(self.plug, self.length, self.socket)
return out
def clean(self):
errdict = {}
if self.date_sold and self.date_acquired > self.date_sold:
errdict["date_sold"] = ["Cannot sell an item before it is acquired"]
self.asset_id = self.asset_id.upper()
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id)
if asset_search is None:
errdict["asset_id"] = [
"An Asset ID can only consist of letters and numbers, with a final number"]
if self.purchase_price and self.purchase_price < 0:
errdict["purchase_price"] = ["A price cannot be negative"]
if self.salvage_value and self.salvage_value < 0:
errdict["salvage_value"] = ["A price cannot be negative"]
if self.is_cable:
if not self.length or self.length <= 0:
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 errdict != {}: # If there was an error when validation
raise ValidationError(errdict)
@receiver(pre_save, sender=Asset)
def pre_save_asset(sender, instance, **kwargs):
"""Automatically fills in hidden members on database access"""
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
if asset_search is None:
instance.asset_id += "1"
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
instance.asset_id_prefix = asset_search.group(1)
instance.asset_id_number = int(asset_search.group(2))

23
assets/static/js/csrf.js Normal file
View File

@@ -0,0 +1,23 @@
$.ajaxSetup({
beforeSend: function(xhr, settings) {
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});

View File

@@ -0,0 +1,92 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
{% load static %}
{% load paginator from filters %}
{% load to_class_name from filters %}
{% block title %}Asset Activity Stream{% endblock %}
{# TODO: Find a way to reduce code duplication...can't just include the content because of the IDs... #}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script src="{% static "js/popover.js" %}"></script>
<script src="{% static "js/moment.min.js" %}"></script>
<script>
$(function () {
$('[data-toggle="popover"]').popover().click(function(){
if($(this).attr('href')){
window.location.href = $(this).attr('href');
}
});
// This keeps timeago values correct, but uses an insane amount of resources
// $(function () {
// setInterval(function() {
// $('.date').each(function (index, dateElem) {
// var $dateElem = $(dateElem);
// var formatted = moment($dateElem.attr('data-date')).fromNow();
// $dateElem.text(formatted);
// })
// });
// }, 10000);
$('.date').each(function (index, dateElem) {
var $dateElem = $(dateElem);
var formatted = moment($dateElem.attr('data-date')).fromNow();
$dateElem.text(formatted);
});
})
</script>
{% endblock %}
{% block content %}
<div class="col-sm-12">
<div class="row">
<div class="col-sm-12">
<h3>Asset Activity Stream</h3>
</div>
<div class="text-right col-sm-12">{% paginator %}</div>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<td>Date</td>
<td>Object</td>
<td>Version ID</td>
<td>User</td>
<td>Changes</td>
<td>Comment</td>
</tr>
</thead>
<tbody>
{% for version in object_list %}
<tr>
<td>{{ version.revision.date_created }}</td>
<td><a href="{{ version.changes.new.get_absolute_url }}">{{version.changes.new|to_class_name}} {{ version.changes.new.asset_id|default:version.changes.new.pk }}</a></td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.changes.old == None %}
{{version.changes.new|to_class_name}} Created
{% else %}
{% include 'RIGS/version_changes.html' %}
{% endif %} </td>
<td>{{ version.changes.revision.comment }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="align-right">{% paginator %}</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% load asset_templatetags %}
{% block title %}Asset {{ object.asset_id }}{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% if duplicate %}
Duplication of Asset: {{ previous_asset_id }}
{% else %}
Create Asset
{% endif %}
</h1>
</div>
{% if duplicate %}
<form method="post" id="asset_update_form" action="{% url 'asset_duplicate' pk=previous_asset_id%}">
{% else %}
<form method="post" id="asset_update_form" action="{% url 'asset_create'%}">
{% 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">
{% include 'partials/asset_form.html' %}
</div>
</div>
<div class="row">
<div class="col-md-6">
{% include 'partials/purchasedetails_form.html' %}
</div>
<div class="col-md-6" hidden="true" id="cable-table">
{% include 'partials/cable_form.html' %}
</div>
<div class="col-md-4">
{% include 'partials/parent_form.html' %}
</div>
</div>
<div class="row">
<div class="col-md-12">
{% include 'partials/asset_buttons.html' %}
</div>
</div>
</form>
{% endblock %}
{% block js%}
<script>
function checkIfCableHidden() {
if (document.getElementById("id_is_cable").checked) {
document.getElementById("cable-table").hidden = false;
} else {
document.getElementById("cable-table").hidden = true;
}
}
checkIfCableHidden();
</script>
{%endblock%}

View File

@@ -0,0 +1,65 @@
{% extends 'base_assets.html' %}
{% block title %}Asset List{% endblock %}
{% load paginator from filters %}
{% load widget_tweaks %}
{% block content %}
<div class="page-header">
<h1 class="text-center">Asset List</h1>
</div>
<form id="asset-search-form" method="get" class="form-inline pull-right">
<div class="input-group pull-right" style="width: auto;">
{% render_field form.query|add_class:'form-control' placeholder='Search by Asset ID/Desc/Serial' style="width: 250px"%}
<label for="query" class="sr-only">Asset ID/Description/Serial Number:</label>
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
</div>
<br>
<div style="margin-top: 1em;" class="pull-right">
<div class="form-group">
<label for="category" class="sr-only">Category</label>
{% render_field form.category|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
</div>
<div class="form-group">
<label for="status" class="sr-only">Status</label>
{% render_field form.status|attr:'multiple'|add_class:'form-control selectpicker' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div>
<!---TODO: Auto filter whenever an option is selected, instead of using a button -->
<button type="submit" class="btn btn-default">Filter</button>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>Asset ID</th>
<th>Description</th>
<th>Category</th>
<th>Status</th>
<th class="hidden-xs">Quick Links</th>
</tr>
</thead>
<tbody id="asset_table_body">
{% include 'partials/asset_list_table_body.html' %}
</tbody>
</table>
{% if is_paginated %}
<div class="text-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static "js/bootstrap-select.js" %}"></script>
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
{% endblock %}

View File

@@ -0,0 +1,72 @@
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% load asset_templatetags %}
{% block title %}Asset {{ object.asset_id }}{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% if edit and object %}
Edit Asset: {{ object.asset_id }}
{% else %}
Asset: {{ object.asset_id }}
{% endif %}
</h1>
</div>
<form method="post" id="asset_update_form" action="{% url 'asset_update' pk=object.asset_id%}">
{% 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">
{% include 'partials/asset_form.html' %}
</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"
{% if not object.is_cable %} hidden="true" {% endif %} id="cable-table">
{% include 'partials/cable_form.html' %}
</div>
<div class="col-md-4">
{% include 'partials/parent_form.html' %}
</div>
</div>
<div class="row">
<div class="col-md-12">
{% include 'partials/asset_buttons.html' %}
</div>
</div>
</form>
{% if not edit %}
<div class="col-sm-12 text-right">
<div>
<a href="{% url 'asset_history' object.asset_id %}" title="View Revision History">
Last edited at {{ object.last_edited_at|default:'never' }} by {{ object.last_edited_by.name|default:'nobody' }}
</a>
</div>
</div>
{% endif %}
{% endblock %}
{% block js%}
{% if edit %}
<script>
function checkIfCableHidden() {
if (document.getElementById("id_is_cable").checked) {
document.getElementById("cable-table").hidden = false;
} else {
document.getElementById("cable-table").hidden = true;
}
}
checkIfCableHidden();
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% for asset in object_list %}
<a href="javascript:void(0);" onclick="insertInParentField({{ asset.pk }}, '{{ asset }}')">
{{ asset.asset_id }} - {{ asset.description }}
</a>
<br>
{% empty %}
No assets match given ID
{% endfor %}
<script>
function insertInParentField(pk, str) {
$('#hidden_parent_id').val(pk);
$('#parent_id').val(str);
M.updateTextFields();
}
</script>

View File

@@ -0,0 +1,68 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_assets.html" %}
{% load to_class_name from filters %}
{% load paginator from filters %}
{% load static %}
{% block title %}{{object|to_class_name}} {{ object.asset_id }} - Revision History{% endblock %}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script src="{% static "js/popover.js" %}"></script>
<script>
$(function () {
$('[data-toggle="popover"]').popover().click(function(){
if($(this).attr('href')){
window.location.href = $(this).attr('href');
}
});
})
</script>
{% endblock %}
{% block content %}
<div class="col-sm-12">
<div class="row">
<div class="col-sm-12">
<h3><a href="{{ object.get_absolute_url }}">{{object|to_class_name}} {{ object.asset_id|default:object.pk }}</a> - Revision History</h3>
</div>
<div class="text-right col-sm-12">{% paginator %}</div>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<td>Date</td>
<td>Version ID</td>
<td>User</td>
<td>Changes</td>
<td>Comment</td>
</tr>
</thead>
<tbody>
{% for version in object_list %}
<tr>
<td>{{ version.revision.date_created }}</td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.changes.old is None %}
{{object|to_class_name}} Created
{% else %}
{% include 'RIGS/version_changes.html' %}
{% endif %}
</td>
<td>
{{ version.revision.comment }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="align-right">{% paginator %}</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% 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-default"><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>
{% endif %}

View File

@@ -0,0 +1,61 @@
{% load widget_tweaks %}
{% load asset_templatetags %}
<div class="panel panel-default">
<div class="panel-heading">
Asset Details
</div>
<div class="panel-body">
{% if create or edit or duplicate %}
<div class="form-group">
<label for="{{ form.asset_id.id_for_label }}">Asset ID</label>
{% if duplicate %}
{% render_field form.asset_id|add_class:'form-control' value=object.asset_id %}
{% elif object.asset_id %}
{% render_field form.asset_id|attr:'readonly'|add_class:'disabled_input form-control' value=object.asset_id %}
{% else %}
{% render_field form.asset_id|add_class:'form-control' %}
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.description.id_for_label }}">Description</label>
{% render_field form.description|add_class:'form-control' value=object.description %}
</div>
<div class="form-group">
<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'%}
</div>
<div class="form-group">
<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>
<!---TODO: Lower default number of lines in comments box-->
<div class="form-group">
<label for="{{ form.comments.id_for_label }}">Comments</label>
{% render_field form.comments|add_class:'form-control' %}
</div>
{% else %}
<dt>Asset ID</dt>
<dd>{{ object.asset_id }}</dd>
<dt>Description</dt>
<dd style="overflow-wrap: break-word;">{{ object.description }}</dd>
<dt>Category</dt>
<dd>{{ object.category }}</dd>
<dt>Status</dt>
<dd>{{ object.status }}</dd>
<dt>Serial Number</dt>
<dd>{{ object.serial_number|default:'-' }}</dd>
<dt>Comments</dt>
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|linebreaksbr }}</dd>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,18 @@
{% 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:"" }}>
<td style="vertical-align: middle;"><a href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></td>
<td style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
<td style="vertical-align: middle;">{{ item.category }}</td>
<td style="vertical-align: middle;">{{ item.status }}</td>
<td class="hidden-xs">
<div class="btn-group" role="group">
<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>
</td>
</tr>
{% endfor %}

View File

@@ -0,0 +1,65 @@
<select name="parent" id="parent_id" class="selectpicker">
{% if object.parent%}
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
{% endif %}
</select>
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static "js/bootstrap-select.js" %}"></script>
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
{% endblock %}
{% block js %}
{{ js.super }}
<script>
$('#parent_id')
.selectpicker({
liveSearch: true
})
.ajaxSelectPicker({
ajax: {
url: '{% url 'asset_search_json'%}',
type: "get",
data: function () {
var params = {
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
};
return params;
}
},
locale: {
emptyTitle: 'Search for item...'
},
preprocessData: function(data){
var assets = [];
if(data.length){
var len = data.length;
for(var i = 0; i < len; i++){
var curr = data[i];
assets.push(
{
'value': curr.id,
'text': curr.label,
'disabled': false
}
);
}
assets.push(
{
'value': null,
'text': "No parent"
});
}
return assets;
},
preserveSelected: false
});
</script>
{% endblock js %}

Some files were not shown because too many files have changed in this diff Show More