Compare commits
81 Commits
travis-fix
...
assets_emb
| Author | SHA1 | Date | |
|---|---|---|---|
| aaaa9fc742 | |||
|
|
e0c6a56263 | ||
| 6aba0478f0 | |||
| 4ad12ab40a | |||
| 13205770f1 | |||
| dfb612367b | |||
| 4fb0a0ffe7 | |||
| 7361605ffa | |||
| 6bb0c88c72 | |||
| 82a30ca77d | |||
|
|
97c0dffbd3 | ||
|
|
3b28eafc82 | ||
| ca8253894a | |||
| 01a87e0e0b | |||
|
|
7c876348d7 | ||
|
ddc23ce4e5
|
|||
|
602ccc15ea
|
|||
|
|
b77615b9b9 | ||
|
|
228d72b7b2 | ||
|
62541194ee
|
|||
|
0d8fd99d92
|
|||
|
9d51a82f31
|
|||
|
c059227d5d
|
|||
|
2c334196d5
|
|||
| 4f036af85a | |||
| 5210afc772 | |||
|
|
4da8040351 | ||
|
|
1a49bb50e5 | ||
|
|
86b349f60e | ||
|
|
35997aa882 | ||
|
|
faa4573f6d | ||
|
|
7babaee44c | ||
|
|
c0fe176495 | ||
|
|
b269069b6a | ||
|
|
7e06b5a162 | ||
|
|
a18bb07d78 | ||
|
|
bd28d2054e | ||
|
|
14836f135c | ||
|
|
5f8a77586a | ||
|
|
e5b7fdbae1 | ||
|
|
f1c8dca8c4 | ||
|
|
843b76d8ea | ||
|
|
e81af9e479 | ||
|
|
efab8c8cef | ||
|
|
3f7531e157 | ||
|
|
988f3dced4 | ||
|
|
bd3240c2bc | ||
|
|
7fbbe2871f | ||
|
|
109ece508b | ||
|
|
971144c2e6 | ||
|
|
a3c6edda0b | ||
|
|
71bb4696b8 | ||
|
|
2a3ed0b763 | ||
|
|
632fa56353 | ||
|
|
f3020fc783 | ||
|
|
9fdf5e674e | ||
|
|
cfe03a8628 | ||
|
|
eccf224b63 | ||
|
|
0b2c86ebb5 | ||
|
|
f616857131 | ||
|
|
60fb90a50c | ||
|
|
66f024e961 | ||
|
|
06daacf611 | ||
|
|
c74bc945b6 | ||
|
|
3c605d2976 | ||
|
|
9720066fd7 | ||
|
|
b157e3b187 | ||
|
|
19030fdf2f | ||
|
|
42450b5a22 | ||
|
|
ce11df9bbc | ||
|
|
82e664c5e0 | ||
|
|
8098b33698 | ||
|
|
9c2603557c | ||
|
|
f4209f21dc | ||
|
|
1e3c021a76 | ||
|
|
d8f9256252 | ||
|
|
014bab6c1f | ||
|
|
8872084cab | ||
|
|
76ceb15000 | ||
|
|
7dff951f28 | ||
|
|
05feb7df1c |
2
.gitignore
vendored
@@ -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
@@ -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"]
|
||||
1
Procfile
@@ -1 +1,2 @@
|
||||
release: python manage.py migrate
|
||||
web: gunicorn PyRIGS.wsgi --log-file -
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
def get_oembed(login_url, request, oembed_view, kwargs):
|
||||
context = {}
|
||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
|
||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||
resp = render(request, 'login_redirect.html', context=context)
|
||||
return resp
|
||||
|
||||
|
||||
def has_oembed(oembed_view, login_url=None):
|
||||
if not login_url:
|
||||
from django.conf import settings
|
||||
login_url = settings.LOGIN_URL
|
||||
|
||||
def _dec(view_func):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
if oembed_view is not None:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
_checklogin.__doc__ = view_func.__doc__
|
||||
_checklogin.__dict__ = view_func.__dict__
|
||||
return _checklogin
|
||||
return _dec
|
||||
|
||||
|
||||
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
||||
"""
|
||||
Decorator for views that checks that the user passes the given test.
|
||||
@@ -23,13 +51,9 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if test_func(request.user):
|
||||
return view_func(request, *args, **kwargs)
|
||||
elif not request.user.is_authenticated():
|
||||
elif not request.user.is_authenticated:
|
||||
if oembed_view is not None:
|
||||
context = {}
|
||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
|
||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||
resp = render(request, 'login_redirect.html', context=context)
|
||||
return resp
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
else:
|
||||
|
||||
@@ -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__))
|
||||
|
||||
@@ -35,6 +36,7 @@ if STAGING:
|
||||
if DEBUG:
|
||||
ALLOWED_HOSTS.append('localhost')
|
||||
ALLOWED_HOSTS.append('example.com')
|
||||
ALLOWED_HOSTS.append('127.0.0.1')
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
if not DEBUG:
|
||||
@@ -56,6 +58,7 @@ INSTALLED_APPS = (
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'RIGS',
|
||||
'assets',
|
||||
|
||||
'debug_toolbar',
|
||||
'registration',
|
||||
@@ -65,7 +68,7 @@ INSTALLED_APPS = (
|
||||
'raven.contrib.django.raven_compat',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
MIDDLEWARE = (
|
||||
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
@@ -74,7 +77,6 @@ MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -12,12 +12,13 @@ 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')),
|
||||
url('^user/', include('registration.backends.default.urls')),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
@@ -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:
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@ import datetime
|
||||
import re
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.urls import reverse_lazy
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from RIGS import models, forms
|
||||
from django_ical.views import ICalFeed
|
||||
from django.db.models import Q
|
||||
from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
260
RIGS/management/commands/generateSampleRIGSData.py
Normal 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())
|
||||
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('postedAt', models.DateTimeField(auto_now=True)),
|
||||
('message', models.TextField()),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
|
||||
@@ -33,11 +33,11 @@ class Migration(migrations.Migration):
|
||||
('payment_method', models.CharField(blank=True, null=True, max_length=255)),
|
||||
('payment_received', models.CharField(blank=True, null=True, max_length=255)),
|
||||
('purchase_order', models.CharField(blank=True, null=True, max_length=255)),
|
||||
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events')),
|
||||
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in')),
|
||||
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic')),
|
||||
('organisation', models.ForeignKey(to='RIGS.Organisation')),
|
||||
('person', models.ForeignKey(to='RIGS.Person')),
|
||||
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events', on_delete=models.CASCADE)),
|
||||
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', on_delete=models.CASCADE)),
|
||||
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', on_delete=models.CASCADE)),
|
||||
('organisation', models.ForeignKey(to='RIGS.Organisation', on_delete=models.CASCADE)),
|
||||
('person', models.ForeignKey(to='RIGS.Person', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -52,7 +52,7 @@ class Migration(migrations.Migration):
|
||||
('quantity', models.IntegerField()),
|
||||
('cost', models.DecimalField(max_digits=10, decimal_places=2)),
|
||||
('order', models.IntegerField()),
|
||||
('event', models.ForeignKey(to='RIGS.Event', related_name='item')),
|
||||
('event', models.ForeignKey(to='RIGS.Event', related_name='item', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -75,7 +75,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='venue',
|
||||
field=models.ForeignKey(to='RIGS.Venue'),
|
||||
field=models.ForeignKey(to='RIGS.Venue', on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -14,26 +14,26 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='based_on',
|
||||
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True),
|
||||
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='checked_in_by',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True,
|
||||
null=True),
|
||||
null=True, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='mic',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True),
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='organisation',
|
||||
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True),
|
||||
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
||||
@@ -19,8 +19,8 @@ class Migration(migrations.Migration):
|
||||
('run', models.BooleanField(default=False)),
|
||||
('derig', models.BooleanField(default=False)),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('event', models.ForeignKey(related_name='crew', to='RIGS.Event')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('event', models.ForeignKey(related_name='crew', to='RIGS.Event', on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='eventitem',
|
||||
name='event',
|
||||
field=models.ForeignKey(related_name='items', to='RIGS.Event'),
|
||||
field=models.ForeignKey(related_name='items', to='RIGS.Event', on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='person',
|
||||
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person'),
|
||||
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person', on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -14,13 +14,13 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='venue',
|
||||
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True),
|
||||
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventitem',
|
||||
name='event',
|
||||
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event'),
|
||||
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event', on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('invoice_date', models.DateField(auto_now_add=True)),
|
||||
('void', models.BooleanField()),
|
||||
('event', models.OneToOneField(to='RIGS.Event')),
|
||||
('event', models.OneToOneField(to='RIGS.Event', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
|
||||
('date', models.DateField()),
|
||||
('amount', models.DecimalField(help_text=b'Please use ex. VAT', max_digits=10, decimal_places=2)),
|
||||
('method', models.CharField(max_length=2, choices=[(b'C', b'Cash'), (b'I', b'Internal'), (b'E', b'External'), (b'SU', b'SU Core'), (b'M', b'Members')])),
|
||||
('invoice', models.ForeignKey(to='RIGS.Invoice')),
|
||||
('invoice', models.ForeignKey(to='RIGS.Invoice', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='mic',
|
||||
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
||||
('account_code', models.CharField(max_length=50, null=True, blank=True)),
|
||||
('amount', models.DecimalField(verbose_name=b'authorisation amount', max_digits=10, decimal_places=2)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('event', models.ForeignKey(related_name='authroisations', to='RIGS.Event')),
|
||||
('event', models.ForeignKey(related_name='authroisations', to='RIGS.Event', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='event',
|
||||
field=models.OneToOneField(related_name='authorisation', to='RIGS.Event'),
|
||||
field=models.OneToOneField(related_name='authorisation', to='RIGS.Event', on_delete=models.CASCADE),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='eventauthorisation',
|
||||
name='sent_by',
|
||||
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL),
|
||||
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='auth_request_by',
|
||||
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
|
||||
18
RIGS/migrations/0033_auto_20180325_0016.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.3 on 2018-03-25 00:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0032_auto_20170904_2355'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='last_name',
|
||||
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0034_event_risk_assessment_edit_url.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0035_auto_20191124_1319.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -18,7 +18,7 @@ from collections import Counter
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
|
||||
# Create your models here.
|
||||
@@ -302,9 +302,9 @@ class Event(models.Model, RevisionMixin):
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
person = models.ForeignKey('Person', null=True, blank=True)
|
||||
organisation = models.ForeignKey('Organisation', blank=True, null=True)
|
||||
venue = models.ForeignKey('Venue', blank=True, null=True)
|
||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||
@@ -323,9 +323,9 @@ class Event(models.Model, RevisionMixin):
|
||||
meet_info = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# Crew management
|
||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True)
|
||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True, on_delete=models.CASCADE)
|
||||
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
||||
verbose_name="MIC")
|
||||
verbose_name="MIC", on_delete=models.CASCADE)
|
||||
|
||||
# Monies
|
||||
payment_method = models.CharField(max_length=255, blank=True, null=True)
|
||||
@@ -334,10 +334,13 @@ class Event(models.Model, RevisionMixin):
|
||||
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
|
||||
|
||||
# Authorisation request details
|
||||
auth_request_by = models.ForeignKey('Profile', null=True, blank=True)
|
||||
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
||||
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
|
||||
@@ -485,7 +488,7 @@ class Event(models.Model, RevisionMixin):
|
||||
|
||||
|
||||
class EventItem(models.Model):
|
||||
event = models.ForeignKey('Event', related_name='items', blank=True)
|
||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
quantity = models.IntegerField()
|
||||
@@ -504,8 +507,8 @@ class EventItem(models.Model):
|
||||
|
||||
|
||||
class EventCrew(models.Model):
|
||||
event = models.ForeignKey('Event', related_name='crew')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
rig = models.BooleanField(default=False)
|
||||
run = models.BooleanField(default=False)
|
||||
derig = models.BooleanField(default=False)
|
||||
@@ -514,13 +517,13 @@ class EventCrew(models.Model):
|
||||
|
||||
@reversion.register
|
||||
class EventAuthorisation(models.Model, RevisionMixin):
|
||||
event = models.OneToOneField('Event', related_name='authorisation')
|
||||
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
||||
email = models.EmailField()
|
||||
name = models.CharField(max_length=255)
|
||||
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
|
||||
account_code = models.CharField(max_length=50, blank=True, null=True)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||
sent_by = models.ForeignKey('RIGS.Profile')
|
||||
sent_by = models.ForeignKey('RIGS.Profile', on_delete=models.CASCADE)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
|
||||
@@ -532,7 +535,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Invoice(models.Model):
|
||||
event = models.OneToOneField('Event')
|
||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||
invoice_date = models.DateField(auto_now_add=True)
|
||||
void = models.BooleanField(default=False)
|
||||
|
||||
@@ -584,7 +587,7 @@ class Payment(models.Model):
|
||||
(ADJUSTMENT, 'TEC Adjustment'),
|
||||
)
|
||||
|
||||
invoice = models.ForeignKey('Invoice')
|
||||
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)
|
||||
|
||||
@@ -6,18 +6,19 @@ import urllib.parse
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
from django.views import generic
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.core import signing
|
||||
from django.http import HttpResponse
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db.models import Q
|
||||
from django.contrib import messages
|
||||
from django.utils.decorators import method_decorator
|
||||
from 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
|
||||
@@ -138,7 +158,11 @@ class EventDuplicate(EventUpdate):
|
||||
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
|
||||
new = copy.copy(old) # Make a copy of the object in memory
|
||||
new.based_on = old # Make the new event based on the old event
|
||||
new.purchase_order = None
|
||||
new.purchase_order = None # Remove old PO
|
||||
|
||||
# Clear checked in by if it's a dry hire
|
||||
if new.dry_hire is True:
|
||||
new.checked_in_by = None
|
||||
|
||||
# Remove all the authorisation information from the new event
|
||||
new.auth_request_to = None
|
||||
@@ -340,8 +364,10 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
||||
'sent_by': self.request.user.pk,
|
||||
}),
|
||||
}
|
||||
if email == event.person.email:
|
||||
if event.person is not None and email == event.person.email:
|
||||
context['to_name'] = event.person.name
|
||||
elif event.organisation is not None and email == event.organisation.email:
|
||||
context['to_name'] = event.organisation.name
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
|
||||
@@ -380,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)
|
||||
|
||||
@@ -50,8 +50,10 @@ def send_eventauthorisation_success_email(instance):
|
||||
'object': instance,
|
||||
}
|
||||
|
||||
if instance.email == instance.event.person.email:
|
||||
if instance.event.person is not None and instance.email == instance.event.person.email:
|
||||
context['to_name'] = instance.event.person.name
|
||||
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
||||
context['to_name'] = instance.event.organisation.name
|
||||
|
||||
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
|
||||
|
||||
@@ -89,6 +91,10 @@ def send_eventauthorisation_success_email(instance):
|
||||
client_email.send(fail_silently=True)
|
||||
mic_email.send(fail_silently=True)
|
||||
|
||||
# Set event to booked now that it's authorised
|
||||
instance.event.status = models.Event.BOOKED
|
||||
instance.event.save()
|
||||
|
||||
|
||||
def on_revision_commit(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -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{
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load paginator from filters %}
|
||||
|
||||
{% block title %}Event Archive{% endblock %}
|
||||
|
||||
@@ -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> </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"> </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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
{% load multiply from filters %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load paginator from filters %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p>
|
||||
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
|
||||
for <b>£{{ object.amount }}</b>
|
||||
by <b>{{ object.name }}</b> as of <b>{{ object.last_edited_at }}</b>.
|
||||
by <b>{{ object.name }}</b> as of <b>{{ object.event.last_edited_at }}</b>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Invoice {{ object.pk }}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load paginator from filters %}
|
||||
|
||||
{% block title %}Invoices{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
9
RIGS/templates/RIGS/password_reset_disable.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Password Reset Disabled{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Password reset is disabled</h1>
|
||||
<p> We are very sorry for the inconvenience, but due to a security vulnerability, password reset is currently disabled until the vulnerability can be patched.</p>
|
||||
<p> If you are locked out of your account, please contact an administrator and we can manually perform a reset</p>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Update Profile {{object.name}}{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base_rigs.html' %}
|
||||
|
||||
{% block title %}Rigboard{% endblock %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -18,7 +18,7 @@ from selenium.webdriver.support.ui import WebDriverWait
|
||||
from RIGS import models
|
||||
|
||||
from reversion import revisions as reversion
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.core import mail, signing
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import date
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -19,7 +19,7 @@ urlpatterns = [
|
||||
url('^user/login/$', views.login, name='login'),
|
||||
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
|
||||
|
||||
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}),
|
||||
url(r'^user/password_reset/$', views.PasswordResetDisabled.as_view()),
|
||||
|
||||
# People
|
||||
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
|
||||
@@ -101,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'),
|
||||
@@ -185,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')),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.views import generic
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -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
|
||||
|
||||
"""
|
||||
@@ -34,7 +35,7 @@ class Index(generic.TemplateView):
|
||||
|
||||
|
||||
def login(request, **kwargs):
|
||||
if request.user.is_authenticated():
|
||||
if request.user.is_authenticated:
|
||||
next = request.GET.get('next', '/')
|
||||
return HttpResponseRedirect(next)
|
||||
else:
|
||||
@@ -49,7 +50,7 @@ def login(request, **kwargs):
|
||||
# check for it before logging the user in
|
||||
@csrf_exempt
|
||||
def login_embed(request, **kwargs):
|
||||
if request.user.is_authenticated():
|
||||
if request.user.is_authenticated:
|
||||
next = request.GET.get('next', '/')
|
||||
return HttpResponseRedirect(next)
|
||||
else:
|
||||
@@ -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
|
||||
}
|
||||
|
||||
'''
|
||||
@@ -389,3 +392,7 @@ class ResetApiKey(generic.RedirectView):
|
||||
self.request.user.save()
|
||||
|
||||
return reverse_lazy('profile_detail')
|
||||
|
||||
|
||||
class PasswordResetDisabled(generic.TemplateView):
|
||||
template_name = "RIGS/password_reset_disable.html"
|
||||
|
||||
0
assets/__init__.py
Normal file
32
assets/admin.py
Normal 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
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AssetsConfig(AppConfig):
|
||||
name = 'assets'
|
||||
9
assets/filters.py
Normal 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
@@ -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)
|
||||
0
assets/management/__init__.py
Normal file
0
assets/management/commands/__init__.py
Normal file
23
assets/management/commands/deleteSampleData.py
Normal 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()
|
||||
120
assets/management/commands/generateSampleAssetsData.py
Normal 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()
|
||||
229
assets/management/commands/import_old_db.py
Normal 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]))
|
||||
110
assets/management/commands/update_old_db_file.py
Normal 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)
|
||||
86
assets/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0002_auto_20180301_1654.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0003_auto_20180301_1700.py
Normal 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),
|
||||
),
|
||||
]
|
||||
19
assets/migrations/0004_auto_20180301_1711.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0005_auto_20180301_1725.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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'),)},
|
||||
),
|
||||
]
|
||||
51
assets/migrations/0008_auto_20191206_2124.py
Normal 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,
|
||||
),
|
||||
]
|
||||
32
assets/migrations/0009_auto_20200103_2215.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 2.0.13 on 2020-01-03 22:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
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'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assetstatus',
|
||||
name='display_class',
|
||||
field=models.CharField(blank=True, help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80, null=True),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
]
|
||||
0
assets/migrations/__init__.py
Normal file
179
assets/models.py
Normal 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, blank=True, null=True, 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
@@ -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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
92
assets/templates/asset_activity_table.html
Normal 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 %}
|
||||