mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-12 09:39:42 +00:00
Compare commits
27 Commits
0c900d2447
...
subhire
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f240e581 | |||
| 2e4b84c94e | |||
| 87f2de46a1 | |||
| 1615e27767 | |||
| 773f55ac84 | |||
| 63a2f6d47b | |||
| 8393e85b74 | |||
| 311c02d554 | |||
| e100f5a1d4 | |||
| eb07990f4c | |||
| 7b7c1b86de | |||
| 2b8945c513 | |||
| eb3638b93a | |||
|
1660f51e55
|
|||
|
9feea56211
|
|||
|
951227e68b
|
|||
|
6e8779c81b
|
|||
|
e0da6a3120
|
|||
|
0c80ef1b72
|
|||
|
0f127d8ca4
|
|||
|
04ec728972
|
|||
|
bede8b4176
|
|||
|
8cade512d1
|
|||
|
418219940b
|
|||
| 948a41f43a | |||
|
4449efcced
|
|||
|
8b0cd13159
|
5
.github/workflows/django.yml
vendored
5
.github/workflows/django.yml
vendored
@@ -12,7 +12,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PYTHONDONTWRITEBYTECODE: 1
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
@@ -42,8 +41,8 @@ jobs:
|
|||||||
pipenv run python3 manage.py makemigrations --check --dry-run
|
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||||
pipenv run python3 manage.py collectstatic --noinput
|
pipenv run python3 manage.py collectstatic --noinput
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: pipenv run pytest -n auto --cov
|
run: pipenv run pytest -n auto -vv --cov
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v2
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: failure-screenshots ${{ matrix.test-group }}
|
name: failure-screenshots ${{ matrix.test-group }}
|
||||||
|
|||||||
29
Pipfile
29
Pipfile
@@ -19,10 +19,9 @@ cssutils = "~=1.0.2"
|
|||||||
dj-database-url = "~=0.5.0"
|
dj-database-url = "~=0.5.0"
|
||||||
dj-static = "~=0.0.6"
|
dj-static = "~=0.0.6"
|
||||||
Django = "~=3.2"
|
Django = "~=3.2"
|
||||||
django-debug-toolbar = "~=4.0.0"
|
django-debug-toolbar = "~=3.2"
|
||||||
django-filter = "~=2.4.0"
|
django-filter = "~=2.4.0"
|
||||||
django-ical = "~=1.7.1"
|
django-ical = "~=1.8.3"
|
||||||
django-recurrence = "~=1.10.3"
|
|
||||||
django-registration-redux = "~=2.9"
|
django-registration-redux = "~=2.9"
|
||||||
django-reversion = "~=3.0.9"
|
django-reversion = "~=3.0.9"
|
||||||
django-widget-tweaks = "~=1.4.8"
|
django-widget-tweaks = "~=1.4.8"
|
||||||
@@ -34,12 +33,12 @@ idna = "~=2.10"
|
|||||||
Markdown = "~=3.3.3"
|
Markdown = "~=3.3.3"
|
||||||
msgpack = "~=1.0.2"
|
msgpack = "~=1.0.2"
|
||||||
pep517 = "~=0.9.1"
|
pep517 = "~=0.9.1"
|
||||||
Pillow = "~=10.0.1"
|
Pillow = "~=9.3.0"
|
||||||
premailer = "~=3.7.0"
|
premailer = "~=3.7.0"
|
||||||
progress = "~=1.5"
|
progress = "~=1.5"
|
||||||
psutil = "~=5.8.0"
|
psutil = "~=5.8.0"
|
||||||
psycopg2 = "~=2.8.6"
|
psycopg2 = "~=2.8.6"
|
||||||
Pygments = "~=2.15.0"
|
Pygments = "~=2.7.4"
|
||||||
pyparsing = "~=2.4.7"
|
pyparsing = "~=2.4.7"
|
||||||
PyPDF2 = "~=1.27.5"
|
PyPDF2 = "~=1.27.5"
|
||||||
PyPOM = "~=2.2.4"
|
PyPOM = "~=2.2.4"
|
||||||
@@ -47,7 +46,7 @@ python-dateutil = "~=2.8.1"
|
|||||||
pytoml = "~=0.1.21"
|
pytoml = "~=0.1.21"
|
||||||
pytz = "~=2020.5"
|
pytz = "~=2020.5"
|
||||||
reportlab = "*"
|
reportlab = "*"
|
||||||
requests = "~=2.31.0"
|
requests = "~=2.25.1"
|
||||||
retrying = "~=1.3.3"
|
retrying = "~=1.3.3"
|
||||||
simplejson = "~=3.17.2"
|
simplejson = "~=3.17.2"
|
||||||
six = "~=1.15.0"
|
six = "~=1.15.0"
|
||||||
@@ -56,8 +55,8 @@ sqlparse = "~=0.4.2"
|
|||||||
static3 = "~=0.7.0"
|
static3 = "~=0.7.0"
|
||||||
svg2rlg = "~=0.3"
|
svg2rlg = "~=0.3"
|
||||||
tini = "~=3.0.1"
|
tini = "~=3.0.1"
|
||||||
tornado = "~=6.3"
|
tornado = "~=6.1"
|
||||||
urllib3 = "~=1.26.18"
|
urllib3 = "~=1.26.5"
|
||||||
whitenoise = "~=5.2.0"
|
whitenoise = "~=5.2.0"
|
||||||
yolk = "~=0.4.3"
|
yolk = "~=0.4.3"
|
||||||
zipp = "~=3.4.0"
|
zipp = "~=3.4.0"
|
||||||
@@ -76,10 +75,9 @@ django-hCaptcha = "*"
|
|||||||
importlib-metadata = "*"
|
importlib-metadata = "*"
|
||||||
django-hcaptcha = "*"
|
django-hcaptcha = "*"
|
||||||
"z3c.rml" = "*"
|
"z3c.rml" = "*"
|
||||||
pikepdf = "*"
|
|
||||||
django-queryable-properties = "*"
|
django-queryable-properties = "*"
|
||||||
django-mass-edit = "*"
|
django-mass-edit = "*"
|
||||||
selenium = "~=4.9.1"
|
selenium = "~=3.141.0"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pycodestyle = "~=2.9.1"
|
pycodestyle = "~=2.9.1"
|
||||||
@@ -91,14 +89,11 @@ pluggy = "*"
|
|||||||
pytest-splinter = "*"
|
pytest-splinter = "*"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-reverse = "*"
|
pytest-reverse = "*"
|
||||||
|
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
||||||
|
PyPOM = {extras = [ "splinter",], version = "*"}
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
|
|
||||||
[dev-packages.pytest-xdist]
|
[pipenv]
|
||||||
extras = [ "psutil",]
|
allow_prereleases = true
|
||||||
version = "*"
|
|
||||||
|
|
||||||
[dev-packages.PyPOM]
|
|
||||||
extras = [ "splinter",]
|
|
||||||
version = "*"
|
|
||||||
|
|||||||
1327
Pipfile.lock
generated
1327
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ STAGING = env('STAGING', cast=bool, default=False)
|
|||||||
CI = env('CI', cast=bool, default=False)
|
CI = env('CI', cast=bool, default=False)
|
||||||
|
|
||||||
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']
|
||||||
|
CSRF_TRUSTED_ORIGINS = []
|
||||||
|
|
||||||
if STAGING:
|
if STAGING:
|
||||||
ALLOWED_HOSTS.append('.herokuapp.com')
|
ALLOWED_HOSTS.append('.herokuapp.com')
|
||||||
@@ -35,8 +36,7 @@ if DEBUG:
|
|||||||
ALLOWED_HOSTS.append('localhost')
|
ALLOWED_HOSTS.append('localhost')
|
||||||
ALLOWED_HOSTS.append('example.com')
|
ALLOWED_HOSTS.append('example.com')
|
||||||
ALLOWED_HOSTS.append('127.0.0.1')
|
ALLOWED_HOSTS.append('127.0.0.1')
|
||||||
ALLOWED_HOSTS.append('.app.github.dev')
|
CSRF_TRUSTED_ORIGINS.append('.preview.app.github.dev')
|
||||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
|
||||||
|
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
@@ -66,7 +66,7 @@ INSTALLED_APPS = (
|
|||||||
'assets',
|
'assets',
|
||||||
'training',
|
'training',
|
||||||
|
|
||||||
# 'debug_toolbar',
|
'debug_toolbar',
|
||||||
'registration',
|
'registration',
|
||||||
'reversion',
|
'reversion',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
@@ -77,7 +77,7 @@ INSTALLED_APPS = (
|
|||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
|
'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',
|
||||||
@@ -220,6 +220,8 @@ TIME_ZONE = 'Europe/London'
|
|||||||
|
|
||||||
FORMAT_MODULE_PATH = 'PyRIGS.formats'
|
FORMAT_MODULE_PATH = 'PyRIGS.formats'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
@@ -264,10 +266,3 @@ TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
|||||||
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
|
|
||||||
SECURE_HSTS_SECONDS = 3600
|
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
||||||
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
|
|
||||||
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
|
|
||||||
SECURE_HSTS_PRELOAD = True
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ def screenshot_failure(func):
|
|||||||
if not pathlib.Path("screenshots").is_dir():
|
if not pathlib.Path("screenshots").is_dir():
|
||||||
os.mkdir("screenshots")
|
os.mkdir("screenshots")
|
||||||
self.driver.save_screenshot(screenshot_file)
|
self.driver.save_screenshot(screenshot_file)
|
||||||
print(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr)
|
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return wrapper_func
|
return wrapper_func
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ class TestSampleDataGenerator(TestCase):
|
|||||||
assert Asset.objects.all().count() > 50
|
assert Asset.objects.all().count() > 50
|
||||||
assert Event.objects.all().count() > 100
|
assert Event.objects.all().count() > 100
|
||||||
call_command('deleteSampleData')
|
call_command('deleteSampleData')
|
||||||
assert not Asset.objects.all().exists()
|
assert Asset.objects.all().count() == 0
|
||||||
assert not Event.objects.all().exists()
|
assert Event.objects.all().count() == 0
|
||||||
|
|
||||||
|
|
||||||
@override_settings(DEBUG=True)
|
@override_settings(DEBUG=True)
|
||||||
@@ -76,9 +76,9 @@ def test_unauthenticated(client): # Nothing should be available to the unauthen
|
|||||||
assertTemplateUsed(response, 'login_redirect.html')
|
assertTemplateUsed(response, 'login_redirect.html')
|
||||||
else:
|
else:
|
||||||
if "embed" in str(url):
|
if "embed" in str(url):
|
||||||
expected_url = f"{reverse('login_embed')}?next={request_url}"
|
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||||
else:
|
else:
|
||||||
expected_url = f"{reverse('login')}?next={request_url}"
|
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
||||||
assertRedirects(response, expected_url)
|
assertRedirects(response, expected_url)
|
||||||
call_command('deleteSampleData')
|
call_command('deleteSampleData')
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class Index(generic.TemplateView): # Displays the current rig count along with
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['rig_count'] = models.Event.objects.rig_count()
|
context['rig_count'] = models.Event.objects.rig_count()
|
||||||
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(status=models.Event.CANCELLED).filter(is_rig=True, dry_hire=False)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -134,15 +133,11 @@ class SecureAPIRequest(generic.View):
|
|||||||
results = []
|
results = []
|
||||||
query = reduce(operator.and_, queries)
|
query = reduce(operator.and_, queries)
|
||||||
objects = self.models[model].objects.filter(query)
|
objects = self.models[model].objects.filter(query)
|
||||||
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
|
|
||||||
if model == "profile":
|
|
||||||
objects = objects.filter(is_active=True, is_approved=True)
|
|
||||||
for o in objects:
|
for o in objects:
|
||||||
name = o.display_name if hasattr(o, 'display_name') else o.name
|
|
||||||
data = {
|
data = {
|
||||||
'pk': o.pk,
|
'pk': o.pk,
|
||||||
'value': o.pk,
|
'value': o.pk,
|
||||||
'text': name,
|
'text': o.name,
|
||||||
}
|
}
|
||||||
try: # See if there is a valid update URL
|
try: # See if there is a valid update URL
|
||||||
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
|
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
|
||||||
@@ -187,7 +182,7 @@ class ModalURLMixin:
|
|||||||
url = reverse_lazy('closemodal')
|
url = reverse_lazy('closemodal')
|
||||||
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
|
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=" + serializers.serialize("json", [self.object]))
|
||||||
messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'")
|
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
|
||||||
else:
|
else:
|
||||||
url = reverse_lazy(detail, kwargs={
|
url = reverse_lazy(detail, kwargs={
|
||||||
'pk': self.object.pk,
|
'pk': self.object.pk,
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ For setup information and other such helpful stuff check the [Wiki](https://gith
|
|||||||
- PyRIGS: Base app, stores 'global' information
|
- PyRIGS: Base app, stores 'global' information
|
||||||
- RIGS: Rigboard stuff - event calendar etc
|
- RIGS: Rigboard stuff - event calendar etc
|
||||||
- assets: Database of our kit, testing data etc
|
- assets: Database of our kit, testing data etc
|
||||||
- training: Logs in-house training within various "departments" (sound, lighting etc).
|
|
||||||
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
||||||
- users: Our custom logic for registration and profiles. Semi-modular.
|
- users: Our custom logic for registration and profiles. Semi-modular.
|
||||||
|
- training: SoonTM
|
||||||
|
|
||||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ admin.site.register(models.VatRate, VersionAdmin)
|
|||||||
admin.site.register(models.Event, VersionAdmin)
|
admin.site.register(models.Event, VersionAdmin)
|
||||||
admin.site.register(models.EventItem, VersionAdmin)
|
admin.site.register(models.EventItem, VersionAdmin)
|
||||||
admin.site.register(models.Invoice, VersionAdmin)
|
admin.site.register(models.Invoice, VersionAdmin)
|
||||||
admin.site.register(models.EventCheckIn)
|
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
||||||
@@ -154,9 +153,8 @@ class AssociateAdmin(VersionAdmin):
|
|||||||
|
|
||||||
@admin.register(models.Profile)
|
@admin.register(models.Profile)
|
||||||
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
||||||
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login')
|
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
|
||||||
list_display_links = ['username']
|
list_display_links = ['username']
|
||||||
list_filter = UserAdmin.list_filter + ('is_approved',)
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
(_('Personal info'), {
|
(_('Personal info'), {
|
||||||
@@ -208,8 +206,3 @@ class RiskAssessmentAdmin(VersionAdmin):
|
|||||||
@admin.register(models.EventChecklist)
|
@admin.register(models.EventChecklist)
|
||||||
class EventChecklistAdmin(VersionAdmin):
|
class EventChecklistAdmin(VersionAdmin):
|
||||||
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.PowerTestRecord)
|
|
||||||
class EventChecklistAdmin(VersionAdmin):
|
|
||||||
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
|
||||||
|
|||||||
140
RIGS/forms.py
140
RIGS/forms.py
@@ -44,7 +44,7 @@ class EventForm(forms.ModelForm):
|
|||||||
return simplejson.dumps(items)
|
return simplejson.dumps(items)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super(EventForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['items_json'].initial = self._get_items_json
|
self.fields['items_json'].initial = self._get_items_json
|
||||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
||||||
@@ -121,7 +121,23 @@ 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', 'forum_url']
|
'purchase_order', 'collector']
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireForm(forms.ModelForm):
|
||||||
|
related_models = {
|
||||||
|
'person': models.Person,
|
||||||
|
'organisation': models.Organisation,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
||||||
|
self.fields['end_date'].widget.format = '%Y-%m-%d'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Subhire
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||||
@@ -131,7 +147,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
if self.cleaned_data.get('amount') != self.instance.event.total:
|
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).')
|
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
|
||||||
return super().clean()
|
return super(BaseClientEventAuthorisationForm, self).clean()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@@ -179,7 +195,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
|
|||||||
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
|
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
|
||||||
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
|
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
|
||||||
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
|
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
|
||||||
return super().clean()
|
return super(EventRiskAssessmentForm, self).clean()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
@@ -195,49 +211,91 @@ class EventChecklistForm(forms.ModelForm):
|
|||||||
if field.__class__ == forms.NullBooleanField:
|
if field.__class__ == forms.NullBooleanField:
|
||||||
# Only display yes/no to user, the 'none' is only ever set in the background
|
# Only display yes/no to user, the 'none' is only ever set in the background
|
||||||
field.widget = forms.CheckboxInput()
|
field.widget = forms.CheckboxInput()
|
||||||
|
# Parsed from incoming form data by clean, then saved into models when the form is saved
|
||||||
related_models = {
|
items = {}
|
||||||
'venue': models.Venue,
|
|
||||||
}
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.EventChecklist
|
|
||||||
fields = '__all__'
|
|
||||||
exclude = ['reviewed_at', 'reviewed_by']
|
|
||||||
|
|
||||||
|
|
||||||
class PowerTestRecordForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
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()
|
|
||||||
|
|
||||||
related_models = {
|
related_models = {
|
||||||
'venue': models.Venue,
|
'venue': models.Venue,
|
||||||
'power_mic': models.Profile,
|
'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[f'{field}_{pk}']
|
||||||
|
if value == '':
|
||||||
|
raise forms.ValidationError(f'Add a {field} to crewmember {pk}', code=f'{field}_mismatch')
|
||||||
|
|
||||||
|
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:
|
class Meta:
|
||||||
model = models.PowerTestRecord
|
model = models.EventChecklist
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
exclude = ['reviewed_at', 'reviewed_by']
|
exclude = ['reviewed_at', 'reviewed_by']
|
||||||
|
|
||||||
|
|
||||||
class EventCheckInForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['time'].initial = timezone.now()
|
|
||||||
self.fields['role'].initial = "Crew"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.EventCheckIn
|
|
||||||
fields = '__all__'
|
|
||||||
exclude = ['end_time']
|
|
||||||
|
|
||||||
|
|
||||||
class EditCheckInForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.EventCheckIn
|
|
||||||
fields = '__all__'
|
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ class Command(BaseCommand):
|
|||||||
suspended_structures=bool(random.getrandbits(1)),
|
suspended_structures=bool(random.getrandbits(1)),
|
||||||
outside=bool(random.getrandbits(1)))
|
outside=bool(random.getrandbits(1)))
|
||||||
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
|
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
|
||||||
models.EventChecklist.objects.create(event=new_event,
|
models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles),
|
||||||
safe_parking=bool(random.getrandbits(1)),
|
safe_parking=bool(random.getrandbits(1)),
|
||||||
safe_packing=bool(random.getrandbits(1)),
|
safe_packing=bool(random.getrandbits(1)),
|
||||||
exits=bool(random.getrandbits(1)),
|
exits=bool(random.getrandbits(1)),
|
||||||
@@ -287,4 +287,6 @@ class Command(BaseCommand):
|
|||||||
ear_plugs=bool(random.getrandbits(1)),
|
ear_plugs=bool(random.getrandbits(1)),
|
||||||
hs_location="Locked away safely",
|
hs_location="Locked away safely",
|
||||||
extinguishers_location="Somewhere, I forgot",
|
extinguishers_location="Somewhere, I forgot",
|
||||||
|
earthing=bool(random.getrandbits(1)),
|
||||||
|
pat=bool(random.getrandbits(1)),
|
||||||
date=timezone.now(), venue=random.choice(self.venues))
|
date=timezone.now(), venue=random.choice(self.venues))
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# Generated by Django 3.2.16 on 2023-05-08 15:58
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import versioning.versioning
|
|
||||||
|
|
||||||
def migrate_old_data(apps, schema_editor):
|
|
||||||
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
|
||||||
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
|
|
||||||
for ec in EventChecklist.objects.all():
|
|
||||||
# New highscore for the most pythonic BS I've ever written.
|
|
||||||
PowerTestRecord.objects.create(event=ec.event, venue=ec.venue, reviewed_by=ec.reviewed_by, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created or i.name == "notes")})
|
|
||||||
|
|
||||||
|
|
||||||
def revert(apps, schema_editor):
|
|
||||||
apps.get_model('RIGS', 'PowerTestRecord').objects.all().delete()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0045_alter_profile_is_approved'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PowerTestRecord',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_tests', to='RIGS.event')),
|
|
||||||
('notes', models.TextField(blank=True, default='')),
|
|
||||||
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue')),
|
|
||||||
('reviewed_at', models.DateTimeField(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.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, 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, default='', help_text='Description', max_length=255)),
|
|
||||||
('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.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
|
||||||
('w2_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
|
||||||
('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.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
|
||||||
('w3_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
|
||||||
('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.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
|
||||||
('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_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
'ordering': ['event'],
|
|
||||||
'permissions': [('review_power', 'Can review Power Test Records')],
|
|
||||||
},
|
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
|
||||||
),
|
|
||||||
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
|
||||||
]
|
|
||||||
39
RIGS/migrations/0046_subhire.py
Normal file
39
RIGS/migrations/0046_subhire.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2022-12-16 14:41
|
||||||
|
|
||||||
|
import RIGS.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import versioning.versioning
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0045_alter_profile_is_approved'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Subhire',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('status', models.IntegerField(choices=[(0, 'Provisional'), (1, 'Confirmed'), (2, 'Booked'), (3, 'Cancelled')], default=0)),
|
||||||
|
('start_date', models.DateField()),
|
||||||
|
('start_time', models.TimeField(blank=True, null=True)),
|
||||||
|
('end_date', models.DateField(blank=True, null=True)),
|
||||||
|
('end_time', models.TimeField(blank=True, null=True)),
|
||||||
|
('purchase_order', models.CharField(blank=True, default='', max_length=255, verbose_name='PO')),
|
||||||
|
('insurance_value', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('quote', models.URLField(default='', validators=[RIGS.validators.validate_url])),
|
||||||
|
('events', models.ManyToManyField(to='RIGS.Event')),
|
||||||
|
('organisation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.organisation')),
|
||||||
|
('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.person')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'permissions': [('subhire_finance', 'Can see financial data for subhire - insurance values')],
|
||||||
|
},
|
||||||
|
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Generated by Django 3.2.19 on 2023-05-17 08:44
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_old_data(apps, schema_editor):
|
|
||||||
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
|
||||||
EventCheckIn = apps.get_model('RIGS', 'EventCheckIn')
|
|
||||||
for ec in EventChecklist.objects.all():
|
|
||||||
for crew in ec.crew.all():
|
|
||||||
try:
|
|
||||||
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=ec.vehicles.get(driver=crew.crewmember).vehicle)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end)
|
|
||||||
|
|
||||||
|
|
||||||
def revert(apps, schema_editor):
|
|
||||||
apps.get_model('RIGS', 'EventCheckIn').objects.all().delete()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0046_create_powertests'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EventCheckIn',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('time', models.DateTimeField()),
|
|
||||||
('role', models.CharField(blank=True, max_length=50)),
|
|
||||||
('vehicle', models.CharField(blank=True, max_length=100)),
|
|
||||||
('end_time', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.event')),
|
|
||||||
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
|
||||||
]
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# Generated by Django 3.2.19 on 2023-05-18 11:56
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0047_auto_20230517_0944'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklistvehicle',
|
|
||||||
name='checklist',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklistvehicle',
|
|
||||||
name='driver',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='all_rcds_tested',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='earthing',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='fd_earth_fault',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='fd_phase_rotation',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='fd_pssc',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='fd_voltage_l1',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='fd_voltage_l2',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='fd_voltage_l3',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='labelling',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='pat',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='power_mic',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='public_sockets_tested',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='rcds',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='source_rcd',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='supply_test',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w1_description',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w1_earth_fault',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w1_polarity',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w1_voltage',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w2_description',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w2_earth_fault',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w2_polarity',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w2_voltage',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w3_description',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w3_earth_fault',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w3_polarity',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='w3_voltage',
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
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.AlterField(
|
|
||||||
model_name='eventchecklist',
|
|
||||||
name='reviewed_at',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='reviewed_at',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='riskassessment',
|
|
||||||
name='reviewed_at',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='EventChecklistCrew',
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='EventChecklistVehicle',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Generated by Django 3.2.19 on 2023-05-29 10:23
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0048_auto_20230518_1256'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='fd_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='fd_pssc',
|
|
||||||
field=models.IntegerField(blank=True, help_text='Prospective Short Circuit Current / A', null=True, verbose_name='PSCC'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='w1_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='w1_voltage',
|
|
||||||
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='w2_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='w2_voltage',
|
|
||||||
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='w3_earth_fault',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='powertestrecord',
|
|
||||||
name='w3_voltage',
|
|
||||||
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 3.2.19 on 2023-06-27 11:28
|
|
||||||
|
|
||||||
import RIGS.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0049_auto_20230529_1123'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='event',
|
|
||||||
name='forum_url',
|
|
||||||
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.19 on 2023-07-09 21:23
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0050_event_forum_url'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='payment',
|
|
||||||
name='method',
|
|
||||||
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('T', 'TEC Adjustment')], default='', max_length=2),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
969
RIGS/models.py
969
RIGS/models.py
@@ -1,969 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
from collections import Counter
|
|
||||||
from decimal import Decimal
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
from django import forms
|
|
||||||
from django.db.models import Q, F
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from reversion.models import Version
|
|
||||||
from versioning.versioning import RevisionMixin
|
|
||||||
|
|
||||||
|
|
||||||
def filter_by_pk(filt, query):
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(query)
|
|
||||||
filt = filt | Q(pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
return filt
|
|
||||||
|
|
||||||
|
|
||||||
class Profile(AbstractUser):
|
|
||||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
|
||||||
phone = models.CharField(max_length=13, blank=True, default='')
|
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
|
||||||
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
|
||||||
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
|
||||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
|
||||||
dark_theme = models.BooleanField(default=False)
|
|
||||||
is_supervisor = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def make_api_key(cls):
|
|
||||||
size = 20
|
|
||||||
chars = string.ascii_letters + string.digits
|
|
||||||
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
|
||||||
return new_api_key
|
|
||||||
|
|
||||||
@property
|
|
||||||
def profile_picture(self):
|
|
||||||
url = ""
|
|
||||||
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
|
||||||
url = "https://www.gravatar.com/avatar/" + hashlib.md5(
|
|
||||||
self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
|
|
||||||
return url
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
name = self.get_full_name()
|
|
||||||
if self.initials:
|
|
||||||
name += f' "{self.initials}"'
|
|
||||||
return name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
|
||||||
|
|
||||||
@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):
|
|
||||||
# last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity
|
|
||||||
return Profile.objects.filter(is_approved=False, last_login=None, date_joined_date=timezone.now().date()).count()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def current_event(self):
|
|
||||||
q = EventCheckIn.objects.filter(person=self, end_time=None)
|
|
||||||
return q.latest('time') if q.exists() else None
|
|
||||||
|
|
||||||
|
|
||||||
class ContactableManager(models.Manager):
|
|
||||||
def search(self, query=None):
|
|
||||||
qs = self.get_queryset()
|
|
||||||
if query is not None:
|
|
||||||
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
|
|
||||||
phone__startswith=query) | Q(phone__endswith=query)
|
|
||||||
|
|
||||||
or_lookup = filter_by_pk(or_lookup, query)
|
|
||||||
|
|
||||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
class Person(models.Model, RevisionMixin):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
phone = models.CharField(max_length=15, blank=True, default='')
|
|
||||||
email = models.EmailField(blank=True, default='')
|
|
||||||
address = models.TextField(blank=True, default='')
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
|
|
||||||
objects = ContactableManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = self.name
|
|
||||||
if self.notes is not None:
|
|
||||||
if len(self.notes) > 0:
|
|
||||||
string += "*"
|
|
||||||
return string
|
|
||||||
|
|
||||||
@property
|
|
||||||
def organisations(self):
|
|
||||||
o = []
|
|
||||||
for e in Event.objects.filter(person=self).select_related('organisation'):
|
|
||||||
if e.organisation:
|
|
||||||
o.append(e.organisation)
|
|
||||||
|
|
||||||
# Count up occurances and put them in descending order
|
|
||||||
c = Counter(o)
|
|
||||||
stats = c.most_common()
|
|
||||||
return stats
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('person_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class Organisation(models.Model, RevisionMixin):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
phone = models.CharField(max_length=15, blank=True, default='')
|
|
||||||
email = models.EmailField(blank=True, default='')
|
|
||||||
address = models.TextField(blank=True, default='')
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
union_account = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
objects = ContactableManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = self.name
|
|
||||||
if self.notes is not None:
|
|
||||||
if len(self.notes) > 0:
|
|
||||||
string += "*"
|
|
||||||
return string
|
|
||||||
|
|
||||||
@property
|
|
||||||
def persons(self):
|
|
||||||
p = []
|
|
||||||
for e in Event.objects.filter(organisation=self).select_related('person'):
|
|
||||||
if e.person:
|
|
||||||
p.append(e.person)
|
|
||||||
|
|
||||||
# Count up occurances and put them in descending order
|
|
||||||
c = Counter(p)
|
|
||||||
stats = c.most_common()
|
|
||||||
return stats
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class VatManager(models.Manager):
|
|
||||||
def current_rate(self):
|
|
||||||
return self.find_rate(timezone.now())
|
|
||||||
|
|
||||||
def find_rate(self, date):
|
|
||||||
try:
|
|
||||||
return self.filter(start_at__lte=date).latest()
|
|
||||||
except VatRate.DoesNotExist:
|
|
||||||
r = VatRate
|
|
||||||
r.rate = 0
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class VatRate(models.Model, RevisionMixin):
|
|
||||||
start_at = models.DateField()
|
|
||||||
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
|
||||||
comment = models.CharField(max_length=255)
|
|
||||||
|
|
||||||
objects = VatManager()
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def as_percent(self):
|
|
||||||
return self.rate * 100
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-start_at']
|
|
||||||
get_latest_by = 'start_at'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
|
|
||||||
|
|
||||||
|
|
||||||
class Venue(models.Model, RevisionMixin):
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
phone = models.CharField(max_length=15, blank=True, default='')
|
|
||||||
email = models.EmailField(blank=True, default='')
|
|
||||||
three_phase_available = models.BooleanField(default=False)
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
address = models.TextField(blank=True, default='')
|
|
||||||
|
|
||||||
objects = ContactableManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
string = self.name
|
|
||||||
if self.notes and len(self.notes) > 0:
|
|
||||||
string += "*"
|
|
||||||
return string
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_events(self):
|
|
||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('venue_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class EventManager(models.Manager):
|
|
||||||
def current_events(self):
|
|
||||||
events = self.filter(
|
|
||||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
|
||||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Ends after
|
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Active dry hire
|
|
||||||
(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.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
|
||||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
return events
|
|
||||||
|
|
||||||
def events_in_bounds(self, start, end):
|
|
||||||
events = self.filter(
|
|
||||||
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
|
|
||||||
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
|
|
||||||
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
|
|
||||||
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
|
|
||||||
|
|
||||||
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
|
|
||||||
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
|
|
||||||
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
|
|
||||||
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
|
|
||||||
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
|
|
||||||
|
|
||||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
|
||||||
'organisation',
|
|
||||||
'venue', 'mic')
|
|
||||||
return events
|
|
||||||
|
|
||||||
def rig_count(self):
|
|
||||||
event_count = self.filter(
|
|
||||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
|
||||||
is_rig=True) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
|
||||||
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) | # Ends after
|
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
|
|
||||||
status=Event.CANCELLED)) # Active dry hire
|
|
||||||
).count()
|
|
||||||
return event_count
|
|
||||||
|
|
||||||
def waiting_invoices(self):
|
|
||||||
events = self.filter(
|
|
||||||
(
|
|
||||||
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
|
||||||
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
|
|
||||||
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
|
|
||||||
models.Q(is_rig=True) # Is a rig (not non-rig)
|
|
||||||
).order_by('start_date') \
|
|
||||||
.select_related('person', 'organisation', 'venue', 'mic') \
|
|
||||||
.prefetch_related('items')
|
|
||||||
|
|
||||||
return events
|
|
||||||
|
|
||||||
def search(self, query=None):
|
|
||||||
qs = self.get_queryset()
|
|
||||||
if query is not None:
|
|
||||||
or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(notes__icontains=query)
|
|
||||||
|
|
||||||
or_lookup = filter_by_pk(or_lookup, query)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if query[0] == "N":
|
|
||||||
val = int(query[1:])
|
|
||||||
or_lookup = Q(pk=val) # If string is N###### then do a simple PK filter
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
def validate_forum_url(value):
|
|
||||||
if not value:
|
|
||||||
return # Required error is done the field
|
|
||||||
obj = urlparse(value)
|
|
||||||
if obj.hostname not in ('forum.nottinghamtec.co.uk'):
|
|
||||||
raise ValidationError('URL must point to a location on the TEC Forum')
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
|
||||||
class Event(models.Model, RevisionMixin):
|
|
||||||
# Done to make it much nicer on the database
|
|
||||||
PROVISIONAL = 0
|
|
||||||
CONFIRMED = 1
|
|
||||||
BOOKED = 2
|
|
||||||
CANCELLED = 3
|
|
||||||
EVENT_STATUS_CHOICES = (
|
|
||||||
(PROVISIONAL, 'Provisional'),
|
|
||||||
(CONFIRMED, 'Confirmed'),
|
|
||||||
(BOOKED, 'Booked'),
|
|
||||||
(CANCELLED, 'Cancelled'),
|
|
||||||
)
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
|
||||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
|
||||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
|
||||||
description = models.TextField(blank=True, default='')
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
|
||||||
dry_hire = models.BooleanField(default=False)
|
|
||||||
is_rig = models.BooleanField(default=True)
|
|
||||||
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
|
||||||
null=True)
|
|
||||||
|
|
||||||
# Timing
|
|
||||||
start_date = models.DateField()
|
|
||||||
start_time = models.TimeField(blank=True, null=True)
|
|
||||||
end_date = models.DateField(blank=True, null=True)
|
|
||||||
end_time = models.TimeField(blank=True, null=True)
|
|
||||||
access_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
meet_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
|
|
||||||
# Crew management
|
|
||||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
|
||||||
verbose_name="MIC", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
# Monies
|
|
||||||
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
|
||||||
collector = models.CharField(max_length=255, blank=True, default='', 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(blank=True, default='')
|
|
||||||
|
|
||||||
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
if self.pk:
|
|
||||||
if self.is_rig:
|
|
||||||
return f"N{self.pk:05d}"
|
|
||||||
return self.pk
|
|
||||||
return "????"
|
|
||||||
|
|
||||||
# Calculated values
|
|
||||||
"""
|
|
||||||
EX Vat
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sum_total(self):
|
|
||||||
total = self.items.aggregate(
|
|
||||||
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
|
||||||
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
|
||||||
)['sum_total']
|
|
||||||
if total:
|
|
||||||
return total
|
|
||||||
return Decimal("0.00")
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def vat_rate(self):
|
|
||||||
return VatRate.objects.find_rate(self.start_date)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vat(self):
|
|
||||||
# No VAT is owed on internal transfers
|
|
||||||
if self.internal:
|
|
||||||
return 0
|
|
||||||
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
|
||||||
|
|
||||||
"""
|
|
||||||
Inc VAT
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cancelled(self):
|
|
||||||
return (self.status == self.CANCELLED)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def confirmed(self):
|
|
||||||
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hs_done(self):
|
|
||||||
return self.riskassessment is not None and self.has_checklist and self.has_power
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_checklist(self):
|
|
||||||
return self.checklists.exists()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_power(self):
|
|
||||||
return self.power_tests.exists()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_start_time(self):
|
|
||||||
return self.start_time is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_end_time(self):
|
|
||||||
return self.end_time is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def earliest_time(self):
|
|
||||||
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
|
||||||
|
|
||||||
# Put all the datetimes in a list
|
|
||||||
datetime_list = []
|
|
||||||
|
|
||||||
if self.access_at:
|
|
||||||
datetime_list.append(self.access_at)
|
|
||||||
|
|
||||||
if self.meet_at:
|
|
||||||
datetime_list.append(self.meet_at)
|
|
||||||
|
|
||||||
# If there is no start time defined, pretend it's midnight
|
|
||||||
startTimeFaked = False
|
|
||||||
if self.has_start_time:
|
|
||||||
startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
|
|
||||||
else:
|
|
||||||
startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
|
|
||||||
startTimeFaked = True
|
|
||||||
|
|
||||||
# timezoneIssues - apply the default timezone to the naiive datetime
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
startDateTime = tz.localize(startDateTime)
|
|
||||||
datetime_list.append(startDateTime) # then add it to the list
|
|
||||||
|
|
||||||
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
|
||||||
|
|
||||||
# if we faked it & it's the earliest, better own up
|
|
||||||
if startTimeFaked and earliest == startDateTime:
|
|
||||||
return self.start_date
|
|
||||||
|
|
||||||
return earliest
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_time(self):
|
|
||||||
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
endDate = self.end_date
|
|
||||||
if endDate is None:
|
|
||||||
endDate = self.start_date
|
|
||||||
|
|
||||||
if self.has_end_time:
|
|
||||||
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
endDateTime = tz.localize(endDateTime)
|
|
||||||
|
|
||||||
return endDateTime
|
|
||||||
|
|
||||||
else:
|
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def can_check_in(self):
|
|
||||||
earliest = self.earliest_time
|
|
||||||
if isinstance(self.earliest_time, datetime.date):
|
|
||||||
earliest = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
earliest = tz.localize(earliest)
|
|
||||||
return not self.dry_hire and not self.status == Event.CANCELLED and earliest <= timezone.now()
|
|
||||||
|
|
||||||
objects = EventManager()
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.display_id} | {self.name}"
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
errdict = {}
|
|
||||||
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.']
|
|
||||||
|
|
||||||
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
|
||||||
hasStartAndEnd = self.has_start_time and self.has_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.']
|
|
||||||
|
|
||||||
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):
|
|
||||||
"""Call :meth:`full_clean` before saving."""
|
|
||||||
self.full_clean()
|
|
||||||
super(Event, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventItem(models.Model, RevisionMixin):
|
|
||||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
description = models.TextField(blank=True, default='')
|
|
||||||
quantity = models.IntegerField()
|
|
||||||
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
|
||||||
order = models.IntegerField()
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_cost(self):
|
|
||||||
return self.cost * self.quantity
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['order']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"item {self.name}"
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventAuthorisation(models.Model, RevisionMixin):
|
|
||||||
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
|
||||||
email = models.EmailField()
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
|
||||||
account_code = models.CharField(max_length=50, default='', blank=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('event_detail', kwargs={'pk': self.event_id})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceManager(models.Manager):
|
|
||||||
def outstanding_invoices(self):
|
|
||||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
|
||||||
sql = "SELECT * FROM " \
|
|
||||||
"(SELECT " \
|
|
||||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
|
||||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
|
||||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
|
||||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
|
||||||
"AS sub " \
|
|
||||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
|
||||||
"ORDER BY invoice_date"
|
|
||||||
|
|
||||||
query = self.raw(sql)
|
|
||||||
return query
|
|
||||||
|
|
||||||
def search(self, query=None):
|
|
||||||
qs = self.get_queryset()
|
|
||||||
if query is not None:
|
|
||||||
or_lookup = Q(event__name__icontains=query)
|
|
||||||
|
|
||||||
or_lookup = filter_by_pk(or_lookup, query)
|
|
||||||
|
|
||||||
# try and parse an int
|
|
||||||
try:
|
|
||||||
val = int(query)
|
|
||||||
or_lookup = or_lookup | Q(event__pk=val)
|
|
||||||
except: # noqa
|
|
||||||
# not an integer
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if query[0] == "N":
|
|
||||||
val = int(query[1:])
|
|
||||||
or_lookup = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
|
||||||
elif query[0] == "#":
|
|
||||||
val = int(query[1:])
|
|
||||||
or_lookup = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
|
||||||
except: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
void = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
reversion_perm = 'RIGS.view_invoice'
|
|
||||||
|
|
||||||
objects = InvoiceManager()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sum_total(self):
|
|
||||||
return self.event.sum_total
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return self.event.total
|
|
||||||
|
|
||||||
@property
|
|
||||||
def payment_total(self):
|
|
||||||
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
|
|
||||||
if total:
|
|
||||||
return total
|
|
||||||
return Decimal("0.00")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def balance(self):
|
|
||||||
return self.sum_total - self.payment_total
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_closed(self):
|
|
||||||
return self.balance == 0 or self.void
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"{self.display_id} for Event {self.event.display_id}"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return f"#{self.pk:05d}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-invoice_date']
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class Payment(models.Model, RevisionMixin):
|
|
||||||
CASH = 'C'
|
|
||||||
INTERNAL = 'I'
|
|
||||||
EXTERNAL = 'E'
|
|
||||||
ADJUSTMENT = 'T'
|
|
||||||
METHODS = (
|
|
||||||
(CASH, 'Cash'),
|
|
||||||
(INTERNAL, 'Internal'),
|
|
||||||
(EXTERNAL, 'External'),
|
|
||||||
(ADJUSTMENT, 'TEC Adjustment'),
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
|
||||||
date = models.DateField()
|
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
|
||||||
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
|
||||||
|
|
||||||
reversion_hide = True
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.get_method_display()}: {self.amount}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return f"payment of £{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')
|
|
||||||
|
|
||||||
|
|
||||||
class ReviewableModel(models.Model):
|
|
||||||
reviewed_at = models.DateTimeField(null=True, blank=True)
|
|
||||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
|
||||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def fieldz(self):
|
|
||||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class RiskAssessment(ReviewableModel, 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, default='', 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
|
|
||||||
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?")
|
|
||||||
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, default='', 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, default='', 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, default='', 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, default='', help_text="Who are the persons on site responsible for their use?")
|
|
||||||
rigging_plan = models.URLField(blank=True, default='', 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
|
|
||||||
|
|
||||||
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 Doesn't require checking with a super either way
|
|
||||||
'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]
|
|
||||||
|
|
||||||
def get_event_size_display(self):
|
|
||||||
return self.SIZES[self.event_size][1] + " Event"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.pk} | {self.event}"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('ra_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str(self.event)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class EventChecklist(ReviewableModel, RevisionMixin):
|
|
||||||
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
# General
|
|
||||||
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, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
|
||||||
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
|
|
||||||
|
|
||||||
inverted_fields = []
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['event']
|
|
||||||
permissions = [
|
|
||||||
('review_eventchecklist', 'Can review Event Checklists')
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.pk} - {self.event}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str(self.event)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('ec_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
|
||||||
class PowerTestRecord(ReviewableModel, RevisionMixin):
|
|
||||||
earth_fault_text = "Earth Fault Loop Impedance (Z<small>S</small>) / Ω"
|
|
||||||
pssc_text = "Prospective Short Circuit Current / A"
|
|
||||||
|
|
||||||
event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE)
|
|
||||||
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)
|
|
||||||
notes = models.TextField(blank=True, default='')
|
|
||||||
# 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.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
|
||||||
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text=pssc_text)
|
|
||||||
# Worst case points
|
|
||||||
w1_description = models.CharField(blank=True, default='', 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 / V")
|
|
||||||
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
|
||||||
w2_description = models.CharField(blank=True, default='', 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 / V")
|
|
||||||
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
|
||||||
w3_description = models.CharField(blank=True, default='', 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 / V")
|
|
||||||
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
|
||||||
|
|
||||||
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>")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['event']
|
|
||||||
permissions = [
|
|
||||||
('review_power', 'Can review Power Test Records')
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.pk} - {self.event}"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('pt_detail', kwargs={'pk': self.pk})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def activity_feed_string(self):
|
|
||||||
return str(self.event)
|
|
||||||
|
|
||||||
|
|
||||||
class EventCheckIn(models.Model):
|
|
||||||
event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)
|
|
||||||
person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkins', on_delete=models.CASCADE)
|
|
||||||
time = models.DateTimeField()
|
|
||||||
role = models.CharField(max_length=50, blank=True)
|
|
||||||
vehicle = models.CharField(max_length=100, blank=True)
|
|
||||||
end_time = models.DateTimeField(null=True, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.person} on {self.event}"
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
sass = " Please invent time travel and retry."
|
|
||||||
if self.time > timezone.now():
|
|
||||||
raise ValidationError("May not check in in the future." + sass)
|
|
||||||
if self.end_time and self.end_time < self.time:
|
|
||||||
raise ValidationError("May not check out before you've checked in." + sass)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('event_detail', kwargs={'pk': self.event_id})
|
|
||||||
|
|
||||||
def active(self):
|
|
||||||
return end_time is not None
|
|
||||||
4
RIGS/models/__init__.py
Normal file
4
RIGS/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .models import *
|
||||||
|
from .finance import *
|
||||||
|
from .hs import *
|
||||||
|
from .events import *
|
||||||
467
RIGS/models/events.py
Normal file
467
RIGS/models/events.py
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
|
from RIGS.validators import validate_url
|
||||||
|
from .utils import filter_by_pk
|
||||||
|
from .finance import VatRate
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEventManager(models.Manager):
|
||||||
|
def event_search(self, q, start, end, status):
|
||||||
|
filt = Q()
|
||||||
|
if end:
|
||||||
|
filt &= Q(start_date__lte=end)
|
||||||
|
if start:
|
||||||
|
filt &= Q(start_date__gte=start)
|
||||||
|
|
||||||
|
objects = self.all()
|
||||||
|
|
||||||
|
if q:
|
||||||
|
objects = self.search(q)
|
||||||
|
|
||||||
|
if len(status) > 0:
|
||||||
|
filt &= Q(status__in=status)
|
||||||
|
|
||||||
|
qs = objects.filter(filt).order_by('-start_date')
|
||||||
|
|
||||||
|
# Preselect related for efficiency
|
||||||
|
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
class EventManager(BaseEventManager):
|
||||||
|
def current_events(self):
|
||||||
|
events = self.filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Ends after
|
||||||
|
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Active dry hire
|
||||||
|
(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.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def events_in_bounds(self, start, end):
|
||||||
|
events = self.filter(
|
||||||
|
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
|
||||||
|
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
|
||||||
|
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
|
||||||
|
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
|
||||||
|
|
||||||
|
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
|
||||||
|
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
|
||||||
|
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
|
||||||
|
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
|
||||||
|
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
|
||||||
|
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
||||||
|
'organisation',
|
||||||
|
'venue', 'mic')
|
||||||
|
return events
|
||||||
|
|
||||||
|
def active_dry_hires(self):
|
||||||
|
return self.filter(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)
|
||||||
|
|
||||||
|
def rig_count(self):
|
||||||
|
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
||||||
|
is_rig=True)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True)) | # Ends after
|
||||||
|
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)) # Active dry hire
|
||||||
|
).count()
|
||||||
|
return event_count
|
||||||
|
|
||||||
|
def waiting_invoices(self):
|
||||||
|
events = self.filter(
|
||||||
|
(
|
||||||
|
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||||
|
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
|
||||||
|
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
|
||||||
|
models.Q(is_rig=True) # Is a rig (not non-rig)
|
||||||
|
).order_by('start_date') \
|
||||||
|
.select_related('person', 'organisation', 'venue', 'mic') \
|
||||||
|
.prefetch_related('items')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(notes__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is N###### then do a simple PK filter
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
def find_earliest_event_time(event, datetime_list):
|
||||||
|
# If there is no start time defined, pretend it's midnight
|
||||||
|
startTimeFaked = False
|
||||||
|
if event.has_start_time:
|
||||||
|
startDateTime = datetime.datetime.combine(event.start_date, event.start_time)
|
||||||
|
else:
|
||||||
|
startDateTime = datetime.datetime.combine(event.start_date, datetime.time(00, 00))
|
||||||
|
startTimeFaked = True
|
||||||
|
|
||||||
|
# timezoneIssues - apply the default timezone to the naiive datetime
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
startDateTime = tz.localize(startDateTime)
|
||||||
|
datetime_list.append(startDateTime) # then add it to the list
|
||||||
|
|
||||||
|
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
||||||
|
|
||||||
|
# if we faked it & it's the earliest, better own up
|
||||||
|
if startTimeFaked and earliest == startDateTime:
|
||||||
|
return event.start_date
|
||||||
|
return earliest
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEvent(models.Model, RevisionMixin):
|
||||||
|
# Done to make it much nicer on the database
|
||||||
|
PROVISIONAL = 0
|
||||||
|
CONFIRMED = 1
|
||||||
|
BOOKED = 2
|
||||||
|
CANCELLED = 3
|
||||||
|
EVENT_STATUS_CHOICES = (
|
||||||
|
(PROVISIONAL, 'Provisional'),
|
||||||
|
(CONFIRMED, 'Confirmed'),
|
||||||
|
(BOOKED, 'Booked'),
|
||||||
|
(CANCELLED, 'Cancelled'),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
|
description = models.TextField(blank=True, default='')
|
||||||
|
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
start_date = models.DateField()
|
||||||
|
start_time = models.TimeField(blank=True, null=True)
|
||||||
|
end_date = models.DateField(blank=True, null=True)
|
||||||
|
end_time = models.TimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancelled(self):
|
||||||
|
return (self.status == self.CANCELLED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def confirmed(self):
|
||||||
|
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_start_time(self):
|
||||||
|
return self.start_time is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_end_time(self):
|
||||||
|
return self.end_time is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_time(self):
|
||||||
|
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDate = self.end_date
|
||||||
|
if endDate is None:
|
||||||
|
endDate = self.start_date
|
||||||
|
|
||||||
|
if self.has_end_time:
|
||||||
|
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDateTime = tz.localize(endDateTime)
|
||||||
|
|
||||||
|
return endDateTime
|
||||||
|
|
||||||
|
else:
|
||||||
|
return endDate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
start = self.earliest_time
|
||||||
|
if isinstance(self.earliest_time, datetime.datetime):
|
||||||
|
start = self.earliest_time.date()
|
||||||
|
end = self.latest_time
|
||||||
|
if isinstance(self.latest_time, datetime.datetime):
|
||||||
|
end = self.latest_time.date()
|
||||||
|
return (end - start).days + 1
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
errdict = {}
|
||||||
|
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."]
|
||||||
|
|
||||||
|
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||||
|
hasStartAndEnd = self.has_start_time and self.has_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."]
|
||||||
|
return errdict
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.display_id}: {self.name}"
|
||||||
|
|
||||||
|
@reversion.register(follow=['items'])
|
||||||
|
class Event(BaseEvent):
|
||||||
|
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
||||||
|
verbose_name="MIC", on_delete=models.CASCADE)
|
||||||
|
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
dry_hire = models.BooleanField(default=False)
|
||||||
|
is_rig = models.BooleanField(default=True)
|
||||||
|
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
|
access_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
meet_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
# Dry-hire only
|
||||||
|
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# Monies
|
||||||
|
collector = models.CharField(max_length=255, blank=True, default='', 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(blank=True, default='')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
if self.pk:
|
||||||
|
if self.is_rig:
|
||||||
|
return f"N{self.pk:05d}"
|
||||||
|
return self.pk
|
||||||
|
return "????"
|
||||||
|
|
||||||
|
# Calculated values
|
||||||
|
"""
|
||||||
|
EX Vat
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sum_total(self):
|
||||||
|
total = self.items.aggregate(
|
||||||
|
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||||
|
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||||
|
)['sum_total']
|
||||||
|
if total:
|
||||||
|
return total
|
||||||
|
return Decimal("0.00")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def vat_rate(self):
|
||||||
|
return VatRate.objects.find_rate(self.start_date)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vat(self):
|
||||||
|
# No VAT is owed on internal transfers
|
||||||
|
if self.internal:
|
||||||
|
return 0
|
||||||
|
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Inc VAT
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hs_done(self):
|
||||||
|
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def earliest_time(self):
|
||||||
|
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
|
|
||||||
|
# Put all the datetimes in a list
|
||||||
|
datetime_list = []
|
||||||
|
|
||||||
|
if self.access_at:
|
||||||
|
datetime_list.append(self.access_at)
|
||||||
|
|
||||||
|
if self.meet_at:
|
||||||
|
datetime_list.append(self.meet_at)
|
||||||
|
|
||||||
|
earliest = find_earliest_event_time(self, datetime_list)
|
||||||
|
|
||||||
|
return earliest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def internal(self):
|
||||||
|
return bool(self.organisation and self.organisation.union_account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authorised(self):
|
||||||
|
if self.internal and hasattr(self, 'authorisation'):
|
||||||
|
return self.authorisation.amount == self.total
|
||||||
|
else:
|
||||||
|
return bool(self.purchase_order)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self):
|
||||||
|
if self.cancelled:
|
||||||
|
return "secondary"
|
||||||
|
elif not self.is_rig:
|
||||||
|
return "info"
|
||||||
|
elif not self.mic:
|
||||||
|
return "danger"
|
||||||
|
elif self.confirmed and self.authorised:
|
||||||
|
if self.dry_hire or self.riskassessment:
|
||||||
|
return "success"
|
||||||
|
return "warning"
|
||||||
|
else:
|
||||||
|
return "warning"
|
||||||
|
|
||||||
|
objects = EventManager()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def get_edit_url(self):
|
||||||
|
return reverse('event_update', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
errdict = super().clean()
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Call :meth:`full_clean` before saving."""
|
||||||
|
self.full_clean()
|
||||||
|
super(Event, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventItem(models.Model, RevisionMixin):
|
||||||
|
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True, default='')
|
||||||
|
quantity = models.IntegerField()
|
||||||
|
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
order = models.IntegerField()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return self.cost * self.quantity
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['order']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"item {self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventAuthorisation(models.Model, RevisionMixin):
|
||||||
|
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
||||||
|
email = models.EmailField()
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
||||||
|
account_code = models.CharField(max_length=50, default='', blank=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('event_detail', kwargs={'pk': self.event_id})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireManager(BaseEventManager):
|
||||||
|
def current_events(self):
|
||||||
|
events = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now().date())) # Ends after
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time').select_related('person', 'organisation')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def event_count(self):
|
||||||
|
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now()))
|
||||||
|
).count()
|
||||||
|
return event_count
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class Subhire(BaseEvent):
|
||||||
|
insurance_value = models.DecimalField(max_digits=10, decimal_places=2) # TODO Validate if this is over notifiable threshold
|
||||||
|
events = models.ManyToManyField(Event)
|
||||||
|
quote = models.URLField(default='', validators=[validate_url])
|
||||||
|
|
||||||
|
objects = SubhireManager()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rig(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dry_hire(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
return f"S{self.pk:05d}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self):
|
||||||
|
return "purple"
|
||||||
|
|
||||||
|
def get_edit_url(self):
|
||||||
|
return reverse('subhire_update', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('subhire_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def earliest_time(self):
|
||||||
|
return find_earliest_event_time(self, [])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = [
|
||||||
|
('subhire_finance', 'Can see financial data for subhire - insurance values')
|
||||||
|
]
|
||||||
170
RIGS/models/finance.py
Normal file
170
RIGS/models/finance.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
from .utils import filter_by_pk
|
||||||
|
|
||||||
|
|
||||||
|
class VatManager(models.Manager):
|
||||||
|
def current_rate(self):
|
||||||
|
return self.find_rate(timezone.now())
|
||||||
|
|
||||||
|
def find_rate(self, date):
|
||||||
|
try:
|
||||||
|
return self.filter(start_at__lte=date).latest()
|
||||||
|
except VatRate.DoesNotExist:
|
||||||
|
r = VatRate
|
||||||
|
r.rate = 0
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class VatRate(models.Model, RevisionMixin):
|
||||||
|
start_at = models.DateField()
|
||||||
|
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
||||||
|
comment = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
objects = VatManager()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_percent(self):
|
||||||
|
return self.rate * 100
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-start_at']
|
||||||
|
get_latest_by = 'start_at'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceManager(models.Manager):
|
||||||
|
def outstanding_invoices(self):
|
||||||
|
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||||
|
sql = "SELECT * FROM " \
|
||||||
|
"(SELECT " \
|
||||||
|
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||||
|
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||||
|
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||||
|
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||||
|
"AS sub " \
|
||||||
|
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||||
|
"ORDER BY invoice_date"
|
||||||
|
|
||||||
|
query = self.raw(sql)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(event__name__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
or_lookup = or_lookup | Q(event__pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
||||||
|
elif query[0] == "#":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
void = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
reversion_perm = 'RIGS.view_invoice'
|
||||||
|
|
||||||
|
objects = InvoiceManager()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sum_total(self):
|
||||||
|
return self.event.sum_total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return self.event.total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def payment_total(self):
|
||||||
|
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
|
||||||
|
if total:
|
||||||
|
return total
|
||||||
|
return Decimal("0.00")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self):
|
||||||
|
return self.sum_total - self.payment_total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
return self.balance == 0 or self.void
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"{self.display_id} for Event {self.event.display_id}"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
return f"#{self.pk:05d}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-invoice_date']
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class Payment(models.Model, RevisionMixin):
|
||||||
|
CASH = 'C'
|
||||||
|
INTERNAL = 'I'
|
||||||
|
EXTERNAL = 'E'
|
||||||
|
SUCORE = 'SU'
|
||||||
|
ADJUSTMENT = 'T'
|
||||||
|
METHODS = (
|
||||||
|
(CASH, 'Cash'),
|
||||||
|
(INTERNAL, 'Internal'),
|
||||||
|
(EXTERNAL, 'External'),
|
||||||
|
(SUCORE, 'SU Core'),
|
||||||
|
(ADJUSTMENT, 'TEC Adjustment'),
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
||||||
|
date = models.DateField()
|
||||||
|
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
||||||
|
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.get_method_display()}: {self.amount}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"payment of £{self.amount}"
|
||||||
243
RIGS/models/hs.py
Normal file
243
RIGS/models/hs.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
|
from RIGS.validators import validate_url
|
||||||
|
|
||||||
|
|
||||||
|
@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, default='', 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
|
||||||
|
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?")
|
||||||
|
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, default='', 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, default='', 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, default='', 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, default='', help_text="Who are the persons on site responsible for their use?")
|
||||||
|
rigging_plan = models.URLField(blank=True, default='', 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 Doesn't require checking with a super either way
|
||||||
|
'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')
|
||||||
|
]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def fieldz(self):
|
||||||
|
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||||
|
|
||||||
|
@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]
|
||||||
|
|
||||||
|
def get_event_size_display(self):
|
||||||
|
return self.SIZES[self.event_size][1] + " Event"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{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, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
||||||
|
extinguishers_location = models.CharField(blank=True, default='', 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.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, 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, default='', 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.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
w2_description = models.CharField(blank=True, default='', 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.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
w3_description = models.CharField(blank=True, default='', 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.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", 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')
|
||||||
|
]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def fieldz(self):
|
||||||
|
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{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 f"{self.vehicle} driven by {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 f"{self.crewmember} ({self.role})"
|
||||||
173
RIGS/models/models.py
Normal file
173
RIGS/models/models.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
from .events import Event
|
||||||
|
from .utils import filter_by_pk
|
||||||
|
|
||||||
|
|
||||||
|
class Profile(AbstractUser):
|
||||||
|
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||||
|
phone = models.CharField(max_length=13, blank=True, default='')
|
||||||
|
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||||
|
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
||||||
|
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||||
|
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||||
|
dark_theme = models.BooleanField(default=False)
|
||||||
|
is_supervisor = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_api_key(cls):
|
||||||
|
size = 20
|
||||||
|
chars = string.ascii_letters + string.digits
|
||||||
|
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
||||||
|
return new_api_key
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profile_picture(self):
|
||||||
|
url = ""
|
||||||
|
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
||||||
|
url = "https://www.gravatar.com/avatar/" + hashlib.md5(
|
||||||
|
self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
|
||||||
|
return url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
name = self.get_full_name()
|
||||||
|
if self.initials:
|
||||||
|
name += f' "{self.initials}"'
|
||||||
|
return name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def admins(cls):
|
||||||
|
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def users_awaiting_approval_count(cls):
|
||||||
|
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ContactableManager(models.Manager):
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
|
||||||
|
phone__startswith=query) | Q(phone__endswith=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class Person(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes is not None:
|
||||||
|
if len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def organisations(self):
|
||||||
|
o = []
|
||||||
|
for e in Event.objects.filter(person=self).select_related('organisation'):
|
||||||
|
if e.organisation:
|
||||||
|
o.append(e.organisation)
|
||||||
|
|
||||||
|
# Count up occurances and put them in descending order
|
||||||
|
c = Counter(o)
|
||||||
|
stats = c.most_common()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('person_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class Organisation(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
union_account = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes is not None:
|
||||||
|
if len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def persons(self):
|
||||||
|
p = []
|
||||||
|
for e in Event.objects.filter(organisation=self).select_related('person'):
|
||||||
|
if e.person:
|
||||||
|
p.append(e.person)
|
||||||
|
|
||||||
|
# Count up occurances and put them in descending order
|
||||||
|
c = Counter(p)
|
||||||
|
stats = c.most_common()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class Venue(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
three_phase_available = models.BooleanField(default=False)
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes and len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('venue_detail', kwargs={'pk': self.pk})
|
||||||
9
RIGS/models/utils.py
Normal file
9
RIGS/models/utils.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
def filter_by_pk(filt, query):
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
filt = filt | Q(pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
return filt
|
||||||
@@ -3,7 +3,6 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import datetime
|
|
||||||
|
|
||||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -111,7 +110,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|||||||
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
|
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
'link_suffix': reverse("admin:RIGS_profile_changelist") + f'?is_approved__exact=0&date_joined__date={timezone.now().date()}',
|
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
|
||||||
'number_of_users': models.Profile.users_awaiting_approval_count(),
|
'number_of_users': models.Profile.users_awaiting_approval_count(),
|
||||||
'to_name': admin.first_name
|
'to_name': admin.first_name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,22 @@
|
|||||||
{% if perms.RIGS.add_event %}
|
{% if perms.RIGS.add_event %}
|
||||||
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
|
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
|
||||||
New Event</a>
|
New Event</a>
|
||||||
|
<a class="dropdown-item" href="{% url 'subhire_create' %}"><span class="fas fa-truck"></span>
|
||||||
|
New Subhire</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.RIGS.view_riskassessment %}
|
{% if perms.RIGS.view_riskassessment %}
|
||||||
<li class="nav-item"><a class="nav-link" href="{% url 'hs_list' %}">H&S</a></li>
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownHS" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
H&S
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="navbarDropdownHS">
|
||||||
|
<a class="dropdown-item" href="{% url 'hs_list' %}"><span class="fas fa-eye"></span> Overview</a>
|
||||||
|
<a class="dropdown-item" href="{% url 'ra_list' %}"><span class="fas fa-file-medical"></span> Risk Assessments</a>
|
||||||
|
<a class="dropdown-item" href="{% url 'ec_list' %}"><span class="fas fa-tasks"></span> Event Checklists</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.RIGS.view_invoice %}
|
{% if perms.RIGS.view_invoice %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|||||||
@@ -1,198 +1,131 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Calendar{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'js/moment.js' %}"></script>
|
<script src="{% static 'js/moment.js' %}"></script>
|
||||||
<script src="{% static 'js/main.js' %}"></script>
|
<script>
|
||||||
<script>
|
$(document).ready(function() {
|
||||||
viewToUrl = {
|
|
||||||
'timeGridWeek':'week',
|
|
||||||
'timeGridDay':'day',
|
|
||||||
'dayGridMonth':'month'
|
|
||||||
}
|
|
||||||
viewFromUrl = {
|
|
||||||
'week':'timeGridWeek',
|
|
||||||
'day':'timeGridDay',
|
|
||||||
'month':'dayGridMonth'
|
|
||||||
}
|
|
||||||
var calendar; //Need to access it from jquery ready
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var calendarEl = document.getElementById('calendar');
|
|
||||||
|
|
||||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
|
||||||
firstDay: 1,
|
|
||||||
themeSystem: 'bootstrap',
|
|
||||||
aspectRatio: 1.5,
|
|
||||||
eventTimeFormat: {
|
|
||||||
'hour': '2-digit',
|
|
||||||
'minute': '2-digit',
|
|
||||||
'hour12': false
|
|
||||||
},
|
|
||||||
headerToolbar: false,
|
|
||||||
editable: false,
|
|
||||||
dayMaxEventRows: true, // allow "more" link when too many events
|
|
||||||
events: function(fetchInfo, successCallback, failureCallback) {
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/event',
|
|
||||||
dataType: 'json',
|
|
||||||
data: {
|
|
||||||
start: moment(fetchInfo.startStr).format("YYYY-MM-DD[T]HH:mm:ss"),
|
|
||||||
end: moment(fetchInfo.endStr).format("YYYY-MM-DD[T]HH:mm:ss")
|
|
||||||
},
|
|
||||||
success: function(doc) {
|
|
||||||
var events = [];
|
|
||||||
colours = {
|
|
||||||
'Provisional': '#FFE89B',
|
|
||||||
'Confirmed': '#3AB54A' ,
|
|
||||||
'Booked': '#3AB54A' ,
|
|
||||||
'Cancelled': 'grey' ,
|
|
||||||
'non-rig': '#25AAE2'
|
|
||||||
};
|
|
||||||
$(doc).each(function() {
|
|
||||||
end = $(this).attr('latest')
|
|
||||||
allDay = false
|
|
||||||
if(end.indexOf("T") < 0){ //If latest does not contain a time
|
|
||||||
end = moment(end + " 23:59").format("YYYY-MM-DD[T]HH:mm:ss")
|
|
||||||
allDay = true
|
|
||||||
}
|
|
||||||
|
|
||||||
thisEvent = {
|
|
||||||
'start': $(this).attr('earliest'),
|
|
||||||
'end': end,
|
|
||||||
'className': 'modal-href',
|
|
||||||
'title': $(this).attr('title'),
|
|
||||||
'url': $(this).attr('url'),
|
|
||||||
'allDay': allDay
|
|
||||||
}
|
|
||||||
|
|
||||||
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
|
||||||
thisEvent['color'] = colours[$(this).attr('status')];
|
|
||||||
}else{
|
|
||||||
thisEvent['color'] = colours['non-rig'];
|
|
||||||
}
|
|
||||||
events.push(thisEvent);
|
|
||||||
});
|
|
||||||
successCallback(events);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
datesSet: function(info) {
|
|
||||||
var view = info.view;
|
|
||||||
// Set the title of the view
|
|
||||||
$('#calendar-header').text(view.title);
|
|
||||||
|
|
||||||
// Enable/Disable "Today" button as required
|
|
||||||
let $today = $('#today-button');
|
|
||||||
if(moment().isBetween(view.currentStart, view.currentEnd)){
|
|
||||||
//Today is within the current view
|
|
||||||
$today.prop('disabled', true);
|
|
||||||
}else{
|
|
||||||
$today.prop('disabled', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set active view select button
|
|
||||||
let $month = $('#month-button');
|
|
||||||
let $week = $('#week-button');
|
|
||||||
let $day = $('#day-button');
|
|
||||||
switch(view.type){
|
|
||||||
case 'dayGridMonth':
|
|
||||||
$month.addClass('active');
|
|
||||||
$week.removeClass('active');
|
|
||||||
$day.removeClass('active');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'timeGridWeek':
|
|
||||||
$month.removeClass('active');
|
|
||||||
$week.addClass('active');
|
|
||||||
$day.removeClass('active');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'timeGridDay':
|
|
||||||
$month.removeClass('active');
|
|
||||||
$week.removeClass('active');
|
|
||||||
$day.addClass('active');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
$(document).ready(function() {
|
|
||||||
// set some button listeners
|
// set some button listeners
|
||||||
$('#next-button').click(function(){ calendar.next(); });
|
|
||||||
$('#prev-button').click(function(){ calendar.prev(); });
|
|
||||||
$('#today-button').click(function(){ calendar.today(); });
|
$('#today-button').click(function(){ calendar.today(); });
|
||||||
$('#month-button').click(function(){ calendar.changeView('dayGridMonth'); });
|
|
||||||
$('#week-button').click(function(){ calendar.changeView('timeGridWeek'); });
|
|
||||||
$('#day-button').click(function(){ calendar.changeView('timeGridDay'); });
|
|
||||||
$('#go-to-date-input').change(function(){
|
$('#go-to-date-input').change(function(){
|
||||||
if(moment($('#go-to-date-input').val()).isValid()){
|
if(moment($('#go-to-date-input').val()).isValid()){
|
||||||
$('#go-to-date-button').prop('disabled', false);
|
document.getElementById('go-to-date-button').classList.remove('disabled');
|
||||||
|
document.getElementById('go-to-date-button').href = "?month=" + moment($('#go-to-date-input').val()).format("YYYY-MM");
|
||||||
} else{
|
} else{
|
||||||
$('#go-to-date-button').prop('disabled', true);
|
document.getElementById('go-to-date-button').classList.add('disabled');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#go-to-date-button').click(function(){
|
|
||||||
day = moment($('#go-to-date-input').val());
|
|
||||||
if(day.isValid()){
|
|
||||||
calendar.gotoDate(day.format("YYYY-MM-DD"));
|
|
||||||
} else{
|
|
||||||
alert('Invalid Date');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
{% if view and date %}
|
|
||||||
// Go to the initial settings, if they're valid
|
|
||||||
view = viewFromUrl['{{view}}'];
|
|
||||||
calendar.changeView(view);
|
|
||||||
day = moment('{{date}}');
|
|
||||||
if(day.isValid()){
|
|
||||||
calendar.gotoDate(day.format("YYYY-MM-DD"));
|
|
||||||
} else{
|
|
||||||
console.log('Supplied date is invalid - using default')
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.week {
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
grid-gap: 2px 10px;
|
||||||
|
border: 1px solid black;
|
||||||
|
height: 8em;
|
||||||
|
align-content: start;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
display:contents;
|
||||||
|
}
|
||||||
|
.day-label {
|
||||||
|
grid-row-start: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin:0;
|
||||||
|
font-size: 1em !important;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day, .day-label, .event {
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
background-color: #CCC;
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-end {
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-start {
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.event {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-span="1"] { grid-column-end: span 1; }
|
||||||
|
[data-span="2"] { grid-column-end: span 2; }
|
||||||
|
[data-span="3"] { grid-column-end: span 3; }
|
||||||
|
[data-span="4"] { grid-column-end: span 4; }
|
||||||
|
[data-span="5"] { grid-column-end: span 5; }
|
||||||
|
[data-span="6"] { grid-column-end: span 6; }
|
||||||
|
[data-span="7"] { grid-column-end: span 7; }
|
||||||
|
|
||||||
|
.day > a {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row justify-content-center mb-1">
|
||||||
<div class="col-sm-12">
|
<a class="btn btn-info col-2" href="{% url 'web_calendar' %}?{{ prev_month }}"><span class="fas fa-chevron-left"></span> Previous Month</a>
|
||||||
<div class="pull-left">
|
<div class="form-inline col-4">
|
||||||
<span id="calendar-header" class="h2"></span>
|
<div class="input-group">
|
||||||
</div>
|
<input type="date" id="go-to-date-input" placeholder="Go to date..." class="form-control">
|
||||||
<div class="form-inline float-right btn-page my-3">
|
<span class="input-group-append">
|
||||||
<div class="input-group mx-2">
|
<a class="btn btn-success" id="go-to-date-button">Go!</a>
|
||||||
<input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
|
</span>
|
||||||
<span class="input-group-append">
|
|
||||||
<button class="btn btn-success" id="go-to-date-button" type="button" disabled>Go!</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group mx-2">
|
|
||||||
<button type="button" class="btn btn-primary" id="today-button">Today</button>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group mx-2">
|
|
||||||
<button type="button" class="btn btn-secondary" id="prev-button"><span class="fas fa-chevron-left"></span></button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="next-button"><span class="fas fa-chevron-right"></span></button>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group ml-2">
|
|
||||||
<button type="button" class="btn btn-light" id="month-button">Month</button>
|
|
||||||
<button type="button" class="btn btn-light" id="week-button">Week</button>
|
|
||||||
<button type="button" class="btn btn-light" id="day-button">Day</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<div id='calendar'></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary col-2" id="today-button">Today</button>
|
||||||
|
<a class="btn btn-info mx-2 col-2" href="{% url 'web_calendar' %}?{{ next_month }}"><span class="fas fa-chevron-right"></span> Next Month</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="week" style="height: 2em;">
|
||||||
|
<div class="week-day">Monday</div>
|
||||||
|
<div class="week-day">Tuesday</div>
|
||||||
|
<div class="week-day">Wednesday</div>
|
||||||
|
<div class="week-day">Thursday</div>
|
||||||
|
<div class="week-day">Friday</div>
|
||||||
|
<div class="week-day">Saturday</div>
|
||||||
|
<div class="week-day">Sunday</div>
|
||||||
|
</div>
|
||||||
|
{% for week in weeks %}
|
||||||
|
<div class="week">
|
||||||
|
{% for day in week %}
|
||||||
|
{% if day.0 != 0 %}
|
||||||
|
<div class="day" id="{{day.0}}">
|
||||||
|
<h3 class="day-label text-muted">{{day.0}}</h3>
|
||||||
|
{{ day.2|safe }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="day"><span style="grid-row-start: 1;"> <span></div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
29
RIGS/templates/dashboards/productions.html
Normal file
29
RIGS/templates/dashboards/productions.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends 'base_rigs.html' %}
|
||||||
|
|
||||||
|
{% load button from filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Upcoming Events</div>
|
||||||
|
<div class="card-body">{{ rig_count }}</div>
|
||||||
|
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Upcoming Subhire</div>
|
||||||
|
<div class="card-body">{{ subhire_count }}</div>
|
||||||
|
<div class="card-footer"><a href={% url 'subhire_list' %}>View</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Active Dry Hires</div>
|
||||||
|
<div class="card-body">{{ hire_count }}</div>
|
||||||
|
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -15,38 +15,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% include 'partials/archive_form.html' %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 py-2">
|
<div class="col-sm-12">
|
||||||
<form class="form-inline" method="GET">
|
|
||||||
<div class="input-group mx-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text">Start</span>
|
|
||||||
</div>
|
|
||||||
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<div class="input-group mx-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text">End</span>
|
|
||||||
</div>
|
|
||||||
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<div class="input-group mx-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<span class="input-group-text">Keyword</span>
|
|
||||||
</div>
|
|
||||||
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
|
|
||||||
{% for status in statuses %}
|
|
||||||
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% button 'search' %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12" style="container-type: inline-size;">
|
|
||||||
{% with object_list as events %}
|
{% with object_list as events %}
|
||||||
{% include 'partials/event_table.html' %}
|
{% include 'partials/event_table.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
|
|
||||||
{% load markdown_tags %}
|
{% load markdown_tags %}
|
||||||
{% load button from filters %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3 py-3">
|
<div class="row my-3 py-3">
|
||||||
@@ -27,12 +25,10 @@
|
|||||||
{% include 'partials/hs_details.html' %}
|
{% include 'partials/hs_details.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.is_rig %}
|
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
||||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
<div class="col-md-8 py-3">
|
||||||
<div class="col-md-8 py-3">
|
{% include 'partials/auth_details.html' %}
|
||||||
{% include 'partials/auth_details.html' %}
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
@@ -49,48 +45,17 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<h4>Event Items</h4>
|
||||||
{% include 'partials/item_table.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'partials/item_table.html' %}
|
||||||
|
{% if event.subhire_set.count > 0 %}
|
||||||
|
<div class="card-body"><h4>Associated Subhires</h4></div>
|
||||||
|
{% with event.subhire_set.all as events %}
|
||||||
|
{% include 'partials/event_table.html' %}
|
||||||
|
{%endwith%}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if event.can_check_in %}
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header">Crew Record</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Name</th>
|
|
||||||
<th scope="col">Vehicle</th>
|
|
||||||
<th scope="col">Start Time</th>
|
|
||||||
<th scope="col">Role</th>
|
|
||||||
<th scope="col">End Time</th>
|
|
||||||
<th scope="col">{% if request.user.pk is event.mic.pk %}<a href="{% url 'event_checkin_override' event.pk %}" class="btn btn-sm btn-success"><span class="fas fa-plus"></span> Add</a>{% endif %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="crewmembers">
|
|
||||||
{% for crew in object.crew.all %}
|
|
||||||
<tr>
|
|
||||||
<td>{{crew.person}}</td>
|
|
||||||
<td>{{crew.vehicle|default:"None"}}</td>
|
|
||||||
<td>{{crew.time}}</td>
|
|
||||||
<td>{{crew.role}}</td>
|
|
||||||
<td>{% if crew.end_time %}{{crew.end_time}}{% else %}<span class="text-success fas fa-clock" data-toggle="tooltip" title="This person is currently checked into this event"></span>{% endif %}</td>
|
|
||||||
<td>{% if crew.end_time %}{% if crew.person.pk == request.user.pk or event.mic.pk == request.user.pk %}{% button 'edit' 'edit_checkin' crew.pk clazz='btn-sm modal-href' %}{% endif %}{%endif%}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center bg-warning">Apparently this event happened by magic...</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% include 'partials/event_detail_buttons.html' %}
|
{% include 'partials/event_detail_buttons.html' %}
|
||||||
|
|||||||
@@ -106,6 +106,10 @@
|
|||||||
title="Things that aren't service-based, like training, meetings and site visits.">
|
title="Things that aren't service-based, like training, meetings and site visits.">
|
||||||
<button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button>
|
<button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button>
|
||||||
</span>
|
</span>
|
||||||
|
<span data-toggle="tooltip"
|
||||||
|
title="Record equipment hired in from other companies">
|
||||||
|
<a href="{% url 'subhire_create' %}" class="btn bg-warning w-25">Subhire</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,7 +213,7 @@
|
|||||||
<div class="col-sm-9 col-md-7 col-lg-8">
|
<div class="col-sm-9 col-md-7 col-lg-8">
|
||||||
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
||||||
{% if venue %}
|
{% if venue %}
|
||||||
<option value="{{venue.id}}" selected="selected" data-update_url="{% url 'venue_update' venue.id %}">{{ venue }}</option>
|
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,7 +235,7 @@
|
|||||||
<label for="{{ form.start_date.id_for_label }}"
|
<label for="{{ form.start_date.id_for_label }}"
|
||||||
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-8">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
|
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
|
||||||
{% render_field form.start_date class+="form-control" %}
|
{% render_field form.start_date class+="form-control" %}
|
||||||
@@ -246,7 +250,7 @@
|
|||||||
<label for="{{ form.end_date.id_for_label }}"
|
<label for="{{ form.end_date.id_for_label }}"
|
||||||
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-8">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
|
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
|
||||||
{% render_field form.end_date class+="form-control" %}
|
{% render_field form.end_date class+="form-control" %}
|
||||||
@@ -334,26 +338,12 @@
|
|||||||
|
|
||||||
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
||||||
<label for="{{ form.purchase_order.id_for_label }}"
|
<label for="{{ form.purchase_order.id_for_label }}"
|
||||||
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
|
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
{% render_field form.purchase_order class+="form-control" %}
|
{% render_field form.purchase_order class+="form-control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" data-toggle="tooltip" title="The thread for this event on the TEC Forum">
|
|
||||||
<label for="{{ form.forum_url.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">Forum Thread</label>
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<p class="small mb-0">Paste URL</p>
|
|
||||||
{% render_field form.forum_url class+="form-control" %}
|
|
||||||
{% if object.pk %}
|
|
||||||
<p class="small mb-0">or</p>
|
|
||||||
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank">
|
|
||||||
<span class="fas fa-plus"></span> <span class="hidden-xs">Create Forum Thread</span></a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
<div class="col-12 text-right my-3">
|
<div class="col-12 text-right my-3">
|
||||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||||
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
||||||
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
|
||||||
class="hidden-xs">Create Power Test</span></a>
|
|
||||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +30,21 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
|
||||||
|
<dd class="col-6">
|
||||||
|
{% if object.power_mic %}
|
||||||
|
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
<p>List vehicles and their drivers</p>
|
||||||
|
<ul>
|
||||||
|
{% for i in object.vehicles.all %}
|
||||||
|
<li>{{i}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,11 +82,168 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">Crew Record</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Crewmember</th>
|
||||||
|
<th scope="col">Start Time</th>
|
||||||
|
<th scope="col">Role</th>
|
||||||
|
<th scope="col">End Time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="crewmemberst">
|
||||||
|
{% for crew in object.crew.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{crew.crewmember}}</td>
|
||||||
|
<td>{{crew.start}}</td>
|
||||||
|
<td>{{crew.role}}</td>
|
||||||
|
<td>{{crew.end}}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if object.event.riskassessment.event_size == 0 %}
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.rcds|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.supply_test|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.earthing|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.pat|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{% else %}
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.source_rcd|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'labelling'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.labelling|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.earthing|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.pat|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<hr>
|
||||||
|
<p>Tests at first distro</p>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="text-center">Test</th>
|
||||||
|
<th scope="col" colspan="3" class="text-center">Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
|
||||||
|
<th>{{ object|help_text:'fd_voltage_l1' }}</th>
|
||||||
|
<th>{{ object|help_text:'fd_voltage_l2' }}</th>
|
||||||
|
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ object.fd_voltage_l1 }}</td>
|
||||||
|
<td>{{ object.fd_voltage_l2 }}</td>
|
||||||
|
<td>{{ object.fd_voltage_l3 }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ object|help_text:'fd_phase_rotation'|safe }}</th>
|
||||||
|
<td colspan="3">{{ object.fd_phase_rotation|yesnoi }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ object|help_text:'fd_earth_fault'|safe}}</th>
|
||||||
|
<td colspan="3">{{ object.fd_earth_fault }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ object|help_text:'fd_pssc'}}</th>
|
||||||
|
<td colspan="3">{{ object.fd_pssc }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr>
|
||||||
|
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="text-center">Test</th>
|
||||||
|
<th scope="col" class="text-center">Point 1</th>
|
||||||
|
<th scope="col" class="text-center">Point 2</th>
|
||||||
|
<th scope="col" class="text-center">Point 3</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ object|help_text:'w1_description'|safe}}</th>
|
||||||
|
<td>{{ object.w1_description }}</td>
|
||||||
|
<td>{{ object.w2_description|default:'' }}</td>
|
||||||
|
<td>{{ object.w3_description|default:'' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ object|help_text:'w1_polarity'|safe}}</th>
|
||||||
|
<td>{{ object.w1_polarity|yesnoi }}</td>
|
||||||
|
<td>{{ object.w2_polarity|default:''|yesnoi }}</td>
|
||||||
|
<td>{{ object.w3_polarity|default:''|yesnoi }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ object|help_text:'w1_voltage'|safe}}</th>
|
||||||
|
<td>{{ object.w1_voltage }}</td>
|
||||||
|
<td>{{ object.w2_voltage|default:'' }}</td>
|
||||||
|
<td>{{ object.w3_voltage|default:'' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ object|help_text:'w1_earth_fault'|safe}}</th>
|
||||||
|
<td>{{ object.w1_earth_fault }}</td>
|
||||||
|
<td>{{ object.w2_earth_fault|default:'' }}</td>
|
||||||
|
<td>{{ object.w3_earth_fault|default:'' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr>
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-10">{{ object|help_text:'all_rcds_tested'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.all_rcds_tested|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-10">{{ object|help_text:'public_sockets_tested'|safe }}</dt>
|
||||||
|
<dd class="col-2">
|
||||||
|
{{ object.public_sockets_tested|yesnoi }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<hr>
|
||||||
|
{% include 'partials/ec_power_info.html' %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-12 text-right">
|
<div class="col-12 text-right">
|
||||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||||
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
|
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
|
||||||
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
|
||||||
class="hidden-xs">Create Power Test</span></a>
|
|
||||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 text-right">
|
<div class="col-12 text-right">
|
||||||
|
|||||||
@@ -13,19 +13,57 @@
|
|||||||
{% block preload_js %}
|
{% block preload_js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
<script src="{% static 'js/selects.js' %}"></script>
|
||||||
<script src="{% static 'js/interaction.js' %}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('button[data-action=add]').on('click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
let target = $($(this).attr('data-target'));
|
||||||
|
let newID = Number(target.attr('data-pk'));
|
||||||
|
let newRow = $($(this).attr('data-clone'))
|
||||||
|
.clone().attr('style', "")
|
||||||
|
.attr('id', function(i, val){
|
||||||
|
return val.split("_")[0] + '_' + newID;
|
||||||
|
})
|
||||||
|
.appendTo(target);
|
||||||
|
newRow.find('select,input').attr('name', function(i, val){
|
||||||
|
return val.split("_")[0] + '_' + newID;
|
||||||
|
})//Disabled is to prevent the hidden row being sent to the form
|
||||||
|
.removeAttr('disabled');
|
||||||
|
newRow.find('button[data-action=delete]').attr('data-id', newID);
|
||||||
|
newRow.find('select').addClass('selectpicker');
|
||||||
|
newRow.find('.selectpicker').selectpicker('refresh');
|
||||||
|
$(".selectpicker").each(function(){initPicker($(this))});
|
||||||
|
initDatetime();
|
||||||
|
$(target).attr('data-pk', newID - 1);
|
||||||
|
});
|
||||||
|
$(document).on('click', 'button[data-action=delete]', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
$(this).closest('tr').remove();
|
||||||
|
});
|
||||||
|
//Somewhat rudimentary way of ensuring people fill in completely (if it hits the database validation the whole table row disappears when the page reloads...)
|
||||||
|
//the not is to avoid adding it to some of bootstrap-selects extra crap
|
||||||
|
$('#vehiclest,#crewmemberst').on('change', 'select,input', function () {
|
||||||
|
$(this).closest('tr').find("select,input").not(':input[type=search]').attr('required', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
{% include 'form_errors.html' %}
|
{% include 'form_errors.html' %}
|
||||||
|
{% if edit %}
|
||||||
<form role="form" method="POST" action="{% if edit %}{% url 'ec_edit' pk=object.pk %}{% else %}{% url 'event_ec' pk=event.pk %}{% endif %}">
|
<form role="form" method="POST" action="{% url 'ec_edit' pk=object.pk %}">
|
||||||
|
{% else %}
|
||||||
|
<form role="form" method="POST" action="{% url 'event_ec' pk=event.pk %}">
|
||||||
|
{% endif %}
|
||||||
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
||||||
value="{{event.pk}}"/>
|
value="{{event.pk}}"/>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -56,12 +94,60 @@
|
|||||||
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
|
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
|
||||||
<label for="{{ form.venue.id_for_label }}"
|
<label for="{{ form.venue.id_for_label }}"
|
||||||
class="col-4 col-form-label">{{ form.venue.label }}</label>
|
class="col-4 col-form-label">{{ form.venue.label }}</label>
|
||||||
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
||||||
{% if venue %}
|
{% if venue %}
|
||||||
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
|
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
|
||||||
|
{% elif event.venue %}
|
||||||
|
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
|
||||||
|
<label for="{{ form.power_mic.id_for_label }}"
|
||||||
|
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
|
||||||
|
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
|
||||||
|
{% if power_mic %}
|
||||||
|
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
|
||||||
|
{% elif event.riskassessment.power_mic %}
|
||||||
|
<option value="{{event.riskassessment.power_mic.pk}}" selected="selected">{{ event.riskassessment.power_mic.name }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="pt-3 font-weight-bold">List vehicles and their drivers</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Vehicle</th>
|
||||||
|
<th scope="col">Driver</th>
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="vehiclest" data-pk="-1">
|
||||||
|
<tr id="vehicles_new" style="display: none;">
|
||||||
|
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td>
|
||||||
|
<td><select data-container="body" class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" name="driver_new" disabled="true"></select></td>
|
||||||
|
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
|
||||||
|
</tr>
|
||||||
|
{% for i in object.vehicles.all %}
|
||||||
|
<tr id="vehicles_{{i.pk}}">
|
||||||
|
<td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td>
|
||||||
|
<td>
|
||||||
|
<select data-container="body" name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||||
|
{% if i.driver != '' %}
|
||||||
|
<option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{i.pk}}' data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<button type="button" class="btn btn-secondary" id="vehicle-add" data-action='add' data-target='#vehiclest' data-clone='#vehicles_new'><span class="fas fa-plus"></span> Add Vehicle</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,6 +175,176 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row my-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Crew Record</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Person</th>
|
||||||
|
<th scope="col">Start Time</th>
|
||||||
|
<th scope="col">Role</th>
|
||||||
|
<th scope="col">End Time</th>
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="crewmemberst" data-pk="-1">
|
||||||
|
<tr id="crew_new" style="display: none;">
|
||||||
|
<td>
|
||||||
|
<select name="crewmember_new" class="form-control" data-container="body" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select>
|
||||||
|
</td>
|
||||||
|
<td style="min-width: 15ch"><input name="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled=""/></td>
|
||||||
|
<td style="min-width: 15ch"><input name="role_new" type="text" class="form-control" value="{{ i.role }}" disabled="true"/></td>
|
||||||
|
<td style="min-width: 15ch"><input name="end_new" type="datetime-local" class="form-control" value="{{ i.end }}" disabled="true" /></td>
|
||||||
|
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
|
||||||
|
</tr>
|
||||||
|
{% for crew in object.crew.all %}
|
||||||
|
<tr id="crew_{{crew.pk}}">
|
||||||
|
<td>
|
||||||
|
<select data-container="body" name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||||
|
{% if crew.crewmember != '' %}
|
||||||
|
<option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td><input name="start_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.start|date:'Y-m-d' }}T{{ crew.start|date:'H:i:s' }}"/></td>
|
||||||
|
<td><input name="role_{{crew.pk}}" type="text" class="form-control" value="{{ crew.role }}"/></td>
|
||||||
|
<td><input name="end_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.end|date:'Y-m-d' }}T{{ crew.end|date:'H:i:s' }}"/></td>
|
||||||
|
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="text-right">
|
||||||
|
<button type="button" class="btn btn-secondary" data-action='add' data-target='#crewmemberst' data-clone='#crew_new'><span class="fas fa-plus"></span> Add Crewmember</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if event.riskassessment.event_size == 0 %}
|
||||||
|
<div class="row my-3" id="size-0">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-header">Electrical Checks <small>for ‘Small’ TEC Events <6kVA (approx. 26A)</small></div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row my-3" id="size-1">
|
||||||
|
<div class="col-12">
|
||||||
|
{% if event.riskassessment.event_size == 1 %}
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-header">Electrical Checks <small>for ‘Medium’ TEC Events </small></div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% else %}
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
|
||||||
|
<hr>
|
||||||
|
<p>Tests at first distro</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="text-center">Test</th>
|
||||||
|
<th scope="col" colspan="3" class="text-center">Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
|
||||||
|
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
|
||||||
|
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
|
||||||
|
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% render_field form.fd_voltage_l1 class+="form-control" style="min-width: 5rem;" %}</td>
|
||||||
|
<td>{% render_field form.fd_voltage_l2 class+="form-control" style="min-width: 5rem;" %}</td>
|
||||||
|
<td>{% render_field form.fd_voltage_l3 class+="form-control" style="min-width: 5rem;" %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{form.fd_phase_rotation.help_text|safe}}</th>
|
||||||
|
<td colspan="3">{% include 'partials/checklist_checkbox.html' with formitem=form.fd_phase_rotation %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{form.fd_earth_fault.help_text|safe}}</th>
|
||||||
|
<td colspan="3">{% render_field form.fd_earth_fault class+="form-control" %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{form.fd_pssc.help_text|safe}}</th>
|
||||||
|
<td colspan="3">{% render_field form.fd_pssc class+="form-control" %}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="text-center">Test</th>
|
||||||
|
<th scope="col" class="text-center">Point 1</th>
|
||||||
|
<th scope="col" class="text-center">Point 2</th>
|
||||||
|
<th scope="col" class="text-center">Point 3</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{form.w1_description.help_text|safe}}</th>
|
||||||
|
<td>{% render_field form.w1_description class+="form-control" style="min-width: 5rem;" %}</td>
|
||||||
|
<td>{% render_field form.w2_description class+="form-control" style="min-width: 5rem;" %}</td>
|
||||||
|
<td>{% render_field form.w3_description class+="form-control" style="min-width: 5rem;" %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{form.w1_polarity.help_text|safe}}</th>
|
||||||
|
<td>{% render_field form.w1_polarity %}</td>
|
||||||
|
<td>{% render_field form.w2_polarity %}</td>
|
||||||
|
<td>{% render_field form.w3_polarity %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{form.w1_voltage.help_text|safe}}</th>
|
||||||
|
<td>{% render_field form.w1_voltage class+="form-control" %}</td>
|
||||||
|
<td>{% render_field form.w2_voltage class+="form-control" %}</td>
|
||||||
|
<td>{% render_field form.w3_voltage class+="form-control" %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{form.w1_earth_fault.help_text|safe}}</th>
|
||||||
|
<td>{% render_field form.w1_earth_fault class+="form-control" %}</td>
|
||||||
|
<td>{% render_field form.w2_earth_fault class+="form-control" %}</td>
|
||||||
|
<td>{% render_field form.w3_earth_fault class+="form-control" %}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
|
||||||
|
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
|
||||||
|
{% include 'partials/ec_power_info.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-sm-12 text-right">
|
<div class="col-sm-12 text-right">
|
||||||
{% button 'submit' %}
|
{% button 'submit' %}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
|
||||||
{% load widget_tweaks %}
|
|
||||||
{% load static %}
|
|
||||||
{% load button from filters %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ block.super }}
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block preload_js %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
|
||||||
<script src="{% static 'js/easymde.min.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
|
||||||
<script src="{% static 'js/interaction.js' %}"></script>
|
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="col-12">
|
|
||||||
{% include 'form_errors.html' %}
|
|
||||||
<form id="checkin" role="form" method="POST" action="{{ form.action|default:request.path }}">
|
|
||||||
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
|
||||||
value="{{event.pk}}"/>
|
|
||||||
{% if not request.is_ajax and self.request.user.pk is form.event.mic.pk %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.person.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">{{ form.person.label }}</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
|
||||||
{% if person %}
|
|
||||||
<option value="{{form.person.value}}" selected="selected" >{{ person.name }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<input type="hidden" name="{{ form.person.name }}" id="{{ form.person.id_for_label }}"
|
|
||||||
value="{{request.user.pk}}"/>
|
|
||||||
{% endif %}
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.time.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">Start Time</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
{% render_field form.time class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.role.id_for_label }}" class="col col-form-label">Role</label>
|
|
||||||
<div class="row pl-3">
|
|
||||||
<div class="col-md-6 col-sm-12">
|
|
||||||
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_role').value='MIC'">MIC</button>
|
|
||||||
<button type="button" class="btn btn-danger" onclick="document.getElementById('id_role').value='Power MIC'">Power MIC</button>
|
|
||||||
<button type="button" class="btn btn-info" onclick="document.getElementById('id_role').value='Crew'">Crew</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mt-2">
|
|
||||||
{% render_field form.role class+="form-control" placeholder="Other (enter text)" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.vehicle.id_for_label }}" class="col col-form-label">Vehicle (if applicable)</label>
|
|
||||||
<div class="row pl-3">
|
|
||||||
<div class="col-md-6 col-sm-12">
|
|
||||||
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_vehicle').value='Virgil'"><span class="fas fa-truck-moving"></span> Virgil</button>
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('id_vehicle').value='Virgil + Erms'"><span class="fas fa-trailer"></span><span class="fas fa-truck-moving"></span> Virgil + Erms</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mt-2">
|
|
||||||
{% render_field form.vehicle class+="form-control" placeholder="Other (enter text)" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if edit or manual %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.end_time.id_for_label }}"
|
|
||||||
class="col-sm-4 col-form-label">End Time</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
{% render_field form.end_time class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if not request.is_ajax %}
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-sm-12 text-right">
|
|
||||||
{% button 'submit' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block footer %}
|
|
||||||
<div class="col-sm-12 text-right pr-0">
|
|
||||||
<button type="submit" class="btn btn-primary" title="Save" form="checkin"
|
|
||||||
><span class="fas fa-save align-middle"></span> <span class="d-none d-sm-inline align-middle">Save</span></button>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
<th scope="col">Dates</th>
|
<th scope="col">Dates</th>
|
||||||
<th scope="col">RA</th>
|
<th scope="col">RA</th>
|
||||||
<th scope="col">Checklists</th>
|
<th scope="col">Checklists</th>
|
||||||
<th scope="col">Power Records</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -36,14 +35,6 @@
|
|||||||
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
||||||
class="d-none d-sm-inline">Create</span></a>
|
class="d-none d-sm-inline">Create</span></a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
{% for record in event.power_tests.all %}
|
|
||||||
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
|
|
||||||
<br/>
|
|
||||||
{% endfor %}
|
|
||||||
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
|
|
||||||
class="hidden-xs">Create</span></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr class="bg-warning text-dark">
|
<tr class="bg-warning text-dark">
|
||||||
|
|||||||
59
RIGS/templates/hs/hs_object_list.html
Normal file
59
RIGS/templates/hs/hs_object_list.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{% extends 'base_rigs.html' %}
|
||||||
|
{% load paginator from filters %}
|
||||||
|
{% load help_text from filters %}
|
||||||
|
{% load verbose_name from filters %}
|
||||||
|
{% load get_field from filters %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} List{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>{{title}} List</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mb-0 table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Event</th>
|
||||||
|
{# mmm hax #}
|
||||||
|
{% if object_list.0 != None %}
|
||||||
|
{% for field in object_list.0.fieldz %}
|
||||||
|
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for object in object_list %}
|
||||||
|
<tr class="{% if object.reviewed_by %}table-success{%endif%}">
|
||||||
|
{# General #}
|
||||||
|
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
|
||||||
|
{% for field in object_list.0.fieldz %}
|
||||||
|
<td>{{ object|get_field:field }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
{# Buttons #}
|
||||||
|
<td>
|
||||||
|
{% include 'partials/hs_status.html' %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr class="bg-warning">
|
||||||
|
<td colspan="6">Nothing found</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if is_paginated %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
{% paginator %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
|
||||||
{% load help_text from filters %}
|
|
||||||
{% load profile_by_index from filters %}
|
|
||||||
{% load yesnoi from filters %}
|
|
||||||
{% load button from filters %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 text-right my-3">
|
|
||||||
{% button 'edit' url='pt_edit' pk=object.pk %}
|
|
||||||
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
|
||||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">{% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
|
|
||||||
<dd class="col-6">
|
|
||||||
{% if object.power_mic %}
|
|
||||||
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
|
|
||||||
{% else %}
|
|
||||||
None
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-6">Venue</dt>
|
|
||||||
<dd class="col-6">
|
|
||||||
{% if object.venue %}
|
|
||||||
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
|
|
||||||
{{ object.venue }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-6">Notes</dt>
|
|
||||||
<dd class="col-6">
|
|
||||||
{{ object.notes }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
{% if object.event.riskassessment.event_size == 0 %}
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.rcds|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.supply_test|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.earthing|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.pat|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
{% else %}
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.source_rcd|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'labelling'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.labelling|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.earthing|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.pat|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
<hr>
|
|
||||||
<p>Tests at first distro</p>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="text-center">Test</th>
|
|
||||||
<th scope="col" colspan="3" class="text-center">Value</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th>
|
|
||||||
<th>{{ object|help_text:'fd_voltage_l1' }}</th>
|
|
||||||
<th>{{ object|help_text:'fd_voltage_l2' }}</th>
|
|
||||||
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{{ object.fd_voltage_l1 }}</td>
|
|
||||||
<td>{{ object.fd_voltage_l2 }}</td>
|
|
||||||
<td>{{ object.fd_voltage_l3 }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ object|help_text:'fd_phase_rotation'|safe }}</th>
|
|
||||||
<td colspan="3">{{ object.fd_phase_rotation|yesnoi }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ object|help_text:'fd_earth_fault'|safe}}</th>
|
|
||||||
<td colspan="3">{{ object.fd_earth_fault }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ object|help_text:'fd_pssc'}}</th>
|
|
||||||
<td colspan="3">{{ object.fd_pssc }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<hr>
|
|
||||||
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="text-center">Test</th>
|
|
||||||
<th scope="col" class="text-center">Point 1</th>
|
|
||||||
<th scope="col" class="text-center">Point 2</th>
|
|
||||||
<th scope="col" class="text-center">Point 3</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ object|help_text:'w1_description'|safe}}</th>
|
|
||||||
<td>{{ object.w1_description }}</td>
|
|
||||||
<td>{{ object.w2_description|default:'' }}</td>
|
|
||||||
<td>{{ object.w3_description|default:'' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ object|help_text:'w1_polarity'|safe}}</th>
|
|
||||||
<td>{{ object.w1_polarity|yesnoi }}</td>
|
|
||||||
<td>{{ object.w2_polarity|default:''|yesnoi }}</td>
|
|
||||||
<td>{{ object.w3_polarity|default:''|yesnoi }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ object|help_text:'w1_voltage'|safe}}</th>
|
|
||||||
<td>{{ object.w1_voltage }}</td>
|
|
||||||
<td>{{ object.w2_voltage|default:'' }}</td>
|
|
||||||
<td>{{ object.w3_voltage|default:'' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{ object|help_text:'w1_earth_fault'|safe}}</th>
|
|
||||||
<td>{{ object.w1_earth_fault }}</td>
|
|
||||||
<td>{{ object.w2_earth_fault|default:'' }}</td>
|
|
||||||
<td>{{ object.w3_earth_fault|default:'' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<hr>
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-10">{{ object|help_text:'all_rcds_tested'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.all_rcds_tested|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
<dt class="col-10">{{ object|help_text:'public_sockets_tested'|safe }}</dt>
|
|
||||||
<dd class="col-2">
|
|
||||||
{{ object.public_sockets_tested|yesnoi }}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
<hr>
|
|
||||||
{% include 'partials/ec_power_info.html' %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 text-right">
|
|
||||||
{% button 'edit' url='pt_edit' pk=object.pk %}
|
|
||||||
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
|
|
||||||
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-12 text-right">
|
|
||||||
{% include 'partials/last_edited.html' with target="powertestrecord_history" %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
|
|
||||||
{% load widget_tweaks %}
|
|
||||||
{% load static %}
|
|
||||||
{% load help_text from filters %}
|
|
||||||
{% load profile_by_index from filters %}
|
|
||||||
{% load button from filters %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ block.super }}
|
|
||||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block preload_js %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script src="{% static 'js/autocompleter.js' %}"></script>
|
|
||||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="col-12">
|
|
||||||
{% include 'form_errors.html' %}
|
|
||||||
{% if edit %}
|
|
||||||
<form role="form" method="POST" action="{% url 'pt_edit' pk=object.pk %}">
|
|
||||||
{% else %}
|
|
||||||
<form role="form" method="POST" action="{% url 'event_pt' pk=event.pk %}">
|
|
||||||
{% endif %}
|
|
||||||
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
|
||||||
value="{{event.pk}}"/>
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Event Information</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-4">Event Date</dt>
|
|
||||||
<dd class="col-8">{{ event.start_date}}{%if event.end_date %}-{{ event.end_date}}{%endif%}</dd>
|
|
||||||
<dt class="col-4">Event Name</dt>
|
|
||||||
<dd class="col-8">{{ event.name }}</dd>
|
|
||||||
<dt class="col-4">Client</dt>
|
|
||||||
<dd class="col-8">{{ event.person }}</dd>
|
|
||||||
<dt class="col-4">Event Size</dt>
|
|
||||||
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
|
|
||||||
</dl>
|
|
||||||
<hr>
|
|
||||||
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
|
|
||||||
<label for="{{ form.power_mic.id_for_label }}"
|
|
||||||
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
|
|
||||||
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
|
|
||||||
{% if power_mic %}
|
|
||||||
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
|
|
||||||
<label for="{{ form.venue.id_for_label }}"
|
|
||||||
class="col-4 col-form-label">{{ form.venue.label }}</label>
|
|
||||||
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
|
|
||||||
{% if venue %}
|
|
||||||
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<label for="{{ form.notes.id_for_label }}">Notes</label>
|
|
||||||
{% render_field form.notes class+="form-control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if event.riskassessment.event_size == 0 %}
|
|
||||||
<div class="row my-3" id="size-0">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-success">
|
|
||||||
<div class="card-header">Electrical Checks <small>for ‘Small’ TEC Events <6kVA (approx. 26A)</small></div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="row my-3" id="size-1">
|
|
||||||
<div class="col-12">
|
|
||||||
{% if event.riskassessment.event_size == 1 %}
|
|
||||||
<div class="card border-warning">
|
|
||||||
<div class="card-header">Electrical Checks <small>for ‘Medium’ TEC Events </small></div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% else %}
|
|
||||||
<div class="card border-danger">
|
|
||||||
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
|
|
||||||
{% endif %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
|
|
||||||
<hr>
|
|
||||||
<p>Tests at first distro</p>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="text-center">Test</th>
|
|
||||||
<th scope="col" colspan="3" class="text-center">Value</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th>
|
|
||||||
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
|
|
||||||
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
|
|
||||||
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{% render_field form.fd_voltage_l1 class+="form-control" style="min-width: 5rem;" %}</td>
|
|
||||||
<td>{% render_field form.fd_voltage_l2 class+="form-control" style="min-width: 5rem;" %}</td>
|
|
||||||
<td>{% render_field form.fd_voltage_l3 class+="form-control" style="min-width: 5rem;" %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{form.fd_phase_rotation.help_text|safe}}</th>
|
|
||||||
<td colspan="3">{% include 'partials/checklist_checkbox.html' with formitem=form.fd_phase_rotation %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{form.fd_earth_fault.help_text|safe}}</th>
|
|
||||||
<td colspan="3">{% render_field form.fd_earth_fault class+="form-control" %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{form.fd_pssc.help_text|safe}}</th>
|
|
||||||
<td colspan="3">{% render_field form.fd_pssc class+="form-control" %}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="text-center">Test</th>
|
|
||||||
<th scope="col" class="text-center">Point 1</th>
|
|
||||||
<th scope="col" class="text-center">Point 2</th>
|
|
||||||
<th scope="col" class="text-center">Point 3</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{form.w1_description.help_text|safe}}</th>
|
|
||||||
<td>{% render_field form.w1_description class+="form-control" style="min-width: 5rem;" %}</td>
|
|
||||||
<td>{% render_field form.w2_description class+="form-control" style="min-width: 5rem;" %}</td>
|
|
||||||
<td>{% render_field form.w3_description class+="form-control" style="min-width: 5rem;" %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{form.w1_polarity.help_text|safe}}</th>
|
|
||||||
<td>{% render_field form.w1_polarity %}</td>
|
|
||||||
<td>{% render_field form.w2_polarity %}</td>
|
|
||||||
<td>{% render_field form.w3_polarity %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{form.w1_voltage.help_text|safe}}</th>
|
|
||||||
<td>{% render_field form.w1_voltage class+="form-control" %}</td>
|
|
||||||
<td>{% render_field form.w2_voltage class+="form-control" %}</td>
|
|
||||||
<td>{% render_field form.w3_voltage class+="form-control" %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{{form.w1_earth_fault.help_text|safe}}</th>
|
|
||||||
<td>{% render_field form.w1_earth_fault class+="form-control" %}</td>
|
|
||||||
<td>{% render_field form.w2_earth_fault class+="form-control" %}</td>
|
|
||||||
<td>{% render_field form.w3_earth_fault class+="form-control" %}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
|
|
||||||
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
|
|
||||||
{% include 'partials/ec_power_info.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-sm-12 text-right">
|
|
||||||
{% button 'submit' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
});
|
});
|
||||||
$('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() {
|
$('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() {
|
||||||
$('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value));
|
$('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value));
|
||||||
//$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
|
$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
|
||||||
});
|
});
|
||||||
$('input[type=radio][name=special_structures]').change(function() {
|
$('input[type=radio][name=special_structures]').change(function() {
|
||||||
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));
|
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));
|
||||||
|
|||||||
32
RIGS/templates/partials/archive_form.html
Normal file
32
RIGS/templates/partials/archive_form.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% load get_list from filters %}
|
||||||
|
{% load button from filters %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 py-2">
|
||||||
|
<form class="form-inline" method="GET">
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Start</span>
|
||||||
|
</div>
|
||||||
|
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">End</span>
|
||||||
|
</div>
|
||||||
|
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Keyword</span>
|
||||||
|
</div>
|
||||||
|
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
|
||||||
|
{% for status in statuses %}
|
||||||
|
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% button 'search' %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<h5 class="py-3"><a class="btn btn-info" data-toggle="collapse" href="#values" aria-expanded="false" aria-controls="values">View Threshold Values</a></h5>
|
<h5 class="py-3"><a class="btn btn-info" data-toggle="collapse" href="#values" aria-expanded="false" aria-controls="values">View Threshold Values</a></h5>
|
||||||
<div class="row collapse" id="values">
|
<div class="row collapse" id="values">
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -33,23 +33,20 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Distro</th>
|
<th scope="row">Distro</th>
|
||||||
<th scope="row">Max PSCC with Single Phase Supply (kA)</th>
|
<th scope="row">Max PSSC (kA)</th>
|
||||||
<th scope="row">Max PSCC with Three Phase Supply (kA)</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Intel & Toblerone distros</td>
|
<td>Intel & Toblerone distros</td>
|
||||||
<td>6</td>
|
<td>6</td>
|
||||||
<td>3</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>All other distros</td>
|
<td>All other distros</td>
|
||||||
<td>10</td>
|
<td>10</td>
|
||||||
<td>5</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p><strong>Voltage Drop on Circuit:</strong> ≤5% (approx. 12v)</p>
|
<p><strong>Voltage Drop on Circuit:</strong> 5% (approx. 12v)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,15 +47,5 @@
|
|||||||
class="fas fa-pound-sign"></span>
|
class="fas fa-pound-sign"></span>
|
||||||
<span class="d-none d-sm-inline">Invoice</span></a>
|
<span class="d-none d-sm-inline">Invoice</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a>
|
|
||||||
|
|
||||||
{% if event.can_check_in %}
|
|
||||||
{% if request.user.current_event %}
|
|
||||||
<a href="{% url 'event_checkout' %}" class="btn btn-warning">Check Out</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'event_checkin' event.pk %}" class="btn btn-success modal-href"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,15 +77,6 @@
|
|||||||
<dt class="col-sm-6">PO</dt>
|
<dt class="col-sm-6">PO</dt>
|
||||||
<dd class="col-sm-6">{{ object.purchase_order }}</dd>
|
<dd class="col-sm-6">{{ object.purchase_order }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-6">Forum Thread</dt>
|
|
||||||
{% if object.forum_url %}
|
|
||||||
<dd class="col-6"><a href="{{object.forum_url}}">{{object.forum_url}}</a></dd>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank"><span
|
|
||||||
class="fas fa-plus"></span> <span
|
|
||||||
class="hidden-xs">Create Forum Thread</span></a>
|
|
||||||
{% endif %}
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div id="event_status">
|
<div>
|
||||||
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
|
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
|
||||||
{% if event.is_rig %}
|
{% if event.is_rig %}
|
||||||
{% if event.sum_total > 0 %}
|
{% if event.sum_total > 0 %}
|
||||||
@@ -20,16 +20,14 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
|
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.has_checklist %}
|
{% endif %}
|
||||||
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span> {% if event.checklists.count > 1 %}({{event.checklists.count}}){% endif %}</span>
|
{% if not event.dry_hire %}
|
||||||
|
{% if event.hs_done %}
|
||||||
|
{# TODO Display status of all checklists #}
|
||||||
|
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
|
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.has_power %}
|
|
||||||
<span class="badge badge-success">Power Record: <span class="fas fa-check"></span> {% if event.power_tests.count > 1 %}({{event.power_tests.count}}){% endif %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge badge-danger">Power Record: <span class="fas fa-times"></span></span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.RIGS.view_invoice %}
|
{% if perms.RIGS.view_invoice %}
|
||||||
{% if event.invoice %}
|
{% if event.invoice %}
|
||||||
|
|||||||
@@ -1,200 +1,91 @@
|
|||||||
{% load namewithnotes from filters %}
|
{% load namewithnotes from filters %}
|
||||||
{% load markdown_tags %}
|
{% load markdown_tags %}
|
||||||
<style>
|
<div class="table-responsive">
|
||||||
#event_table {
|
<table class="table mb-0" id="event_table">
|
||||||
display: grid;
|
<thead>
|
||||||
grid-template-columns: max-content auto;
|
<tr>
|
||||||
column-gap: 1em;
|
<th scope="col">#</th>
|
||||||
}
|
<th scope="col">Dates & Times</th>
|
||||||
.eventgrid {
|
<th scope="col">Event Details</th>
|
||||||
display: inherit;
|
<th scope="col">MIC</th>
|
||||||
grid-column: 1/5;
|
</tr>
|
||||||
grid-template-columns: subgrid;
|
</thead>
|
||||||
padding: 1em;
|
<tbody>
|
||||||
dt {
|
{% for event in events %}
|
||||||
float: left;
|
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||||
clear: left;
|
<!---Number-->
|
||||||
margin-right: 10px;
|
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||||
}
|
<!--Dates & Times-->
|
||||||
dd {
|
<td id="event_dates" style="text-align: justify;">
|
||||||
margin-left: 0px;
|
{% if not event.cancelled %}
|
||||||
}
|
{% if event.meet_at %}
|
||||||
}
|
<span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
|
||||||
.grid-header {
|
{% endif %}
|
||||||
border-bottom: 1px solid grey;
|
{% if event.access_at %}
|
||||||
border-top: 1px solid grey;
|
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
|
||||||
}
|
{% endif %}
|
||||||
#event_status {
|
{% endif %}
|
||||||
grid-column-start: 3;
|
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
|
||||||
}
|
{% if event.has_start_time %}
|
||||||
#event_mic {
|
{{ event.start_time|date:"H:i" }}
|
||||||
grid-row-start: 1;
|
{% endif %}</strong>
|
||||||
grid-column-start: 4;
|
</span>
|
||||||
}
|
{% if event.end_date %}
|
||||||
.c-none {
|
<br>
|
||||||
display: none;
|
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
|
||||||
}
|
{% if event.has_end_time %}
|
||||||
.c-inline {
|
{{ event.end_time|date:"H:i" }}
|
||||||
display: inline;
|
{% endif %}</strong>
|
||||||
}
|
</span>
|
||||||
@container (width <= 500px) {
|
{% endif %}
|
||||||
#event_table {
|
</td>
|
||||||
grid-template-columns: 1fr !important;
|
<!---Details-->
|
||||||
}
|
<td id="event_details" class="w-100">
|
||||||
.eventgrid {
|
<h4>
|
||||||
grid-column: 1/1 !important;
|
<a href="{{event.get_absolute_url}}">
|
||||||
padding: 0.5em;
|
{{ event.name }}
|
||||||
}
|
</a>
|
||||||
.grid-header {
|
{% if event.venue %}
|
||||||
display: none;
|
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
|
||||||
}
|
{% endif %}
|
||||||
#event_dates {
|
{% if event.dry_hire %}
|
||||||
order: 2;
|
<span class="badge badge-secondary">Dry Hire</span>
|
||||||
}
|
{% endif %}
|
||||||
#event_status {
|
</h4>
|
||||||
order: 3;
|
{% if event.is_rig and not event.cancelled %}
|
||||||
}
|
<h5>
|
||||||
#event_mic {
|
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
|
||||||
grid-row-start: auto;
|
{% if event.organisation %}
|
||||||
grid-column-start: 4;
|
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
|
||||||
}
|
{% endif %}
|
||||||
}
|
</h5>
|
||||||
@container (width <= 700px) {
|
{% endif %}
|
||||||
#event_table {
|
{% if not event.cancelled and event.description %}
|
||||||
grid-template-columns: max-content;
|
<p>{{ event.description|markdown }}</p>
|
||||||
column-gap: 0.5em;
|
{% endif %}
|
||||||
}
|
{% include 'partials/event_status.html' %}
|
||||||
.eventgrid {
|
</td>
|
||||||
grid-column: 1/3;
|
<!---MIC-->
|
||||||
border: 1px solid grey;
|
<td id="event_mic" class="text-nowrap">
|
||||||
}
|
{% if event.mic %}
|
||||||
#event_dates {
|
{% if perms.RIGS.view_profile %}
|
||||||
grid-row: 2;
|
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
|
||||||
grid-column: 1;
|
{% endif %}
|
||||||
}
|
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
|
||||||
#event_number {
|
{{ event.mic }}
|
||||||
grid-row: 1;
|
{% if perms.RIGS.view_profile %}
|
||||||
grid-column: 1;
|
</a>
|
||||||
}
|
{% endif %}
|
||||||
#event_mic {
|
{% elif event.is_rig %}
|
||||||
grid-column: 2;
|
<span class="fas fa-user-slash"></span>
|
||||||
}
|
{% endif %}
|
||||||
#event_status {
|
</td>
|
||||||
grid-column: span 2;
|
</tr>
|
||||||
}
|
{% empty %}
|
||||||
.grid-header, .c-md-none {
|
<tr class="bg-warning">
|
||||||
display: none;
|
<td colspan="4">No events found</td>
|
||||||
}
|
</tr>
|
||||||
}
|
{% endfor %}
|
||||||
@container (width > 700px) {
|
</tbody>
|
||||||
.c-lg-block {
|
</table>
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.c-lg-inline {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.c-lg-none, .c-md-none {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id="event_table">
|
|
||||||
<div class="eventgrid grid-header font-weight-bold">
|
|
||||||
<div id="event_number">#</div>
|
|
||||||
<div id="event_dates">Dates & Times</div>
|
|
||||||
<div>Event Details</div>
|
|
||||||
<div id="event_mic">MIC</div>
|
|
||||||
</div>
|
|
||||||
{% for event in events %}
|
|
||||||
<div class="eventgrid {% if event.cancelled %}
|
|
||||||
table-secondary
|
|
||||||
{% elif not event.is_rig %}
|
|
||||||
table-info
|
|
||||||
{% elif not event.mic %}
|
|
||||||
table-danger
|
|
||||||
{% elif event.confirmed and event.authorised %}
|
|
||||||
{% if event.dry_hire or event.riskassessment %}
|
|
||||||
table-success
|
|
||||||
{% else %}
|
|
||||||
table-warning
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
table-warning
|
|
||||||
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
|
||||||
<!---Number-->
|
|
||||||
<div class="font-weight-bold c-none c-lg-block" id="event_number">{{ event.display_id }}</div>
|
|
||||||
<!--Dates & Times-->
|
|
||||||
<div id="event_dates" style="min-width: 180px;">
|
|
||||||
<dl>
|
|
||||||
{% if not event.cancelled %}
|
|
||||||
{% if event.meet_at %}
|
|
||||||
<dt class="font-weight-normal">Meet:</dt>
|
|
||||||
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.meet_at|date:"D d/m/Y H:i" }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
{% if event.access_at %}
|
|
||||||
<dt class="font-weight-normal">Access:</dt>
|
|
||||||
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.access_at|date:"D d/m/Y H:i" }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<dt class="font-weight-normal">Start:</dt>
|
|
||||||
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.start_date|date:"D d/m/Y" }}
|
|
||||||
{% if event.has_start_time %}
|
|
||||||
{{ event.start_time|date:"H:i" }}
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
{% if event.end_date %}
|
|
||||||
<dt class="font-weight-normal">End:</dt>
|
|
||||||
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.end_date|date:"D d/m/Y" }}
|
|
||||||
{% if event.has_end_time %}
|
|
||||||
{{ event.end_time|date:"H:i" }}
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
{% endif %}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<!---Details-->
|
|
||||||
<div id="event_details" class="w-100">
|
|
||||||
<h4>
|
|
||||||
<a href="{% url 'event_detail' event.pk %}">
|
|
||||||
<span class="c-inline c-lg-none">{{ event }}</span><span class="c-none c-lg-inline">{{ event.name }}</span>
|
|
||||||
</a>
|
|
||||||
{% if event.dry_hire %}
|
|
||||||
<span class="badge badge-secondary">Dry Hire</span>
|
|
||||||
{% endif %}
|
|
||||||
<br class="c-none c-lg-inline">
|
|
||||||
{% if event.venue %}
|
|
||||||
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
|
|
||||||
{% endif %}
|
|
||||||
</h4>
|
|
||||||
{% if event.is_rig and not event.cancelled %}
|
|
||||||
<h5>
|
|
||||||
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
|
|
||||||
{% if event.organisation %}
|
|
||||||
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
|
|
||||||
{% endif %}
|
|
||||||
</h5>
|
|
||||||
{% endif %}
|
|
||||||
{% if not event.cancelled and event.description %}
|
|
||||||
<p>{{ event.description|markdown }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% include 'partials/event_status.html' %}
|
|
||||||
<!---MIC-->
|
|
||||||
<div id="event_mic" class="text-nowrap">
|
|
||||||
<span class="c-md-none align-middle">MIC:</span>
|
|
||||||
{% if event.mic %}
|
|
||||||
{% if perms.RIGS.view_profile %}
|
|
||||||
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
|
|
||||||
{% endif %}
|
|
||||||
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
|
|
||||||
{{ event.mic }}
|
|
||||||
{% if perms.RIGS.view_profile %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% elif event.is_rig %}
|
|
||||||
<span class="fas fa-exclamation"></span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,18 +10,10 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<h5>Event Checklists:</h5>
|
<h5>Event Checklists:</h5>
|
||||||
{% for checklist in event.checklists.all %}
|
{% for checklist in event.checklists.all %}
|
||||||
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
|
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
|
||||||
<br/>
|
<br/>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
|
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
|
||||||
class="hidden-xs">Create</span></a>
|
class="hidden-xs">Create</span></a>
|
||||||
<hr>
|
|
||||||
<h5>Power Test Records:</h5>
|
|
||||||
{% for record in event.power_tests.all %}
|
|
||||||
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
|
|
||||||
<br/>
|
|
||||||
{% endfor %}
|
|
||||||
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
|
|
||||||
class="hidden-xs">Create</span></a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<button type="button" class="btn btn-success btn-sm item-add"
|
<button type="button" class="btn btn-success btn-sm item-add"
|
||||||
data-toggle="modal"
|
data-toggle="modal"
|
||||||
data-target="#itemModal">
|
data-target="#itemModal">
|
||||||
<i class="fas fa-plus"></i> Add Item
|
<span class="fas fa-plus"></span> Add Item
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
84
RIGS/templates/partials/subhire_table.html
Normal file
84
RIGS/templates/partials/subhire_table.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{% load linked_name from filters %}
|
||||||
|
{% load markdown_tags %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mb-0" id="event_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">#</th>
|
||||||
|
<th scope="col">Dates & Times</th>
|
||||||
|
<th scope="col">Hire Details</th>
|
||||||
|
<th scope="col">Associated Event(s)</th>
|
||||||
|
{% if perms.RIGS.subhire_finance %}
|
||||||
|
<th scope="col">Insurance Value</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for event in events %}
|
||||||
|
<tr {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||||
|
<!---Number-->
|
||||||
|
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||||
|
<!--Dates & Times-->
|
||||||
|
<td id="event_dates" style="text-align: justify;">
|
||||||
|
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
|
||||||
|
{% if event.has_start_time %}
|
||||||
|
{{ event.start_time|date:"H:i" }}
|
||||||
|
{% endif %}</strong>
|
||||||
|
</span>
|
||||||
|
{% if event.end_date %}
|
||||||
|
<br>
|
||||||
|
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
|
||||||
|
{% if event.has_end_time %}
|
||||||
|
{{ event.end_time|date:"H:i" }}
|
||||||
|
{% endif %}</strong>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<!---Details-->
|
||||||
|
<td id="event_details" class="w-100">
|
||||||
|
<h4>
|
||||||
|
<a href="{{event.get_absolute_url}}">
|
||||||
|
{{ event.name }}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<h5>
|
||||||
|
Primary Contact: {{ event.person|linked_name }}
|
||||||
|
{% if event.organisation %}
|
||||||
|
({{ event.organisation|linked_name }})
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
{% if not event.cancelled and event.description %}
|
||||||
|
<p>{{ event.description|markdown }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'partials/event_status.html' %}
|
||||||
|
</td>
|
||||||
|
<td class="p-0 text-nowrap">
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for event in event.events.all %}
|
||||||
|
<li class="list-group-item"><a href="{{event.get_absolute_url}}">{{ event }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
{% if perms.RIGS.subhire_finance %}
|
||||||
|
<td id="insurance_value" class="text-nowrap">
|
||||||
|
£{{ event.insurance_value }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr class="bg-warning">
|
||||||
|
<td colspan="4">No events found</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td>Total Value:</td>
|
||||||
|
<td>£{{ total_value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -29,15 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row pt-3">
|
<div class="row pt-3">
|
||||||
<label class="col-sm-4 col-form-label"
|
<label class="col-sm-4 col-form-label"
|
||||||
for="{{ form.method.id_for_label }}">{{ form.method.label }}
|
for="{{ form.method.id_for_label }}">{{ form.method.label }}</label>
|
||||||
<span class="fas fa-info-circle text-info" data-toggle="collapse" data-target="#collapse" aria-expanded="false" aria-controls="collapse"></span>
|
|
||||||
<ul class="collapse" id="collapse">
|
|
||||||
<li>Cash - Self Explanatory</li>
|
|
||||||
<li>Internal - Transfers within the Students' Union only</li>
|
|
||||||
<li>External - All other transfers (<em>including</em> the University)</li>
|
|
||||||
<li>TEC Adjustment - Manual corrections</li>
|
|
||||||
</ul>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
{% render_field form.method class+="form-control" %}
|
{% render_field form.method class+="form-control" %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row align-items-center justify-content-between py-2 align-middle">
|
<div class="row align-items-center justify-content-between py-2 align-middle">
|
||||||
<div class="col-sm-12 col-md align-middle d-flex flex-wrap">
|
<div class="col-sm-12 col-md align-middle">
|
||||||
Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span class="table-warning mr-1 px-2 rounded text-nowrap">Action Required</span><span class="table-danger mr-1 px-2 rounded text-nowrap">Needs MIC</span><span class="table-secondary mr-1 px-2 rounded">Cancelled</span><span class="table-info px-2 rounded text-nowrap">Non-Rig</span>
|
Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span class="table-warning mr-1 px-2 rounded">Action Required</span><span class="table-danger mr-1 px-2 rounded">Needs MIC</span><span class="table-secondary mr-1 px-2 rounded">Cancelled</span><span class="table-info px-2 rounded">Non-Rig</span>
|
||||||
</div>
|
</div>
|
||||||
{% if perms.RIGS.add_event %}
|
{% if perms.RIGS.add_event %}
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div style="container-type: inline-size;">
|
|
||||||
{% include 'partials/event_table.html' %}
|
{% include 'partials/event_table.html' %}
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
76
RIGS/templates/subhire_detail.html
Normal file
76
RIGS/templates/subhire_detail.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
|
|
||||||
|
{% load markdown_tags %}
|
||||||
|
{% load button from filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row my-3 py-3">
|
||||||
|
<div class="col-sm-12 text-right mb-2">
|
||||||
|
{% button 'edit' 'subhire_update' object.pk %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% include 'partials/contact_details.html' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-header">Hire Details</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6">Name</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.name }}</dd>
|
||||||
|
<dt class="col-sm-6">Event Starts</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.start_date|date:"D d M Y" }} {{ object.start_time|date:"H:i" }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Event Ends</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.end_date|date:"D d M Y" }} {{ object.end_time|date:"H:i" }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Status</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.get_status_display }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">PO</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.po }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mt-2">
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-header">Equipment Information</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6">Description</dt>
|
||||||
|
<dd class="col-sm-6">{{ object.description }}</dd>
|
||||||
|
{% if perms.RIGS.subhire_finance %}
|
||||||
|
<dt class="col-sm-6">Insurance Value</dt>
|
||||||
|
<dd class="col-sm-6">£{{ object.insurance_value }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
<dt class="col-sm-6">Quote</dt>
|
||||||
|
<dd class="col-sm-6"><a href="{{ object.quote }}">View</a></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mt-2">
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-header">Associated Event(s)</div>
|
||||||
|
{% with object.events.all as events %}
|
||||||
|
{% include 'partials/event_table.html' %}
|
||||||
|
{%endwith%}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||||
|
<div class="col-sm-12 text-right">
|
||||||
|
{% include 'partials/last_edited.html' with target="event_history" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% if request.is_ajax %}
|
||||||
|
{% block footer %}
|
||||||
|
{% if perms.RIGS.view_event %}
|
||||||
|
{% include 'partials/last_edited.html' with target="event_history" %}
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'subhire_detail' object.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
|
||||||
|
{% endblock %}
|
||||||
|
{% endif %}
|
||||||
202
RIGS/templates/subhire_form.html
Normal file
202
RIGS/templates/subhire_form.html
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
{% extends 'base_rigs.html' %}
|
||||||
|
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
{% load static %}
|
||||||
|
{% load multiply from filters %}
|
||||||
|
{% load button from filters %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block preload_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/selects.js' %}"></script>
|
||||||
|
<script src="{% static 'js/easymde.min.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/autocompleter.js' %}"></script>
|
||||||
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
|
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
setupMDE('#id_description');
|
||||||
|
});
|
||||||
|
$(function () {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="row" role="form" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="col-12">
|
||||||
|
{% include 'form_errors.html' %}
|
||||||
|
</div>
|
||||||
|
{# Contact details #}
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Contact Details</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group" data-toggle="tooltip">
|
||||||
|
<label for="{{ form.person.id_for_label }}">Primary Contact</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9">
|
||||||
|
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
|
||||||
|
{% if person %}
|
||||||
|
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 align-right">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{% url 'person_create' %}" class="btn btn-success modal-href"
|
||||||
|
data-target="#{{ form.person.id_for_label }}">
|
||||||
|
<span class="fas fa-plus"></span>
|
||||||
|
</a>
|
||||||
|
<a {% if form.person.value %}href="{% url 'person_update' form.person.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.person.id_for_label }}-update" data-target="#{{ form.person.id_for_label }}">
|
||||||
|
<span class="fas fa-user-edit"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.organisation.id_for_label }}">Hire Company</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9">
|
||||||
|
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}">
|
||||||
|
{% if organisation %}
|
||||||
|
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 align-right">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{% url 'organisation_create' %}" class="btn btn-success modal-href"
|
||||||
|
data-target="#{{ form.organisation.id_for_label }}">
|
||||||
|
<span class="fas fa-plus"></span>
|
||||||
|
</a>
|
||||||
|
<a {% if form.organisation.value %}href="{% url 'organisation_update' form.organisation.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.organisation.id_for_label }}-update" data-target="#{{ form.organisation.id_for_label }}">
|
||||||
|
<span class="fas fa-edit"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Associated Event(s)</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<select multiple name="events" id="events_id" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='event' %}">
|
||||||
|
{% if object.events.count > 0 %}
|
||||||
|
{% for event in object.events.all %}
|
||||||
|
<option value="{{event.id}}" selected>{{ event }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{# Event details #}
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<div class="card card-default">
|
||||||
|
<div class="card-header">Hire Details</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group" data-toggle="tooltip" title="Name of the event, displays on rigboard and on paperwork">
|
||||||
|
<label for="{{ form.name.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% render_field form.name class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.start_date.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
|
||||||
|
{% render_field form.start_date class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="Start time of event, can be left blank">
|
||||||
|
{% render_field form.start_time class+="form-control" step="60" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.end_date.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
|
||||||
|
{% render_field form.end_date class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="End time of event, leave blank if unknown">
|
||||||
|
{% render_field form.end_time class+="form-control" step="60" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" data-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
|
||||||
|
<label for="{{ form.status.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">{{ form.status.label }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% render_field form.status class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.purchase_order.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{% render_field form.purchase_order class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Equipment Information</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.description.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
{% render_field form.description class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.insurance_value.id_for_label }}"
|
||||||
|
class="col-sm-6 col-form-label">{{ form.insurance_value.label }}</label>
|
||||||
|
<div class="col-sm-8 input-group">
|
||||||
|
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||||
|
{% render_field form.insurance_value class+="form-control" %}
|
||||||
|
</div>
|
||||||
|
<div class="border border-info p-2 rounded mt-1 font-weight-bold" style="border-width: thin thin thin thick !important;">
|
||||||
|
If this value is greater than £50,000 then please email productions@nottinghamtec.co.uk in addition to complete the additional insurance requirements
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.quote.id_for_label }}" class="col-sm-6 col-form-label">{{ form.quote.label }} (TEC SharePoint link)</label>
|
||||||
|
<div class="col-sm-12">{% render_field form.quote class+="form-control" %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 text-right my-3">
|
||||||
|
{% button 'submit' %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
27
RIGS/templates/subhire_list.html
Normal file
27
RIGS/templates/subhire_list.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{% extends 'base_rigs.html' %}
|
||||||
|
{% load paginator from filters %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block preload_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/selects.js' %}" async></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'partials/archive_form.html' %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
{% with object_list as events %}
|
||||||
|
{% include 'partials/subhire_table.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% paginator %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -171,10 +171,13 @@ def title_spaced(string):
|
|||||||
@register.filter(needs_autoescape=True)
|
@register.filter(needs_autoescape=True)
|
||||||
def namewithnotes(obj, url, autoescape=True):
|
def namewithnotes(obj, url, autoescape=True):
|
||||||
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
|
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
|
||||||
return mark_safe(obj.name + f" <a href='{reverse(url, kwargs={'pk': obj.pk})}'><span class='fas fa-sticky-note'></span></a>")
|
return mark_safe(obj.name + " <a href='{}'><span class='fas fa-sticky-note'></span></a>".format(reverse(url, kwargs={'pk': obj.pk})))
|
||||||
else:
|
else:
|
||||||
return obj.name
|
return obj.name
|
||||||
|
|
||||||
|
@register.filter(needs_autoescape=True)
|
||||||
|
def linked_name(object, autoescape=True):
|
||||||
|
return mark_safe(f"<a href='{object.get_absolute_url()}'>{object.name}</a>")
|
||||||
|
|
||||||
@register.filter(needs_autoescape=True)
|
@register.filter(needs_autoescape=True)
|
||||||
def linkornone(target, namespace=None, autoescape=True):
|
def linkornone(target, namespace=None, autoescape=True):
|
||||||
@@ -183,7 +186,7 @@ def linkornone(target, namespace=None, autoescape=True):
|
|||||||
link = namespace + "://" + target
|
link = namespace + "://" + target
|
||||||
else:
|
else:
|
||||||
link = target
|
link = target
|
||||||
return mark_safe(f"<a href='{link}' target='_blank'><span class='overflow-ellipsis'>{target}</span></a>")
|
return mark_safe("<a href='{}' target='_blank'><span class='overflow-ellipsis'>{}</span></a>".format(link, str(target)))
|
||||||
else:
|
else:
|
||||||
return "None"
|
return "None"
|
||||||
|
|
||||||
|
|||||||
@@ -43,22 +43,15 @@ def venue(db):
|
|||||||
|
|
||||||
@pytest.fixture # TODO parameterise with Event sizes
|
@pytest.fixture # TODO parameterise with Event sizes
|
||||||
def checklist(basic_event, venue, admin_user, ra):
|
def checklist(basic_event, venue, admin_user, ra):
|
||||||
checklist = models.EventChecklist.objects.create(event=basic_event, safe_parking=False,
|
checklist = models.EventChecklist.objects.create(event=basic_event, power_mic=admin_user, safe_parking=False,
|
||||||
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
||||||
ear_plugs=False, hs_location="Locked away safely",
|
ear_plugs=False, hs_location="Locked away safely",
|
||||||
extinguishers_location="Somewhere, I forgot",
|
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
|
||||||
date=timezone.now(), venue=venue)
|
date=timezone.now(), venue=venue)
|
||||||
yield checklist
|
yield checklist
|
||||||
checklist.delete()
|
checklist.delete()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def power_test(basic_event, venue, admin_user, ra):
|
|
||||||
power_test = models.PowerTestRecord.objects.create(event=basic_event, venue=venue)
|
|
||||||
yield power_test
|
|
||||||
power_test.delete()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def many_events(db, admin_user, scope="class"):
|
def many_events(db, admin_user, scope="class"):
|
||||||
many_events = {
|
many_events = {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class CreateEvent(FormPage):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def select_event_type(self, type_name):
|
def select_event_type(self, type_name):
|
||||||
self.find_element(By.XPATH, f'//button[.="{type_name}"]').click()
|
self.find_element(By.XPATH, '//button[.="{}"]'.format(type_name)).click()
|
||||||
|
|
||||||
def item_row(self, ID):
|
def item_row(self, ID):
|
||||||
return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID))
|
return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID))
|
||||||
@@ -230,6 +230,11 @@ class CreateEventChecklist(FormPage):
|
|||||||
URL_TEMPLATE = 'event/{event_id}/checklist'
|
URL_TEMPLATE = 'event/{event_id}/checklist'
|
||||||
|
|
||||||
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
||||||
|
_power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]")
|
||||||
|
_add_vehicle_locator = (By.XPATH, "//button[contains(., 'Vehicle')]")
|
||||||
|
_add_crew_locator = (By.XPATH, "//button[contains(., 'Crew')]")
|
||||||
|
_vehicle_row_locator = ('xpath', "//tr[@id[starts-with(., 'vehicle') and not(contains(.,'new'))]]")
|
||||||
|
_crew_row_locator = ('xpath', "//tr[@id[starts-with(., 'crew') and not(contains(.,'new'))]]")
|
||||||
|
|
||||||
form_items = {
|
form_items = {
|
||||||
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
|
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
|
||||||
@@ -240,20 +245,6 @@ class CreateEventChecklist(FormPage):
|
|||||||
'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')),
|
'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')),
|
||||||
'hs_location': (regions.TextBox, (By.ID, 'id_hs_location')),
|
'hs_location': (regions.TextBox, (By.ID, 'id_hs_location')),
|
||||||
'extinguishers_location': (regions.TextBox, (By.ID, 'id_extinguishers_location')),
|
'extinguishers_location': (regions.TextBox, (By.ID, 'id_extinguishers_location')),
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success(self):
|
|
||||||
return '{event_id}' not in self.driver.current_url
|
|
||||||
|
|
||||||
|
|
||||||
class CreatePowerTestRecord(FormPage):
|
|
||||||
URL_TEMPLATE = 'event/{event_id}/power'
|
|
||||||
|
|
||||||
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
|
||||||
_power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]")
|
|
||||||
|
|
||||||
form_items = {
|
|
||||||
'rcds': (regions.CheckBox, (By.ID, 'id_rcds')),
|
'rcds': (regions.CheckBox, (By.ID, 'id_rcds')),
|
||||||
'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')),
|
'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')),
|
||||||
'earthing': (regions.CheckBox, (By.ID, 'id_earthing')),
|
'earthing': (regions.CheckBox, (By.ID, 'id_earthing')),
|
||||||
@@ -272,10 +263,58 @@ class CreatePowerTestRecord(FormPage):
|
|||||||
'w1_earth_fault': (regions.TextBox, (By.ID, 'id_w1_earth_fault')),
|
'w1_earth_fault': (regions.TextBox, (By.ID, 'id_w1_earth_fault')),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def add_vehicle(self):
|
||||||
|
self.find_element(*self._add_vehicle_locator).click()
|
||||||
|
|
||||||
|
def add_crew(self):
|
||||||
|
self.find_element(*self._add_crew_locator).click()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def power_mic(self):
|
def power_mic(self):
|
||||||
return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector))
|
return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vehicles(self):
|
||||||
|
return [self.VehicleRow(self, el) for el in self.find_elements(*self._vehicle_row_locator)]
|
||||||
|
|
||||||
|
class VehicleRow(Region):
|
||||||
|
_name_locator = ('xpath', ".//input")
|
||||||
|
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return regions.TextBox(self, self.root.find_element(*self._name_locator))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vehicle(self):
|
||||||
|
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def crew(self):
|
||||||
|
return [self.CrewRow(self, el) for el in self.find_elements(*self._crew_row_locator)]
|
||||||
|
|
||||||
|
class CrewRow(Region):
|
||||||
|
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
|
||||||
|
_start_time_locator = ('xpath', ".//input[@name[starts-with(., 'start') and not(contains(.,'new'))]]")
|
||||||
|
_end_time_locator = ('xpath', ".//input[@name[starts-with(., 'end') and not(contains(.,'new'))]]")
|
||||||
|
_role_locator = ('xpath', ".//input[@name[starts-with(., 'role') and not(contains(.,'new'))]]")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def crewmember(self):
|
||||||
|
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_time(self):
|
||||||
|
return regions.DateTimePicker(self, self.root.find_element(*self._start_time_locator))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def end_time(self):
|
||||||
|
return regions.DateTimePicker(self, self.root.find_element(*self._end_time_locator))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def role(self):
|
||||||
|
return regions.TextBox(self, self.root.find_element(*self._role_locator))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self):
|
def success(self):
|
||||||
return '{event_id}' not in self.driver.current_url
|
return '{event_id}' not in self.driver.current_url
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
|
|||||||
|
|
||||||
class Header(Region):
|
class Header(Region):
|
||||||
def find_link(self, link_text):
|
def find_link(self, link_text):
|
||||||
return self.driver.find_element(By.PARTIAL_LINK_TEXT, link_text)
|
return self.driver.find_element_by_partial_link_text(link_text)
|
||||||
|
|
||||||
|
|
||||||
class ItemRow(Region):
|
class ItemRow(Region):
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ class TestEventDuplicate(BaseRigboardTest):
|
|||||||
|
|
||||||
self.assertFalse(newEvent.authorised)
|
self.assertFalse(newEvent.authorised)
|
||||||
|
|
||||||
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element(By.XPATH, '//h2').text)
|
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element_by_xpath('//h2').text)
|
||||||
self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible
|
self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible
|
||||||
|
|
||||||
# Check the new items are visible
|
# Check the new items are visible
|
||||||
@@ -327,25 +327,26 @@ class TestEventDuplicate(BaseRigboardTest):
|
|||||||
self.assertIn("Test Item 2", table.text)
|
self.assertIn("Test Item 2", table.text)
|
||||||
self.assertIn("Test Item 3", table.text)
|
self.assertIn("Test Item 3", table.text)
|
||||||
|
|
||||||
infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..')
|
infoPanel = self.driver.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
||||||
self.assertIn("N%05d" % self.testEvent.pk, infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
self.assertIn("N%05d" % self.testEvent.pk,
|
||||||
|
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
# Check the PO hasn't carried through
|
# Check the PO hasn't carried through
|
||||||
self.assertNotIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text)
|
self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
self.assertIn("N%05d" % self.testEvent.pk,
|
self.assertIn("N%05d" % self.testEvent.pk,
|
||||||
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
self.driver.get(self.live_server_url + '/event/' + str(self.testEvent.pk)) # Go back to the old event
|
self.driver.get(self.live_server_url + '/event/' + str(self.testEvent.pk)) # Go back to the old event
|
||||||
|
|
||||||
# Check that based-on hasn't crept into the old event
|
# Check that based-on hasn't crept into the old event
|
||||||
infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..')
|
infoPanel = self.driver.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
||||||
self.assertNotIn("N%05d" % self.testEvent.pk,
|
self.assertNotIn("N%05d" % self.testEvent.pk,
|
||||||
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
# Check the PO remains on the old event
|
# Check the PO remains on the old event
|
||||||
self.assertIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text)
|
self.assertIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
self.assertNotIn("N%05d" % self.testEvent.pk,
|
self.assertNotIn("N%05d" % self.testEvent.pk,
|
||||||
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
|
||||||
# Check the items are as they were
|
# Check the items are as they were
|
||||||
table = self.page.item_table # ID number is known, see above
|
table = self.page.item_table # ID number is known, see above
|
||||||
@@ -676,6 +677,14 @@ def small_ec(page, admin_user):
|
|||||||
page.ear_plugs = True
|
page.ear_plugs = True
|
||||||
page.hs_location = "The Moon"
|
page.hs_location = "The Moon"
|
||||||
page.extinguishers_location = "With the rest of the fire"
|
page.extinguishers_location = "With the rest of the fire"
|
||||||
|
# If we do this first the search fails, for ... reasons
|
||||||
|
page.power_mic.search(admin_user.name)
|
||||||
|
page.power_mic.toggle()
|
||||||
|
assert not page.power_mic.is_open
|
||||||
|
page.earthing = True
|
||||||
|
page.rcds = True
|
||||||
|
page.supply_test = True
|
||||||
|
page.pat = True
|
||||||
|
|
||||||
|
|
||||||
def test_ec_create_small(logged_in_browser, live_server, admin_user, ra):
|
def test_ec_create_small(logged_in_browser, live_server, admin_user, ra):
|
||||||
@@ -696,15 +705,14 @@ def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra)
|
|||||||
page.ear_plugs = True
|
page.ear_plugs = True
|
||||||
page.hs_location = "Death Valley"
|
page.hs_location = "Death Valley"
|
||||||
page.extinguishers_location = "With the rest of the fire"
|
page.extinguishers_location = "With the rest of the fire"
|
||||||
|
# If we do this first the search fails, for ... reasons
|
||||||
|
page.power_mic.search(admin_user.name)
|
||||||
|
page.power_mic.toggle()
|
||||||
|
assert not page.power_mic.is_open
|
||||||
|
|
||||||
# Gotta scroll to make the button clickable
|
# Gotta scroll to make the button clickable
|
||||||
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||||
page.submit()
|
|
||||||
assert page.success
|
|
||||||
|
|
||||||
|
|
||||||
def test_power_checklist(logged_in_browser, live_server, admin_user, power_test, medium_ra):
|
|
||||||
page = pages.CreatePowerTestRecord(logged_in_browser.driver, live_server.url, event_id=medium_ra.event.pk).open()
|
|
||||||
page.earthing = True
|
page.earthing = True
|
||||||
page.pat = True
|
page.pat = True
|
||||||
page.source_rcd = True
|
page.source_rcd = True
|
||||||
@@ -719,15 +727,56 @@ def test_power_checklist(logged_in_browser, live_server, admin_user, power_test,
|
|||||||
page.w1_polarity = True
|
page.w1_polarity = True
|
||||||
page.w1_voltage = 240
|
page.w1_voltage = 240
|
||||||
page.w1_earth_fault = "0.42"
|
page.w1_earth_fault = "0.42"
|
||||||
# If we do this first the search fails, for ... reasons
|
|
||||||
page.power_mic.search(admin_user.name)
|
|
||||||
page.power_mic.toggle()
|
|
||||||
assert not page.power_mic.is_open
|
|
||||||
|
|
||||||
page.submit()
|
page.submit()
|
||||||
assert page.success
|
assert page.success
|
||||||
|
|
||||||
|
|
||||||
|
def test_ec_create_vehicle(logged_in_browser, live_server, admin_user, checklist):
|
||||||
|
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
|
||||||
|
small_ec(page, admin_user)
|
||||||
|
page.add_vehicle()
|
||||||
|
assert len(page.vehicles) == 1
|
||||||
|
vehicle_name = 'Brian'
|
||||||
|
page.vehicles[0].name.set_value(vehicle_name)
|
||||||
|
# Appears we're moving too fast for javascript...
|
||||||
|
t.sleep(1)
|
||||||
|
page.vehicles[0].vehicle.search(admin_user.first_name)
|
||||||
|
t.sleep(1)
|
||||||
|
page.submit()
|
||||||
|
assert page.success
|
||||||
|
# Check data is correct
|
||||||
|
checklist.refresh_from_db()
|
||||||
|
vehicle = models.EventChecklistVehicle.objects.get(checklist=checklist.pk)
|
||||||
|
assert vehicle_name == vehicle.vehicle
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Test validation of end before start
|
||||||
|
def test_ec_create_crew(logged_in_browser, live_server, admin_user, checklist):
|
||||||
|
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
|
||||||
|
small_ec(page, admin_user)
|
||||||
|
page.add_crew()
|
||||||
|
assert len(page.crew) == 1
|
||||||
|
role = "MIC"
|
||||||
|
start_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 9, 0))
|
||||||
|
end_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 10, 30))
|
||||||
|
crew = page.crew[0]
|
||||||
|
t.sleep(2)
|
||||||
|
crew.crewmember.search(admin_user.first_name)
|
||||||
|
t.sleep(2)
|
||||||
|
crew.role.set_value(role)
|
||||||
|
crew.start_time.set_value(start_time)
|
||||||
|
crew.end_time.set_value(end_time)
|
||||||
|
page.submit()
|
||||||
|
assert page.success
|
||||||
|
# Check data is correct
|
||||||
|
crew_obj = models.EventChecklistCrew.objects.get(checklist=checklist.pk)
|
||||||
|
assert admin_user.pk == crew_obj.crewmember.pk
|
||||||
|
assert role == crew_obj.role
|
||||||
|
assert start_time == crew_obj.start
|
||||||
|
assert end_time == crew_obj.end
|
||||||
|
|
||||||
|
|
||||||
# TODO Can I loop through all the boolean fields and test them at once?
|
# TODO Can I loop through all the boolean fields and test them at once?
|
||||||
def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event):
|
def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event):
|
||||||
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open()
|
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open()
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ class TestPrintPaperwork(TestCase):
|
|||||||
|
|
||||||
def test_login_redirect(client, django_user_model):
|
def test_login_redirect(client, django_user_model):
|
||||||
request_url = reverse('event_embed', kwargs={'pk': 1})
|
request_url = reverse('event_embed', kwargs={'pk': 1})
|
||||||
expected_url = f"{reverse('login_embed')}?next={request_url}"
|
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||||
|
|
||||||
# Request the page and check it redirects
|
# Request the page and check it redirects
|
||||||
response = client.get(request_url, follow=True)
|
response = client.get(request_url, follow=True)
|
||||||
@@ -372,8 +372,7 @@ def test_ra_redirect(admin_client, admin_user, ra):
|
|||||||
|
|
||||||
|
|
||||||
class TestMarkdownTemplateTags(TestCase):
|
class TestMarkdownTemplateTags(TestCase):
|
||||||
with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md"), encoding="utf-8") as f:
|
markdown = open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")).read()
|
||||||
markdown = f.read()
|
|
||||||
|
|
||||||
def test_html_safe(self):
|
def test_html_safe(self):
|
||||||
html = markdown_filter(self.markdown)
|
html = markdown_filter(self.markdown)
|
||||||
|
|||||||
55
RIGS/urls.py
55
RIGS/urls.py
@@ -43,12 +43,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Rigboard
|
# Rigboard
|
||||||
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
|
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
|
||||||
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
|
re_path(r'^rigboard/calendar/$', login_required()(views.WebCalendar.as_view()),
|
||||||
name='web_calendar'),
|
name='web_calendar'),
|
||||||
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
|
|
||||||
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
|
|
||||||
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
|
|
||||||
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
|
|
||||||
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +66,22 @@ urlpatterns = [
|
|||||||
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
|
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
|
||||||
name='event_duplicate'),
|
name='event_duplicate'),
|
||||||
|
|
||||||
|
|
||||||
|
# Subhire
|
||||||
|
path('subhire/<int:pk>/', login_required(views.SubhireDetail.as_view()),
|
||||||
|
name='subhire_detail'),
|
||||||
|
path('subhire/create/', permission_required_with_403('RIGS.add_event')(views.SubhireCreate.as_view()),
|
||||||
|
name='subhire_create'),
|
||||||
|
path('subhire/<int:pk>/edit', permission_required_with_403('RIGS.change_event')(views.SubhireEdit.as_view()),
|
||||||
|
name='subhire_update'),
|
||||||
|
path('subhire/list/', login_required(views.SubhireList.as_view()),
|
||||||
|
name='subhire_list'),
|
||||||
|
|
||||||
|
# Dashboards
|
||||||
|
path('dashboard/productions/', views.ProductionsDashboard.as_view(),
|
||||||
|
name='productions_dashboard'),
|
||||||
|
|
||||||
|
|
||||||
# Event H&S
|
# Event H&S
|
||||||
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
||||||
|
|
||||||
@@ -79,8 +91,10 @@ urlpatterns = [
|
|||||||
name='ra_detail'),
|
name='ra_detail'),
|
||||||
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
||||||
name='ra_edit'),
|
name='ra_edit'),
|
||||||
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.MarkReviewed.as_view()),
|
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentList.as_view()),
|
||||||
name='ra_review', kwargs={'model': 'RiskAssessment'}),
|
name='ra_list'),
|
||||||
|
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()),
|
||||||
|
name='ra_review'),
|
||||||
path('event/ra/<int:pk>/print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'),
|
path('event/ra/<int:pk>/print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'),
|
||||||
|
|
||||||
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
||||||
@@ -89,29 +103,10 @@ urlpatterns = [
|
|||||||
name='ec_detail'),
|
name='ec_detail'),
|
||||||
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
||||||
name='ec_edit'),
|
name='ec_edit'),
|
||||||
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.MarkReviewed.as_view()),
|
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistList.as_view()),
|
||||||
name='ec_review', kwargs={'model': 'EventChecklist'}),
|
name='ec_list'),
|
||||||
|
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.EventChecklistReview.as_view()),
|
||||||
path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()),
|
name='ec_review'),
|
||||||
name='event_pt'),
|
|
||||||
path('event/power/<int:pk>/', login_required(views.PowerTestDetail.as_view()),
|
|
||||||
name='pt_detail'),
|
|
||||||
path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()),
|
|
||||||
name='pt_edit'),
|
|
||||||
path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()),
|
|
||||||
name='pt_review', kwargs={'model': 'PowerTestRecord'}),
|
|
||||||
|
|
||||||
path('event/<int:pk>/checkin/', login_required(views.EventCheckIn.as_view()),
|
|
||||||
name='event_checkin'),
|
|
||||||
path('event/checkout/', login_required(views.EventCheckOut.as_view()),
|
|
||||||
name='event_checkout'),
|
|
||||||
path('event/<int:pk>/checkin/edit/', login_required(views.EventCheckInEdit.as_view()),
|
|
||||||
name='edit_checkin'),
|
|
||||||
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()),
|
|
||||||
name='event_checkin_override'),
|
|
||||||
|
|
||||||
path('event/<int:pk>/thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'),
|
|
||||||
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
|
|
||||||
|
|
||||||
# Finance
|
# Finance
|
||||||
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
||||||
|
|||||||
56
RIGS/utils.py
Normal file
56
RIGS/utils.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from datetime import datetime, timedelta, date
|
||||||
|
import calendar
|
||||||
|
from calendar import HTMLCalendar
|
||||||
|
from RIGS.models import Event, Subhire
|
||||||
|
|
||||||
|
|
||||||
|
class Calendar(HTMLCalendar):
|
||||||
|
def __init__(self, year=None, month=None):
|
||||||
|
self.year = year
|
||||||
|
self.month = month
|
||||||
|
super(Calendar, self).__init__()
|
||||||
|
|
||||||
|
def get_html(self, day, event):
|
||||||
|
return f"<a href='{event.get_absolute_url()}' class='modal-href' style='display: contents;'><div class='event event-start event-end bg-{event.color}' data-span='{event.length}' style='grid-column-start: calc({day[1]} + 1)'>{event}</div></a>"
|
||||||
|
|
||||||
|
def formatmonth(self, withyear=True):
|
||||||
|
events = Event.objects.filter(start_date__year=self.year, start_date__month=self.month).order_by("start_date")
|
||||||
|
subhires = Subhire.objects.filter(start_date__year=self.year, start_date__month=self.month).order_by("start_date")
|
||||||
|
weeks = self.monthdays2calendar(self.year, self.month)
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for week in weeks:
|
||||||
|
weeks_events = []
|
||||||
|
|
||||||
|
for day in week:
|
||||||
|
# Events that have started this week
|
||||||
|
events_per_day = events.filter(start_date__day=day[0])
|
||||||
|
subhires_per_day = subhires.filter(start_date__day=day[0])
|
||||||
|
event_html = ""
|
||||||
|
for event in events_per_day:
|
||||||
|
event_html += self.get_html(day, event)
|
||||||
|
for sh in subhires_per_day:
|
||||||
|
event_html += self.get_html(day, sh)
|
||||||
|
weeks_events.append((day[0], day[1], event_html))
|
||||||
|
data.append(weeks_events)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_date(req_day):
|
||||||
|
if req_day:
|
||||||
|
year, month = (int(x) for x in req_day.split('-'))
|
||||||
|
return date(year, month, day=1)
|
||||||
|
return datetime.today()
|
||||||
|
|
||||||
|
def prev_month(d):
|
||||||
|
first = d.replace(day=1)
|
||||||
|
prev_month = first - timedelta(days=1)
|
||||||
|
month = f'month={str(prev_month.year)}-{str(prev_month.month)}'
|
||||||
|
return month
|
||||||
|
|
||||||
|
def next_month(d):
|
||||||
|
days_in_month = calendar.monthrange(d.year, d.month)[1]
|
||||||
|
last = d.replace(day=days_in_month)
|
||||||
|
next_month = last + timedelta(days=1)
|
||||||
|
month = f'month={str(next_month.year)}-{str(next_month.month)}'
|
||||||
|
return month
|
||||||
10
RIGS/validators.py
Normal file
10
RIGS/validators.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -3,3 +3,5 @@ from .finance import *
|
|||||||
from .hs import *
|
from .hs import *
|
||||||
from .ical import *
|
from .ical import *
|
||||||
from .rigboard import *
|
from .rigboard import *
|
||||||
|
from .subhire import *
|
||||||
|
from .dashboards import *
|
||||||
14
RIGS/views/dashboards.py
Normal file
14
RIGS/views/dashboards.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.views import generic
|
||||||
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionsDashboard(generic.TemplateView):
|
||||||
|
template_name = 'dashboards/productions.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = "Productions Dashboard"
|
||||||
|
context['rig_count'] = models.Event.objects.rig_count()
|
||||||
|
context['subhire_count'] = models.Subhire.objects.event_count()
|
||||||
|
context['hire_count'] = models.Event.objects.active_dry_hires().count()
|
||||||
|
return context
|
||||||
274
RIGS/views/hs.py
274
RIGS/views/hs.py
@@ -1,39 +1,16 @@
|
|||||||
from django.apps import apps
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
from RIGS.views.rigboard import get_related
|
from RIGS.views.rigboard import get_related
|
||||||
from PyRIGS.views import PrintView, ModalURLMixin
|
from PyRIGS.views import PrintView
|
||||||
from django.shortcuts import redirect
|
|
||||||
|
|
||||||
|
|
||||||
class HSCreateView(generic.CreateView):
|
class EventRiskAssessmentCreate(generic.CreateView):
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
event = models.Event.objects.get(pk=self.kwargs.get('pk'))
|
|
||||||
context['event'] = event
|
|
||||||
context['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}'
|
|
||||||
get_related(context['form'], context)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class MarkReviewed(generic.RedirectView):
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
|
||||||
obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk'))
|
|
||||||
with reversion.create_revision():
|
|
||||||
reversion.set_user(self.request.user)
|
|
||||||
obj.reviewed_by = self.request.user
|
|
||||||
obj.reviewed_at = timezone.now()
|
|
||||||
obj.save()
|
|
||||||
return self.request.META.get('HTTP_REFERER', reverse('hs_list'))
|
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentCreate(HSCreateView):
|
|
||||||
model = models.RiskAssessment
|
model = models.RiskAssessment
|
||||||
template_name = 'hs/risk_assessment_form.html'
|
template_name = 'hs/risk_assessment_form.html'
|
||||||
form_class = forms.EventRiskAssessmentForm
|
form_class = forms.EventRiskAssessmentForm
|
||||||
@@ -46,12 +23,28 @@ class EventRiskAssessmentCreate(HSCreateView):
|
|||||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||||
|
|
||||||
if ra is not None:
|
if ra is not None:
|
||||||
return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk}))
|
return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
|
||||||
|
|
||||||
return super().get(self)
|
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'] = f'Create Risk Assessment for Event {event.display_id}'
|
||||||
|
get_related(context['form'], context)
|
||||||
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse('ra_detail', kwargs={'pk': self.object.pk})
|
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
class EventRiskAssessmentEdit(generic.UpdateView):
|
||||||
@@ -64,10 +57,10 @@ class EventRiskAssessmentEdit(generic.UpdateView):
|
|||||||
ra.reviewed_by = None
|
ra.reviewed_by = None
|
||||||
ra.reviewed_at = None
|
ra.reviewed_at = None
|
||||||
ra.save()
|
ra.save()
|
||||||
return reverse('ra_detail', kwargs={'pk': self.object.pk})
|
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
|
||||||
rpk = self.kwargs.get('pk')
|
rpk = self.kwargs.get('pk')
|
||||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
ra = models.RiskAssessment.objects.get(pk=rpk)
|
||||||
context['event'] = ra.event
|
context['event'] = ra.event
|
||||||
@@ -82,17 +75,47 @@ class EventRiskAssessmentDetail(generic.DetailView):
|
|||||||
template_name = 'hs/risk_assessment_detail.html'
|
template_name = 'hs/risk_assessment_detail.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventRiskAssessmentList(generic.ListView):
|
||||||
|
paginate_by = 20
|
||||||
|
model = models.RiskAssessment
|
||||||
|
template_name = 'hs/hs_object_list.html'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
||||||
|
|
||||||
|
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'
|
||||||
|
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):
|
class EventChecklistDetail(generic.DetailView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'hs/event_checklist_detail.html'
|
template_name = 'hs/event_checklist_detail.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventChecklistDetail, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -107,10 +130,10 @@ class EventChecklistEdit(generic.UpdateView):
|
|||||||
ec.reviewed_by = None
|
ec.reviewed_by = None
|
||||||
ec.reviewed_at = None
|
ec.reviewed_at = None
|
||||||
ec.save()
|
ec.save()
|
||||||
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventChecklistEdit, self).get_context_data(**kwargs)
|
||||||
pk = self.kwargs.get('pk')
|
pk = self.kwargs.get('pk')
|
||||||
ec = models.EventChecklist.objects.get(pk=pk)
|
ec = models.EventChecklist.objects.get(pk=pk)
|
||||||
context['event'] = ec.event
|
context['event'] = ec.event
|
||||||
@@ -120,7 +143,7 @@ class EventChecklistEdit(generic.UpdateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistCreate(HSCreateView):
|
class EventChecklistCreate(generic.CreateView):
|
||||||
model = models.EventChecklist
|
model = models.EventChecklist
|
||||||
template_name = 'hs/event_checklist_form.html'
|
template_name = 'hs/event_checklist_form.html'
|
||||||
form_class = forms.EventChecklistForm
|
form_class = forms.EventChecklistForm
|
||||||
@@ -129,84 +152,64 @@ class EventChecklistCreate(HSCreateView):
|
|||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
epk = kwargs.get('pk')
|
epk = kwargs.get('pk')
|
||||||
event = models.Event.objects.get(pk=epk)
|
event = models.Event.objects.get(pk=epk)
|
||||||
|
|
||||||
# Check if RA exists
|
# Check if RA exists
|
||||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||||
|
|
||||||
if ra is None:
|
if ra is None:
|
||||||
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
|
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
|
||||||
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
|
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
|
||||||
return super().get(self)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
return super(EventChecklistCreate, self).get(self)
|
||||||
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_form(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
form = super(EventChecklistCreate, self).get_form(**kwargs)
|
||||||
if context['event'].venue:
|
epk = self.kwargs.get('pk')
|
||||||
context['venue'] = context['event'].venue
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class PowerTestDetail(generic.DetailView):
|
|
||||||
model = models.PowerTestRecord
|
|
||||||
template_name = 'hs/power_detail.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['page_title'] = f"Power Test Record for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class PowerTestEdit(generic.UpdateView):
|
|
||||||
model = models.PowerTestRecord
|
|
||||||
template_name = 'hs/power_form.html'
|
|
||||||
form_class = forms.PowerTestRecordForm
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
ec = self.get_object()
|
|
||||||
ec.reviewed_by = None
|
|
||||||
ec.reviewed_at = None
|
|
||||||
ec.save()
|
|
||||||
return reverse('pt_detail', kwargs={'pk': self.object.pk})
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
pk = self.kwargs.get('pk')
|
|
||||||
ec = models.PowerTestRecord.objects.get(pk=pk)
|
|
||||||
context['event'] = ec.event
|
|
||||||
context['edit'] = True
|
|
||||||
context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}'
|
|
||||||
get_related(context['form'], context)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class PowerTestCreate(HSCreateView):
|
|
||||||
model = models.PowerTestRecord
|
|
||||||
template_name = 'hs/power_form.html'
|
|
||||||
form_class = forms.PowerTestRecordForm
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
epk = kwargs.get('pk')
|
|
||||||
event = models.Event.objects.get(pk=epk)
|
event = models.Event.objects.get(pk=epk)
|
||||||
# Check if RA exists
|
form.instance.event = event
|
||||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
return form
|
||||||
|
|
||||||
if ra is None:
|
|
||||||
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.')
|
|
||||||
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
|
|
||||||
|
|
||||||
return super().get(self)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('pt_detail', kwargs={'pk': self.object.pk})
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(EventChecklistCreate, self).get_context_data(**kwargs)
|
||||||
if context['event'].venue:
|
epk = self.kwargs.get('pk')
|
||||||
context['venue'] = context['event'].venue
|
event = models.Event.objects.get(pk=epk)
|
||||||
if context['event'].riskassessment.power_mic:
|
context['event'] = event
|
||||||
context['power_mic'] = context['event'].riskassessment.power_mic
|
context['page_title'] = f'Create Event Checklist for Event {event.display_id}'
|
||||||
return context
|
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/hs_object_list.html'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
||||||
|
|
||||||
|
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'
|
||||||
|
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):
|
class HSList(generic.ListView):
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
@@ -214,10 +217,10 @@ class HSList(generic.ListView):
|
|||||||
template_name = 'hs/hs_list.html'
|
template_name = 'hs/hs_list.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).exclude(dry_hire=True).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super(HSList, self).get_context_data(**kwargs)
|
||||||
context['page_title'] = 'H&S Overview'
|
context['page_title'] = 'H&S Overview'
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -230,64 +233,3 @@ class RAPrint(PrintView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
|
context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class EventCheckIn(generic.CreateView, ModalURLMixin):
|
|
||||||
model = models.EventCheckIn
|
|
||||||
template_name = 'hs/eventcheckin_form.html'
|
|
||||||
form_class = forms.EventCheckInForm
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...!
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk'))
|
|
||||||
context['page_title'] = f'Check In to Event {context["event"].display_id}'
|
|
||||||
# get_related(context['form'], context)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventCheckInOverride(generic.CreateView):
|
|
||||||
model = models.EventCheckIn
|
|
||||||
template_name = 'hs/eventcheckin_form.html'
|
|
||||||
form_class = forms.EditCheckInForm
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk'))
|
|
||||||
context['page_title'] = f'Manually add Check In to Event {context["event"].display_id}'
|
|
||||||
context['manual'] = True
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventCheckInEdit(generic.UpdateView, ModalURLMixin):
|
|
||||||
model = models.EventCheckIn
|
|
||||||
template_name = 'hs/eventcheckin_form.html'
|
|
||||||
form_class = forms.EditCheckInForm
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
obj = self.get_object()
|
|
||||||
if not obj.person == self.request.user and not obj.event.mic == self.request.user:
|
|
||||||
return redirect(self.request.META.get('HTTP_REFERER', '/'))
|
|
||||||
return super().dispatch(request)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...!
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['event'] = self.object.event
|
|
||||||
context['page_title'] = f'Edit Check In for Event {context["event"].display_id}'
|
|
||||||
context['edit'] = True
|
|
||||||
# get_related(context['form'], context)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EventCheckOut(generic.RedirectView):
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
|
||||||
checkin = self.request.user.current_event()
|
|
||||||
if checkin:
|
|
||||||
checkin.end_time = timezone.now()
|
|
||||||
checkin.save()
|
|
||||||
return self.request.META.get('HTTP_REFERER', '/')
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytz
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django_ical.views import ICalFeed
|
from django_ical.views import ICalFeed
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
|
||||||
class CalendarICS(ICalFeed):
|
class CalendarICS(ICalFeed):
|
||||||
"""
|
"""
|
||||||
@@ -31,6 +33,7 @@ class CalendarICS(ICalFeed):
|
|||||||
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['subhire'] = request.GET.get('subhire', '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'
|
||||||
@@ -40,42 +43,46 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
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 '') + ('Subhires' if params['subhire'] else '') + '\n'
|
||||||
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
desc += "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
||||||
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] 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 = timezone.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
|
type_filters = 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)
|
type_filters = type_filters | Q(dry_hire=True, is_rig=True)
|
||||||
|
|
||||||
if params['non-rig']:
|
if params['non-rig']:
|
||||||
typeFilters = typeFilters | Q(is_rig=False)
|
type_filters = type_filters | Q(is_rig=False)
|
||||||
|
|
||||||
if params['rig']:
|
if params['rig']:
|
||||||
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
type_filters = type_filters | Q(is_rig=True, dry_hire=False)
|
||||||
|
|
||||||
statusFilters = Q(pk=None) # Need something that is false for every entry
|
status_filters = Q(pk=None) # Need something that is false for every entry
|
||||||
|
|
||||||
if params['cancelled']:
|
if params['cancelled']:
|
||||||
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
status_filters = status_filters | Q(status=models.Event.CANCELLED)
|
||||||
if params['provisional']:
|
if params['provisional']:
|
||||||
statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
|
status_filters = status_filters | Q(status=models.Event.PROVISIONAL)
|
||||||
if params['confirmed']:
|
if params['confirmed']:
|
||||||
statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
status_filters = status_filters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||||
|
|
||||||
filter = filter & typeFilters & statusFilters
|
filter = filter & type_filters & status_filters
|
||||||
|
|
||||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
events = models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||||
'venue', 'mic')
|
'venue', 'mic')
|
||||||
|
|
||||||
|
subhires = models.Subhire.objects.filter(status_filters).order_by('-start_date').select_related('person', 'organisation')
|
||||||
|
|
||||||
|
return list(chain(events, subhires))
|
||||||
|
|
||||||
def item_title(self, item):
|
def item_title(self, item):
|
||||||
title = ''
|
title = ''
|
||||||
|
|
||||||
@@ -106,30 +113,32 @@ class CalendarICS(ICalFeed):
|
|||||||
return item.latest_time
|
return item.latest_time
|
||||||
|
|
||||||
def item_location(self, item):
|
def item_location(self, item):
|
||||||
return item.venue
|
if hasattr(item, 'venue'):
|
||||||
|
return item.venue
|
||||||
|
return ""
|
||||||
|
|
||||||
def item_description(self, item):
|
def item_description(self, item):
|
||||||
# Create a nice information-rich description
|
# Create a nice information-rich description
|
||||||
# note: only making use of information available to "non-keyholders"
|
# note: only making use of information available to "non-keyholders"
|
||||||
|
|
||||||
tz = pytz.timezone(self.timezone)
|
|
||||||
|
|
||||||
desc = f'Rig ID = {item.display_id}\n'
|
desc = f'Rig ID = {item.display_id}\n'
|
||||||
desc += f'Event = {item.name}\n'
|
desc += f'Event = {item.name}\n'
|
||||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
if hasattr(item, 'venue'):
|
||||||
|
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 += f'Status = {item.get_status_display()}\n'
|
desc += f'Status = {item.get_status_display()}\n'
|
||||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
if hasattr(item, 'mic'):
|
||||||
|
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.meet_at:
|
if hasattr(item, 'meet_at') and 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'
|
timezone.make_aware(item.meet_at).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||||
if item.access_at:
|
if hasattr(item, 'access_at') and 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'
|
timezone.make_aware(item.access_at).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'
|
||||||
@@ -140,8 +149,6 @@ class CalendarICS(ICalFeed):
|
|||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.description:
|
if item.description:
|
||||||
desc += f'Event Description:\n{item.description}\n\n'
|
desc += f'Event Description:\n{item.description}\n\n'
|
||||||
# if item.notes: // Need to add proper keyholder checks before this gets put back
|
|
||||||
# desc += 'Notes:\n'+item.notes+'\n\n'
|
|
||||||
|
|
||||||
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
|
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ import copy
|
|||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import premailer
|
import premailer
|
||||||
import simplejson
|
|
||||||
import urllib
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from envparse import env
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -18,18 +11,15 @@ from django.core.exceptions import SuspiciousOperation
|
|||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.urls import reverse
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
|
|
||||||
from PyRIGS import decorators
|
from PyRIGS import decorators
|
||||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
|
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms, utils
|
||||||
|
|
||||||
__author__ = 'ghost'
|
__author__ = 'ghost'
|
||||||
|
|
||||||
@@ -47,14 +37,25 @@ class RigboardIndex(generic.TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WebCalendar(generic.TemplateView):
|
class WebCalendar(generic.ListView):
|
||||||
|
model = models.Event
|
||||||
template_name = 'calendar.html'
|
template_name = 'calendar.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['view'] = kwargs.get('view', '')
|
# use today's date for the calendar
|
||||||
context['date'] = kwargs.get('date', '')
|
d = utils.get_date(self.request.GET.get('month', None))
|
||||||
# context['page_title'] = "Calendar"
|
context['prev_month'] = utils.prev_month(d)
|
||||||
|
context['next_month'] = utils.next_month(d)
|
||||||
|
|
||||||
|
# Instantiate our calendar class with today's year and date
|
||||||
|
cal = utils.Calendar(d.year, d.month)
|
||||||
|
|
||||||
|
# Call the formatmonth method, which returns our calendar as a table
|
||||||
|
html_cal = cal.formatmonth(withyear=True)
|
||||||
|
# context['calendar'] = mark_safe(html_cal)
|
||||||
|
context['weeks'] = html_cal
|
||||||
|
context['page_title'] = d.strftime("%B %Y")
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -68,10 +69,6 @@ class EventDetail(generic.DetailView, ModalURLMixin):
|
|||||||
if self.object.dry_hire:
|
if self.object.dry_hire:
|
||||||
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
||||||
context['page_title'] = title
|
context['page_title'] = title
|
||||||
if is_ajax(self.request):
|
|
||||||
context['override'] = "base_ajax.html"
|
|
||||||
else:
|
|
||||||
context['override'] = 'base_assets.html'
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -207,27 +204,7 @@ 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()
|
qs = self.model.objects.event_search(self.request.GET.get('q', None), start, end, self.request.GET.get('status', ""))
|
||||||
if end != "":
|
|
||||||
filter &= Q(start_date__lte=end)
|
|
||||||
if start:
|
|
||||||
filter &= Q(start_date__gte=start)
|
|
||||||
|
|
||||||
q = self.request.GET.get('q', "")
|
|
||||||
objects = self.model.objects.all()
|
|
||||||
|
|
||||||
if q:
|
|
||||||
objects = self.model.objects.search(q)
|
|
||||||
|
|
||||||
status = self.request.GET.getlist('status', "")
|
|
||||||
|
|
||||||
if len(status) > 0:
|
|
||||||
filter &= Q(status__in=status)
|
|
||||||
|
|
||||||
qs = objects.filter(filter).order_by('-start_date')
|
|
||||||
|
|
||||||
# Preselect related for efficiency
|
|
||||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
|
||||||
|
|
||||||
if not qs.exists():
|
if not qs.exists():
|
||||||
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.")
|
||||||
@@ -384,41 +361,3 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
|||||||
context['to_name'] = self.request.GET.get('to_name', None)
|
context['to_name'] = self.request.GET.get('to_name', None)
|
||||||
context['target'] = 'event_authorise_form_preview'
|
context['target'] = 'event_authorise_form_preview'
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CreateForumThread(generic.base.RedirectView):
|
|
||||||
permanent = False
|
|
||||||
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
|
||||||
event = get_object_or_404(models.Event, pk=kwargs['pk'])
|
|
||||||
|
|
||||||
if event.forum_url:
|
|
||||||
return event.forum_url
|
|
||||||
|
|
||||||
params = {
|
|
||||||
'title': str(event),
|
|
||||||
'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}',
|
|
||||||
'category': 'rig-info'
|
|
||||||
}
|
|
||||||
return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}'
|
|
||||||
|
|
||||||
|
|
||||||
class RecieveForumWebhook(generic.View):
|
|
||||||
@method_decorator(csrf_exempt)
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}"
|
|
||||||
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed):
|
|
||||||
return HttpResponseForbidden('Invalid signature header')
|
|
||||||
# Check if this is the right kind of event. The webhook filters by category on the forum side
|
|
||||||
if request.headers.get('X-Discourse-Event') == "topic_created":
|
|
||||||
body = simplejson.loads(request.body.decode('utf-8'))
|
|
||||||
event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros
|
|
||||||
event = models.Event.objects.filter(pk=event_id).first()
|
|
||||||
if event:
|
|
||||||
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}"
|
|
||||||
event.save()
|
|
||||||
return HttpResponse(status=202)
|
|
||||||
return HttpResponse(status=204)
|
|
||||||
|
|||||||
62
RIGS/views/subhire.py
Normal file
62
RIGS/views/subhire.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views import generic
|
||||||
|
from django.db.models import Sum
|
||||||
|
from PyRIGS.views import ModalURLMixin, get_related
|
||||||
|
from RIGS import models, forms
|
||||||
|
from RIGS.views import EventArchive
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireDetail(generic.DetailView, ModalURLMixin):
|
||||||
|
template_name = 'subhire_detail.html'
|
||||||
|
model = models.Subhire
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = f"{self.object.display_id} | {self.object.name}"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireCreate(generic.CreateView):
|
||||||
|
model = models.Subhire
|
||||||
|
form_class = forms.SubhireForm
|
||||||
|
template_name = 'subhire_form.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = "New Subhire"
|
||||||
|
context['edit'] = True
|
||||||
|
form = context['form']
|
||||||
|
get_related(form, context)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('subhire_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireEdit(generic.UpdateView):
|
||||||
|
model = models.Subhire
|
||||||
|
form_class = forms.SubhireForm
|
||||||
|
template_name = 'subhire_form.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = f"Edit Subhire: {self.object.display_id} | {self.object.name}"
|
||||||
|
context['edit'] = True
|
||||||
|
form = context['form']
|
||||||
|
get_related(form, context)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('subhire_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireList(EventArchive):
|
||||||
|
template_name = 'subhire_list.html'
|
||||||
|
model = models.Subhire
|
||||||
|
paginate_by = 25
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['total_value'] = self.get_queryset().aggregate(sum=Sum('insurance_value'))['sum']
|
||||||
|
context['page_title'] = "Subhire List"
|
||||||
|
return context
|
||||||
4
app.json
4
app.json
@@ -4,7 +4,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
|
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
|
||||||
},
|
},
|
||||||
"stack": "heroku-22",
|
"stack": "heroku-20",
|
||||||
"env": {
|
"env": {
|
||||||
"DEBUG": {
|
"DEBUG": {
|
||||||
"required": true
|
"required": true
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"url": "heroku/nodejs"
|
"url": "heroku/nodejs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "heroku/python"
|
"url": "https://github.com/nottinghamtec/heroku-buildpack-python"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.19 on 2023-05-24 22:03
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('assets', '0027_asset_nickname'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='asset',
|
|
||||||
name='length',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='m', max_digits=10, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -135,7 +135,7 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
# Cable assets
|
# Cable assets
|
||||||
is_cable = models.BooleanField(default=False)
|
is_cable = models.BooleanField(default=False)
|
||||||
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
|
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
length = models.DecimalField(decimal_places=2, max_digits=10,
|
length = models.DecimalField(decimal_places=1, max_digits=10,
|
||||||
blank=True, null=True, help_text='m')
|
blank=True, null=True, help_text='m')
|
||||||
csa = models.DecimalField(decimal_places=2, max_digits=10,
|
csa = models.DecimalField(decimal_places=2, max_digits=10,
|
||||||
blank=True, null=True, help_text='mm²')
|
blank=True, null=True, help_text='mm²')
|
||||||
@@ -192,5 +192,5 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
return str(self.asset_id)
|
return str(self.asset_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self):
|
def name(self):
|
||||||
return f"{self.display_id} | {self.description}"
|
return f"{self.display_id} | {self.description}"
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
function onAuditClick(assetID) {
|
function onAuditClick(assetID) {
|
||||||
$('#' + assetID).remove();
|
$('#' + assetID).remove();
|
||||||
}
|
}
|
||||||
$('#modal').on('hidden.bs.modal', function (e) {
|
|
||||||
searchbar = document.getElementById('id_q');
|
|
||||||
searchbar.value = "";
|
|
||||||
setTimeout(searchbar.focus(), 2000);
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
<script src="{% static 'js/selects.js' %}"></script>
|
||||||
<script src="{% static 'js/easymde.min.js' %}"></script>
|
<script src="{% static 'js/easymde.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
@@ -34,10 +35,9 @@
|
|||||||
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
|
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static "js/tooltip.js" %}"></script>
|
|
||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(document).ready(function () {
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
setupMDE('#id_comments');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="asset_table_body">
|
<tbody id="asset_table_body">
|
||||||
{% for item in object_list %}
|
{% for item in object_list %}
|
||||||
<tr class="table-{{ item.status.display_class|default:'' }} assetRow" id="{{ item.asset_id }}">
|
<tr class="table-{{ item.status.display_class|default:'' }} assetRow">
|
||||||
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
|
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
|
||||||
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
|
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
|
||||||
<td class="assetCategory align-middle">{{ item.category }}</td>
|
<td class="assetCategory align-middle">{{ item.category }}</td>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load title_spaced from filters %}
|
{% load title_spaced from filters %}
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
{% if not nolabel %}<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>{%endif%}
|
<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
||||||
{% if append or prepend %}
|
{% if append or prepend %}
|
||||||
<div class="input-group {{col}} flex-nowrap">
|
<div class="input-group {{col}}">
|
||||||
{% if prepend %}
|
{% if prepend %}
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span class="input-group-text">{{ prepend }}</span>
|
<span class="input-group-text">{{ prepend }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% render_field field|add_class:'form-control' style=style %}
|
{% render_field field|add_class:'form-control' %}
|
||||||
{% if append %}
|
{% if append %}
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<span class="input-group-text">{{ append }}</span>
|
<span class="input-group-text">{{ append }}</span>
|
||||||
@@ -17,6 +17,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% render_field field|add_class:'form-control' class+=col style=style %}
|
{% render_field field|add_class:'form-control' class+=col %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||||
{% render_field form.purchase_price|add_class:'form-control'|set_data:"toggle:tooltip" value=object.purchase_price title="Ex. VAT" %}
|
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
|
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||||
{% render_field form.replacement_cost|add_class:'form-control'|set_data:"toggle:tooltip" value=object.replacement_cost title="Ex. VAT" %}
|
{% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -38,17 +38,3 @@ def test_asset(db, category, status):
|
|||||||
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
|
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
|
||||||
yield asset
|
yield asset
|
||||||
asset.delete()
|
asset.delete()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_status_2(db):
|
|
||||||
status = models.AssetStatus.objects.create(name="Lost", should_show=False)
|
|
||||||
yield status
|
|
||||||
status.delete()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_asset_2(db, category, test_status_2):
|
|
||||||
asset, created = models.Asset.objects.get_or_create(asset_id="10", description="Working Mic", status=test_status_2, category=category, date_acquired=datetime.date(2001, 10, 20), replacement_cost=1000)
|
|
||||||
yield asset
|
|
||||||
asset.delete()
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class AssetForm(FormPage):
|
|||||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||||
'comments': (regions.TextBox, (By.ID, 'id_comments')),
|
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
import pytest
|
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
@@ -54,45 +53,45 @@ class TestAssetList(AutoLoginTest):
|
|||||||
self.assertEqual("10", asset_ids[2])
|
self.assertEqual("10", asset_ids[2])
|
||||||
self.assertEqual("C1", asset_ids[3])
|
self.assertEqual("C1", asset_ids[3])
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
self.page.set_query("10")
|
||||||
|
self.page.search()
|
||||||
|
self.assertTrue(len(self.page.assets) == 1)
|
||||||
|
self.assertEqual("Working Mic", self.page.assets[0].description)
|
||||||
|
self.assertEqual("10", self.page.assets[0].id)
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="Fails on CI for unknown reason", raises=AssertionError)
|
self.page.set_query("light")
|
||||||
def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type):
|
self.page.search()
|
||||||
page = pages.AssetList(logged_in_browser.driver, live_server.url).open()
|
self.assertTrue(len(self.page.assets) == 1)
|
||||||
page.set_query(test_asset.asset_id)
|
self.assertEqual("A light", self.page.assets[0].description)
|
||||||
page.search()
|
|
||||||
assert len(page.assets) == 1
|
|
||||||
assert page.assets[0].description == test_asset.description
|
|
||||||
assert page.assets[0].id == test_asset.asset_id
|
|
||||||
|
|
||||||
page.set_query(test_asset.description)
|
self.page.set_query("Random string")
|
||||||
page.search()
|
self.page.search()
|
||||||
assert len(page.assets) == 1
|
self.assertTrue(len(self.page.assets) == 0)
|
||||||
assert page.assets[0].description == test_asset.description
|
|
||||||
|
|
||||||
page.set_query("Random string")
|
self.page.set_query("")
|
||||||
page.search()
|
self.page.search()
|
||||||
assert len(page.assets) == 0
|
# Only working stuff shown by default
|
||||||
|
self.assertTrue(len(self.page.assets) == 2)
|
||||||
|
|
||||||
page.set_query("")
|
self.page.status_selector.toggle()
|
||||||
page.search()
|
self.assertTrue(self.page.status_selector.is_open)
|
||||||
# Only working stuff shown by default
|
self.page.status_selector.select_all()
|
||||||
assert len(page.assets) == 1
|
self.page.status_selector.toggle()
|
||||||
|
self.assertFalse(self.page.status_selector.is_open)
|
||||||
|
self.page.filter()
|
||||||
|
self.assertTrue(len(self.page.assets) == 4)
|
||||||
|
|
||||||
page.status_selector.toggle()
|
self.page.category_selector.toggle()
|
||||||
assert page.status_selector.is_open
|
self.assertTrue(self.page.category_selector.is_open)
|
||||||
page.status_selector.select_all()
|
self.page.category_selector.set_option("Sound", True)
|
||||||
page.status_selector.toggle()
|
self.page.category_selector.close()
|
||||||
assert not page.status_selector.is_open
|
self.assertFalse(self.page.category_selector.is_open)
|
||||||
page.filter()
|
self.page.filter()
|
||||||
assert len(page.assets) == 2
|
self.assertTrue(len(self.page.assets) == 2)
|
||||||
|
asset_ids = list(map(lambda x: x.id, self.page.assets))
|
||||||
page.category_selector.toggle()
|
self.assertEqual("1", asset_ids[0])
|
||||||
assert page.category_selector.is_open
|
self.assertEqual("10", asset_ids[1])
|
||||||
page.category_selector.set_option(category.name, True)
|
|
||||||
page.category_selector.close()
|
|
||||||
assert not page.category_selector.is_open
|
|
||||||
page.filter()
|
|
||||||
assert len(page.assets) == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):
|
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):
|
||||||
@@ -196,7 +195,7 @@ class TestAssetForm(AutoLoginTest):
|
|||||||
# self.assertTrue(self.page.parent_selector.options[0].selected)
|
# self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||||
self.page.parent_selector.toggle()
|
self.page.parent_selector.toggle()
|
||||||
|
|
||||||
self.assertFalse(self.driver.find_element(By.ID, 'cable-table').is_displayed())
|
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||||
|
|
||||||
self.page.submit()
|
self.page.submit()
|
||||||
self.assertTrue(self.page.success)
|
self.assertTrue(self.page.success)
|
||||||
@@ -351,7 +350,7 @@ class TestAssetAudit(AutoLoginTest):
|
|||||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||||
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
||||||
self.page.modal.close()
|
self.page.modal.close()
|
||||||
self.assertFalse(self.driver.find_element(By.ID, 'modal').is_displayed())
|
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||||
# Make sure audit log was NOT filled out
|
# Make sure audit log was NOT filled out
|
||||||
audited = models.Asset.objects.get(asset_id=asset_row.id)
|
audited = models.Asset.objects.get(asset_id=asset_row.id)
|
||||||
assert audited.last_audited_by is None
|
assert audited.last_audited_by is None
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ function fonts(done) {
|
|||||||
function styles(done) {
|
function styles(done) {
|
||||||
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
|
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
|
||||||
return gulp.src(['pipeline/source_assets/scss/**/*.scss',
|
return gulp.src(['pipeline/source_assets/scss/**/*.scss',
|
||||||
'node_modules/fullcalendar/main.css',
|
|
||||||
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
|
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
|
||||||
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
|
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
|
||||||
'node_modules/easymde/dist/easymde.min.css'
|
'node_modules/easymde/dist/easymde.min.css'
|
||||||
@@ -59,7 +58,6 @@ function scripts() {
|
|||||||
'node_modules/html5sortable/dist/html5sortable.min.js',
|
'node_modules/html5sortable/dist/html5sortable.min.js',
|
||||||
'node_modules/clipboard/dist/clipboard.min.js',
|
'node_modules/clipboard/dist/clipboard.min.js',
|
||||||
'node_modules/moment/moment.js',
|
'node_modules/moment/moment.js',
|
||||||
'node_modules/fullcalendar/main.js',
|
|
||||||
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
|
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
|
||||||
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
|
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
|
||||||
'node_modules/easymde/dist/easymde.min.js',
|
'node_modules/easymde/dist/easymde.min.js',
|
||||||
@@ -79,7 +77,7 @@ function browserSync(done) {
|
|||||||
spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'});
|
spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'});
|
||||||
// TODO Wait for Django server to come up before browsersync, it seems inconsistent
|
// TODO Wait for Django server to come up before browsersync, it seems inconsistent
|
||||||
browsersync.init({
|
browsersync.init({
|
||||||
notify: true,
|
notify: false,
|
||||||
open: false,
|
open: false,
|
||||||
port: 8001,
|
port: 8001,
|
||||||
proxy: '127.0.0.1:8000'
|
proxy: '127.0.0.1:8000'
|
||||||
|
|||||||
2752
package-lock.json
generated
2752
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@
|
|||||||
"clipboard": "^2.0.8",
|
"clipboard": "^2.0.8",
|
||||||
"cssnano": "^5.0.13",
|
"cssnano": "^5.0.13",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
"fullcalendar": "^5.10.1",
|
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-flatten": "^0.4.0",
|
"gulp-flatten": "^0.4.0",
|
||||||
@@ -28,13 +27,13 @@
|
|||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"konami": "^1.6.3",
|
"konami": "^1.6.3",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"node-sass": "^9.0.0",
|
"node-sass": "^7.0.3",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.5",
|
||||||
"uglify-js": "^3.14.5"
|
"uglify-js": "^3.14.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browser-sync": "^3.0.2"
|
"browser-sync": "^2.27.11"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gulp": "gulp",
|
"gulp": "gulp",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ function initPicker(obj) {
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
//console.log(obj.data);
|
|
||||||
if (!obj.data('noclear')) {
|
if (!obj.data('noclear')) {
|
||||||
obj.prepend($("<option></option>")
|
obj.prepend($("<option></option>")
|
||||||
.attr("value",'')
|
.attr("value",'')
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ function setupItemTable(items_json) {
|
|||||||
newitem = -1;
|
newitem = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nl2br(str, is_xhtml) {
|
||||||
|
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
|
||||||
|
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
return $('<div/>').text(str).html();
|
return $('<div/>').text(str).html();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ Date.prototype.getISOString = function () {
|
|||||||
var dd = this.getDate().toString();
|
var dd = this.getDate().toString();
|
||||||
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
|
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
|
||||||
};
|
};
|
||||||
jQuery(document).ready(function () {
|
$(document).ready(function () {
|
||||||
jQuery(document).on('click', '.modal-href', function (e) {
|
$(document).on('click', '.modal-href', function (e) {
|
||||||
$link = jQuery(this);
|
$link = $(this);
|
||||||
// Anti modal inception
|
// Anti modal inception
|
||||||
if ($link.parents('#modal').length == 0) {
|
if ($link.parents('#modal').length == 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
modaltarget = $link.data('target');
|
modaltarget = $link.data('target');
|
||||||
modalobject = "";
|
modalobject = "";
|
||||||
jQuery('#modal').load($link.attr('href'), function (e) {
|
$('#modal').load($link.attr('href'), function (e) {
|
||||||
jQuery('#modal').modal();
|
$('#modal').modal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -23,7 +23,6 @@ jQuery(document).ready(function () {
|
|||||||
s.type = 'text/javascript';
|
s.type = 'text/javascript';
|
||||||
document.body.appendChild(s);
|
document.body.appendChild(s);
|
||||||
s.src = '{% static "js/asteroids.min.js"%}';
|
s.src = '{% static "js/asteroids.min.js"%}';
|
||||||
ga('send', 'event', 'easter_egg', 'activated');
|
|
||||||
}
|
}
|
||||||
easter_egg.load();
|
easter_egg.load();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,8 +77,17 @@
|
|||||||
border-collapse: separate !important;
|
border-collapse: separate !important;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
}
|
}
|
||||||
|
#event_table tr th {
|
||||||
|
border-right: 0 !important;
|
||||||
|
}
|
||||||
|
#event_table tr td {
|
||||||
|
border-left: 0 !important;
|
||||||
|
}
|
||||||
|
#event_table tr td:not(:last-child) {
|
||||||
|
border-right: 0 !important;
|
||||||
|
}
|
||||||
@each $color, $value in $theme-colors {
|
@each $color, $value in $theme-colors {
|
||||||
table.table-#{$color} {
|
.table-#{$color} {
|
||||||
> td,th {
|
> td,th {
|
||||||
border: 0.3em solid theme-color-level($color, -6) !important;
|
border: 0.3em solid theme-color-level($color, -6) !important;
|
||||||
}
|
}
|
||||||
@@ -87,11 +96,6 @@
|
|||||||
background-color: #222 !important;
|
background-color: #222 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#event_row.table-#{$color} {
|
|
||||||
border: 0.3em solid theme-color-level($color, -6) !important;
|
|
||||||
background-color: #222 !important;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
del {
|
del {
|
||||||
color: black;
|
color: black;
|
||||||
@@ -152,7 +156,4 @@
|
|||||||
.modal {
|
.modal {
|
||||||
overflow-y: auto !important; //Bootstrap Dark Theme overrides this to none for some insane reason so we need to change it back
|
overflow-y: auto !important; //Bootstrap Dark Theme overrides this to none for some insane reason so we need to change it back
|
||||||
}
|
}
|
||||||
.text-muted {
|
|
||||||
color: #c9c9c9 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,12 +281,7 @@ html.embedded {
|
|||||||
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
||||||
padding-right: 1rem !important;
|
padding-right: 1rem !important;
|
||||||
}
|
}
|
||||||
// New implementation of class dropped in Bootstrap 3
|
|
||||||
.dl-horizontal {
|
.badge-purple, .bg-purple {
|
||||||
display: grid;
|
background-color: #800080 !important;
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 0.7rem 0;
|
|
||||||
}
|
|
||||||
.dl-horizontal > dd, .dl-horizontal .markdown > p {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<a class="skip-link" href='#main'>Skip to content</a>
|
<a class="skip-link" href='#main'>Skip to content</a>
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
{% if request.user.current_event %}
|
|
||||||
<div class="bg-primary d-flex justify-content-between align-items-center"><span class="ml-2">You are currently checked in to <a href="{{request.user.current_event.event.get_absolute_url}}" class="text-white">{{request.user.current_event.event}}</a></span><a href="{% url 'event_checkout'%}" class="btn btn-warning">Check Out</a></div>
|
|
||||||
{% endif %}
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
|
<a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
data = $(this).serialize();
|
data = $(this).serialize();
|
||||||
action = $(this).attr('action');
|
action = $(this).attr('action');
|
||||||
|
console.log(action)
|
||||||
$.post(action, data, function(resp) {
|
$.post(action, data, function(resp) {
|
||||||
$('#modal').html(resp);
|
$('#modal').html(resp);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,13 +8,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<h1 class="col-sm-12 pb-3">R<small class="text-muted">ig</small> I<small class="text-muted">nformation</small> G<small class="text-muted">athering</small> S<small class="text-muted">ystem</small></h1>
|
<h1 class="col-sm-12 pb-3">R<small class="text-muted">ig</small> I<small class="text-muted">nformation</small> G<small class="text-muted">athering</small> S<small class="text-muted">ystem</small></h1>
|
||||||
<h2 class="col-sm-12 pb-3">Welcome back {{ user.get_full_name }}, there {%if rig_count == 1 %}is one rig coming up{%else%}are {{ rig_count|apnumber }} rigs coming up.{%endif%}</h2>
|
<h2 class="col-sm-12 pb-3">Welcome back {{ user.get_full_name }}, there {%if rig_count == 1 %}is one rig coming up{%else%}are {{ rig_count|apnumber }} rigs coming up.{%endif%}</h2>
|
||||||
{% if now %}
|
|
||||||
<div class="col-sm-12 alert alert-primary rounded-0 mx-auto">
|
|
||||||
{% for event in now %}
|
|
||||||
Event {{ event }} is happening today! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="col-sm-4 mb-3">
|
<div class="col-sm-4 mb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<img class="card-img-top d-none d-sm-block" src="{% static 'imgs/rigs.jpg' %}" alt="Some lights and haze, very purple" style="height: 150px; object-fit: cover;">
|
<img class="card-img-top d-none d-sm-block" src="{% static 'imgs/rigs.jpg' %}" alt="Some lights and haze, very purple" style="height: 150px; object-fit: cover;">
|
||||||
@@ -31,6 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 mb-3">
|
<div class="col-sm-4 mb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
{% now "m-d" as todays_date %}
|
||||||
<img class="card-img-top d-none d-sm-block" src="{% if todays_date == '04-01' %}{% static 'imgs/tappytaptap.gif' %}{%else%}{% static 'imgs/assets.jpg' %}{%endif%}" alt="M32 sound desk close up of the faders" style="height: 150px; object-fit: cover;">
|
<img class="card-img-top d-none d-sm-block" src="{% if todays_date == '04-01' %}{% static 'imgs/tappytaptap.gif' %}{%else%}{% static 'imgs/assets.jpg' %}{%endif%}" alt="M32 sound desk close up of the faders" style="height: 150px; object-fit: cover;">
|
||||||
<h4 class="card-header">Asset Database</h4>
|
<h4 class="card-header">Asset Database</h4>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
|
|||||||
@@ -105,10 +105,6 @@ class TrainingItem(models.Model):
|
|||||||
def display_id(self):
|
def display_id(self):
|
||||||
return f"{self.category.reference_number}.{self.reference_number}"
|
return f"{self.category.reference_number}.{self.reference_number}"
|
||||||
|
|
||||||
@property
|
|
||||||
def display_name(self):
|
|
||||||
return f"{self.display_id} | {self.name}"
|
|
||||||
|
|
||||||
@display_id.filter
|
@display_id.filter
|
||||||
@classmethod
|
@classmethod
|
||||||
def display_id(cls, lookup, value):
|
def display_id(cls, lookup, value):
|
||||||
@@ -373,7 +369,7 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
|
|||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('trainee_detail', kwargs={'pk': self.trainee_id})
|
return reverse('trainee_detail', kwargs={'pk': self.trainee.pk})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["trainee", "level"]
|
unique_together = ["trainee", "level"]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<br><small>{{ item.description }}</small>
|
<br><small>{{ item.description }}</small>
|
||||||
{% if item.prerequisites.exists %}
|
{% if item.prerequisites.exists %}
|
||||||
<div class="ml-3 font-italic">
|
<div class="ml-3 font-italic">
|
||||||
<p class="text-info mb-0">Competency Assessment Prerequisites:</p>
|
<p class="text-info mb-0">Passed Out Prerequisites:</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for p in item.prerequisites.all %}
|
{% for p in item.prerequisites.all %}
|
||||||
<li>{{p}}</li>
|
<li>{{p}}</li>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<spacer length="4" />
|
<spacer length="4" />
|
||||||
<para>{{ item.description }}</para>
|
<para>{{ item.description }}</para>
|
||||||
{% if item.prerequisites.exists %}
|
{% if item.prerequisites.exists %}
|
||||||
<h4>Competency Assessment Prerequisites:</h4>
|
<h4>Prerequisites:</h4>
|
||||||
<ul bulletFontSize="5">
|
<ul bulletFontSize="5">
|
||||||
{% for p in item.prerequisites.all %}
|
{% for p in item.prerequisites.all %}
|
||||||
<li><para>{{p}}</para></li>
|
<li><para>{{p}}</para></li>
|
||||||
|
|||||||
@@ -78,10 +78,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %}</li>{% endfor %}</ul></td>
|
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||||
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %}</li>{% endfor %}</ul></td>
|
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||||
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %}</li>{% endfor %}</ul></td>
|
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
<p>Please Note:</p>
|
<p>Please Note:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Technical Assistant status is automatically valid when the item requirements are met.</li>
|
<li>Technical Assistant status is automatically valid when the item requirements are met.</li>
|
||||||
<li>Technician status is also automatic. Notification of completion should be made at the next general meeting.</li>
|
<li>Technician status is also automatic, but notification of status should be made at the next general meeting, at which point 'approval' should be granted on the system.</li>
|
||||||
<li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li>
|
<li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<sup>Correct as of 24th May 2023, check the Training Policy.</sup>
|
<sup>Correct as of 3rd September 2021, check the Training Policy.</sup>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for level in object_list %}
|
{% for level in object_list %}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def confirm_button(user, trainee, level):
|
|||||||
if level.user_has_requirements(trainee):
|
if level.user_has_requirements(trainee):
|
||||||
string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>"
|
string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>"
|
||||||
if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'):
|
if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'):
|
||||||
string += f"<a class='btn btn-info' href='{reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk})}'>Confirm</a>"
|
string += "<a class='btn btn-info' href='{}'>Confirm</a>".format(reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk}))
|
||||||
return mark_safe(string)
|
return mark_safe(string)
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
|||||||
page.submit()
|
page.submit()
|
||||||
assert page.success
|
assert page.success
|
||||||
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
|
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
|
||||||
assert qualification.supervisor_id == supervisor.pk
|
assert qualification.supervisor.pk == supervisor.pk
|
||||||
assert qualification.date == date
|
assert qualification.date == date
|
||||||
assert qualification.notes == "A note"
|
assert qualification.notes == "A note"
|
||||||
assert qualification.depth == models.TrainingItemQualification.STARTED
|
assert qualification.depth == models.TrainingItemQualification.STARTED
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def test_add_qualification_reversion(admin_client, trainee, training_item, super
|
|||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
qual = models.TrainingItemQualification.objects.last()
|
qual = models.TrainingItemQualification.objects.last()
|
||||||
assert qual is not None
|
assert qual is not None
|
||||||
assert training_item.pk == qual.item_id
|
assert training_item.pk == qual.item.pk
|
||||||
# Ensure only one revision has been created
|
# Ensure only one revision has been created
|
||||||
assert Revision.objects.count() == 1
|
assert Revision.objects.count() == 1
|
||||||
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})
|
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ class ItemListExport(PrintListView):
|
|||||||
model = models.TrainingItem
|
model = models.TrainingItem
|
||||||
template_name = 'item_list.xml'
|
template_name = 'item_list.xml'
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.model.objects.filter(active=True)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['filename'] = "TrainingItemList.pdf"
|
context['filename'] = "TrainingItemList.pdf"
|
||||||
@@ -265,5 +262,5 @@ class ItemQualifications(generic.ListView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["page_title"] = f"People Qualified In {models.TrainingItem.objects.get(pk=self.kwargs['pk'])}"
|
context["page_title"] = f"People Qualified In {self.object_list[0].item}"
|
||||||
return context
|
return context
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user