mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-08 16:09:40 +00:00
Compare commits
1 Commits
5174a442bc
...
feature/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7889d80e |
@@ -1,9 +1,6 @@
|
|||||||
[run]
|
[run]
|
||||||
source =
|
source =
|
||||||
./
|
./
|
||||||
plugins =
|
|
||||||
django_coverage_plugin
|
|
||||||
|
|
||||||
omit =
|
omit =
|
||||||
*/migrations/*
|
*/migrations/*
|
||||||
*/tests/*
|
|
||||||
|
|||||||
73
.github/workflows/django.yml
vendored
73
.github/workflows/django.yml
vendored
@@ -1,73 +0,0 @@
|
|||||||
name: Django CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
test-group: ["RIGS", "versioning", "users", "assets"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
- name: Cache python deps
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ env.pythonLocation }}
|
|
||||||
key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }}
|
|
||||||
- name: Setup Chromedriver
|
|
||||||
if: \!${{ matrix.parallel }}
|
|
||||||
run: |
|
|
||||||
wget https://chromedriver.storage.googleapis.com/2.36/chromedriver_linux64.zip
|
|
||||||
unzip chromedriver_linux64.zip
|
|
||||||
export PATH=$PATH:$(pwd)
|
|
||||||
chmod +x chromedriver
|
|
||||||
export PATH=$PATH:/usr/lib/chromium-browser/
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install pycodestyle coverage coveralls django_coverage_plugin
|
|
||||||
pip install --upgrade --upgrade-strategy eager -r requirements.txt
|
|
||||||
python manage.py collectstatic --noinput
|
|
||||||
- name: Basic Checks
|
|
||||||
run: |
|
|
||||||
pycodestyle . --exclude=migrations,importer*
|
|
||||||
python manage.py check
|
|
||||||
python manage.py makemigrations --check --dry-run
|
|
||||||
- name: Run Tests
|
|
||||||
run: coverage run -p -m pytest --cov=${{ matrix.test-group }} --cov-append -n 8 ${{ matrix.test-group }}/tests/
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: failure-screenshots ${{ matrix.test-group }}
|
|
||||||
path: screenshots/
|
|
||||||
retention-days: 5
|
|
||||||
- name: Upload Coverage
|
|
||||||
run: coverage combine && coveralls --service=github
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
COVERALLS_FLAG_NAME: ${{ matrix.test-group }}
|
|
||||||
COVERALLS_PARALLEL: true
|
|
||||||
coveralls:
|
|
||||||
name: Indicate completion to coveralls.io
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: python:3-slim
|
|
||||||
steps:
|
|
||||||
- name: Finished
|
|
||||||
run: |
|
|
||||||
pip3 install --upgrade coveralls
|
|
||||||
coveralls --service=github --finish
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,7 +25,6 @@ var/
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Continer extras
|
# Continer extras
|
||||||
.vagrant
|
.vagrant
|
||||||
@@ -45,7 +44,6 @@ htmlcov/
|
|||||||
.tox/
|
.tox/
|
||||||
.coverage
|
.coverage
|
||||||
.cache
|
.cache
|
||||||
.pytest_cache
|
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
|
||||||
@@ -108,5 +106,3 @@ atlassian-ide-plugin.xml
|
|||||||
com_crashlytics_export_strings.xml
|
com_crashlytics_export_strings.xml
|
||||||
crashlytics.properties
|
crashlytics.properties
|
||||||
crashlytics-build.properties
|
crashlytics-build.properties
|
||||||
.vscode/
|
|
||||||
screenshots/
|
|
||||||
|
|||||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PyRIGS
|
||||||
5
.idea/encodings.xml
generated
Normal file
5
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
|
||||||
|
</project>
|
||||||
|
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/pyrigs.iml" filepath="$PROJECT_DIR$/.idea/pyrigs.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
5
.idea/scopes/scope_settings.xml
generated
Normal file
5
.idea/scopes/scope_settings.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="DependencyValidationManager">
|
||||||
|
<state>
|
||||||
|
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
||||||
1156
.rubocop.yml
Normal file
1156
.rubocop.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
|||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.scss
|
*.scss
|
||||||
*.md
|
*.md
|
||||||
|
*.rb
|
||||||
|
Vagrantfile
|
||||||
|
config/vagrant/*
|
||||||
|
config/vagrant.yml
|
||||||
|
|||||||
21
.travis.yml
Normal file
21
.travis.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
"2.7"
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- "export DISPLAY=:99.0"
|
||||||
|
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- pip install coveralls codeclimate-test-reporter
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
script:
|
||||||
|
- coverage run manage.py test RIGS
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- coveralls
|
||||||
|
- codeclimate-test-reporter
|
||||||
5
DiscourseAuth/admin.py
Normal file
5
DiscourseAuth/admin.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from models import AuthAttempt, DiscourseUserLink
|
||||||
|
|
||||||
|
admin.site.register(AuthAttempt)
|
||||||
|
admin.site.register(DiscourseUserLink)
|
||||||
22
DiscourseAuth/migrations/0001_initial.py
Normal file
22
DiscourseAuth/migrations/0001_initial.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import DiscourseAuth.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AuthAttempt',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('nonce', models.CharField(default=DiscourseAuth.models.gen_nonce, max_length=25)),
|
||||||
|
('created', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
19
DiscourseAuth/migrations/0002_auto_20170126_1513.py
Normal file
19
DiscourseAuth/migrations/0002_auto_20170126_1513.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('DiscourseAuth', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='authattempt',
|
||||||
|
old_name='created',
|
||||||
|
new_name='created_at',
|
||||||
|
),
|
||||||
|
]
|
||||||
19
DiscourseAuth/migrations/0003_auto_20170126_1621.py
Normal file
19
DiscourseAuth/migrations/0003_auto_20170126_1621.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('DiscourseAuth', '0002_auto_20170126_1513'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='authattempt',
|
||||||
|
old_name='created_at',
|
||||||
|
new_name='created',
|
||||||
|
),
|
||||||
|
]
|
||||||
24
DiscourseAuth/migrations/0004_discourseuserlink.py
Normal file
24
DiscourseAuth/migrations/0004_discourseuserlink.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('DiscourseAuth', '0003_auto_20170126_1621'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DiscourseUserLink',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('discourse_user_id', models.IntegerField()),
|
||||||
|
('django_user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
19
DiscourseAuth/migrations/0005_auto_20170128_1707.py
Normal file
19
DiscourseAuth/migrations/0005_auto_20170128_1707.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('DiscourseAuth', '0004_discourseuserlink'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='discourseuserlink',
|
||||||
|
name='discourse_user_id',
|
||||||
|
field=models.IntegerField(unique=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
47
DiscourseAuth/models.py
Normal file
47
DiscourseAuth/models.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import uuid
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class AuthAttemptManager(models.Manager):
|
||||||
|
expiryMinutes = 10
|
||||||
|
|
||||||
|
def get_acceptable(self):
|
||||||
|
oldestAcceptableNonce = datetime.now() - timedelta(minutes=self.expiryMinutes)
|
||||||
|
return super(AuthAttemptManager, self).get_queryset().filter(created__gte=oldestAcceptableNonce)
|
||||||
|
|
||||||
|
def purge_unacceptable(self):
|
||||||
|
oldestAcceptableNonce = datetime.now() - timedelta(minutes=self.expiryMinutes)
|
||||||
|
super(AuthAttemptManager, self).get_queryset().filter(created__lt=oldestAcceptableNonce).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def gen_nonce():
|
||||||
|
# return "THISISANONCETHATWEWILLREUSE"
|
||||||
|
return uuid.uuid4()
|
||||||
|
|
||||||
|
|
||||||
|
class AuthAttempt(models.Model):
|
||||||
|
nonce = models.CharField(max_length=25, default=gen_nonce)
|
||||||
|
created = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
objects = AuthAttemptManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "AuthAttempt at " + str(self.created)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseUserLink(models.Model):
|
||||||
|
django_user = models.OneToOneField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
discourse_user_id = models.IntegerField(unique=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{} - {}".format(self.discourse_user_id, str(self.django_user))
|
||||||
14
DiscourseAuth/templates/DiscourseAuth/associate_user.html
Normal file
14
DiscourseAuth/templates/DiscourseAuth/associate_user.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Associate User" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>You are currently logged in to django as "{{ djangouser }}". If this isn't you please log out</p>
|
||||||
|
<p>If you would like to link Discourse account "{{ discourseuser }}" to your django account, click below. This will remove any existing links.</p>
|
||||||
|
<input type="submit" value="{% trans 'Link my accounts' %}" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
16
DiscourseAuth/templates/DiscourseAuth/disassociate_user.html
Normal file
16
DiscourseAuth/templates/DiscourseAuth/disassociate_user.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Associate User" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if haslink %}
|
||||||
|
<form method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Your account is currently linked to a Discourse account. To remove this link, click below. You will no longer be able to login using Discourse.</p>
|
||||||
|
<input type="submit" value="{% trans 'Un-Link my accounts' %}" />
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p>Your account is not currently linked to Discourse.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
11
DiscourseAuth/urls.py
Normal file
11
DiscourseAuth/urls.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from django.conf.urls import patterns, url
|
||||||
|
|
||||||
|
from views import StartDiscourseAuth, ContinueDiscourseAuth, NewDiscourseUser, AssociateDiscourseUser, DisassociateDiscourseUser
|
||||||
|
|
||||||
|
urlpatterns = patterns('DiscourseAuth',
|
||||||
|
url(r'^start/$', StartDiscourseAuth.as_view(), name='start-auth'),
|
||||||
|
url(r'^continue/$', ContinueDiscourseAuth.as_view(), name='continue-auth'),
|
||||||
|
url(r'^new/$', NewDiscourseUser.as_view(), name='new-user'),
|
||||||
|
url(r'^associate/$', AssociateDiscourseUser.as_view(), name='associate-user'),
|
||||||
|
url(r'^disassociate/$', DisassociateDiscourseUser.as_view(), name='disassociate-user')
|
||||||
|
)
|
||||||
253
DiscourseAuth/views.py
Normal file
253
DiscourseAuth/views.py
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
|
||||||
|
from django.views.generic import View, FormView, TemplateView
|
||||||
|
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
import urllib
|
||||||
|
from hashlib import sha256
|
||||||
|
import hmac
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
|
from models import AuthAttempt, DiscourseUserLink
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from registration.forms import RegistrationForm
|
||||||
|
|
||||||
|
|
||||||
|
class StartDiscourseAuth(View):
|
||||||
|
http_method_names = ['get']
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Where do we want to go once authentication is complete?
|
||||||
|
request.session['discourse_next'] = request.GET.get('next', "/")
|
||||||
|
|
||||||
|
# Generate random 'nonce'
|
||||||
|
attempt = AuthAttempt.objects.create()
|
||||||
|
nonce = attempt.nonce
|
||||||
|
|
||||||
|
# Where do we want discourse to send authenticated users?
|
||||||
|
redirect_uri = reverse('continue-auth')
|
||||||
|
redirect_uri = request.build_absolute_uri(redirect_uri)
|
||||||
|
|
||||||
|
# Data to sent to Discourse (payload)
|
||||||
|
data = {
|
||||||
|
'nonce': nonce,
|
||||||
|
'return_sso_url': redirect_uri
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = urllib.urlencode(data)
|
||||||
|
b64payload = b64encode(payload.encode())
|
||||||
|
|
||||||
|
sig = hmac.new(settings.DISCOURSE_SECRET_KEY.encode(), b64payload, sha256).hexdigest()
|
||||||
|
|
||||||
|
return HttpResponseRedirect(settings.DISCOURSE_BASE_URL + '/session/sso_provider?' + urllib.urlencode({'sso': b64payload, 'sig': sig}))
|
||||||
|
|
||||||
|
|
||||||
|
class ContinueDiscourseAuth(View):
|
||||||
|
http_method_names = ['get']
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Where do we want to go once authentication is complete?
|
||||||
|
nextUrl = request.session.get('discourse_next', "/")
|
||||||
|
|
||||||
|
rawSig = request.GET['sig']
|
||||||
|
rawSSO = request.GET['sso']
|
||||||
|
|
||||||
|
payload = urllib.unquote(rawSSO)
|
||||||
|
|
||||||
|
computed_sig = hmac.new(
|
||||||
|
settings.DISCOURSE_SECRET_KEY.encode(),
|
||||||
|
payload.encode(),
|
||||||
|
sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
successful = hmac.compare_digest(computed_sig, rawSig.encode())
|
||||||
|
|
||||||
|
if not successful: # The signature doesn't match, not legit
|
||||||
|
raise ValidationError("Signature does not match, data has been manipulated")
|
||||||
|
|
||||||
|
decodedPayload = urllib.unquote_plus(b64decode(urllib.unquote(payload)).decode())
|
||||||
|
data = dict(data.split("=") for data in decodedPayload.split('&'))
|
||||||
|
|
||||||
|
# Get the nonce that's been returned by discourse
|
||||||
|
returnedNonce = data['nonce']
|
||||||
|
|
||||||
|
try: # See if it's in the database
|
||||||
|
storedAttempt = AuthAttempt.objects.get_acceptable().get(nonce=returnedNonce)
|
||||||
|
except AuthAttempt.DoesNotExist: # If it's not, this attempt is not valid
|
||||||
|
raise ValidationError("Nonce does not exist in database")
|
||||||
|
|
||||||
|
# Delete the nonce from the database - don't allow it to be reused
|
||||||
|
storedAttempt.delete()
|
||||||
|
# While we're at it, delete all the other expired attempts
|
||||||
|
AuthAttempt.objects.purge_unacceptable()
|
||||||
|
|
||||||
|
# If we've got this far, the attempt is valid, so let's load user information
|
||||||
|
external_id = int(data['external_id'])
|
||||||
|
|
||||||
|
# See if the user is already linked to a django user
|
||||||
|
try:
|
||||||
|
userLink = DiscourseUserLink.objects.get(discourse_user_id=external_id)
|
||||||
|
except DiscourseUserLink.DoesNotExist:
|
||||||
|
return self.linkNewUser(request, data)
|
||||||
|
|
||||||
|
# Load the user
|
||||||
|
user = userLink.django_user
|
||||||
|
# Slightly hacky way to login user without calling authenticate()
|
||||||
|
user.backend = "%s.%s" % (ModelBackend.__module__, ModelBackend.__name__)
|
||||||
|
# Login the user
|
||||||
|
login(request, user)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(nextUrl)
|
||||||
|
|
||||||
|
def linkNewUser(self, request, data):
|
||||||
|
# Great, let's save the new user info in the session
|
||||||
|
request.session['discourse_data'] = data
|
||||||
|
request.session['discourse_started_registration'] = time.time()
|
||||||
|
|
||||||
|
if request.user is not None:
|
||||||
|
return HttpResponseRedirect(reverse('associate-user'))
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse('new-user'))
|
||||||
|
|
||||||
|
|
||||||
|
class SocialRegisterForm(RegistrationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields.pop('password1')
|
||||||
|
self.fields.pop('password2')
|
||||||
|
|
||||||
|
self.fields['email'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
initial = getattr(self, 'initial', None)
|
||||||
|
if(initial['email'] != self.cleaned_data['email']):
|
||||||
|
raise ValidationError("You cannot change the email")
|
||||||
|
|
||||||
|
return initial['email']
|
||||||
|
|
||||||
|
|
||||||
|
class AssociateDiscourseUser(TemplateView):
|
||||||
|
template_name = "DiscourseAuth/associate_user.html"
|
||||||
|
|
||||||
|
@method_decorator(login_required) # Require user is logged in for associating their account
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.data = self.request.session.get('discourse_data', None)
|
||||||
|
timeStarted = self.request.session.get('discourse_started_registration', 0)
|
||||||
|
|
||||||
|
max_reg_time = 20 * 60 # Seconds
|
||||||
|
|
||||||
|
if timeStarted < (time.time() - max_reg_time):
|
||||||
|
raise PermissionDenied('The Discourse authentication has expired, please try again')
|
||||||
|
|
||||||
|
if self.data is None:
|
||||||
|
raise PermissionDenied('Discourse authentication data is not present in this session')
|
||||||
|
|
||||||
|
return super(AssociateDiscourseUser, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, request, **kwargs):
|
||||||
|
return super(AssociateDiscourseUser, self).get(self, request, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
c = super(AssociateDiscourseUser, self).get_context_data()
|
||||||
|
c['discourseuser'] = self.data['username']
|
||||||
|
c['djangouser'] = self.request.user.username
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
def post(self, request, **kwargs):
|
||||||
|
DiscourseUserLink.objects.filter(Q(django_user=request.user) | Q(discourse_user_id=self.data['external_id'])).delete()
|
||||||
|
DiscourseUserLink.objects.create(django_user=request.user, discourse_user_id=self.data['external_id'])
|
||||||
|
|
||||||
|
messages.success(self.request, 'Accounts successfully linked, you are now logged in.')
|
||||||
|
|
||||||
|
# Redirect them to the discourse login URL
|
||||||
|
nextUrl = "{}?next={}".format(reverse('start-auth'), request.session.get('discourse_next', "/"))
|
||||||
|
return HttpResponseRedirect(nextUrl)
|
||||||
|
|
||||||
|
|
||||||
|
class NewDiscourseUser(FormView):
|
||||||
|
template_name = 'registration/registration_form.html'
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.data = self.request.session.get('discourse_data', None)
|
||||||
|
timeStarted = self.request.session.get('discourse_started_registration', 0)
|
||||||
|
|
||||||
|
max_reg_time = 20 * 60 # Seconds
|
||||||
|
|
||||||
|
if timeStarted < (time.time() - max_reg_time):
|
||||||
|
raise PermissionDenied('The Discourse authentication has expired, please try again')
|
||||||
|
|
||||||
|
if self.data is None:
|
||||||
|
raise PermissionDenied('Discourse authentication data is not present in this session')
|
||||||
|
|
||||||
|
return super(NewDiscourseUser, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
data = self.data
|
||||||
|
initialForm = {
|
||||||
|
'username': data['username'],
|
||||||
|
'email': data['email'],
|
||||||
|
'name': data['name']
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialForm
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
if settings.DISCOURSE_REGISTRATION_FORM:
|
||||||
|
return settings.DISCOURSE_REGISTRATION_FORM
|
||||||
|
else:
|
||||||
|
return SocialRegisterForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# This method is called when valid form data has been POSTed.
|
||||||
|
user = get_user_model().objects.create_user(**form.cleaned_data)
|
||||||
|
|
||||||
|
# Link the user to Discourse account
|
||||||
|
DiscourseUserLink.objects.filter(discourse_user_id=self.data['external_id']).delete()
|
||||||
|
DiscourseUserLink.objects.create(django_user=user, discourse_user_id=self.data['external_id'])
|
||||||
|
|
||||||
|
messages.success(self.request, 'Account successfully created, you are now logged in.')
|
||||||
|
|
||||||
|
# Redirect them to the discourse login URL
|
||||||
|
nextUrl = "{}?next={}".format(reverse('start-auth'), self.request.session.get('discourse_next', "/"))
|
||||||
|
return HttpResponseRedirect(nextUrl)
|
||||||
|
|
||||||
|
|
||||||
|
class DisassociateDiscourseUser(TemplateView):
|
||||||
|
template_name = "DiscourseAuth/disassociate_user.html"
|
||||||
|
|
||||||
|
@method_decorator(login_required) # Require user is logged in for associating their account
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
return super(DisassociateDiscourseUser, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
c = super(DisassociateDiscourseUser, self).get_context_data()
|
||||||
|
|
||||||
|
links = DiscourseUserLink.objects.filter(django_user=self.request.user)
|
||||||
|
|
||||||
|
c['haslink'] = links.count() > 0
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
def post(self, request, **kwargs):
|
||||||
|
DiscourseUserLink.objects.filter(django_user=request.user).delete()
|
||||||
|
|
||||||
|
return self.get(self, request, **kwargs)
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,12 +0,0 @@
|
|||||||
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
Procfile
@@ -1,2 +1 @@
|
|||||||
release: python manage.py migrate
|
|
||||||
web: gunicorn PyRIGS.wsgi --log-file -
|
web: gunicorn PyRIGS.wsgi --log-file -
|
||||||
|
|||||||
@@ -1,42 +1,12 @@
|
|||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render_to_response
|
||||||
|
from django.template import RequestContext
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from RIGS import models
|
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):
|
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.
|
Decorator for views that checks that the user passes the given test.
|
||||||
@@ -54,20 +24,22 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
|||||||
def _checklogin(request, *args, **kwargs):
|
def _checklogin(request, *args, **kwargs):
|
||||||
if test_func(request.user):
|
if test_func(request.user):
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
elif not request.user.is_authenticated:
|
elif not request.user.is_authenticated():
|
||||||
if oembed_view is not None:
|
if oembed_view is not None:
|
||||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
extra_context = {}
|
||||||
|
extra_context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
|
||||||
|
extra_context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||||
|
resp = render_to_response('login_redirect.html', extra_context, context_instance=RequestContext(request))
|
||||||
|
return resp
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||||
else:
|
else:
|
||||||
resp = render(request, '403.html')
|
resp = render_to_response('403.html', context_instance=RequestContext(request))
|
||||||
resp.status_code = 403
|
resp.status_code = 403
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
_checklogin.__doc__ = view_func.__doc__
|
_checklogin.__doc__ = view_func.__doc__
|
||||||
_checklogin.__dict__ = view_func.__dict__
|
_checklogin.__dict__ = view_func.__dict__
|
||||||
return _checklogin
|
return _checklogin
|
||||||
|
|
||||||
return _dec
|
return _dec
|
||||||
|
|
||||||
|
|
||||||
@@ -85,13 +57,12 @@ def api_key_required(function):
|
|||||||
Failed users will be given a 403 error.
|
Failed users will be given a 403 error.
|
||||||
Should only be used for urls which include <api_pk> and <api_key> kwargs
|
Should only be used for urls which include <api_pk> and <api_key> kwargs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
|
|
||||||
userid = kwargs.get('api_pk')
|
userid = kwargs.get('api_pk')
|
||||||
key = kwargs.get('api_key')
|
key = kwargs.get('api_key')
|
||||||
|
|
||||||
error_resp = render(request, '403.html')
|
error_resp = render_to_response('403.html', context_instance=RequestContext(request))
|
||||||
error_resp.status_code = 403
|
error_resp.status_code = 403
|
||||||
|
|
||||||
if key is None:
|
if key is None:
|
||||||
@@ -107,21 +78,4 @@ def api_key_required(function):
|
|||||||
if user_object.api_key != key:
|
if user_object.api_key != key:
|
||||||
return error_resp
|
return error_resp
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
|
|
||||||
return wrap
|
|
||||||
|
|
||||||
|
|
||||||
def nottinghamtec_address_required(function):
|
|
||||||
"""
|
|
||||||
Checks that the current user has an email address ending @nottinghamtec.co.uk
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrap(request, *args, **kwargs):
|
|
||||||
# Fail if current user's email address isn't @nottinghamtec.co.uk
|
|
||||||
if not request.user.email.endswith('@nottinghamtec.co.uk'):
|
|
||||||
error_resp = render(request, 'eventauthorisation_request_error.html')
|
|
||||||
return error_resp
|
|
||||||
|
|
||||||
return function(request, *args, **kwargs)
|
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
DATETIME_FORMAT = ('d/m/Y H:i')
|
DATETIME_FORMAT = ('d/m/Y H:i')
|
||||||
DATE_FORMAT = ('d/m/Y')
|
DATE_FORMAT = ('d/m/Y')
|
||||||
TIME_FORMAT = ('H:i')
|
TIME_FORMAT = ('H:i')
|
||||||
@@ -10,49 +10,39 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
|||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
import os
|
import os
|
||||||
import raven
|
|
||||||
import secrets
|
|
||||||
import datetime
|
|
||||||
from envparse import env
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
|
SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
|
||||||
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = env('DEBUG', cast=bool, default=True)
|
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
|
||||||
|
|
||||||
STAGING = env('STAGING', cast=bool, default=False)
|
STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
|
||||||
|
|
||||||
CI = env('CI', cast=bool, default=False)
|
TEMPLATE_DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||||
|
|
||||||
if STAGING:
|
if STAGING:
|
||||||
ALLOWED_HOSTS.append('.herokuapp.com')
|
ALLOWED_HOSTS.append('.herokuapp.com')
|
||||||
|
|
||||||
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')
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
|
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
|
||||||
|
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
||||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
ADMINS = (
|
||||||
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
('Tom Price', 'tomtom5152@gmail.com')
|
||||||
if DEBUG:
|
)
|
||||||
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
@@ -60,11 +50,8 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.humanize',
|
|
||||||
'versioning',
|
|
||||||
'users',
|
|
||||||
'RIGS',
|
'RIGS',
|
||||||
'assets',
|
'DiscourseAuth',
|
||||||
|
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
'registration',
|
'registration',
|
||||||
@@ -74,23 +61,24 @@ INSTALLED_APPS = (
|
|||||||
'raven.contrib.django.raven_compat',
|
'raven.contrib.django.raven_compat',
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE_CLASSES = (
|
||||||
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
|
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
|
||||||
'reversion.middleware.RevisionMiddleware',
|
'reversion.middleware.RevisionMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
ROOT_URLCONF = 'PyRIGS.urls'
|
ROOT_URLCONF = 'PyRIGS.urls'
|
||||||
|
|
||||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
@@ -102,10 +90,9 @@ DATABASES = {
|
|||||||
|
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
|
|
||||||
DATABASES['default'] = dj_database_url.config()
|
DATABASES['default'] = dj_database_url.config()
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'disable_existing_loggers': False,
|
'disable_existing_loggers': False,
|
||||||
@@ -133,12 +120,12 @@ LOGGING = {
|
|||||||
'mail_admins': {
|
'mail_admins': {
|
||||||
'class': 'django.utils.log.AdminEmailHandler',
|
'class': 'django.utils.log.AdminEmailHandler',
|
||||||
'level': 'ERROR',
|
'level': 'ERROR',
|
||||||
# But the emails are plain text by default - HTML is nicer
|
# But the emails are plain text by default - HTML is nicer
|
||||||
'include_html': True,
|
'include_html': True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
# Again, default Django configuration to email unhandled exceptions
|
# Again, default Django configuration to email unhandled exceptions
|
||||||
'django.request': {
|
'django.request': {
|
||||||
'handlers': ['mail_admins'],
|
'handlers': ['mail_admins'],
|
||||||
'level': 'ERROR',
|
'level': 'ERROR',
|
||||||
@@ -153,63 +140,43 @@ LOGGING = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tests lock up SQLite otherwise
|
import raven
|
||||||
if STAGING or CI:
|
|
||||||
CACHES = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif DEBUG:
|
|
||||||
CACHES = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
CACHES = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
|
|
||||||
'LOCATION': 'cache_table',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RAVEN_CONFIG = {
|
RAVEN_CONFIG = {
|
||||||
'dsn': env('RAVEN_DSN', default=""),
|
'dsn': os.environ.get('RAVEN_DSN'),
|
||||||
|
# If you are using git, you can also automatically configure the
|
||||||
|
# release based on the git info.
|
||||||
|
# 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
|
||||||
}
|
}
|
||||||
|
|
||||||
# User system
|
# User system
|
||||||
AUTH_USER_MODEL = 'RIGS.Profile'
|
AUTH_USER_MODEL = 'RIGS.Profile'
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = '/'
|
LOGIN_REDIRECT_URL = '/'
|
||||||
LOGIN_URL = '/user/login/'
|
LOGIN_URL = '/user/login'
|
||||||
LOGOUT_URL = '/user/logout/'
|
LOGOUT_URL = '/user/logout'
|
||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
# reCAPTCHA settings
|
# reCAPTCHA settings
|
||||||
RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
|
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', None)
|
||||||
RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
|
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', None)
|
||||||
NOCAPTCHA = True
|
NOCAPTCHA = True
|
||||||
|
|
||||||
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
|
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAILER_TEST = False
|
EMAILER_TEST = False
|
||||||
if not DEBUG or EMAILER_TEST:
|
if not DEBUG or EMAILER_TEST:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
EMAIL_HOST = env('EMAIL_HOST')
|
EMAIL_HOST = os.environ.get('EMAIL_HOST')
|
||||||
EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25)
|
EMAIL_PORT = int(os.environ.get('EMAIL_PORT'))
|
||||||
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
|
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
|
||||||
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
|
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
|
||||||
EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False)
|
EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0)))
|
||||||
EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False)
|
EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0)))
|
||||||
DEFAULT_FROM_EMAIL = env('EMAIL_FROM')
|
DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM')
|
||||||
else:
|
else:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||||
|
|
||||||
@@ -225,10 +192,22 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Need to allow seconds as datetime-local input type spits out a time that has seconds
|
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M','%Y-%m-%dT%H:%M:%S')
|
||||||
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
|
||||||
|
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.core.context_processors.debug",
|
||||||
|
"django.core.context_processors.i18n",
|
||||||
|
"django.core.context_processors.media",
|
||||||
|
"django.core.context_processors.static",
|
||||||
|
"django.core.context_processors.tz",
|
||||||
|
"django.core.context_processors.request",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||||
@@ -236,30 +215,14 @@ STATIC_DIRS = (
|
|||||||
os.path.join(BASE_DIR, 'static/')
|
os.path.join(BASE_DIR, 'static/')
|
||||||
)
|
)
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATE_DIRS = (
|
||||||
{
|
os.path.join(BASE_DIR, 'templates'),
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
)
|
||||||
'DIRS': [
|
|
||||||
os.path.join(BASE_DIR, 'templates')
|
|
||||||
],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
"django.contrib.auth.context_processors.auth",
|
|
||||||
"django.template.context_processors.debug",
|
|
||||||
"django.template.context_processors.i18n",
|
|
||||||
"django.template.context_processors.media",
|
|
||||||
"django.template.context_processors.static",
|
|
||||||
"django.template.context_processors.tz",
|
|
||||||
"django.template.context_processors.request",
|
|
||||||
"django.contrib.messages.context_processors.messages",
|
|
||||||
],
|
|
||||||
'debug': DEBUG
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
USE_GRAVATAR = True
|
USE_GRAVATAR=True
|
||||||
|
|
||||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||||
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
|
||||||
|
|
||||||
|
DISCOURSE_SECRET_KEY = 'pB1f94Mfky1Y0eOrk2UjB1VqnAZ52P7v'
|
||||||
|
DISCOURSE_BASE_URL = 'https://forum.nottinghamtec.co.uk'
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
from django.test import LiveServerTestCase
|
|
||||||
from selenium import webdriver
|
|
||||||
from RIGS import models as rigsmodels
|
|
||||||
from . import pages
|
|
||||||
import os
|
|
||||||
import pytz
|
|
||||||
from datetime import date, time, datetime, timedelta
|
|
||||||
from django.conf import settings
|
|
||||||
import PyRIGS.settings
|
|
||||||
import sys
|
|
||||||
import pathlib
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
def create_datetime(year, month, day, hour, min):
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
|
|
||||||
|
|
||||||
|
|
||||||
def create_browser():
|
|
||||||
options = webdriver.ChromeOptions()
|
|
||||||
options.add_argument("--window-size=1920,1080")
|
|
||||||
options.add_argument("--headless")
|
|
||||||
if settings.CI:
|
|
||||||
options.add_argument("--no-sandbox")
|
|
||||||
driver = webdriver.Chrome(options=options)
|
|
||||||
return driver
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTest(LiveServerTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUpClass()
|
|
||||||
self.driver = create_browser()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super().tearDown()
|
|
||||||
self.driver.quit()
|
|
||||||
|
|
||||||
|
|
||||||
class AutoLoginTest(BaseTest):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.profile = rigsmodels.Profile(
|
|
||||||
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
|
|
||||||
self.profile.set_password("EventTestPassword")
|
|
||||||
self.profile.save()
|
|
||||||
loginPage = pages.LoginPage(self.driver, self.live_server_url).open()
|
|
||||||
loginPage.login("EventTest", "EventTestPassword")
|
|
||||||
|
|
||||||
|
|
||||||
def screenshot_failure(func):
|
|
||||||
def wrapper_func(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
func(self, *args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
screenshot_name = func.__module__ + "." + func.__qualname__
|
|
||||||
screenshot_file = "screenshots/" + func.__qualname__ + ".png"
|
|
||||||
if not pathlib.Path("screenshots").is_dir():
|
|
||||||
os.mkdir("screenshots")
|
|
||||||
self.driver.save_screenshot(screenshot_file)
|
|
||||||
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
|
||||||
raise e
|
|
||||||
return wrapper_func
|
|
||||||
|
|
||||||
|
|
||||||
def screenshot_failure_cls(cls):
|
|
||||||
for attr in cls.__dict__:
|
|
||||||
if callable(getattr(cls, attr)) and attr.startswith("test"):
|
|
||||||
setattr(cls, attr, screenshot_failure(getattr(cls, attr)))
|
|
||||||
return cls
|
|
||||||
|
|
||||||
|
|
||||||
# Checks if animation is done
|
|
||||||
class animation_is_finished():
|
|
||||||
def __call__(self, driver):
|
|
||||||
numberAnimating = driver.execute_script('return $(":animated").length')
|
|
||||||
finished = numberAnimating == 0
|
|
||||||
if finished:
|
|
||||||
import time
|
|
||||||
time.sleep(0.1)
|
|
||||||
return finished
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
from pypom import Page, Region
|
|
||||||
from selenium.webdriver.common.action_chains import ActionChains
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
from selenium.webdriver import Chrome
|
|
||||||
from selenium.common.exceptions import NoSuchElementException
|
|
||||||
from PyRIGS.tests import regions
|
|
||||||
|
|
||||||
|
|
||||||
class BasePage(Page):
|
|
||||||
form_items = {}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
if name in self.form_items:
|
|
||||||
element = self.form_items[name]
|
|
||||||
form_element = element[0](self, self.find_element(*element[1]))
|
|
||||||
return form_element.value
|
|
||||||
else:
|
|
||||||
return super().__getattribute__(name)
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
if name in self.form_items:
|
|
||||||
element = self.form_items[name]
|
|
||||||
form_element = element[0](self, self.find_element(*element[1]))
|
|
||||||
form_element.set_value(value)
|
|
||||||
else:
|
|
||||||
self.__dict__[name] = value
|
|
||||||
|
|
||||||
|
|
||||||
class FormPage(BasePage):
|
|
||||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
|
||||||
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
|
||||||
|
|
||||||
def remove_all_required(self):
|
|
||||||
self.driver.execute_script(
|
|
||||||
"Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
|
||||||
self.driver.execute_script(
|
|
||||||
"Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
|
||||||
|
|
||||||
def submit(self):
|
|
||||||
previous_errors = self.errors
|
|
||||||
submit = self.find_element(*self._submit_locator)
|
|
||||||
ActionChains(self.driver).move_to_element(submit).perform()
|
|
||||||
submit.click()
|
|
||||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def errors(self):
|
|
||||||
try:
|
|
||||||
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
|
|
||||||
return error_page.errors
|
|
||||||
except NoSuchElementException:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class LoginPage(BasePage):
|
|
||||||
URL_TEMPLATE = '/user/login'
|
|
||||||
|
|
||||||
_username_locator = (By.ID, 'id_username')
|
|
||||||
_password_locator = (By.ID, 'id_password')
|
|
||||||
_submit_locator = (By.ID, 'id_submit')
|
|
||||||
_error_locator = (By.CSS_SELECTOR, '.errorlist>li')
|
|
||||||
|
|
||||||
def login(self, username, password):
|
|
||||||
username_element = self.find_element(*self._username_locator)
|
|
||||||
username_element.clear()
|
|
||||||
username_element.send_keys(username)
|
|
||||||
|
|
||||||
password_element = self.find_element(*self._password_locator)
|
|
||||||
password_element.clear()
|
|
||||||
password_element.send_keys(password)
|
|
||||||
|
|
||||||
self.find_element(*self._submit_locator).click()
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
from pypom import Region
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.conf import settings
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
from selenium.webdriver.support import expected_conditions
|
|
||||||
from selenium.webdriver.remote.webelement import WebElement
|
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
|
||||||
from selenium.webdriver.support.select import Select
|
|
||||||
from selenium.webdriver.common.keys import Keys
|
|
||||||
from selenium.common.exceptions import NoSuchElementException
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bool_from_string(string):
|
|
||||||
# Used to convert from attribute strings to boolean values, written after I found this:
|
|
||||||
# >>> bool("false")
|
|
||||||
# True
|
|
||||||
if string == "true":
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_time_format():
|
|
||||||
# Default
|
|
||||||
time_format = "%H%M"
|
|
||||||
if settings.CI: # The CI is American
|
|
||||||
time_format = "%I%M%p"
|
|
||||||
return time_format
|
|
||||||
|
|
||||||
|
|
||||||
def get_date_format():
|
|
||||||
date_format = "%d%m%Y"
|
|
||||||
if settings.CI: # And try as I might I can't stop it being so
|
|
||||||
date_format = "%m%d%Y"
|
|
||||||
return date_format
|
|
||||||
|
|
||||||
|
|
||||||
class BootstrapSelectElement(Region):
|
|
||||||
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
|
|
||||||
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
|
|
||||||
_option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a.dropdown-item')
|
|
||||||
_select_all_locator = (By.CLASS_NAME, 'bs-select-all')
|
|
||||||
_deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
|
|
||||||
_search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
|
|
||||||
_status_locator = (By.CLASS_NAME, 'status')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_open(self):
|
|
||||||
return parse_bool_from_string(self.find_element(*self._main_button_locator).get_attribute("aria-expanded"))
|
|
||||||
|
|
||||||
def toggle(self):
|
|
||||||
original_state = self.is_open
|
|
||||||
option_box = self.find_element(*self._option_box_locator)
|
|
||||||
if not original_state:
|
|
||||||
self.wait.until(expected_conditions.invisibility_of_element(option_box))
|
|
||||||
else:
|
|
||||||
self.wait.until(expected_conditions.visibility_of(option_box))
|
|
||||||
return self.find_element(*self._main_button_locator).click()
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
if not self.is_open:
|
|
||||||
self.toggle()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.is_open:
|
|
||||||
self.toggle()
|
|
||||||
|
|
||||||
def select_all(self):
|
|
||||||
self.find_element(*self._select_all_locator).click()
|
|
||||||
|
|
||||||
def deselect_all(self):
|
|
||||||
self.find_element(*self._deselect_all_locator).click()
|
|
||||||
|
|
||||||
def search(self, query):
|
|
||||||
search_box = self.find_element(*self._search_locator)
|
|
||||||
self.open()
|
|
||||||
search_box.clear()
|
|
||||||
search_box.send_keys(query)
|
|
||||||
status_text = self.find_element(*self._status_locator)
|
|
||||||
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def options(self):
|
|
||||||
options = list(self.find_elements(*self._option_locator))
|
|
||||||
return [self.BootstrapSelectOption(self, i) for i in options]
|
|
||||||
|
|
||||||
def set_option(self, name, selected):
|
|
||||||
options = list((x for x in self.options if x.name == name))
|
|
||||||
assert len(options) == 1
|
|
||||||
options[0].set_selected(selected)
|
|
||||||
|
|
||||||
class BootstrapSelectOption(Region):
|
|
||||||
_text_locator = (By.CLASS_NAME, 'text')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def selected(self):
|
|
||||||
return parse_bool_from_string(self.root.get_attribute("aria-selected"))
|
|
||||||
|
|
||||||
def toggle(self):
|
|
||||||
self.root.click()
|
|
||||||
|
|
||||||
def set_selected(self, selected):
|
|
||||||
if self.selected != selected:
|
|
||||||
self.toggle()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.find_element(*self._text_locator).text
|
|
||||||
|
|
||||||
|
|
||||||
class TextBox(Region):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return self.root.get_attribute("value")
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.root.clear()
|
|
||||||
self.root.send_keys(value)
|
|
||||||
|
|
||||||
|
|
||||||
class CheckBox(Region):
|
|
||||||
def toggle(self):
|
|
||||||
self.root.click()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return parse_bool_from_string(self.root.get_attribute("checked"))
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
if value != self.value:
|
|
||||||
self.toggle()
|
|
||||||
|
|
||||||
|
|
||||||
class RadioSelect(Region): # Currently only works for yes/no radio selects
|
|
||||||
def set_value(self, value):
|
|
||||||
if value:
|
|
||||||
value = "0"
|
|
||||||
else:
|
|
||||||
value = "1"
|
|
||||||
self.find_element(By.XPATH, "//label[@for='{}_{}']".format(self.root.get_attribute("id"), value)).click()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
try:
|
|
||||||
return parse_bool_from_string(self.find_element(By.CSS_SELECTOR, '.custom-control-input:checked').get_attribute("value").lower())
|
|
||||||
except NoSuchElementException:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class DatePicker(Region):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d")
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.root.clear()
|
|
||||||
self.root.send_keys(value.strftime(get_date_format()))
|
|
||||||
|
|
||||||
|
|
||||||
class TimePicker(Region):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.root.clear()
|
|
||||||
self.root.send_keys(value.strftime(get_time_format()))
|
|
||||||
|
|
||||||
|
|
||||||
class DateTimePicker(Region):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.root.clear()
|
|
||||||
|
|
||||||
date = value.date().strftime(get_date_format())
|
|
||||||
time = value.time().strftime(get_time_format())
|
|
||||||
|
|
||||||
self.root.send_keys(date)
|
|
||||||
self.root.send_keys(Keys.TAB)
|
|
||||||
self.root.send_keys(time)
|
|
||||||
|
|
||||||
|
|
||||||
class SingleSelectPicker(Region):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
picker = Select(self.root)
|
|
||||||
return picker.first_selected_option.text
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
picker = Select(self.root)
|
|
||||||
picker.select_by_visible_text(value)
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorPage(Region):
|
|
||||||
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
|
|
||||||
|
|
||||||
class ErrorItem(Region):
|
|
||||||
_field_selector = (By.CSS_SELECTOR, "dt")
|
|
||||||
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def field_name(self):
|
|
||||||
return self.find_element(*self._field_selector).text
|
|
||||||
|
|
||||||
@property
|
|
||||||
def errors(self):
|
|
||||||
return [x.text for x in self.find_elements(*self._error_selector)]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def errors(self):
|
|
||||||
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
|
|
||||||
errors = {}
|
|
||||||
for error in error_items:
|
|
||||||
errors[error.field_name] = error.errors
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
class Modal(Region):
|
|
||||||
_submit_locator = (By.CSS_SELECTOR, '.btn-primary')
|
|
||||||
_header_selector = (By.TAG_NAME, 'h4')
|
|
||||||
|
|
||||||
form_items = {
|
|
||||||
'name': (TextBox, (By.ID, 'id_name'))
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def header(self):
|
|
||||||
return self.find_element(*self._header_selector).text
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_open(self):
|
|
||||||
return self.root.is_displayed()
|
|
||||||
|
|
||||||
def submit(self):
|
|
||||||
self.root.find_element(*self._submit_locator).click()
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
if name in self.form_items:
|
|
||||||
element = self.form_items[name]
|
|
||||||
form_element = element[0](self, self.find_element(*element[1]))
|
|
||||||
return form_element.value
|
|
||||||
else:
|
|
||||||
return super().__getattribute__(name)
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
if name in self.form_items:
|
|
||||||
element = self.form_items[name]
|
|
||||||
form_element = element[0](self, self.find_element(*element[1]))
|
|
||||||
form_element.set_value(value)
|
|
||||||
else:
|
|
||||||
self.__dict__[name] = value
|
|
||||||
@@ -1,44 +1,26 @@
|
|||||||
from django.urls import path, re_path
|
from django.conf.urls import patterns, include, url
|
||||||
from django.conf.urls import include
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from registration.backends.default.views import RegistrationView
|
||||||
from django.contrib.auth.views import LoginView
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
from PyRIGS.decorators import permission_required_with_403
|
|
||||||
import RIGS
|
import RIGS
|
||||||
import users
|
from RIGS import regbackend
|
||||||
import versioning
|
import DiscourseAuth.urls
|
||||||
from PyRIGS import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = patterns('',
|
||||||
path('', include('versioning.urls')),
|
# Examples:
|
||||||
path('', include('RIGS.urls')),
|
# url(r'^$', 'PyRIGS.views.home', name='home'),
|
||||||
path('assets/', include('assets.urls')),
|
# url(r'^blog/', include('blog.urls')),
|
||||||
|
|
||||||
path('', login_required(views.Index.as_view()), name='index'),
|
url(r'^', include('RIGS.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')),
|
||||||
|
|
||||||
# API
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
|
url(r'^discourse-auth/', include(DiscourseAuth.urls)),
|
||||||
name="api_secure"),
|
)
|
||||||
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
|
|
||||||
name="api_secure"),
|
|
||||||
|
|
||||||
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
|
|
||||||
path('search_help/', views.SearchHelp.as_view(), name='search_help'),
|
|
||||||
|
|
||||||
path('', include('users.urls')),
|
|
||||||
|
|
||||||
path('admin/', admin.site.urls),
|
|
||||||
]
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += staticfiles_urlpatterns()
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
|
||||||
import debug_toolbar
|
|
||||||
urlpatterns = [
|
|
||||||
re_path(r'^__debug__/', include(debug_toolbar.urls)),
|
|
||||||
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
|
|
||||||
] + urlpatterns
|
|
||||||
252
PyRIGS/views.py
252
PyRIGS/views.py
@@ -1,252 +0,0 @@
|
|||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.http.response import HttpResponseRedirect
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
|
||||||
from django.views import generic
|
|
||||||
from django.contrib.auth.views import LoginView
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.core import serializers
|
|
||||||
from django.conf import settings
|
|
||||||
import simplejson
|
|
||||||
from django.contrib import messages
|
|
||||||
import datetime
|
|
||||||
import pytz
|
|
||||||
import operator
|
|
||||||
from registration.views import RegistrationView
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
|
|
||||||
from RIGS import models, forms
|
|
||||||
from assets import models as asset_models
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
from django.views.decorators.cache import never_cache, cache_page
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
|
|
||||||
|
|
||||||
def is_ajax(request):
|
|
||||||
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
|
||||||
|
|
||||||
# Displays the current rig count along with a few other bits and pieces
|
|
||||||
|
|
||||||
|
|
||||||
class Index(generic.TemplateView):
|
|
||||||
template_name = 'index.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(Index, self).get_context_data(**kwargs)
|
|
||||||
context['rig_count'] = models.Event.objects.rig_count()
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class SecureAPIRequest(generic.View):
|
|
||||||
models = {
|
|
||||||
'venue': models.Venue,
|
|
||||||
'person': models.Person,
|
|
||||||
'organisation': models.Organisation,
|
|
||||||
'profile': models.Profile,
|
|
||||||
'event': models.Event,
|
|
||||||
'supplier': asset_models.Supplier
|
|
||||||
}
|
|
||||||
|
|
||||||
perms = {
|
|
||||||
'venue': 'RIGS.view_venue',
|
|
||||||
'person': 'RIGS.view_person',
|
|
||||||
'organisation': 'RIGS.view_organisation',
|
|
||||||
'profile': 'RIGS.view_profile',
|
|
||||||
'event': None,
|
|
||||||
'supplier': None
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
|
||||||
Validate the request is allowed based on user permissions.
|
|
||||||
Raises 403 if denied.
|
|
||||||
Potential to add API key validation at a later date.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __validate__(self, request, key, perm):
|
|
||||||
if request.user.is_active:
|
|
||||||
if request.user.is_superuser or perm is None:
|
|
||||||
return True
|
|
||||||
elif request.user.has_perm(perm):
|
|
||||||
return True
|
|
||||||
raise PermissionDenied()
|
|
||||||
|
|
||||||
def get(self, request, model, pk=None, param=None):
|
|
||||||
# Request permission validation things
|
|
||||||
key = request.GET.get('apikey', None)
|
|
||||||
perm = self.perms[model]
|
|
||||||
self.__validate__(request, key, perm)
|
|
||||||
|
|
||||||
# Response format where applicable
|
|
||||||
format = request.GET.get('format', 'json')
|
|
||||||
fields = request.GET.get('fields', None)
|
|
||||||
if fields:
|
|
||||||
fields = fields.split(",")
|
|
||||||
|
|
||||||
# Supply data for one record
|
|
||||||
if pk:
|
|
||||||
object = get_object_or_404(self.models[model], pk=pk)
|
|
||||||
data = serializers.serialize(format, [object], fields=fields)
|
|
||||||
return HttpResponse(data, content_type="application/" + format)
|
|
||||||
|
|
||||||
# Supply data for autocomplete ajax request in json form
|
|
||||||
term = request.GET.get('q', None)
|
|
||||||
if term:
|
|
||||||
if fields is None: # Default to just name
|
|
||||||
fields = ['name']
|
|
||||||
|
|
||||||
# Build a list of Q objects for use later
|
|
||||||
queries = []
|
|
||||||
for part in term.split(" "):
|
|
||||||
qs = []
|
|
||||||
for field in fields:
|
|
||||||
q = Q(**{field + "__icontains": part})
|
|
||||||
qs.append(q)
|
|
||||||
queries.append(reduce(operator.or_, qs))
|
|
||||||
|
|
||||||
# Build the data response list
|
|
||||||
results = []
|
|
||||||
query = reduce(operator.and_, queries)
|
|
||||||
objects = self.models[model].objects.filter(query)
|
|
||||||
for o in objects:
|
|
||||||
data = {
|
|
||||||
'pk': o.pk,
|
|
||||||
'value': o.pk,
|
|
||||||
'text': o.name,
|
|
||||||
}
|
|
||||||
try: # See if there is a valid update URL
|
|
||||||
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
results.append(data)
|
|
||||||
|
|
||||||
# return a data response
|
|
||||||
json = simplejson.dumps(results)
|
|
||||||
return HttpResponse(json, content_type="application/json") # Always json
|
|
||||||
|
|
||||||
start = request.GET.get('start', None)
|
|
||||||
end = request.GET.get('end', None)
|
|
||||||
|
|
||||||
if model == "event" and start and end:
|
|
||||||
# Probably a calendar request
|
|
||||||
start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
|
|
||||||
end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
|
|
||||||
|
|
||||||
objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for item in objects:
|
|
||||||
data = {
|
|
||||||
'pk': item.pk,
|
|
||||||
'title': item.name,
|
|
||||||
'is_rig': item.is_rig,
|
|
||||||
'status': str(item.get_status_display()),
|
|
||||||
'earliest': item.earliest_time.isoformat(),
|
|
||||||
'latest': item.latest_time.isoformat(),
|
|
||||||
'url': str(item.get_absolute_url())
|
|
||||||
}
|
|
||||||
|
|
||||||
results.append(data)
|
|
||||||
json = simplejson.dumps(results)
|
|
||||||
return HttpResponse(json, content_type="application/json") # Always json
|
|
||||||
|
|
||||||
return HttpResponse(model)
|
|
||||||
|
|
||||||
|
|
||||||
class ModalURLMixin:
|
|
||||||
def get_close_url(self, update, detail):
|
|
||||||
if is_ajax(self.request):
|
|
||||||
url = reverse_lazy('closemodal')
|
|
||||||
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
|
|
||||||
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
|
|
||||||
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
|
|
||||||
else:
|
|
||||||
url = reverse_lazy(detail, kwargs={
|
|
||||||
'pk': self.object.pk,
|
|
||||||
})
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
class GenericListView(generic.ListView):
|
|
||||||
template_name = 'generic_list.html'
|
|
||||||
paginate_by = 20
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(GenericListView, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = self.model.__name__ + "s"
|
|
||||||
if is_ajax(self.request):
|
|
||||||
context['override'] = "base_ajax.html"
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
q = self.request.GET.get('q', "")
|
|
||||||
|
|
||||||
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(
|
|
||||||
phone__startswith=q) | Q(phone__endswith=q)
|
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter)
|
|
||||||
|
|
||||||
orderBy = self.request.GET.get('orderBy', "name")
|
|
||||||
if orderBy != "":
|
|
||||||
object_list = object_list.order_by(orderBy)
|
|
||||||
return object_list
|
|
||||||
|
|
||||||
|
|
||||||
class GenericDetailView(generic.DetailView):
|
|
||||||
template_name = "generic_detail.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(GenericDetailView, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
|
|
||||||
if is_ajax(self.request):
|
|
||||||
context['override'] = "base_ajax.html"
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class GenericUpdateView(generic.UpdateView):
|
|
||||||
template_name = "generic_form.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(GenericUpdateView, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = "Edit {}".format(self.model.__name__)
|
|
||||||
if is_ajax(self.request):
|
|
||||||
context['override'] = "base_ajax.html"
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class GenericCreateView(generic.CreateView):
|
|
||||||
template_name = "generic_form.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(GenericCreateView, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = "Create {}".format(self.model.__name__)
|
|
||||||
if is_ajax(self.request):
|
|
||||||
context['override'] = "base_ajax.html"
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class SearchHelp(generic.TemplateView):
|
|
||||||
template_name = 'search_help.html'
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
|
||||||
May optionally also include some javascript in a success message to cause a load of
|
|
||||||
the new information onto the page.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class CloseModal(generic.TemplateView):
|
|
||||||
template_name = 'closemodal.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return {'messages': messages.get_messages(self.request)}
|
|
||||||
@@ -7,10 +7,9 @@ For more information on this file, see
|
|||||||
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application # noqa
|
from django.core.wsgi import get_wsgi_application
|
||||||
from dj_static import Cling # noqa
|
from dj_static import Cling
|
||||||
|
|
||||||
application = Cling(get_wsgi_application())
|
application = Cling(get_wsgi_application())
|
||||||
|
|||||||
104
README.md
104
README.md
@@ -1,17 +1,97 @@
|
|||||||
# TEC PA & Lighting - PyRIGS #
|
# TEC PA & Lighting - PyRIGS #
|
||||||

|
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|
||||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
[](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop)
|
||||||
|
|
||||||
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
|
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
|
||||||
|
|
||||||
For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
|
The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails.
|
||||||
|
|
||||||
# Apps
|
At this stage the project is very early on, and the main focus has been on getting a working system that can be tested and put into use ASAP due to the imminent failure of the existing system. Because of this, the documentation is still quite weak, but this should be fixed as time goes on.
|
||||||
- PyRIGS: Base app, stores 'global' information
|
|
||||||
- RIGS: Rigboard stuff - event calendar etc
|
|
||||||
- assets: Database of our kit, testing data etc
|
|
||||||
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
|
||||||
- users: Our custom logic for registration and profiles. Semi-modular.
|
|
||||||
- training: SoonTM
|
|
||||||
|
|
||||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
This document is intended to get you up and running, but if don't care about what I have to say, just clone the sodding repository and have a poke around with what's in it, but for GODS SAKE DO NOT PUSH WITHOUT TESTING.
|
||||||
|
|
||||||
|
### What is this repository for? ###
|
||||||
|
For the rapid development of the application for medium term deployment, the main branch is being used.
|
||||||
|
Once the application is deployed in a production environment, other branches should be used to properly stage edits and pushes of new features. When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
|
||||||
|
|
||||||
|
Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework.
|
||||||
|
|
||||||
|
### Editing ###
|
||||||
|
It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git.
|
||||||
|
|
||||||
|
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
|
||||||
|
|
||||||
|
Please contact TJP for details on how to acquire these.
|
||||||
|
|
||||||
|
### Python Environment ###
|
||||||
|
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Mainly the C implementation of Python 2 (CPython 2) has been used (specifically the Python 2.7 standard). Most of the application has been written with Python 3 in mind however, and should run without issue. Some level of testing on Python 3 has been done, but there is no guarantee it will work (for more information on this please see [[Python Version]] on the wiki)
|
||||||
|
|
||||||
|
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
|
||||||
|
|
||||||
|
#### PyCharm ####
|
||||||
|
If you are using the prefered PyCharm IDE, then this should be quite easy.
|
||||||
|
|
||||||
|
1. Select "File/Settings" -> "Project Interpreter"
|
||||||
|
2. Click the small cog in the top right
|
||||||
|
3. Select "Create VirtualEnv"
|
||||||
|
4. Enter a name and a location. This doesn't matter where, just make sure it makes sense and you remember it incase you need it later (I recommend calling it "pyrigs" in "~/.virtualenvs/pyrigs")
|
||||||
|
5. Select the base interpreter to your Python 3 base interpreter (Python 2 will work, just be careful)
|
||||||
|
6. Click OK, you *don't* want to inherit global packages or make it available to all projects.
|
||||||
|
7. Open a file such as manage.py. PyCharm should winge that dependances aren't installed. This might take a while to register, but give it change. When it does, click the button to install them and let it do it's thing. If for some reason PyCharm should decide that it doesn't want to help you here, see below for the console instructions on how to do this manually.
|
||||||
|
|
||||||
|
To run the Django application follow these steps
|
||||||
|
|
||||||
|
1. Select "Run/Edit Configurations"
|
||||||
|
2. Create a new "Django server", give it a sensible name for when you need it later.
|
||||||
|
3. You might need to set the interpreter to be your virtualenv.
|
||||||
|
4. Click "OK"
|
||||||
|
5. Run the application
|
||||||
|
|
||||||
|
#### Console Based ####
|
||||||
|
If you aren't using PyCharm, or want to use a console for some reason, this is really easy, there is even [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to help things along. Simply run
|
||||||
|
```
|
||||||
|
virtualenv <dir>
|
||||||
|
```
|
||||||
|
Where dir is the directory you wish to create the virtualenv in.
|
||||||
|
|
||||||
|
Next activate the virtualenv.
|
||||||
|
```
|
||||||
|
Windows
|
||||||
|
<virtualenv_dir>/Scripts/activate.bat
|
||||||
|
|
||||||
|
Unix
|
||||||
|
source <virtualenv_dir>/bin/activate
|
||||||
|
```
|
||||||
|
Finally install the requirements using pip
|
||||||
|
```
|
||||||
|
cd <pyrigs project directory>
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
This might take a while, but be patient and you should then be ready to go.
|
||||||
|
|
||||||
|
To run the server under normal conditions when you are already in the virtualenv (see above)
|
||||||
|
```
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
Please refer to Django documentation for a full list of options available here.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
```
|
||||||
|
python manage.py flush
|
||||||
|
```
|
||||||
|
Then load the sample data using the command:
|
||||||
|
```
|
||||||
|
python manage.py generateSampleData
|
||||||
|
```
|
||||||
|
4 user accounts are created for convenience:
|
||||||
|
|
||||||
|
|Username |Password |
|
||||||
|
|---------|---------|
|
||||||
|
|superuser|superuser|
|
||||||
|
|finance |finance |
|
||||||
|
|keyholder|keyholder|
|
||||||
|
|basic |basic |
|
||||||
|
|
||||||
|
### Committing, pushing and testing ###
|
||||||
|
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
default_app_config = 'RIGS.apps.RIGSAppConfig'
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
from users import forms as user_forms
|
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from reversion.admin import VersionAdmin
|
import reversion
|
||||||
|
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
@@ -13,31 +12,21 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
admin.site.register(models.VatRate, VersionAdmin)
|
admin.site.register(models.VatRate, reversion.VersionAdmin)
|
||||||
admin.site.register(models.Event, VersionAdmin)
|
admin.site.register(models.Event, reversion.VersionAdmin)
|
||||||
admin.site.register(models.EventItem, VersionAdmin)
|
admin.site.register(models.EventItem, reversion.VersionAdmin)
|
||||||
admin.site.register(models.Invoice, VersionAdmin)
|
admin.site.register(models.Invoice)
|
||||||
|
admin.site.register(models.Payment)
|
||||||
|
|
||||||
def approve_user(modeladmin, request, queryset):
|
|
||||||
queryset.update(is_approved=True)
|
|
||||||
|
|
||||||
|
|
||||||
approve_user.short_description = "Approve selected users"
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Profile)
|
@admin.register(models.Profile)
|
||||||
class ProfileAdmin(UserAdmin):
|
class ProfileAdmin(UserAdmin):
|
||||||
# Don't know how to add 'is_approved' whilst preserving the default list...
|
|
||||||
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
(_('Personal info'), {
|
(_('Personal info'), {
|
||||||
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
||||||
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
|
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||||
'groups', 'user_permissions')}),
|
'groups', 'user_permissions')}),
|
||||||
(_('Important dates'), {
|
(_('Important dates'), {
|
||||||
'fields': ('last_login', 'date_joined')}),
|
'fields': ('last_login', 'date_joined')}),
|
||||||
@@ -48,12 +37,11 @@ class ProfileAdmin(UserAdmin):
|
|||||||
'fields': ('username', 'password1', 'password2'),
|
'fields': ('username', 'password1', 'password2'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
form = user_forms.ProfileChangeForm
|
form = forms.ProfileChangeForm
|
||||||
add_form = user_forms.ProfileCreationForm
|
add_form = forms.ProfileCreationForm
|
||||||
actions = [approve_user]
|
|
||||||
|
|
||||||
|
|
||||||
class AssociateAdmin(VersionAdmin):
|
class AssociateAdmin(reversion.VersionAdmin):
|
||||||
list_display = ('id', 'name', 'number_of_events')
|
list_display = ('id', 'name', 'number_of_events')
|
||||||
search_fields = ['id', 'name']
|
search_fields = ['id', 'name']
|
||||||
list_display_links = ['id', 'name']
|
list_display_links = ['id', 'name']
|
||||||
@@ -105,7 +93,8 @@ class AssociateAdmin(VersionAdmin):
|
|||||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||||
'forms': forms
|
'forms': forms
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context,
|
||||||
|
current_app=self.admin_site.name)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Person)
|
@admin.register(models.Person)
|
||||||
@@ -124,13 +113,3 @@ class VenueAdmin(AssociateAdmin):
|
|||||||
class OrganisationAdmin(AssociateAdmin):
|
class OrganisationAdmin(AssociateAdmin):
|
||||||
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
||||||
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
|
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.RiskAssessment)
|
|
||||||
class RiskAssessmentAdmin(VersionAdmin):
|
|
||||||
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.EventChecklist)
|
|
||||||
class EventChecklistAdmin(VersionAdmin):
|
|
||||||
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class RIGSAppConfig(AppConfig):
|
|
||||||
name = 'RIGS'
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
import RIGS.signals
|
|
||||||
117
RIGS/finance.py
117
RIGS/finance.py
@@ -1,8 +1,9 @@
|
|||||||
|
import cStringIO as StringIO
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.urls import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@@ -11,29 +12,21 @@ from django.template.loader import get_template
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
import reversion
|
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceIndex(generic.ListView):
|
class InvoiceIndex(generic.ListView):
|
||||||
model = models.Invoice
|
model = models.Invoice
|
||||||
template_name = 'invoice_list.html'
|
template_name = 'RIGS/invoice_list_active.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(InvoiceIndex, self).get_context_data(**kwargs)
|
context = super(InvoiceIndex, self).get_context_data(**kwargs)
|
||||||
total = 0
|
total = 0
|
||||||
for i in context['object_list']:
|
for i in context['object_list']:
|
||||||
total += i.balance
|
total += i.balance
|
||||||
context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total)
|
context['total'] = total
|
||||||
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
|
context['count'] = len(list(context['object_list']))
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -55,21 +48,15 @@ class InvoiceIndex(generic.ListView):
|
|||||||
|
|
||||||
class InvoiceDetail(generic.DetailView):
|
class InvoiceDetail(generic.DetailView):
|
||||||
model = models.Invoice
|
model = models.Invoice
|
||||||
template_name = 'invoice_detail.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(InvoiceDetail, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = "Invoice {} ({})".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class InvoicePrint(generic.View):
|
class InvoicePrint(generic.View):
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
invoice = get_object_or_404(models.Invoice, pk=pk)
|
invoice = get_object_or_404(models.Invoice, pk=pk)
|
||||||
object = invoice.event
|
object = invoice.event
|
||||||
template = get_template('event_print.xml')
|
template = get_template('RIGS/event_print.xml')
|
||||||
|
copies = ('TEC', 'Client')
|
||||||
context = {
|
context = RequestContext(request, {
|
||||||
'object': object,
|
'object': object,
|
||||||
'fonts': {
|
'fonts': {
|
||||||
'opensans': {
|
'opensans': {
|
||||||
@@ -79,17 +66,19 @@ class InvoicePrint(generic.View):
|
|||||||
},
|
},
|
||||||
'invoice': invoice,
|
'invoice': invoice,
|
||||||
'current_user': request.user,
|
'current_user': request.user,
|
||||||
'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
|
})
|
||||||
}
|
|
||||||
|
|
||||||
rml = template.render(context)
|
rml = template.render(context)
|
||||||
|
buffer = StringIO.StringIO()
|
||||||
|
|
||||||
buffer = rml2pdf.parseString(rml)
|
buffer = rml2pdf.parseString(rml)
|
||||||
|
|
||||||
pdfData = buffer.read()
|
pdfData = buffer.read()
|
||||||
|
|
||||||
|
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
response = HttpResponse(content_type='application/pdf')
|
||||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
response['Content-Disposition'] = "filename=Invoice %05d | %s.pdf" % (invoice.pk, escapedEventName)
|
||||||
response.write(pdfData)
|
response.write(pdfData)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -105,10 +94,8 @@ class InvoiceVoid(generic.View):
|
|||||||
return HttpResponseRedirect(reverse_lazy('invoice_list'))
|
return HttpResponseRedirect(reverse_lazy('invoice_list'))
|
||||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
|
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
|
||||||
|
|
||||||
|
|
||||||
class InvoiceDelete(generic.DeleteView):
|
class InvoiceDelete(generic.DeleteView):
|
||||||
model = models.Invoice
|
model = models.Invoice
|
||||||
template_name = 'invoice_confirm_delete.html'
|
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
@@ -127,58 +114,24 @@ class InvoiceDelete(generic.DeleteView):
|
|||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return self.request.POST.get('next')
|
return self.request.POST.get('next')
|
||||||
|
|
||||||
|
|
||||||
class InvoiceArchive(generic.ListView):
|
class InvoiceArchive(generic.ListView):
|
||||||
model = models.Invoice
|
model = models.Invoice
|
||||||
template_name = 'invoice_list_archive.html'
|
template_name = 'RIGS/invoice_list_archive.html'
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(InvoiceArchive, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = "Invoice Archive"
|
|
||||||
context['description'] = "This page displays all invoices: outstanding, paid, and void"
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
q = self.request.GET.get('q', "")
|
|
||||||
|
|
||||||
filter = Q(event__name__icontains=q)
|
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
filter = filter | Q(pk=val)
|
|
||||||
filter = filter | Q(event__pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if q[0] == "N":
|
|
||||||
val = int(q[1:])
|
|
||||||
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
|
||||||
elif q[0] == "#":
|
|
||||||
val = int(q[1:])
|
|
||||||
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
|
|
||||||
|
|
||||||
return object_list
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceWaiting(generic.ListView):
|
class InvoiceWaiting(generic.ListView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
template_name = 'invoice_list_waiting.html'
|
template_name = 'RIGS/event_invoice.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
|
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
|
||||||
total = 0
|
total = 0
|
||||||
for obj in self.get_objects():
|
for obj in self.get_objects():
|
||||||
total += obj.sum_total
|
total += obj.sum_total
|
||||||
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(self.get_objects()), total)
|
context['total'] = total
|
||||||
|
context['count'] = len(self.get_objects())
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -189,11 +142,11 @@ class InvoiceWaiting(generic.ListView):
|
|||||||
events = self.model.objects.filter(
|
events = self.model.objects.filter(
|
||||||
(
|
(
|
||||||
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||||
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||||
) & Q(invoice__isnull=True) & # Has not already been invoiced
|
) & Q(invoice__isnull=True) # Has not already been invoiced
|
||||||
Q(is_rig=True) # Is a rig (not non-rig)
|
& Q(is_rig=True) # Is a rig (not non-rig)
|
||||||
|
|
||||||
).order_by('start_date') \
|
).order_by('start_date') \
|
||||||
.select_related('person',
|
.select_related('person',
|
||||||
'organisation',
|
'organisation',
|
||||||
'venue', 'mic') \
|
'venue', 'mic') \
|
||||||
@@ -203,10 +156,7 @@ class InvoiceWaiting(generic.ListView):
|
|||||||
|
|
||||||
|
|
||||||
class InvoiceEvent(generic.View):
|
class InvoiceEvent(generic.View):
|
||||||
@transaction.atomic()
|
|
||||||
@reversion.create_revision()
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
reversion.set_user(self.request.user)
|
|
||||||
epk = kwargs.get('pk')
|
epk = kwargs.get('pk')
|
||||||
event = models.Event.objects.get(pk=epk)
|
event = models.Event.objects.get(pk=epk)
|
||||||
invoice, created = models.Invoice.objects.get_or_create(event=event)
|
invoice, created = models.Invoice.objects.get_or_create(event=event)
|
||||||
@@ -215,35 +165,22 @@ class InvoiceEvent(generic.View):
|
|||||||
invoice.invoice_date = datetime.date.today()
|
invoice.invoice_date = datetime.date.today()
|
||||||
messages.success(self.request, 'Invoice created successfully')
|
messages.success(self.request, 'Invoice created successfully')
|
||||||
|
|
||||||
if kwargs.get('void'):
|
|
||||||
invoice.void = not invoice.void
|
|
||||||
invoice.save()
|
|
||||||
messages.warning(self.request, 'Invoice voided')
|
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
|
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
|
||||||
|
|
||||||
|
|
||||||
class PaymentCreate(generic.CreateView):
|
class PaymentCreate(generic.CreateView):
|
||||||
model = models.Payment
|
model = models.Payment
|
||||||
fields = ['invoice', 'date', 'amount', 'method']
|
fields = ['invoice', 'date', 'amount', 'method']
|
||||||
template_name = 'payment_form.html'
|
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super(generic.CreateView, self).get_initial()
|
initial = super(generic.CreateView, self).get_initial()
|
||||||
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
|
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
|
||||||
if invoicepk is None:
|
if invoicepk == None:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
invoice = get_object_or_404(models.Invoice, pk=invoicepk)
|
invoice = get_object_or_404(models.Invoice, pk=invoicepk)
|
||||||
initial.update({'invoice': invoice})
|
initial.update({'invoice': invoice})
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
@reversion.create_revision()
|
|
||||||
def form_valid(self, form, *args, **kwargs):
|
|
||||||
reversion.add_to_revision(form.cleaned_data['invoice'])
|
|
||||||
reversion.set_comment("Payment added")
|
|
||||||
return super().form_valid(form, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
messages.info(self.request, "location.reload()")
|
messages.info(self.request, "location.reload()")
|
||||||
return reverse_lazy('closemodal')
|
return reverse_lazy('closemodal')
|
||||||
@@ -251,14 +188,6 @@ class PaymentCreate(generic.CreateView):
|
|||||||
|
|
||||||
class PaymentDelete(generic.DeleteView):
|
class PaymentDelete(generic.DeleteView):
|
||||||
model = models.Payment
|
model = models.Payment
|
||||||
template_name = 'payment_confirm_delete.html'
|
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
@reversion.create_revision()
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
reversion.add_to_revision(self.get_object().invoice)
|
|
||||||
reversion.set_comment("Payment removed")
|
|
||||||
return super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return self.request.POST.get('next')
|
return self.request.POST.get('next')
|
||||||
|
|||||||
224
RIGS/forms.py
224
RIGS/forms.py
@@ -1,29 +1,67 @@
|
|||||||
|
__author__ = 'Ghost'
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
|
||||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||||
from django.db import transaction
|
|
||||||
from registration.forms import RegistrationFormUniqueEmail
|
from registration.forms import RegistrationFormUniqueEmail
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
|
||||||
from captcha.fields import ReCaptchaField
|
from captcha.fields import ReCaptchaField
|
||||||
from reversion import revisions as reversion
|
|
||||||
import simplejson
|
import simplejson
|
||||||
from datetime import datetime
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
|
|
||||||
# Override the django form defaults to use the HTML date/time/datetime UI elements
|
|
||||||
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
# Registration
|
||||||
forms.TimeField.widget = forms.TimeInput(attrs={'type': 'time'}, format='%H:%M')
|
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
||||||
forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M')
|
captcha = ReCaptchaField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Profile
|
||||||
|
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone')
|
||||||
|
|
||||||
|
def clean_initials(self):
|
||||||
|
"""
|
||||||
|
Validate that the supplied initials are unique.
|
||||||
|
"""
|
||||||
|
if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']):
|
||||||
|
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
||||||
|
return self.cleaned_data['initials']
|
||||||
|
|
||||||
|
|
||||||
|
class SocialRegisterForm(ProfileRegistrationFormUniqueEmail):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields.pop('password1')
|
||||||
|
self.fields.pop('password2')
|
||||||
|
|
||||||
|
self.fields['email'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
initial = getattr(self, 'initial', None)
|
||||||
|
if(initial['email'] != self.cleaned_data['email']):
|
||||||
|
raise ValidationError("You cannot change the email")
|
||||||
|
|
||||||
|
return initial['email']
|
||||||
|
|
||||||
|
|
||||||
|
# Login form
|
||||||
|
class PasswordReset(PasswordResetForm):
|
||||||
|
captcha = ReCaptchaField(label='Captcha')
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileCreationForm(UserCreationForm):
|
||||||
|
class Meta(UserCreationForm.Meta):
|
||||||
|
model = models.Profile
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileChangeForm(UserChangeForm):
|
||||||
|
class Meta(UserChangeForm.Meta):
|
||||||
|
model = models.Profile
|
||||||
|
|
||||||
|
|
||||||
# Events Shit
|
# Events Shit
|
||||||
class EventForm(forms.ModelForm):
|
class EventForm(forms.ModelForm):
|
||||||
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
|
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + settings.DATETIME_INPUT_FORMATS
|
||||||
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
||||||
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
||||||
|
|
||||||
@@ -96,14 +134,6 @@ class EventForm(forms.ModelForm):
|
|||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.cleaned_data.get("is_rig") and not (
|
|
||||||
self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
|
|
||||||
raise forms.ValidationError(
|
|
||||||
'You haven\'t provided any client contact details. Please add a person or organisation.',
|
|
||||||
code='contact')
|
|
||||||
return super(EventForm, self).clean()
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
m = super(EventForm, self).save(commit=False)
|
m = super(EventForm, self).save(commit=False)
|
||||||
|
|
||||||
@@ -126,158 +156,4 @@ class EventForm(forms.ModelForm):
|
|||||||
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
||||||
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
||||||
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
||||||
'purchase_order', 'collector']
|
'collector', 'purchase_order']
|
||||||
|
|
||||||
|
|
||||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
|
||||||
tos = forms.BooleanField(required=True, label="Terms of hire")
|
|
||||||
name = forms.CharField(label="Your Name")
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.cleaned_data.get('amount') != self.instance.event.total:
|
|
||||||
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
|
|
||||||
return super(BaseClientEventAuthorisationForm, self).clean()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs)
|
|
||||||
self.fields['uni_id'].required = True
|
|
||||||
self.fields['account_code'].required = True
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.EventAuthorisation
|
|
||||||
fields = ('tos', 'name', 'amount', 'uni_id', 'account_code')
|
|
||||||
|
|
||||||
|
|
||||||
class EventAuthorisationRequestForm(forms.Form):
|
|
||||||
email = forms.EmailField(required=True, label='Authoriser Email')
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(EventRiskAssessmentForm, self).__init__(*args, **kwargs)
|
|
||||||
for name, field in self.fields.items():
|
|
||||||
if str(name) == 'supervisor_consulted':
|
|
||||||
field.widget = forms.CheckboxInput()
|
|
||||||
elif field.__class__ == forms.BooleanField:
|
|
||||||
field.widget = forms.RadioSelect(choices=[
|
|
||||||
(True, 'Yes'),
|
|
||||||
(False, 'No')
|
|
||||||
], attrs={'class': 'custom-control-input', 'required': 'true'})
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
# Check expected values
|
|
||||||
unexpected_values = []
|
|
||||||
for field, value in models.RiskAssessment.expected_values.items():
|
|
||||||
if self.cleaned_data.get(field) != value:
|
|
||||||
unexpected_values.append("<li>{}</li>".format(self._meta.model._meta.get_field(field).help_text))
|
|
||||||
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
|
|
||||||
raise forms.ValidationError("Your answers to these questions: <ul>{}</ul> require consulting with a supervisor.".format(''.join([str(elem) for elem in unexpected_values])), code='unusual_answers')
|
|
||||||
return super(EventRiskAssessmentForm, self).clean()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.RiskAssessment
|
|
||||||
fields = '__all__'
|
|
||||||
exclude = ['reviewed_at', 'reviewed_by']
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(EventChecklistForm, self).__init__(*args, **kwargs)
|
|
||||||
self.fields['date'].widget.format = '%Y-%m-%d'
|
|
||||||
for name, field in self.fields.items():
|
|
||||||
if field.__class__ == forms.NullBooleanField:
|
|
||||||
# Only display yes/no to user, the 'none' is only ever set in the background
|
|
||||||
field.widget = forms.CheckboxInput()
|
|
||||||
# Parsed from incoming form data by clean, then saved into models when the form is saved
|
|
||||||
items = {}
|
|
||||||
|
|
||||||
related_models = {
|
|
||||||
'venue': models.Venue,
|
|
||||||
'power_mic': models.Profile,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Two possible formats
|
|
||||||
def parsedatetime(self, date_string):
|
|
||||||
try:
|
|
||||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
|
|
||||||
except ValueError:
|
|
||||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
|
|
||||||
|
|
||||||
# There's probably a thousand better ways to do this, but this one is mine
|
|
||||||
def clean(self):
|
|
||||||
vehicles = {key: val for key, val in self.data.items()
|
|
||||||
if key.startswith('vehicle')}
|
|
||||||
for key in vehicles:
|
|
||||||
pk = int(key.split('_')[1])
|
|
||||||
driver_key = 'driver_' + str(pk)
|
|
||||||
if(self.data[driver_key] == ''):
|
|
||||||
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
item = models.EventChecklistVehicle.objects.get(pk=pk)
|
|
||||||
except models.EventChecklistVehicle.DoesNotExist:
|
|
||||||
item = models.EventChecklistVehicle()
|
|
||||||
|
|
||||||
item.vehicle = vehicles['vehicle_' + str(pk)]
|
|
||||||
item.driver = models.Profile.objects.get(pk=self.data[driver_key])
|
|
||||||
item.full_clean('checklist')
|
|
||||||
|
|
||||||
# item does not have a database pk yet as it isn't saved
|
|
||||||
self.items['v' + str(pk)] = item
|
|
||||||
|
|
||||||
crewmembers = {key: val for key, val in self.data.items()
|
|
||||||
if key.startswith('crewmember')}
|
|
||||||
other_fields = ['start', 'role', 'end']
|
|
||||||
for key in crewmembers:
|
|
||||||
pk = int(key.split('_')[1])
|
|
||||||
|
|
||||||
for field in other_fields:
|
|
||||||
value = self.data['{}_{}'.format(field, pk)]
|
|
||||||
if value == '':
|
|
||||||
raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field))
|
|
||||||
|
|
||||||
try:
|
|
||||||
item = models.EventChecklistCrew.objects.get(pk=pk)
|
|
||||||
except models.EventChecklistCrew.DoesNotExist:
|
|
||||||
item = models.EventChecklistCrew()
|
|
||||||
|
|
||||||
item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)])
|
|
||||||
item.start = self.parsedatetime(self.data['start_' + str(pk)])
|
|
||||||
item.role = self.data['role_' + str(pk)]
|
|
||||||
item.end = self.parsedatetime(self.data['end_' + str(pk)])
|
|
||||||
item.full_clean('checklist')
|
|
||||||
|
|
||||||
# item does not have a database pk yet as it isn't saved
|
|
||||||
self.items['c' + str(pk)] = item
|
|
||||||
|
|
||||||
return super(EventChecklistForm, self).clean()
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
checklist = super(EventChecklistForm, self).save(commit=False)
|
|
||||||
if (commit):
|
|
||||||
# Remove all existing, to be recreated from the form
|
|
||||||
checklist.vehicles.all().delete()
|
|
||||||
checklist.crew.all().delete()
|
|
||||||
checklist.save()
|
|
||||||
|
|
||||||
for key in self.items:
|
|
||||||
item = self.items[key]
|
|
||||||
reversion.add_to_revision(item)
|
|
||||||
# finish and save new database items
|
|
||||||
item.checklist = checklist
|
|
||||||
item.full_clean()
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
self.items.clear()
|
|
||||||
|
|
||||||
return checklist
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.EventChecklist
|
|
||||||
fields = '__all__'
|
|
||||||
exclude = ['reviewed_at', 'reviewed_by']
|
|
||||||
|
|||||||
217
RIGS/hs.py
217
RIGS/hs.py
@@ -1,217 +0,0 @@
|
|||||||
from RIGS import models, forms
|
|
||||||
from django.views import generic
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from django.db.models import AutoField, ManyToOneRel
|
|
||||||
from django.contrib import messages
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentCreate(generic.CreateView):
|
|
||||||
model = models.RiskAssessment
|
|
||||||
template_name = 'risk_assessment_form.html'
|
|
||||||
form_class = forms.EventRiskAssessmentForm
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
epk = kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
|
|
||||||
# Check if RA exists
|
|
||||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
|
||||||
|
|
||||||
if ra is not None:
|
|
||||||
return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
|
|
||||||
|
|
||||||
return super(EventRiskAssessmentCreate, self).get(self)
|
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
|
||||||
form = super(EventRiskAssessmentCreate, self).get_form(**kwargs)
|
|
||||||
epk = self.kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
form.instance.event = event
|
|
||||||
return form
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventRiskAssessmentCreate, self).get_context_data(**kwargs)
|
|
||||||
epk = self.kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
context['event'] = event
|
|
||||||
context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id)
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
|
||||||
model = models.RiskAssessment
|
|
||||||
template_name = 'risk_assessment_form.html'
|
|
||||||
form_class = forms.EventRiskAssessmentForm
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
ra = self.get_object()
|
|
||||||
ra.reviewed_by = None
|
|
||||||
ra.reviewed_at = None
|
|
||||||
ra.save()
|
|
||||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
|
|
||||||
rpk = self.kwargs.get('pk')
|
|
||||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
|
||||||
context['event'] = ra.event
|
|
||||||
context['edit'] = True
|
|
||||||
context['page_title'] = 'Edit Risk Assessment for Event {}'.format(ra.event.display_id)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentDetail(generic.DetailView):
|
|
||||||
model = models.RiskAssessment
|
|
||||||
template_name = 'risk_assessment_detail.html'
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentList(generic.ListView):
|
|
||||||
paginate_by = 20
|
|
||||||
model = models.RiskAssessment
|
|
||||||
template_name = 'hs_object_list.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
|
|
||||||
context['title'] = 'Risk Assessment'
|
|
||||||
context['view'] = 'ra_detail'
|
|
||||||
context['edit'] = 'ra_edit'
|
|
||||||
context['review'] = 'ra_review'
|
|
||||||
context['perm'] = 'perms.RIGS.review_riskassessment'
|
|
||||||
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentReview(generic.View):
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
rpk = kwargs.get('pk')
|
|
||||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
|
||||||
with reversion.create_revision():
|
|
||||||
reversion.set_user(self.request.user)
|
|
||||||
ra.reviewed_by = self.request.user
|
|
||||||
ra.reviewed_at = timezone.now()
|
|
||||||
ra.save()
|
|
||||||
return HttpResponseRedirect(reverse_lazy('ra_list'))
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistDetail(generic.DetailView):
|
|
||||||
model = models.EventChecklist
|
|
||||||
template_name = 'event_checklist_detail.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventChecklistDetail, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = "Event Checklist for Event {} {}".format(self.object.event.display_id, self.object.event.name)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistEdit(generic.UpdateView):
|
|
||||||
model = models.EventChecklist
|
|
||||||
template_name = 'event_checklist_form.html'
|
|
||||||
form_class = forms.EventChecklistForm
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
ec = self.get_object()
|
|
||||||
ec.reviewed_by = None
|
|
||||||
ec.reviewed_at = None
|
|
||||||
ec.save()
|
|
||||||
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventChecklistEdit, self).get_context_data(**kwargs)
|
|
||||||
pk = self.kwargs.get('pk')
|
|
||||||
ec = models.EventChecklist.objects.get(pk=pk)
|
|
||||||
context['event'] = ec.event
|
|
||||||
context['edit'] = True
|
|
||||||
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
|
|
||||||
form = context['form']
|
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
|
||||||
for field, model in form.related_models.items():
|
|
||||||
value = form[field].value()
|
|
||||||
if value is not None and value != '':
|
|
||||||
context[field] = model.objects.get(pk=value)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistCreate(generic.CreateView):
|
|
||||||
model = models.EventChecklist
|
|
||||||
template_name = 'event_checklist_form.html'
|
|
||||||
form_class = forms.EventChecklistForm
|
|
||||||
|
|
||||||
# From both business logic and programming POVs, RAs must exist before ECs!
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
epk = kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
|
|
||||||
# Check if RA exists
|
|
||||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
|
||||||
|
|
||||||
if ra is None:
|
|
||||||
messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event))
|
|
||||||
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
|
|
||||||
|
|
||||||
return super(EventChecklistCreate, self).get(self)
|
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
|
||||||
form = super(EventChecklistCreate, self).get_form(**kwargs)
|
|
||||||
epk = self.kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
form.instance.event = event
|
|
||||||
return form
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventChecklistCreate, self).get_context_data(**kwargs)
|
|
||||||
epk = self.kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
|
||||||
context['event'] = event
|
|
||||||
context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id)
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistList(generic.ListView):
|
|
||||||
paginate_by = 20
|
|
||||||
model = models.EventChecklist
|
|
||||||
template_name = 'hs_object_list.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventChecklistList, self).get_context_data(**kwargs)
|
|
||||||
context['title'] = 'Event Checklist'
|
|
||||||
context['view'] = 'ec_detail'
|
|
||||||
context['edit'] = 'ec_edit'
|
|
||||||
context['review'] = 'ec_review'
|
|
||||||
context['perm'] = 'perms.RIGS.review_eventchecklist'
|
|
||||||
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistReview(generic.View):
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
rpk = kwargs.get('pk')
|
|
||||||
ec = models.EventChecklist.objects.get(pk=rpk)
|
|
||||||
with reversion.create_revision():
|
|
||||||
reversion.set_user(self.request.user)
|
|
||||||
ec.reviewed_by = self.request.user
|
|
||||||
ec.reviewed_at = timezone.now()
|
|
||||||
ec.save()
|
|
||||||
return HttpResponseRedirect(reverse_lazy('ec_list'))
|
|
||||||
|
|
||||||
|
|
||||||
class HSList(generic.ListView):
|
|
||||||
paginate_by = 20
|
|
||||||
model = models.Event
|
|
||||||
template_name = 'hs_list.html'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return models.Event.objects.all().order_by('-start_date')
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(HSList, self).get_context_data(**kwargs)
|
|
||||||
context['page_title'] = 'H&S Overview'
|
|
||||||
return context
|
|
||||||
85
RIGS/ical.py
85
RIGS/ical.py
@@ -1,19 +1,17 @@
|
|||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
from django_ical.views import ICalFeed
|
from django_ical.views import ICalFeed
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
import datetime
|
import datetime, pytz
|
||||||
import pytz
|
|
||||||
|
|
||||||
|
|
||||||
class CalendarICS(ICalFeed):
|
class CalendarICS(ICalFeed):
|
||||||
"""
|
"""
|
||||||
A simple event calender
|
A simple event calender
|
||||||
"""
|
"""
|
||||||
# Metadata which is passed on to clients
|
#Metadata which is passed on to clients
|
||||||
product_id = 'RIGS'
|
product_id = 'RIGS'
|
||||||
title = 'RIGS Calendar'
|
title = 'RIGS Calendar'
|
||||||
timezone = settings.TIME_ZONE
|
timezone = settings.TIME_ZONE
|
||||||
@@ -29,41 +27,39 @@ class CalendarICS(ICalFeed):
|
|||||||
def get_object(self, request, *args, **kwargs):
|
def get_object(self, request, *args, **kwargs):
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
|
params['dry-hire'] = request.GET.get('dry-hire','true') == 'true'
|
||||||
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
|
params['non-rig'] = request.GET.get('non-rig','true') == 'true'
|
||||||
params['rig'] = request.GET.get('rig', 'true') == 'true'
|
params['rig'] = request.GET.get('rig','true') == 'true'
|
||||||
|
|
||||||
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
|
params['cancelled'] = request.GET.get('cancelled','false') == 'true'
|
||||||
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
|
params['provisional'] = request.GET.get('provisional','true') == 'true'
|
||||||
params['confirmed'] = request.GET.get('confirmed', 'true') == 'true'
|
params['confirmed'] = request.GET.get('confirmed','true') == 'true'
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def description(self, params):
|
def description(self,params):
|
||||||
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
|
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ('Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
||||||
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ('Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
||||||
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
|
||||||
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
|
||||||
|
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
def items(self, params):
|
def items(self, params):
|
||||||
# include events from up to 1 year ago
|
#include events from up to 1 year ago
|
||||||
start = datetime.datetime.now() - datetime.timedelta(days=365)
|
start = datetime.datetime.now() - datetime.timedelta(days=365)
|
||||||
filter = Q(start_date__gte=start)
|
filter = Q(start_date__gte=start)
|
||||||
|
|
||||||
typeFilters = Q(pk=None) # Need something that is false for every entry
|
typeFilters = Q(pk=None) #Need something that is false for every entry
|
||||||
|
|
||||||
if params['dry-hire']:
|
if params['dry-hire']:
|
||||||
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
|
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
|
||||||
|
|
||||||
if params['non-rig']:
|
if params['non-rig']:
|
||||||
typeFilters = typeFilters | Q(is_rig=False)
|
typeFilters = typeFilters | Q(is_rig=False)
|
||||||
|
|
||||||
if params['rig']:
|
if params['rig']:
|
||||||
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
||||||
|
|
||||||
statusFilters = Q(pk=None) # Need something that is false for every entry
|
statusFilters = Q(pk=None) #Need something that is false for every entry
|
||||||
|
|
||||||
if params['cancelled']:
|
if params['cancelled']:
|
||||||
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
||||||
@@ -74,8 +70,7 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
filter = filter & typeFilters & statusFilters
|
filter = filter & typeFilters & statusFilters
|
||||||
|
|
||||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
'venue', 'mic')
|
|
||||||
|
|
||||||
def item_title(self, item):
|
def item_title(self, item):
|
||||||
title = ''
|
title = ''
|
||||||
@@ -92,9 +87,9 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
# Add the rig name
|
# Add the rig name
|
||||||
title += item.name
|
title += item.name
|
||||||
|
|
||||||
# Add the status
|
# Add the status
|
||||||
title += ' (' + str(item.get_status_display()) + ')'
|
title += ' ('+str(item.get_status_display())+')'
|
||||||
|
|
||||||
return title
|
return title
|
||||||
|
|
||||||
@@ -102,12 +97,12 @@ class CalendarICS(ICalFeed):
|
|||||||
return item.earliest_time
|
return item.earliest_time
|
||||||
|
|
||||||
def item_end_datetime(self, item):
|
def item_end_datetime(self, item):
|
||||||
if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
|
if type(item.latest_time) is datetime.date: # Ical end_datetime is non-inclusive, so add a day
|
||||||
return item.latest_time + datetime.timedelta(days=1)
|
return item.latest_time + datetime.timedelta(days=1)
|
||||||
|
|
||||||
return item.latest_time
|
return item.latest_time
|
||||||
|
|
||||||
def item_location(self, item):
|
def item_location(self,item):
|
||||||
return item.venue
|
return item.venue
|
||||||
|
|
||||||
def item_description(self, item):
|
def item_description(self, item):
|
||||||
@@ -116,38 +111,34 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
tz = pytz.timezone(self.timezone)
|
tz = pytz.timezone(self.timezone)
|
||||||
|
|
||||||
desc = 'Rig ID = ' + str(item.pk) + '\n'
|
desc = 'Rig ID = '+str(item.pk)+'\n'
|
||||||
desc += 'Event = ' + item.name + '\n'
|
desc += 'Event = ' + item.name + '\n'
|
||||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
||||||
if item.is_rig and item.person:
|
if item.is_rig and item.person:
|
||||||
desc += 'Client = ' + item.person.name + (
|
desc += 'Client = ' + item.person.name + ( (' for '+item.organisation.name) if item.organisation else '') + '\n'
|
||||||
(' for ' + item.organisation.name) if item.organisation else '') + '\n'
|
|
||||||
desc += 'Status = ' + str(item.get_status_display()) + '\n'
|
desc += 'Status = ' + str(item.get_status_display()) + '\n'
|
||||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||||
|
|
||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.meet_at:
|
if item.meet_at:
|
||||||
desc += 'Crew Meet = ' + (
|
desc += 'Crew Meet = ' + (item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||||
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
|
||||||
if item.access_at:
|
if item.access_at:
|
||||||
desc += 'Access At = ' + (
|
desc += 'Access At = ' + (item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||||
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
|
||||||
if item.start_date:
|
if item.start_date:
|
||||||
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
|
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' '+item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
||||||
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
|
||||||
if item.end_date:
|
if item.end_date:
|
||||||
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + (
|
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' '+item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
|
||||||
(' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
|
|
||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.description:
|
if item.description:
|
||||||
desc += 'Event Description:\n' + item.description + '\n\n'
|
desc += 'Event Description:\n'+item.description+'\n\n'
|
||||||
# if item.notes: // Need to add proper keyholder checks before this gets put back
|
# if item.notes: // Need to add proper keyholder checks before this gets put back
|
||||||
# desc += 'Notes:\n'+item.notes+'\n\n'
|
# desc += 'Notes:\n'+item.notes+'\n\n'
|
||||||
|
|
||||||
base_url = "https://rigs.nottinghamtec.co.uk"
|
|
||||||
desc += 'URL = ' + base_url + str(item.get_absolute_url())
|
|
||||||
|
|
||||||
|
base_url = "http://rigs.nottinghamtec.co.uk"
|
||||||
|
desc += 'URL = '+base_url+str(item.get_absolute_url())
|
||||||
|
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
def item_link(self, item):
|
def item_link(self, item):
|
||||||
@@ -158,8 +149,8 @@ class CalendarICS(ICalFeed):
|
|||||||
# def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though
|
# def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though
|
||||||
# return ''
|
# return ''
|
||||||
|
|
||||||
def item_updated(self, item): # some ical clients will display this
|
def item_updated(self, item): # some ical clients will display this
|
||||||
return item.last_edited_at
|
return item.last_edited_at
|
||||||
|
|
||||||
def item_guid(self, item): # use the rig-id as the ical unique event identifier
|
def item_guid(self, item): # use the rig-id as the ical unique event identifier
|
||||||
return item.pk
|
return item.pk
|
||||||
28
RIGS/importer_tests.py
Normal file
28
RIGS/importer_tests.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
__author__ = 'ghost'
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from importer import fix_email
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFixerTest(unittest.TestCase):
|
||||||
|
def test_correct(self):
|
||||||
|
e = fix_email("tom@ghost.uk.net")
|
||||||
|
self.assertEqual(e, "tom@ghost.uk.net")
|
||||||
|
|
||||||
|
def test_partial(self):
|
||||||
|
e = fix_email("psytp")
|
||||||
|
self.assertEqual(e, "psytp@nottingham.ac.uk")
|
||||||
|
|
||||||
|
def test_none(self):
|
||||||
|
old = None
|
||||||
|
new = fix_email(old)
|
||||||
|
self.assertEqual(old, new)
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
old = ""
|
||||||
|
new = fix_email(old)
|
||||||
|
self.assertEqual(old, new)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -1,11 +1,248 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.core.management import call_command
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.db import transaction
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
|
from RIGS import models
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Adds sample data to use for testing'
|
help = 'Adds sample data to use for testing'
|
||||||
can_import_settings = True
|
can_import_settings = True
|
||||||
|
|
||||||
|
people = []
|
||||||
|
organisations = []
|
||||||
|
venues = []
|
||||||
|
profiles = []
|
||||||
|
|
||||||
|
keyholder_group = None
|
||||||
|
finance_group = None
|
||||||
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
call_command('generateSampleRIGSData')
|
from django.conf import settings
|
||||||
call_command('generateSampleAssetsData')
|
|
||||||
|
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","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"]
|
||||||
|
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","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"]
|
||||||
|
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","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"]
|
||||||
|
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())
|
||||||
|
|||||||
@@ -1,345 +0,0 @@
|
|||||||
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
|
|
||||||
hs_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')
|
|
||||||
self.hs_group = Group.objects.create(name='H&S')
|
|
||||||
|
|
||||||
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",
|
|
||||||
"view_asset", "view_supplier", "change_supplier", "asset_finance",
|
|
||||||
"add_supplier", "view_cabletype", "change_cabletype",
|
|
||||||
"add_cabletype", "view_eventchecklist", "change_eventchecklist",
|
|
||||||
"add_eventchecklist", "view_riskassessment", "change_riskassessment",
|
|
||||||
"add_riskassessment", "add_eventchecklistcrew", "change_eventchecklistcrew",
|
|
||||||
"delete_eventchecklistcrew", "view_eventchecklistcrew", "add_eventchecklistvehicle",
|
|
||||||
"change_eventchecklistvehicle",
|
|
||||||
"delete_eventchecklistvehicle", "view_eventchecklistvehicle", ]
|
|
||||||
financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice",
|
|
||||||
"add_payment", "change_payment", "delete_payment"]
|
|
||||||
hsPerms = keyholderPerms + ["review_riskassessment", "review_eventchecklist"]
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
for permId in hsPerms:
|
|
||||||
self.hs_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, is_approved=True)
|
|
||||||
financeUser.groups.add(self.finance_group)
|
|
||||||
financeUser.groups.add(self.keyholder_group)
|
|
||||||
financeUser.set_password('finance')
|
|
||||||
financeUser.save()
|
|
||||||
|
|
||||||
hsUser = models.Profile.objects.create(username="hs", first_name="HS", last_name="User",
|
|
||||||
initials="HSU",
|
|
||||||
email="hsuser@example.com", is_active=True, is_approved=True)
|
|
||||||
hsUser.groups.add(self.hs_group)
|
|
||||||
hsUser.groups.add(self.keyholder_group)
|
|
||||||
hsUser.set_password('hs')
|
|
||||||
hsUser.save()
|
|
||||||
|
|
||||||
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User",
|
|
||||||
initials="KU",
|
|
||||||
email="keyholderuser@example.com", is_active=True, is_approved=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, is_approved=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 description 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())
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
('postedAt', models.DateTimeField(auto_now=True)),
|
('postedAt', models.DateTimeField(auto_now=True)),
|
||||||
('message', models.TextField()),
|
('message', models.TextField()),
|
||||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -33,11 +33,11 @@ class Migration(migrations.Migration):
|
|||||||
('payment_method', models.CharField(blank=True, null=True, max_length=255)),
|
('payment_method', models.CharField(blank=True, null=True, max_length=255)),
|
||||||
('payment_received', 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)),
|
('purchase_order', models.CharField(blank=True, null=True, max_length=255)),
|
||||||
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events', on_delete=models.CASCADE)),
|
('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', on_delete=models.CASCADE)),
|
('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', on_delete=models.CASCADE)),
|
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic')),
|
||||||
('organisation', models.ForeignKey(to='RIGS.Organisation', on_delete=models.CASCADE)),
|
('organisation', models.ForeignKey(to='RIGS.Organisation')),
|
||||||
('person', models.ForeignKey(to='RIGS.Person', on_delete=models.CASCADE)),
|
('person', models.ForeignKey(to='RIGS.Person')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
@@ -52,7 +52,7 @@ class Migration(migrations.Migration):
|
|||||||
('quantity', models.IntegerField()),
|
('quantity', models.IntegerField()),
|
||||||
('cost', models.DecimalField(max_digits=10, decimal_places=2)),
|
('cost', models.DecimalField(max_digits=10, decimal_places=2)),
|
||||||
('order', models.IntegerField()),
|
('order', models.IntegerField()),
|
||||||
('event', models.ForeignKey(to='RIGS.Event', related_name='item', on_delete=models.CASCADE)),
|
('event', models.ForeignKey(to='RIGS.Event', related_name='item')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
@@ -75,7 +75,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='venue',
|
name='venue',
|
||||||
field=models.ForeignKey(to='RIGS.Venue', on_delete=models.CASCADE),
|
field=models.ForeignKey(to='RIGS.Venue'),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -14,26 +14,26 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='based_on',
|
name='based_on',
|
||||||
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True, on_delete=models.CASCADE),
|
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='checked_in_by',
|
name='checked_in_by',
|
||||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True,
|
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True,
|
||||||
null=True, on_delete=models.CASCADE),
|
null=True),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='mic',
|
name='mic',
|
||||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True, on_delete=models.CASCADE),
|
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='organisation',
|
name='organisation',
|
||||||
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True, on_delete=models.CASCADE),
|
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -19,8 +19,8 @@ class Migration(migrations.Migration):
|
|||||||
('run', models.BooleanField(default=False)),
|
('run', models.BooleanField(default=False)),
|
||||||
('derig', models.BooleanField(default=False)),
|
('derig', models.BooleanField(default=False)),
|
||||||
('notes', models.TextField(blank=True, null=True)),
|
('notes', models.TextField(blank=True, null=True)),
|
||||||
('event', models.ForeignKey(related_name='crew', to='RIGS.Event', on_delete=models.CASCADE)),
|
('event', models.ForeignKey(related_name='crew', to='RIGS.Event')),
|
||||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='eventitem',
|
model_name='eventitem',
|
||||||
name='event',
|
name='event',
|
||||||
field=models.ForeignKey(related_name='items', to='RIGS.Event', on_delete=models.CASCADE),
|
field=models.ForeignKey(related_name='items', to='RIGS.Event'),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='person',
|
name='person',
|
||||||
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person', on_delete=models.CASCADE),
|
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person'),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
@@ -14,13 +14,13 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='venue',
|
name='venue',
|
||||||
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True, on_delete=models.CASCADE),
|
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='eventitem',
|
model_name='eventitem',
|
||||||
name='event',
|
name='event',
|
||||||
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event', on_delete=models.CASCADE),
|
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event'),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
('invoice_date', models.DateField(auto_now_add=True)),
|
('invoice_date', models.DateField(auto_now_add=True)),
|
||||||
('void', models.BooleanField()),
|
('void', models.BooleanField()),
|
||||||
('event', models.OneToOneField(to='RIGS.Event', on_delete=models.CASCADE)),
|
('event', models.OneToOneField(to='RIGS.Event')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
|
|||||||
('date', models.DateField()),
|
('date', models.DateField()),
|
||||||
('amount', models.DecimalField(help_text=b'Please use ex. VAT', max_digits=10, decimal_places=2)),
|
('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')])),
|
('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', on_delete=models.CASCADE)),
|
('invoice', models.ForeignKey(to='RIGS.Invoice')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='mic',
|
name='mic',
|
||||||
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
|
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.9.4 on 2016-03-31 12:02
|
|
||||||
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0024_auto_20160229_2042'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='username',
|
|
||||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='vatrate',
|
|
||||||
name='start_at',
|
|
||||||
field=models.DateField(),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0024_auto_20160229_2042'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EventAuthorisation',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
|
||||||
('email', models.EmailField(max_length=254)),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('uni_id', models.CharField(max_length=10, null=True, verbose_name=b'University ID', blank=True)),
|
|
||||||
('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', on_delete=models.CASCADE)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.11.1 on 2017-05-10 17:46
|
|
||||||
|
|
||||||
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0025_auto_20160331_1302'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='username',
|
|
||||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0025_eventauthorisation'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventauthorisation',
|
|
||||||
name='created_at',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0026_remove_eventauthorisation_created_at'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventauthorisation',
|
|
||||||
name='event',
|
|
||||||
field=models.OneToOneField(related_name='authorisation', to='RIGS.Event', on_delete=models.CASCADE),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0027_eventauthorisation_event_singular'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='eventauthorisation',
|
|
||||||
name='sent_by',
|
|
||||||
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0029_eventauthorisation_sent_by'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='event',
|
|
||||||
name='auth_request_at',
|
|
||||||
field=models.DateTimeField(null=True, blank=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='event',
|
|
||||||
name='auth_request_by',
|
|
||||||
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='event',
|
|
||||||
name='auth_request_to',
|
|
||||||
field=models.EmailField(max_length=254, null=True, blank=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.11.1 on 2017-05-12 20:02
|
|
||||||
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0030_auth_request_sending'),
|
|
||||||
('RIGS', '0026_auto_20170510_1846'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
]
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.11.4 on 2017-09-04 22:55
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('reversion', '0001_squashed_0004_auto_20160611_1202'),
|
|
||||||
('RIGS', '0031_merge_20170512_2102'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RIGSVersion',
|
|
||||||
fields=[
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'indexes': [],
|
|
||||||
'proxy': True,
|
|
||||||
},
|
|
||||||
bases=('reversion.version',),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='event',
|
|
||||||
name='collector',
|
|
||||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='collected by'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='event',
|
|
||||||
name='mic',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_mic', to=settings.AUTH_USER_MODEL, verbose_name='MIC'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='event',
|
|
||||||
name='purchase_order',
|
|
||||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='PO'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventauthorisation',
|
|
||||||
name='amount',
|
|
||||||
field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='authorisation amount'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventauthorisation',
|
|
||||||
name='uni_id',
|
|
||||||
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='University ID'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='payment',
|
|
||||||
name='amount',
|
|
||||||
field=models.DecimalField(decimal_places=2, help_text='Please use ex. VAT', max_digits=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='payment',
|
|
||||||
name='method',
|
|
||||||
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], max_length=2, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='username',
|
|
||||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-01-10 14:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0035_auto_20191124_1319'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='is_approved',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='last_emailed',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-01-11 18:29
|
|
||||||
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
def approve_legacy(apps, schema_editor):
|
|
||||||
Profile = apps.get_model('RIGS', 'Profile')
|
|
||||||
for person in Profile.objects.all():
|
|
||||||
person.is_approved = True
|
|
||||||
person.save()
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0036_profile_is_approved'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(approve_legacy)
|
|
||||||
]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-03-06 20:00
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0037_approve_legacy'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='event',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='invoice',
|
|
||||||
options={'ordering': ['-invoice_date']},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='organisation',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='person',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='profile',
|
|
||||||
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='venue',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
# Generated by Django 3.1.2 on 2021-01-23 19:10
|
|
||||||
|
|
||||||
import RIGS.models
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0038_auto_20200306_2000'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EventChecklist',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('date', models.DateField()),
|
|
||||||
('safe_parking', models.BooleanField(blank=True, help_text='Vehicles parked safely?<br><small>(does not obstruct venue access)</small>', null=True)),
|
|
||||||
('safe_packing', models.BooleanField(blank=True, help_text='Equipment packed away safely?<br><small>(including flightcases)</small>', null=True)),
|
|
||||||
('exits', models.BooleanField(blank=True, help_text='Emergency exits clear?', null=True)),
|
|
||||||
('trip_hazard', models.BooleanField(blank=True, help_text='Appropriate barriers around kit and cabling secured?', null=True)),
|
|
||||||
('warning_signs', models.BooleanField(blank=True, help_text='Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>')),
|
|
||||||
('ear_plugs', models.BooleanField(blank=True, help_text='Ear plugs issued to crew where needed?', null=True)),
|
|
||||||
('hs_location', models.CharField(blank=True, help_text='Location of Safety Bag/Box', max_length=255, null=True)),
|
|
||||||
('extinguishers_location', models.CharField(blank=True, help_text='Location of fire extinguishers', max_length=255, null=True)),
|
|
||||||
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
|
|
||||||
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
|
|
||||||
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
|
|
||||||
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
|
|
||||||
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
|
|
||||||
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
|
|
||||||
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
|
|
||||||
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
|
|
||||||
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
|
|
||||||
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
|
|
||||||
('fd_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True, verbose_name='Earth Fault Loop Impedance')),
|
|
||||||
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
|
|
||||||
('w1_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
|
||||||
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
|
||||||
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
|
||||||
('w1_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
|
||||||
('w2_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
|
||||||
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
|
||||||
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
|
||||||
('w2_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
|
||||||
('w3_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
|
||||||
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
|
||||||
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
|
||||||
('w3_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
|
||||||
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
|
|
||||||
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
|
|
||||||
('reviewed_at', models.DateTimeField(null=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['event'],
|
|
||||||
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
|
|
||||||
},
|
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EventChecklistCrew',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('role', models.CharField(max_length=255)),
|
|
||||||
('start', models.DateTimeField()),
|
|
||||||
('end', models.DateTimeField()),
|
|
||||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
|
|
||||||
],
|
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EventChecklistVehicle',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('vehicle', models.CharField(max_length=255)),
|
|
||||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
|
|
||||||
],
|
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RiskAssessment',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('nonstandard_equipment', models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>TEC's standard risk assessments and method statements?</a>")),
|
|
||||||
('nonstandard_use', models.BooleanField(help_text='Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>')),
|
|
||||||
('contractors', models.BooleanField(help_text='Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>')),
|
|
||||||
('other_companies', models.BooleanField(help_text='Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>')),
|
|
||||||
('crew_fatigue', models.BooleanField(help_text='Is crew fatigue likely to be a risk at any point during this event?')),
|
|
||||||
('general_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
|
||||||
('big_power', models.BooleanField(help_text='Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?')),
|
|
||||||
('outside', models.BooleanField(help_text='Is the event outdoors?')),
|
|
||||||
('generators', models.BooleanField(help_text='Will generators be used?')),
|
|
||||||
('other_companies_power', models.BooleanField(help_text='Will TEC be supplying power to any other companies?')),
|
|
||||||
('nonstandard_equipment_power', models.BooleanField(help_text='Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?')),
|
|
||||||
('multiple_electrical_environments', models.BooleanField(help_text='Will the electrical installation occupy more than one electrical environment?')),
|
|
||||||
('power_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
|
||||||
('power_plan', models.URLField(blank=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
|
|
||||||
('noise_monitoring', models.BooleanField(help_text='Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?')),
|
|
||||||
('sound_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
|
||||||
('known_venue', models.BooleanField(help_text='Is this venue new to you (the MIC) or new to TEC?')),
|
|
||||||
('safe_loading', models.BooleanField(help_text='Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)')),
|
|
||||||
('safe_storage', models.BooleanField(help_text='Are there any problems with safe and secure equipment storage?')),
|
|
||||||
('area_outside_of_control', models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")),
|
|
||||||
('barrier_required', models.BooleanField(help_text='Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?')),
|
|
||||||
('nonstandard_emergency_procedure', models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")),
|
|
||||||
('special_structures', models.BooleanField(help_text='Does the event require use of winch stands, motors, MPT Towers, or staging?')),
|
|
||||||
('suspended_structures', models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")),
|
|
||||||
('persons_responsible_structures', models.TextField(blank=True, help_text='Who are the persons on site responsible for their use?', null=True)),
|
|
||||||
('rigging_plan', models.URLField(blank=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
|
|
||||||
('reviewed_at', models.DateTimeField(null=True)),
|
|
||||||
('supervisor_consulted', models.BooleanField(null=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['event'],
|
|
||||||
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
|
|
||||||
},
|
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventcrew',
|
|
||||||
name='event',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventcrew',
|
|
||||||
name='user',
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='RIGSVersion',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='event',
|
|
||||||
name='risk_assessment_edit_url',
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='first_name',
|
|
||||||
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='EventCrew',
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='riskassessment',
|
|
||||||
name='event',
|
|
||||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='RIGS.event'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='riskassessment',
|
|
||||||
name='power_mic',
|
|
||||||
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='power_mic', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='riskassessment',
|
|
||||||
name='reviewed_by',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='eventchecklistvehicle',
|
|
||||||
name='driver',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='eventchecklistcrew',
|
|
||||||
name='crewmember',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crewed', to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='event',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='RIGS.event'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='power_mic',
|
|
||||||
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='reviewed_by',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='venue',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
498
RIGS/models.py
498
RIGS/models.py
@@ -1,48 +1,40 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.db import models
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from reversion.models import Version
|
|
||||||
import string
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import string
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
from django.db import models
|
||||||
from urllib.parse import urlparse
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Profile(AbstractUser):
|
class Profile(AbstractUser):
|
||||||
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
||||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
||||||
is_approved = models.BooleanField(default=False)
|
|
||||||
last_emailed = models.DateTimeField(blank=True,
|
|
||||||
null=True) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_api_key(cls):
|
def make_api_key(cls):
|
||||||
size = 20
|
size = 20
|
||||||
chars = string.ascii_letters + string.digits
|
chars = string.ascii_letters + string.digits
|
||||||
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
||||||
return new_api_key
|
return new_api_key;
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def profile_picture(self):
|
def profile_picture(self):
|
||||||
url = ""
|
url = ""
|
||||||
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
||||||
url = "https://www.gravatar.com/avatar/" + hashlib.md5(
|
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
|
||||||
self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -56,53 +48,46 @@ class Profile(AbstractUser):
|
|||||||
def latest_events(self):
|
def latest_events(self):
|
||||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def admins(cls):
|
|
||||||
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def users_awaiting_approval_count(cls):
|
|
||||||
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
# TODO move to versioning - currently get import errors with that
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_profile', 'Can view Profile'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RevisionMixin(object):
|
class RevisionMixin(object):
|
||||||
@property
|
|
||||||
def is_first_version(self):
|
|
||||||
versions = Version.objects.get_for_object(self)
|
|
||||||
return len(versions) == 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_version(self):
|
|
||||||
version = Version.objects.get_for_object(self).select_related('revision').first()
|
|
||||||
return version
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_edited_at(self):
|
def last_edited_at(self):
|
||||||
version = self.current_version
|
versions = reversion.get_for_object(self)
|
||||||
if version is None:
|
if versions:
|
||||||
|
version = reversion.get_for_object(self)[0]
|
||||||
|
return version.revision.date_created
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
return version.revision.date_created
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_edited_by(self):
|
def last_edited_by(self):
|
||||||
version = self.current_version
|
versions = reversion.get_for_object(self)
|
||||||
if version is None:
|
if versions:
|
||||||
|
version = reversion.get_for_object(self)[0]
|
||||||
|
return version.revision.user
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
return version.revision.user
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_version_id(self):
|
def current_version_id(self):
|
||||||
version = self.current_version
|
versions = reversion.get_for_object(self)
|
||||||
if version is None:
|
if versions:
|
||||||
|
version = reversion.get_for_object(self)[0]
|
||||||
|
return "V{0} | R{1}".format(version.pk, version.revision.pk)
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
return "V{0} | R{1}".format(version.pk, version.revision.pk)
|
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Person(models.Model, RevisionMixin):
|
class Person(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||||
@@ -138,7 +123,14 @@ class Person(models.Model, RevisionMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_person', 'Can view Persons'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Organisation(models.Model, RevisionMixin):
|
class Organisation(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||||
@@ -175,10 +167,15 @@ class Organisation(models.Model, RevisionMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_organisation', 'Can view Organisations'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VatManager(models.Manager):
|
class VatManager(models.Manager):
|
||||||
def current_rate(self):
|
def current_rate(self):
|
||||||
return self.find_rate(timezone.now())
|
return self.find_rate(datetime.datetime.now())
|
||||||
|
|
||||||
def find_rate(self, date):
|
def find_rate(self, date):
|
||||||
# return self.filter(startAt__lte=date).latest()
|
# return self.filter(startAt__lte=date).latest()
|
||||||
@@ -191,15 +188,14 @@ class VatManager(models.Manager):
|
|||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class VatRate(models.Model, RevisionMixin):
|
class VatRate(models.Model, RevisionMixin):
|
||||||
start_at = models.DateField()
|
start_at = models.DateTimeField()
|
||||||
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
||||||
comment = models.CharField(max_length=255)
|
comment = models.CharField(max_length=255)
|
||||||
|
|
||||||
objects = VatManager()
|
objects = VatManager()
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_percent(self):
|
def as_percent(self):
|
||||||
return self.rate * 100
|
return self.rate * 100
|
||||||
@@ -212,6 +208,8 @@ class VatRate(models.Model, RevisionMixin):
|
|||||||
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
|
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Venue(models.Model, RevisionMixin):
|
class Venue(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||||
@@ -234,19 +232,24 @@ class Venue(models.Model, RevisionMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_venue', 'Can view Venues'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventManager(models.Manager):
|
class EventManager(models.Manager):
|
||||||
def current_events(self):
|
def current_events(self):
|
||||||
events = self.filter(
|
events = self.filter(
|
||||||
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Ends after
|
status=Event.CANCELLED)) | # Ends after
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(
|
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Active dry hire
|
status=Event.CANCELLED)) | # Active dry hire
|
||||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
||||||
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||||
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
|
models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started
|
||||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
||||||
'organisation',
|
'organisation',
|
||||||
'venue', 'mic')
|
'venue', 'mic')
|
||||||
@@ -272,18 +275,21 @@ class EventManager(models.Manager):
|
|||||||
|
|
||||||
def rig_count(self):
|
def rig_count(self):
|
||||||
event_count = self.filter(
|
event_count = self.filter(
|
||||||
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
|
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False,
|
||||||
is_rig=True) & ~models.Q(
|
is_rig=True) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
|
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False, is_rig=True) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Ends after
|
status=Event.CANCELLED)) | # Ends after
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
|
(models.Q(dry_hire=True, start_date__gte=datetime.date.today(), is_rig=True) & ~models.Q(
|
||||||
status=Event.CANCELLED)) # Active dry hire
|
status=Event.CANCELLED)) | # Active dry hire
|
||||||
|
(models.Q(dry_hire=True, checked_in_by__isnull=True, is_rig=True) & (
|
||||||
|
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) # Active dry hire GT
|
||||||
).count()
|
).count()
|
||||||
return event_count
|
return event_count
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
@reversion.register(follow=['items'])
|
||||||
|
@python_2_unicode_compatible
|
||||||
class Event(models.Model, RevisionMixin):
|
class Event(models.Model, RevisionMixin):
|
||||||
# Done to make it much nicer on the database
|
# Done to make it much nicer on the database
|
||||||
PROVISIONAL = 0
|
PROVISIONAL = 0
|
||||||
@@ -298,9 +304,9 @@ class Event(models.Model, RevisionMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
person = models.ForeignKey('Person', null=True, blank=True)
|
||||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
organisation = models.ForeignKey('Organisation', blank=True, null=True)
|
||||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
venue = models.ForeignKey('Venue', blank=True, null=True)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
notes = models.TextField(blank=True, null=True)
|
notes = models.TextField(blank=True, null=True)
|
||||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||||
@@ -319,10 +325,9 @@ class Event(models.Model, RevisionMixin):
|
|||||||
meet_info = models.CharField(max_length=255, blank=True, null=True)
|
meet_info = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
# Crew management
|
# 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,
|
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
||||||
verbose_name="MIC", on_delete=models.CASCADE)
|
verbose_name="MIC")
|
||||||
|
|
||||||
# Monies
|
# Monies
|
||||||
payment_method = models.CharField(max_length=255, blank=True, null=True)
|
payment_method = models.CharField(max_length=255, blank=True, null=True)
|
||||||
@@ -330,18 +335,6 @@ class Event(models.Model, RevisionMixin):
|
|||||||
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
|
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
|
||||||
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
|
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, on_delete=models.CASCADE)
|
|
||||||
auth_request_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
auth_request_to = models.EmailField(null=True, blank=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
if self.is_rig:
|
|
||||||
return str("N%05d" % self.pk)
|
|
||||||
else:
|
|
||||||
return self.pk
|
|
||||||
|
|
||||||
# Calculated values
|
# Calculated values
|
||||||
"""
|
"""
|
||||||
EX Vat
|
EX Vat
|
||||||
@@ -349,6 +342,17 @@ class Event(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sum_total(self):
|
def sum_total(self):
|
||||||
|
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
|
||||||
|
# if connection.vendor == 'postgresql':
|
||||||
|
# sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
|
||||||
|
# else:
|
||||||
|
# sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
|
||||||
|
# total = self.items.raw(sql)[0]
|
||||||
|
# if total.sum_total:
|
||||||
|
# return total.sum_total
|
||||||
|
# total = 0.0
|
||||||
|
# for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
|
||||||
|
# total += item.sum
|
||||||
total = EventItem.objects.filter(event=self).aggregate(
|
total = EventItem.objects.filter(event=self).aggregate(
|
||||||
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||||
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||||
@@ -363,7 +367,7 @@ class Event(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def vat(self):
|
def vat(self):
|
||||||
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
return self.sum_total * self.vat_rate.rate
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Inc VAT
|
Inc VAT
|
||||||
@@ -371,7 +375,7 @@ class Event(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self):
|
def total(self):
|
||||||
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
return self.sum_total + self.vat
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cancelled(self):
|
def cancelled(self):
|
||||||
@@ -381,10 +385,6 @@ class Event(models.Model, RevisionMixin):
|
|||||||
def confirmed(self):
|
def confirmed(self):
|
||||||
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||||
|
|
||||||
@property
|
|
||||||
def hs_done(self):
|
|
||||||
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_start_time(self):
|
def has_start_time(self):
|
||||||
return self.start_time is not None
|
return self.start_time is not None
|
||||||
@@ -445,61 +445,42 @@ class Event(models.Model, RevisionMixin):
|
|||||||
else:
|
else:
|
||||||
return endDate
|
return endDate
|
||||||
|
|
||||||
@property
|
|
||||||
def internal(self):
|
|
||||||
return bool(self.organisation and self.organisation.union_account)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def authorised(self):
|
|
||||||
if self.internal:
|
|
||||||
return self.authorisation.amount == self.total
|
|
||||||
else:
|
|
||||||
return bool(self.purchase_order)
|
|
||||||
|
|
||||||
objects = EventManager()
|
objects = EventManager()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('event_detail', kwargs={'pk': self.pk})
|
return reverse_lazy('event_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{}: {}".format(self.display_id, self.name)
|
return unicode(self.pk) + ": " + self.name
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
errdict = {}
|
|
||||||
if self.end_date and self.start_date > self.end_date:
|
if self.end_date and self.start_date > self.end_date:
|
||||||
errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
|
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
|
||||||
|
|
||||||
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||||
hasStartAndEnd = self.has_start_time and self.has_end_time
|
hasStartAndEnd = self.has_start_time and self.has_end_time
|
||||||
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
||||||
errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
|
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
|
||||||
|
|
||||||
if self.access_at is not None:
|
|
||||||
if self.access_at.date() > self.start_date:
|
|
||||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
|
||||||
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
|
|
||||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
|
||||||
|
|
||||||
if errdict != {}: # If there was an error when validation
|
|
||||||
raise ValidationError(errdict)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Call :meth:`full_clean` before saving."""
|
"""Call :meth:`full_clean` before saving."""
|
||||||
self.full_clean()
|
self.full_clean()
|
||||||
super(Event, self).save(*args, **kwargs)
|
super(Event, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_event', 'Can view Events'),
|
||||||
|
)
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventItem(models.Model, RevisionMixin):
|
class EventItem(models.Model):
|
||||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
event = models.ForeignKey('Event', related_name='items', blank=True)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
quantity = models.IntegerField()
|
quantity = models.IntegerField()
|
||||||
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
order = models.IntegerField()
|
order = models.IntegerField()
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_cost(self):
|
def total_cost(self):
|
||||||
return self.cost * self.quantity
|
return self.cost * self.quantity
|
||||||
@@ -510,37 +491,22 @@ class EventItem(models.Model, RevisionMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
|
return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
class EventCrew(models.Model):
|
||||||
return str("item {}".format(self.name))
|
event = models.ForeignKey('Event', related_name='crew')
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||||
|
rig = models.BooleanField(default=False)
|
||||||
|
run = models.BooleanField(default=False)
|
||||||
|
derig = models.BooleanField(default=False)
|
||||||
|
notes = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@python_2_unicode_compatible
|
||||||
class EventAuthorisation(models.Model, RevisionMixin):
|
class Invoice(models.Model):
|
||||||
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
event = models.OneToOneField('Event')
|
||||||
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('Profile', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['payment_set'])
|
|
||||||
class Invoice(models.Model, RevisionMixin):
|
|
||||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
|
||||||
invoice_date = models.DateField(auto_now_add=True)
|
invoice_date = models.DateField(auto_now_add=True)
|
||||||
void = models.BooleanField(default=False)
|
void = models.BooleanField(default=False)
|
||||||
|
|
||||||
reversion_perm = 'RIGS.view_invoice'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sum_total(self):
|
def sum_total(self):
|
||||||
return self.event.sum_total
|
return self.event.sum_total
|
||||||
@@ -564,26 +530,18 @@ class Invoice(models.Model, RevisionMixin):
|
|||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
return self.balance == 0 or self.void
|
return self.balance == 0 or self.void
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse_lazy('invoice_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return "#{} for Event {}".format(self.display_id, "N%05d" % self.event.pk)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return "{:05d}".format(self.pk)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_invoice', 'Can view Invoices'),
|
||||||
|
)
|
||||||
ordering = ['-invoice_date']
|
ordering = ['-invoice_date']
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@python_2_unicode_compatible
|
||||||
class Payment(models.Model, RevisionMixin):
|
class Payment(models.Model):
|
||||||
CASH = 'C'
|
CASH = 'C'
|
||||||
INTERNAL = 'I'
|
INTERNAL = 'I'
|
||||||
EXTERNAL = 'E'
|
EXTERNAL = 'E'
|
||||||
@@ -597,244 +555,10 @@ class Payment(models.Model, RevisionMixin):
|
|||||||
(ADJUSTMENT, 'TEC Adjustment'),
|
(ADJUSTMENT, 'TEC Adjustment'),
|
||||||
)
|
)
|
||||||
|
|
||||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
invoice = models.ForeignKey('Invoice')
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
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)
|
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s: %d" % (self.get_method_display(), self.amount)
|
return "%s: %d" % (self.get_method_display(), self.amount)
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str("payment of £{}".format(self.amount))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_url(value):
|
|
||||||
if not value:
|
|
||||||
return # Required error is done the field
|
|
||||||
obj = urlparse(value)
|
|
||||||
if obj.hostname not in ('nottinghamtec.sharepoint.com'):
|
|
||||||
raise ValidationError('URL must point to a location on the TEC Sharepoint')
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class RiskAssessment(models.Model, RevisionMixin):
|
|
||||||
SMALL = (0, 'Small')
|
|
||||||
MEDIUM = (1, 'Medium')
|
|
||||||
LARGE = (2, 'Large')
|
|
||||||
SIZES = (SMALL, MEDIUM, LARGE)
|
|
||||||
|
|
||||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
|
||||||
# General
|
|
||||||
nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>"
|
|
||||||
"TEC's standard risk assessments and method statements?</a>")
|
|
||||||
nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>")
|
|
||||||
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
|
|
||||||
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
|
|
||||||
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
|
|
||||||
general_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
|
||||||
|
|
||||||
# Power
|
|
||||||
# event_size = models.IntegerField(blank=True, null=True, choices=SIZES)
|
|
||||||
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
|
|
||||||
# If yes to the above two, you must answer...
|
|
||||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
|
|
||||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
|
|
||||||
outside = models.BooleanField(help_text="Is the event outdoors?")
|
|
||||||
generators = models.BooleanField(help_text="Will generators be used?")
|
|
||||||
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
|
|
||||||
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
|
|
||||||
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
|
|
||||||
power_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
|
||||||
power_plan = models.URLField(blank=True, null=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
|
||||||
|
|
||||||
# Sound
|
|
||||||
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
|
|
||||||
sound_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
|
||||||
|
|
||||||
# Site
|
|
||||||
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
|
|
||||||
safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
|
|
||||||
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
|
|
||||||
area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
|
|
||||||
barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
|
|
||||||
nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
|
|
||||||
|
|
||||||
# Structures
|
|
||||||
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
|
|
||||||
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
|
|
||||||
persons_responsible_structures = models.TextField(blank=True, null=True, help_text="Who are the persons on site responsible for their use?")
|
|
||||||
rigging_plan = models.URLField(blank=True, null=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
|
||||||
|
|
||||||
# Blimey that was a lot of options
|
|
||||||
|
|
||||||
reviewed_at = models.DateTimeField(null=True)
|
|
||||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
|
||||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
supervisor_consulted = models.BooleanField(null=True)
|
|
||||||
|
|
||||||
expected_values = {
|
|
||||||
'nonstandard_equipment': False,
|
|
||||||
'nonstandard_use': False,
|
|
||||||
'contractors': False,
|
|
||||||
'other_companies': False,
|
|
||||||
'crew_fatigue': False,
|
|
||||||
'big_power': False,
|
|
||||||
'generators': False,
|
|
||||||
'other_companies_power': False,
|
|
||||||
'nonstandard_equipment_power': False,
|
|
||||||
'multiple_electrical_environments': False,
|
|
||||||
'noise_monitoring': False,
|
|
||||||
'known_venue': False,
|
|
||||||
'safe_loading': False,
|
|
||||||
'safe_storage': False,
|
|
||||||
'area_outside_of_control': False,
|
|
||||||
'barrier_required': False,
|
|
||||||
'nonstandard_emergency_procedure': False,
|
|
||||||
'special_structures': False,
|
|
||||||
'suspended_structures': False,
|
|
||||||
}
|
|
||||||
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
# Check for idiots
|
|
||||||
if not self.outside and self.generators:
|
|
||||||
raise forms.ValidationError("Engage brain, please. <strong>No generators indoors!(!)</strong>")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['event']
|
|
||||||
permissions = [
|
|
||||||
('review_riskassessment', 'Can review Risk Assessments')
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def event_size(self):
|
|
||||||
# Confirm event size. Check all except generators, since generators entails outside
|
|
||||||
if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
|
|
||||||
return self.LARGE[0]
|
|
||||||
elif self.big_power:
|
|
||||||
return self.MEDIUM[0]
|
|
||||||
else:
|
|
||||||
return self.SMALL[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str(self.event)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse_lazy('ra_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%i - %s" % (self.pk, self.event)
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['vehicles', 'crew'])
|
|
||||||
class EventChecklist(models.Model, RevisionMixin):
|
|
||||||
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
# General
|
|
||||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
|
|
||||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
|
|
||||||
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
|
|
||||||
date = models.DateField()
|
|
||||||
|
|
||||||
# Safety Checks
|
|
||||||
safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?<br><small>(does not obstruct venue access)</small>")
|
|
||||||
safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?<br><small>(including flightcases)</small>")
|
|
||||||
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
|
|
||||||
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
|
|
||||||
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
|
|
||||||
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
|
|
||||||
hs_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of Safety Bag/Box")
|
|
||||||
extinguishers_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of fire extinguishers")
|
|
||||||
|
|
||||||
# Small Electrical Checks
|
|
||||||
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
|
|
||||||
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
|
||||||
|
|
||||||
# Shared electrical checks
|
|
||||||
earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>")
|
|
||||||
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
|
|
||||||
|
|
||||||
# Medium Electrical Checks
|
|
||||||
source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?<br><small>(if cable is more than 3m long) </small>")
|
|
||||||
labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
|
|
||||||
# First Distro
|
|
||||||
fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
|
|
||||||
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
|
|
||||||
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
|
|
||||||
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
|
|
||||||
fd_earth_fault = models.IntegerField(blank=True, null=True, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
|
|
||||||
# Worst case points
|
|
||||||
w1_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
|
|
||||||
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
|
||||||
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
|
||||||
w1_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
w2_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
|
|
||||||
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
|
||||||
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
|
||||||
w2_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
w3_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
|
|
||||||
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
|
||||||
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
|
||||||
w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
|
||||||
|
|
||||||
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
|
||||||
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
|
||||||
|
|
||||||
reviewed_at = models.DateTimeField(null=True)
|
|
||||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
|
||||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
inverted_fields = []
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['event']
|
|
||||||
permissions = [
|
|
||||||
('review_eventchecklist', 'Can review Event Checklists')
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str(self.event)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse_lazy('ec_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%i - %s" % (self.pk, self.event)
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventChecklistVehicle(models.Model, RevisionMixin):
|
|
||||||
checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE)
|
|
||||||
vehicle = models.CharField(max_length=255)
|
|
||||||
driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{} driven by {}".format(self.vehicle, str(self.driver))
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventChecklistCrew(models.Model, RevisionMixin):
|
|
||||||
checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE)
|
|
||||||
crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE)
|
|
||||||
role = models.CharField(max_length=255)
|
|
||||||
start = models.DateTimeField()
|
|
||||||
end = models.DateTimeField()
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.start > self.end:
|
|
||||||
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{} ({})".format(str(self.crewmember), self.role)
|
|
||||||
|
|||||||
13
RIGS/regbackend.py
Normal file
13
RIGS/regbackend.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from RIGS.models import Profile
|
||||||
|
from RIGS.forms import ProfileRegistrationFormUniqueEmail
|
||||||
|
|
||||||
|
def user_created(sender, user, request, **kwargs):
|
||||||
|
form = ProfileRegistrationFormUniqueEmail(request.POST)
|
||||||
|
user.first_name = form.data['first_name']
|
||||||
|
user.last_name = form.data['last_name']
|
||||||
|
user.initials = form.data['initials']
|
||||||
|
user.phone = form.data['phone']
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
from registration.signals import user_registered
|
||||||
|
user_registered.connect(user_created)
|
||||||
341
RIGS/rigboard.py
341
RIGS/rigboard.py
@@ -1,33 +1,23 @@
|
|||||||
|
import os
|
||||||
|
import cStringIO as StringIO
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import urllib.request
|
import urllib2
|
||||||
import urllib.error
|
|
||||||
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.views import generic
|
||||||
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.core import signing
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.core.exceptions import SuspiciousOperation
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.utils import timezone
|
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||||
import simplejson
|
import simplejson
|
||||||
import premailer
|
|
||||||
|
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
from PyRIGS import decorators
|
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import copy
|
import copy
|
||||||
@@ -36,7 +26,7 @@ __author__ = 'ghost'
|
|||||||
|
|
||||||
|
|
||||||
class RigboardIndex(generic.TemplateView):
|
class RigboardIndex(generic.TemplateView):
|
||||||
template_name = 'rigboard.html'
|
template_name = 'RIGS/rigboard.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# get super context
|
# get super context
|
||||||
@@ -44,22 +34,18 @@ class RigboardIndex(generic.TemplateView):
|
|||||||
|
|
||||||
# call out method to get current events
|
# call out method to get current events
|
||||||
context['events'] = models.Event.objects.current_events()
|
context['events'] = models.Event.objects.current_events()
|
||||||
context['page_title'] = "Rigboard"
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WebCalendar(generic.TemplateView):
|
class WebCalendar(generic.TemplateView):
|
||||||
template_name = 'calendar.html'
|
template_name = 'RIGS/calendar.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(WebCalendar, self).get_context_data(**kwargs)
|
context = super(WebCalendar, self).get_context_data(**kwargs)
|
||||||
context['view'] = kwargs.get('view', '')
|
context['view'] = kwargs.get('view','')
|
||||||
context['date'] = kwargs.get('date', '')
|
context['date'] = kwargs.get('date','')
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventDetail(generic.DetailView):
|
class EventDetail(generic.DetailView):
|
||||||
template_name = 'event_detail.html'
|
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +53,7 @@ class EventOembed(generic.View):
|
|||||||
model = models.Event
|
model = models.Event
|
||||||
|
|
||||||
def get(self, request, pk=None):
|
def get(self, request, pk=None):
|
||||||
|
|
||||||
embed_url = reverse('event_embed', args=[pk])
|
embed_url = reverse('event_embed', args=[pk])
|
||||||
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
||||||
|
|
||||||
@@ -74,7 +61,6 @@ class EventOembed(generic.View):
|
|||||||
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'type': 'rich',
|
'type': 'rich',
|
||||||
'height': '250'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
json = simplejson.JSONEncoderForHTML().encode(data)
|
json = simplejson.JSONEncoderForHTML().encode(data)
|
||||||
@@ -82,26 +68,25 @@ class EventOembed(generic.View):
|
|||||||
|
|
||||||
|
|
||||||
class EventEmbed(EventDetail):
|
class EventEmbed(EventDetail):
|
||||||
template_name = 'event_embed.html'
|
template_name = 'RIGS/event_embed.html'
|
||||||
|
|
||||||
|
|
||||||
class EventCreate(generic.CreateView):
|
class EventCreate(generic.CreateView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
form_class = forms.EventForm
|
form_class = forms.EventForm
|
||||||
template_name = 'event_form.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventCreate, self).get_context_data(**kwargs)
|
context = super(EventCreate, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = "New Event"
|
|
||||||
context['edit'] = True
|
context['edit'] = True
|
||||||
context['currentVAT'] = models.VatRate.objects.current_rate()
|
context['currentVAT'] = models.VatRate.objects.current_rate()
|
||||||
|
|
||||||
form = context['form']
|
form = context['form']
|
||||||
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
|
if re.search('"-\d+"', form['items_json'].value()):
|
||||||
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
||||||
|
|
||||||
|
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||||
for field, model in form.related_models.items():
|
for field, model in form.related_models.iteritems():
|
||||||
value = form[field].value()
|
value = form[field].value()
|
||||||
if value is not None and value != '':
|
if value is not None and value != '':
|
||||||
context[field] = model.objects.get(pk=value)
|
context[field] = model.objects.get(pk=value)
|
||||||
@@ -114,59 +99,32 @@ class EventCreate(generic.CreateView):
|
|||||||
class EventUpdate(generic.UpdateView):
|
class EventUpdate(generic.UpdateView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
form_class = forms.EventForm
|
form_class = forms.EventForm
|
||||||
template_name = 'event_form.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventUpdate, self).get_context_data(**kwargs)
|
context = super(EventUpdate, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = "Event {}".format(self.object.display_id)
|
|
||||||
context['edit'] = True
|
context['edit'] = True
|
||||||
|
|
||||||
form = context['form']
|
form = context['form']
|
||||||
|
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||||
for field, model in form.related_models.items():
|
for field, model in form.related_models.iteritems():
|
||||||
value = form[field].value()
|
value = form[field].value()
|
||||||
if value is not None and value != '':
|
if value is not None and value != '':
|
||||||
context[field] = model.objects.get(pk=value)
|
context[field] = model.objects.get(pk=value)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
|
||||||
if hasattr(context, 'duplicate') and not context['duplicate']:
|
|
||||||
# If this event has already been emailed to a client, show a warning
|
|
||||||
if self.object.auth_request_at is not None:
|
|
||||||
messages.info(self.request,
|
|
||||||
'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
|
|
||||||
|
|
||||||
if hasattr(self.object, 'authorised'):
|
|
||||||
messages.warning(self.request,
|
|
||||||
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
|
|
||||||
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class EventDuplicate(EventUpdate):
|
class EventDuplicate(EventUpdate):
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
|
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 = 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.based_on = old # Make the new event based on the old event
|
||||||
new.purchase_order = None # Remove old PO
|
new.purchase_order = None
|
||||||
new.status = new.PROVISIONAL # Return status to provisional
|
|
||||||
|
|
||||||
# Clear checked in by if it's a dry hire
|
if self.request.method in ('POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
|
||||||
if new.dry_hire is True:
|
new.pk = None # This means a new event will be created on save, and all items will be re-created
|
||||||
new.checked_in_by = None
|
|
||||||
|
|
||||||
# Remove all the authorisation information from the new event
|
|
||||||
new.auth_request_to = None
|
|
||||||
new.auth_request_by = None
|
|
||||||
new.auth_request_at = None
|
|
||||||
|
|
||||||
if self.request.method in (
|
|
||||||
'POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
|
|
||||||
new.pk = None # This means a new event will be created on save, and all items will be re-created
|
|
||||||
else:
|
else:
|
||||||
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
||||||
|
|
||||||
@@ -174,63 +132,61 @@ class EventDuplicate(EventUpdate):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventDuplicate, self).get_context_data(**kwargs)
|
context = super(EventDuplicate, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id)
|
|
||||||
context["duplicate"] = True
|
context["duplicate"] = True
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventPrint(generic.View):
|
class EventPrint(generic.View):
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
object = get_object_or_404(models.Event, pk=pk)
|
object = get_object_or_404(models.Event, pk=pk)
|
||||||
template = get_template('event_print.xml')
|
template = get_template('RIGS/event_print.xml')
|
||||||
|
copies = ('TEC', 'Client')
|
||||||
|
|
||||||
merger = PdfFileMerger()
|
merger = PdfFileMerger()
|
||||||
|
|
||||||
context = {
|
for copy in copies:
|
||||||
'object': object,
|
|
||||||
'fonts': {
|
|
||||||
'opensans': {
|
|
||||||
'regular': 'static/fonts/OPENSANS-REGULAR.TTF',
|
|
||||||
'bold': 'static/fonts/OPENSANS-BOLD.TTF',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'quote': True,
|
|
||||||
'current_user': request.user,
|
|
||||||
'filename': 'Event {} {} {}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
|
|
||||||
}
|
|
||||||
|
|
||||||
rml = template.render(context)
|
context = RequestContext(request, { # this should be outside the loop, but bug in 1.8.2 prevents this
|
||||||
buffer = rml2pdf.parseString(rml)
|
'object': object,
|
||||||
merger.append(PdfFileReader(buffer))
|
'fonts': {
|
||||||
buffer.close()
|
'opensans': {
|
||||||
|
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||||
|
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'copy':copy,
|
||||||
|
'current_user':request.user,
|
||||||
|
})
|
||||||
|
|
||||||
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
# context['copy'] = copy # this is the way to do it once we upgrade to Django 1.8.3
|
||||||
merger.append(BytesIO(terms.read()))
|
|
||||||
|
rml = template.render(context)
|
||||||
|
buffer = StringIO.StringIO()
|
||||||
|
|
||||||
|
buffer = rml2pdf.parseString(rml)
|
||||||
|
|
||||||
|
merger.append(PdfFileReader(buffer))
|
||||||
|
|
||||||
|
buffer.close()
|
||||||
|
|
||||||
|
terms = urllib2.urlopen(settings.TERMS_OF_HIRE_URL)
|
||||||
|
merger.append(StringIO.StringIO(terms.read()))
|
||||||
|
|
||||||
merged = BytesIO()
|
merged = BytesIO()
|
||||||
merger.write(merged)
|
merger.write(merged)
|
||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
response = HttpResponse(content_type='application/pdf')
|
||||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
|
||||||
|
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||||
|
|
||||||
|
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
|
||||||
response.write(merged.getvalue())
|
response.write(merged.getvalue())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
class EventArchive(generic.ArchiveIndexView):
|
||||||
class EventArchive(generic.ListView):
|
|
||||||
template_name = "event_archive.html"
|
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
date_field = "start_date"
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
# get super context
|
|
||||||
context = super(EventArchive, self).get_context_data(**kwargs)
|
|
||||||
|
|
||||||
context['start'] = self.request.GET.get('start', None)
|
|
||||||
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
|
|
||||||
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
|
|
||||||
context['page_title'] = 'Event Archive'
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
start = self.request.GET.get('start', None)
|
start = self.request.GET.get('start', None)
|
||||||
end = self.request.GET.get('end', datetime.date.today())
|
end = self.request.GET.get('end', datetime.date.today())
|
||||||
@@ -241,39 +197,19 @@ class EventArchive(generic.ListView):
|
|||||||
"Muppet! Check the dates, it has been fixed for you.")
|
"Muppet! Check the dates, it has been fixed for you.")
|
||||||
start, end = end, start # Stop the impending fail
|
start, end = end, start # Stop the impending fail
|
||||||
|
|
||||||
filter = Q()
|
filter = False
|
||||||
if end != "":
|
if end != "":
|
||||||
filter &= Q(start_date__lte=end)
|
filter = Q(start_date__lte=end)
|
||||||
if start:
|
if start:
|
||||||
filter &= Q(start_date__gte=start)
|
if filter:
|
||||||
|
filter = filter & Q(start_date__gte=start)
|
||||||
|
else:
|
||||||
|
filter = Q(start_date__gte=start)
|
||||||
|
|
||||||
q = self.request.GET.get('q', "")
|
if filter:
|
||||||
|
qs = self.model.objects.filter(filter).order_by('-start_date')
|
||||||
if q != "":
|
else:
|
||||||
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
|
qs = self.model.objects.all().order_by('-start_date')
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(q)
|
|
||||||
qfilter = qfilter | Q(pk=val)
|
|
||||||
except: # noqa not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if q[0] == "N":
|
|
||||||
val = int(q[1:])
|
|
||||||
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
filter &= qfilter
|
|
||||||
|
|
||||||
status = self.request.GET.getlist('status', "")
|
|
||||||
|
|
||||||
if len(status) > 0:
|
|
||||||
filter &= Q(status__in=status)
|
|
||||||
|
|
||||||
qs = self.model.objects.filter(filter).order_by('-start_date')
|
|
||||||
|
|
||||||
# Preselect related for efficiency
|
# Preselect related for efficiency
|
||||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||||
@@ -282,152 +218,3 @@ class EventArchive(generic.ListView):
|
|||||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class EventAuthorise(generic.UpdateView):
|
|
||||||
template_name = 'eventauthorisation_form.html'
|
|
||||||
success_template = 'eventauthorisation_success.html'
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
self.object = form.save()
|
|
||||||
|
|
||||||
self.template_name = self.success_template
|
|
||||||
messages.add_message(self.request, messages.SUCCESS,
|
|
||||||
'Success! Your event has been authorised. ' +
|
|
||||||
'You will also receive email confirmation to %s.' % (self.object.email))
|
|
||||||
return self.render_to_response(self.get_context_data())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def event(self):
|
|
||||||
return models.Event.objects.select_related('organisation', 'person', 'venue').get(pk=self.kwargs['pk'])
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return getattr(self.event, 'authorisation', None)
|
|
||||||
|
|
||||||
def get_form_class(self):
|
|
||||||
return forms.InternalClientEventAuthorisationForm
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventAuthorise, self).get_context_data(**kwargs)
|
|
||||||
context['event'] = self.event
|
|
||||||
context['tos_url'] = settings.TERMS_OF_HIRE_URL
|
|
||||||
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
|
|
||||||
if self.event.dry_hire:
|
|
||||||
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
if self.get_object() is not None and self.get_object().pk is not None:
|
|
||||||
if self.event.authorised:
|
|
||||||
messages.add_message(self.request, messages.WARNING,
|
|
||||||
"This event has already been authorised. "
|
|
||||||
"Reauthorising is not necessary at this time.")
|
|
||||||
else:
|
|
||||||
messages.add_message(self.request, messages.WARNING,
|
|
||||||
"This event has already been authorised, but the amount has changed. " +
|
|
||||||
"Please check the amount and reauthorise.")
|
|
||||||
return super(EventAuthorise, self).get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
|
||||||
form = super(EventAuthorise, self).get_form(**kwargs)
|
|
||||||
form.instance.event = self.event
|
|
||||||
form.instance.email = self.request.email
|
|
||||||
form.instance.sent_by = self.request.sent_by
|
|
||||||
return form
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
# Verify our signature matches up and all is well with the integrity of the URL
|
|
||||||
try:
|
|
||||||
data = signing.loads(kwargs.get('hmac'))
|
|
||||||
assert int(kwargs.get('pk')) == int(data.get('pk'))
|
|
||||||
request.email = data['email']
|
|
||||||
request.sent_by = models.Profile.objects.get(pk=data['sent_by'])
|
|
||||||
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
|
|
||||||
raise SuspiciousOperation(
|
|
||||||
"This URL is invalid. Please ask your TEC contact for a new URL")
|
|
||||||
return super(EventAuthorise, self).dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
|
|
||||||
model = models.Event
|
|
||||||
form_class = forms.EventAuthorisationRequestForm
|
|
||||||
template_name = 'eventauthorisation_request.html'
|
|
||||||
|
|
||||||
@method_decorator(decorators.nottinghamtec_address_required)
|
|
||||||
def dispatch(self, *args, **kwargs):
|
|
||||||
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def object(self):
|
|
||||||
return self.get_object()
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
if self.request.is_ajax():
|
|
||||||
url = reverse_lazy('closemodal')
|
|
||||||
messages.info(self.request, "location.reload()")
|
|
||||||
else:
|
|
||||||
url = reverse_lazy('event_detail', kwargs={
|
|
||||||
'pk': self.object.pk,
|
|
||||||
})
|
|
||||||
messages.add_message(self.request, messages.SUCCESS, "Authorisation request successfully sent.")
|
|
||||||
return url
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
email = form.cleaned_data['email']
|
|
||||||
event = self.object
|
|
||||||
event.auth_request_by = self.request.user
|
|
||||||
event.auth_request_at = timezone.now()
|
|
||||||
event.auth_request_to = email
|
|
||||||
event.save()
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'object': self.object,
|
|
||||||
'request': self.request,
|
|
||||||
'hmac': signing.dumps({
|
|
||||||
'pk': self.object.pk,
|
|
||||||
'email': email,
|
|
||||||
'sent_by': self.request.user.pk,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
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),
|
|
||||||
get_template("eventauthorisation_client_request.txt").render(context),
|
|
||||||
to=[email],
|
|
||||||
reply_to=[self.request.user.email],
|
|
||||||
)
|
|
||||||
css = staticfiles_storage.path('css/email.css')
|
|
||||||
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
|
|
||||||
external_styles=css).transform()
|
|
||||||
msg.attach_alternative(html, 'text/html')
|
|
||||||
|
|
||||||
msg.send()
|
|
||||||
|
|
||||||
return super(EventAuthorisationRequest, self).form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
|
||||||
template_name = "eventauthorisation_client_request.html"
|
|
||||||
model = models.Event
|
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
|
||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
|
||||||
css = staticfiles_storage.path('css/email.css')
|
|
||||||
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
|
|
||||||
assert isinstance(response, HttpResponse)
|
|
||||||
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs)
|
|
||||||
context['hmac'] = signing.dumps({
|
|
||||||
'pk': self.object.pk,
|
|
||||||
'email': self.request.GET.get('email', 'hello@world.test'),
|
|
||||||
'sent_by': self.request.user.pk,
|
|
||||||
})
|
|
||||||
context['to_name'] = self.request.GET.get('to_name', None)
|
|
||||||
return context
|
|
||||||
|
|||||||
150
RIGS/signals.py
150
RIGS/signals.py
@@ -1,150 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import re
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
|
||||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.template.loader import get_template
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from registration.signals import user_activated
|
|
||||||
from premailer import Premailer
|
|
||||||
from z3c.rml import rml2pdf
|
|
||||||
|
|
||||||
from RIGS import models
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
|
|
||||||
|
|
||||||
def send_eventauthorisation_success_email(instance):
|
|
||||||
# Generate PDF first to prevent context conflicts
|
|
||||||
context = {
|
|
||||||
'object': instance.event,
|
|
||||||
'fonts': {
|
|
||||||
'opensans': {
|
|
||||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
|
||||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'receipt': True,
|
|
||||||
'current_user': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
template = get_template('event_print.xml')
|
|
||||||
merger = PdfFileMerger()
|
|
||||||
|
|
||||||
rml = template.render(context)
|
|
||||||
|
|
||||||
buffer = rml2pdf.parseString(rml)
|
|
||||||
merger.append(PdfFileReader(buffer))
|
|
||||||
buffer.close()
|
|
||||||
|
|
||||||
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
|
||||||
merger.append(BytesIO(terms.read()))
|
|
||||||
|
|
||||||
merged = BytesIO()
|
|
||||||
merger.write(merged)
|
|
||||||
|
|
||||||
# Produce email content
|
|
||||||
context = {
|
|
||||||
'object': instance,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
client_email = EmailMultiAlternatives(
|
|
||||||
subject,
|
|
||||||
get_template("eventauthorisation_client_success.txt").render(context),
|
|
||||||
to=[instance.email],
|
|
||||||
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
|
||||||
)
|
|
||||||
|
|
||||||
css = staticfiles_storage.path('css/email.css')
|
|
||||||
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
|
|
||||||
external_styles=css).transform()
|
|
||||||
client_email.attach_alternative(html, 'text/html')
|
|
||||||
|
|
||||||
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
|
||||||
|
|
||||||
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
|
|
||||||
merged.getvalue(),
|
|
||||||
'application/pdf'
|
|
||||||
)
|
|
||||||
|
|
||||||
if instance.event.mic:
|
|
||||||
mic_email_address = instance.event.mic.email
|
|
||||||
else:
|
|
||||||
mic_email_address = settings.AUTHORISATION_NOTIFICATION_ADDRESS
|
|
||||||
|
|
||||||
mic_email = EmailMessage(
|
|
||||||
subject,
|
|
||||||
get_template("eventauthorisation_mic_success.txt").render(context),
|
|
||||||
to=[mic_email_address]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Now we have both emails successfully generated, send them out
|
|
||||||
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:
|
|
||||||
send_eventauthorisation_success_email(instance)
|
|
||||||
|
|
||||||
|
|
||||||
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
|
|
||||||
|
|
||||||
|
|
||||||
def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|
||||||
# Bit more controlled than just emailing all superusers
|
|
||||||
for admin in models.Profile.admins():
|
|
||||||
# Check we've ever emailed them before and if so, if cooldown has passed.
|
|
||||||
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
|
|
||||||
context = {
|
|
||||||
'request': request,
|
|
||||||
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
|
|
||||||
'number_of_users': models.Profile.users_awaiting_approval_count(),
|
|
||||||
'to_name': admin.first_name
|
|
||||||
}
|
|
||||||
|
|
||||||
email = EmailMultiAlternatives(
|
|
||||||
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
|
|
||||||
get_template("admin_awaiting_approval.txt").render(context),
|
|
||||||
to=[admin.email],
|
|
||||||
reply_to=[user.email],
|
|
||||||
)
|
|
||||||
css = staticfiles_storage.path('css/email.css')
|
|
||||||
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
|
||||||
external_styles=css).transform()
|
|
||||||
email.attach_alternative(html, 'text/html')
|
|
||||||
email.send()
|
|
||||||
|
|
||||||
# Update last sent
|
|
||||||
admin.last_emailed = timezone.now()
|
|
||||||
admin.save()
|
|
||||||
|
|
||||||
|
|
||||||
user_activated.connect(send_admin_awaiting_approval_email)
|
|
||||||
|
|
||||||
|
|
||||||
def update_cache(sender, instance, created, **kwargs):
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
|
|
||||||
for model in reversion.get_registered_models():
|
|
||||||
post_save.connect(update_cache, sender=model)
|
|
||||||
27
RIGS/static/config.rb
Normal file
27
RIGS/static/config.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Require any additional compass plugins here.
|
||||||
|
require 'bootstrap-sass'
|
||||||
|
|
||||||
|
# Set this to the root of your project when deployed:
|
||||||
|
http_path = "/static/"
|
||||||
|
css_dir = "css"
|
||||||
|
sass_dir = "scss"
|
||||||
|
images_dir = "img"
|
||||||
|
javascripts_dir = "js"
|
||||||
|
fonts_dir = "fonts"
|
||||||
|
|
||||||
|
# You can select your preferred output style here (can be overridden via the command line):
|
||||||
|
# output_style = :expanded or :nested or :compact or :compressed
|
||||||
|
output_style = :compressed
|
||||||
|
|
||||||
|
# To enable relative paths to assets via compass helper functions. Uncomment:
|
||||||
|
# relative_assets = true
|
||||||
|
|
||||||
|
# To disable debugging comments that display the original location of your selectors. Uncomment:
|
||||||
|
# line_comments = false
|
||||||
|
|
||||||
|
|
||||||
|
# If you prefer the indented syntax, you might want to regenerate this
|
||||||
|
# project again passing --syntax sass, or you can uncomment this:
|
||||||
|
# preferred_syntax = :sass
|
||||||
|
# and then run:
|
||||||
|
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
|
||||||
11
RIGS/static/css/ajax-bootstrap-select.css
Normal file → Executable file
11
RIGS/static/css/ajax-bootstrap-select.css
Normal file → Executable file
@@ -3,16 +3,16 @@
|
|||||||
*
|
*
|
||||||
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
||||||
*
|
*
|
||||||
* @version 1.4.5
|
* @version 1.3.1
|
||||||
* @author Adam Heim - https://github.com/truckingsim
|
* @author Adam Heim - https://github.com/truckingsim
|
||||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
||||||
* @copyright 2019 Adam Heim
|
* @copyright 2015 Adam Heim
|
||||||
* @license Released under the MIT license.
|
* @license Released under the MIT license.
|
||||||
*
|
*
|
||||||
* Contributors:
|
* Contributors:
|
||||||
* Mark Carver - https://github.com/markcarver
|
* Mark Carver - https://github.com/markcarver
|
||||||
*
|
*
|
||||||
* Last build: 2019-04-23 12:18:56 PM EDT
|
* Last build: 2015-01-06 8:43:11 PM EST
|
||||||
*/
|
*/
|
||||||
.bootstrap-select .status {
|
.bootstrap-select .status {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
@@ -23,6 +23,5 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin-bottom: -5px;
|
margin-bottom: -5px;
|
||||||
padding: 10px 20px; }
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7OztFQWVFO0FBQ0Y7RUFDRSxtQkFBbUI7RUFDbkIsV0FBVztFQUNYLFdBQVc7RUFDWCxlQUFlO0VBQ2Ysa0JBQWtCO0VBQ2xCLGdCQUFnQjtFQUNoQixjQUFjO0VBQ2QsbUJBQW1CO0VBQ25CLGtCQUFrQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIvKiFcbiAqIEFqYXggQm9vdHN0cmFwIFNlbGVjdFxuICpcbiAqIEV4dGVuZHMgZXhpc3RpbmcgW0Jvb3RzdHJhcCBTZWxlY3RdIGltcGxlbWVudGF0aW9ucyBieSBhZGRpbmcgdGhlIGFiaWxpdHkgdG8gc2VhcmNoIHZpYSBBSkFYIHJlcXVlc3RzIGFzIHlvdSB0eXBlLiBPcmlnaW5hbGx5IGZvciBDUk9TQ09OLlxuICpcbiAqIEB2ZXJzaW9uIDEuNC41XG4gKiBAYXV0aG9yIEFkYW0gSGVpbSAtIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbVxuICogQGxpbmsgaHR0cHM6Ly9naXRodWIuY29tL3RydWNraW5nc2ltL0FqYXgtQm9vdHN0cmFwLVNlbGVjdFxuICogQGNvcHlyaWdodCAyMDE5IEFkYW0gSGVpbVxuICogQGxpY2Vuc2UgUmVsZWFzZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlLlxuICpcbiAqIENvbnRyaWJ1dG9yczpcbiAqICAgTWFyayBDYXJ2ZXIgLSBodHRwczovL2dpdGh1Yi5jb20vbWFya2NhcnZlclxuICpcbiAqIExhc3QgYnVpbGQ6IDIwMTktMDQtMjMgMTI6MTg6NTYgUE0gRURUXG4gKi9cbi5ib290c3RyYXAtc2VsZWN0IC5zdGF0dXMge1xuICBiYWNrZ3JvdW5kOiAjZjBmMGYwO1xuICBjbGVhcjogYm90aDtcbiAgY29sb3I6ICM5OTk7XG4gIGZvbnQtc2l6ZTogMTFweDtcbiAgZm9udC1zdHlsZTogaXRhbGljO1xuICBmb250LXdlaWdodDogNTAwO1xuICBsaW5lLWhlaWdodDogMTtcbiAgbWFyZ2luLWJvdHRvbTogLTVweDtcbiAgcGFkZGluZzogMTBweCAyMHB4O1xufVxuIl19 */
|
|
||||||
|
|||||||
28
RIGS/static/css/ajax-bootstrap-select.min.css
vendored
28
RIGS/static/css/ajax-bootstrap-select.min.css
vendored
@@ -1,28 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Ajax Bootstrap Select
|
|
||||||
*
|
|
||||||
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
|
||||||
*
|
|
||||||
* @version 1.4.5
|
|
||||||
* @author Adam Heim - https://github.com/truckingsim
|
|
||||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
|
||||||
* @copyright 2019 Adam Heim
|
|
||||||
* @license Released under the MIT license.
|
|
||||||
*
|
|
||||||
* Contributors:
|
|
||||||
* Mark Carver - https://github.com/markcarver
|
|
||||||
*
|
|
||||||
* Last build: 2019-04-23 12:18:56 PM EDT
|
|
||||||
*/
|
|
||||||
.bootstrap-select .status {
|
|
||||||
background: #f0f0f0;
|
|
||||||
clear: both;
|
|
||||||
color: #999;
|
|
||||||
font-size: 11px;
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: -5px;
|
|
||||||
padding: 10px 20px; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7RUFlRTtBQUFDO0VBQTBCLG1CQUFrQjtFQUFDLFdBQVU7RUFBQyxXQUFVO0VBQUMsZUFBYztFQUFDLGtCQUFpQjtFQUFDLGdCQUFlO0VBQUMsY0FBYTtFQUFDLG1CQUFrQjtFQUFDLGtCQUFpQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLyohXG4gKiBBamF4IEJvb3RzdHJhcCBTZWxlY3RcbiAqXG4gKiBFeHRlbmRzIGV4aXN0aW5nIFtCb290c3RyYXAgU2VsZWN0XSBpbXBsZW1lbnRhdGlvbnMgYnkgYWRkaW5nIHRoZSBhYmlsaXR5IHRvIHNlYXJjaCB2aWEgQUpBWCByZXF1ZXN0cyBhcyB5b3UgdHlwZS4gT3JpZ2luYWxseSBmb3IgQ1JPU0NPTi5cbiAqXG4gKiBAdmVyc2lvbiAxLjQuNVxuICogQGF1dGhvciBBZGFtIEhlaW0gLSBodHRwczovL2dpdGh1Yi5jb20vdHJ1Y2tpbmdzaW1cbiAqIEBsaW5rIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbS9BamF4LUJvb3RzdHJhcC1TZWxlY3RcbiAqIEBjb3B5cmlnaHQgMjAxOSBBZGFtIEhlaW1cbiAqIEBsaWNlbnNlIFJlbGVhc2VkIHVuZGVyIHRoZSBNSVQgbGljZW5zZS5cbiAqXG4gKiBDb250cmlidXRvcnM6XG4gKiAgIE1hcmsgQ2FydmVyIC0gaHR0cHM6Ly9naXRodWIuY29tL21hcmtjYXJ2ZXJcbiAqXG4gKiBMYXN0IGJ1aWxkOiAyMDE5LTA0LTIzIDEyOjE4OjU2IFBNIEVEVFxuICovLmJvb3RzdHJhcC1zZWxlY3QgLnN0YXR1c3tiYWNrZ3JvdW5kOiNmMGYwZjA7Y2xlYXI6Ym90aDtjb2xvcjojOTk5O2ZvbnQtc2l6ZToxMXB4O2ZvbnQtc3R5bGU6aXRhbGljO2ZvbnQtd2VpZ2h0OjUwMDtsaW5lLWhlaWdodDoxO21hcmdpbi1ib3R0b206LTVweDtwYWRkaW5nOjEwcHggMjBweH0iXX0= */
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
.autocomplete {
|
|
||||||
background: white;
|
|
||||||
z-index: 1000;
|
|
||||||
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
overflow: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid rgba(50, 50, 50, 0.6); }
|
|
||||||
|
|
||||||
.autocomplete * {
|
|
||||||
font: inherit; }
|
|
||||||
|
|
||||||
.autocomplete > div {
|
|
||||||
padding: 0 4px; }
|
|
||||||
|
|
||||||
.autocomplete .group {
|
|
||||||
background: #eee; }
|
|
||||||
|
|
||||||
.autocomplete > div:hover:not(.group),
|
|
||||||
.autocomplete > div.selected {
|
|
||||||
background: #81ca91;
|
|
||||||
cursor: pointer; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImF1dG9jb21wbGV0ZS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0E7RUFDSSxpQkFBaUI7RUFDakIsYUFBYTtFQUNiLDRHQUE0RztFQUM1RyxjQUFjO0VBQ2Qsc0JBQXNCO0VBQ3RCLHVDQUF1QyxFQUFBOztBQUczQztFQUNJLGFBQWEsRUFBQTs7QUFHakI7RUFDSSxjQUFjLEVBQUE7O0FBR2xCO0VBQ0ksZ0JBQWdCLEVBQUE7O0FBR3BCOztFQUVJLG1CQUFtQjtFQUNuQixlQUFlLEVBQUEiLCJmaWxlIjoiYXV0b2NvbXBsZXRlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIlxyXG4uYXV0b2NvbXBsZXRlIHtcclxuICAgIGJhY2tncm91bmQ6IHdoaXRlO1xyXG4gICAgei1pbmRleDogMTAwMDtcclxuICAgIGZvbnQ6IDE0cHgvMjJweCBcIi1hcHBsZS1zeXN0ZW1cIiwgQmxpbmtNYWNTeXN0ZW1Gb250LCBcIlNlZ29lIFVJXCIsIFJvYm90bywgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBBcmlhbCwgc2Fucy1zZXJpZjtcclxuICAgIG92ZXJmbG93OiBhdXRvO1xyXG4gICAgYm94LXNpemluZzogYm9yZGVyLWJveDtcclxuICAgIGJvcmRlcjogMXB4IHNvbGlkIHJnYmEoNTAsIDUwLCA1MCwgMC42KTtcclxufVxyXG5cclxuLmF1dG9jb21wbGV0ZSAqIHtcclxuICAgIGZvbnQ6IGluaGVyaXQ7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXYge1xyXG4gICAgcGFkZGluZzogMCA0cHg7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgLmdyb3VwIHtcclxuICAgIGJhY2tncm91bmQ6ICNlZWU7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXY6aG92ZXI6bm90KC5ncm91cCksXHJcbi5hdXRvY29tcGxldGUgPiBkaXYuc2VsZWN0ZWQge1xyXG4gICAgYmFja2dyb3VuZDogIzgxY2E5MTtcclxuICAgIGN1cnNvcjogcG9pbnRlcjtcclxufVxyXG5cclxuIl19 */
|
|
||||||
570
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
570
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
File diff suppressed because one or more lines are too long
455
RIGS/static/css/bootstrap-select.css
vendored
455
RIGS/static/css/bootstrap-select.css
vendored
File diff suppressed because one or more lines are too long
6
RIGS/static/css/bootstrap-select.min.css
vendored
Normal file
6
RIGS/static/css/bootstrap-select.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,39 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0px; }
|
|
||||||
|
|
||||||
.main-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse; }
|
|
||||||
|
|
||||||
.client-header {
|
|
||||||
background-image: url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");
|
|
||||||
background-color: #222;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 28px; }
|
|
||||||
.client-header .logos {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 640px; }
|
|
||||||
.client-header img {
|
|
||||||
height: 110px; }
|
|
||||||
|
|
||||||
.content-container {
|
|
||||||
width: 100%; }
|
|
||||||
.content-container .content {
|
|
||||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: left; }
|
|
||||||
.content-container .content .button-container {
|
|
||||||
width: 100%; }
|
|
||||||
.content-container .content .button-container .button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background-color: #357ebf;
|
|
||||||
border-radius: 4px; }
|
|
||||||
.content-container .content .button-container .button a {
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVtYWlsLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxXQUFXLEVBQUE7O0FBR2I7RUFDRSxXQUFXO0VBQ1gseUJBQXlCLEVBQUE7O0FBSTNCO0VBQ0UsMkVBQTJFO0VBQzNFLHNCQUFzQjtFQUN0Qiw0QkFBNEI7RUFDNUIsMkJBQTJCO0VBRTNCLFdBQVc7RUFFWCxtQkFBbUIsRUFBQTtFQVJyQjtJQVdJLFdBQVc7SUFDWCxnQkFBZ0IsRUFBQTtFQVpwQjtJQWdCSSxhQUFhLEVBQUE7O0FBSWpCO0VBQ0UsV0FBVyxFQUFBO0VBRGI7SUFJSSx3RUFBd0U7SUFFeEUsV0FBVztJQUNYLGdCQUFnQjtJQUNoQixhQUFhO0lBQ2IsZ0JBQWdCLEVBQUE7SUFUcEI7TUFZTSxXQUFXLEVBQUE7TUFaakI7UUFlUSxpQkFBaUI7UUFDakIseUJBaERjO1FBaURkLGtCQUFrQixFQUFBO1FBakIxQjtVQW9CVSxXQUFXO1VBQ1gscUJBQXFCLEVBQUEiLCJmaWxlIjoiZW1haWwuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiJGJ1dHRvbl9jb2xvcjogIzM1N2ViZjtcblxuYm9keXtcbiAgbWFyZ2luOiAwcHg7XG59XG5cbi5tYWluLXRhYmxle1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcblxufVxuXG4uY2xpZW50LWhlYWRlciB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybChcImh0dHBzOi8vd3d3Lm5vdHRpbmdoYW10ZWMuY28udWsvaW1ncy93b2YyMDE0LTEuanBnXCIpO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMjIyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXI7XG5cbiAgd2lkdGg6IDEwMCU7XG5cbiAgbWFyZ2luLWJvdHRvbTogMjhweDtcblxuICAubG9nb3N7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2NDBweDtcbiAgfVxuXG4gIGltZyB7XG4gICAgaGVpZ2h0OiAxMTBweDtcbiAgfVxufVxuXG4uY29udGVudC1jb250YWluZXJ7XG4gIHdpZHRoOiAxMDAlO1xuXG4gIC5jb250ZW50IHtcbiAgICBmb250LWZhbWlseTogXCJPcGVuIFNhbnNcIiwgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmO1xuXG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2MDBweDtcbiAgICBwYWRkaW5nOiAxMHB4O1xuICAgIHRleHQtYWxpZ246IGxlZnQ7XG5cbiAgICAuYnV0dG9uLWNvbnRhaW5lcntcbiAgICAgIHdpZHRoOiAxMDAlO1xuXG4gICAgICAuYnV0dG9uIHtcbiAgICAgICAgcGFkZGluZzogNnB4IDEycHg7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6ICRidXR0b25fY29sb3I7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDRweDtcblxuICAgICAgICBhIHtcbiAgICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG4gICAgICAgIH1cblxuICAgICAgfVxuXG4gICAgfVxuXG4gIH1cbn1cblxuIl19 */
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user