mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-08 07:59:42 +00:00
Compare commits
34 Commits
subhire
...
f35ce88acc
| Author | SHA1 | Date | |
|---|---|---|---|
|
f35ce88acc
|
|||
|
ee5468fdd7
|
|||
|
1ce6ec3284
|
|||
|
677f352524
|
|||
|
8b3102b136
|
|||
|
e2b1dc1d05
|
|||
|
c9ba228bd2
|
|||
|
9f4cd41d23
|
|||
|
2049d0f76d
|
|||
|
29db3b5a0c
|
|||
|
53b09e47b8
|
|||
|
097e7c2481
|
|||
| 16874073e9 | |||
|
|
d03a4e115f | ||
| e1b87b412a | |||
| 54b44404ba | |||
|
|
26942b80dd | ||
|
|
888300490c | ||
| 9201f9d896 | |||
| 9fae129e26 | |||
|
|
8d45e260dd | ||
| 7d8dddb952 | |||
| 1104f10c91 | |||
| 3d5efba0af | |||
| 9a44aaf557 | |||
| 550eff83ee | |||
| bf3da5ae25 | |||
| 87aa87bc0f | |||
| 01a0b8f831 | |||
|
724762a1e8
|
|||
|
6ea5dc9698
|
|||
|
|
eb45db8950 | ||
|
b1a2859f1b
|
|||
|
dc71c2de62
|
5
.github/workflows/django.yml
vendored
5
.github/workflows/django.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
@@ -41,8 +42,8 @@ jobs:
|
||||
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||
pipenv run python3 manage.py collectstatic --noinput
|
||||
- name: Run Tests
|
||||
run: pipenv run pytest -n auto -vv --cov
|
||||
- uses: actions/upload-artifact@v2
|
||||
run: pipenv run pytest -n auto --cov
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: failure-screenshots ${{ matrix.test-group }}
|
||||
|
||||
23
Pipfile
23
Pipfile
@@ -19,9 +19,10 @@ cssutils = "~=1.0.2"
|
||||
dj-database-url = "~=0.5.0"
|
||||
dj-static = "~=0.0.6"
|
||||
Django = "~=3.2"
|
||||
django-debug-toolbar = "~=3.2"
|
||||
django-debug-toolbar = "~=4.0.0"
|
||||
django-filter = "~=2.4.0"
|
||||
django-ical = "~=1.8.3"
|
||||
django-ical = "~=1.7.1"
|
||||
django-recurrence = "~=1.10.3"
|
||||
django-registration-redux = "~=2.9"
|
||||
django-reversion = "~=3.0.9"
|
||||
django-widget-tweaks = "~=1.4.8"
|
||||
@@ -46,7 +47,7 @@ python-dateutil = "~=2.8.1"
|
||||
pytoml = "~=0.1.21"
|
||||
pytz = "~=2020.5"
|
||||
reportlab = "*"
|
||||
requests = "~=2.25.1"
|
||||
requests = "~=2.31.0"
|
||||
retrying = "~=1.3.3"
|
||||
simplejson = "~=3.17.2"
|
||||
six = "~=1.15.0"
|
||||
@@ -55,7 +56,7 @@ sqlparse = "~=0.4.2"
|
||||
static3 = "~=0.7.0"
|
||||
svg2rlg = "~=0.3"
|
||||
tini = "~=3.0.1"
|
||||
tornado = "~=6.1"
|
||||
tornado = "~=6.3"
|
||||
urllib3 = "~=1.26.5"
|
||||
whitenoise = "~=5.2.0"
|
||||
yolk = "~=0.4.3"
|
||||
@@ -75,9 +76,10 @@ django-hCaptcha = "*"
|
||||
importlib-metadata = "*"
|
||||
django-hcaptcha = "*"
|
||||
"z3c.rml" = "*"
|
||||
pikepdf = "*"
|
||||
django-queryable-properties = "*"
|
||||
django-mass-edit = "*"
|
||||
selenium = "~=3.141.0"
|
||||
selenium = "~=4.9.1"
|
||||
|
||||
[dev-packages]
|
||||
pycodestyle = "~=2.9.1"
|
||||
@@ -89,11 +91,14 @@ pluggy = "*"
|
||||
pytest-splinter = "*"
|
||||
pytest = "*"
|
||||
pytest-reverse = "*"
|
||||
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
||||
PyPOM = {extras = [ "splinter",], version = "*"}
|
||||
|
||||
[requires]
|
||||
python_version = "3.10"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
[dev-packages.pytest-xdist]
|
||||
extras = [ "psutil",]
|
||||
version = "*"
|
||||
|
||||
[dev-packages.PyPOM]
|
||||
extras = [ "splinter",]
|
||||
version = "*"
|
||||
|
||||
932
Pipfile.lock
generated
932
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,6 @@ STAGING = env('STAGING', cast=bool, default=False)
|
||||
CI = env('CI', cast=bool, default=False)
|
||||
|
||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||
CSRF_TRUSTED_ORIGINS = []
|
||||
|
||||
if STAGING:
|
||||
ALLOWED_HOSTS.append('.herokuapp.com')
|
||||
@@ -36,7 +35,6 @@ if DEBUG:
|
||||
ALLOWED_HOSTS.append('localhost')
|
||||
ALLOWED_HOSTS.append('example.com')
|
||||
ALLOWED_HOSTS.append('127.0.0.1')
|
||||
CSRF_TRUSTED_ORIGINS.append('.preview.app.github.dev')
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
if not DEBUG:
|
||||
@@ -66,7 +64,7 @@ INSTALLED_APPS = (
|
||||
'assets',
|
||||
'training',
|
||||
|
||||
'debug_toolbar',
|
||||
# 'debug_toolbar',
|
||||
'registration',
|
||||
'reversion',
|
||||
'widget_tweaks',
|
||||
@@ -77,7 +75,7 @@ INSTALLED_APPS = (
|
||||
MIDDLEWARE = (
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
'reversion.middleware.RevisionMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
@@ -220,8 +218,6 @@ TIME_ZONE = 'Europe/London'
|
||||
|
||||
FORMAT_MODULE_PATH = 'PyRIGS.formats'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
@@ -266,3 +262,10 @@ TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
||||
|
||||
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():
|
||||
os.mkdir("screenshots")
|
||||
self.driver.save_screenshot(screenshot_file)
|
||||
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
||||
print(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr)
|
||||
raise e
|
||||
|
||||
return wrapper_func
|
||||
|
||||
@@ -59,8 +59,8 @@ class TestSampleDataGenerator(TestCase):
|
||||
assert Asset.objects.all().count() > 50
|
||||
assert Event.objects.all().count() > 100
|
||||
call_command('deleteSampleData')
|
||||
assert Asset.objects.all().count() == 0
|
||||
assert Event.objects.all().count() == 0
|
||||
assert not Asset.objects.all().exists()
|
||||
assert not Event.objects.all().exists()
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@@ -76,9 +76,9 @@ def test_unauthenticated(client): # Nothing should be available to the unauthen
|
||||
assertTemplateUsed(response, 'login_redirect.html')
|
||||
else:
|
||||
if "embed" in str(url):
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
expected_url = f"{reverse('login_embed')}?next={request_url}"
|
||||
else:
|
||||
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
||||
expected_url = f"{reverse('login')}?next={request_url}"
|
||||
assertRedirects(response, expected_url)
|
||||
call_command('deleteSampleData')
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class Index(generic.TemplateView): # Displays the current rig count along with
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['rig_count'] = models.Event.objects.rig_count()
|
||||
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(dry_hire=True).exclude(status=models.Event.CANCELLED)
|
||||
return context
|
||||
|
||||
|
||||
@@ -133,11 +134,15 @@ class SecureAPIRequest(generic.View):
|
||||
results = []
|
||||
query = reduce(operator.and_, queries)
|
||||
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:
|
||||
name = o.display_name if hasattr(o, 'display_name') else o.name
|
||||
data = {
|
||||
'pk': o.pk,
|
||||
'value': o.pk,
|
||||
'text': o.name,
|
||||
'text': name,
|
||||
}
|
||||
try: # See if there is a valid update URL
|
||||
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
|
||||
@@ -182,7 +187,7 @@ class ModalURLMixin:
|
||||
url = reverse_lazy('closemodal')
|
||||
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
|
||||
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
|
||||
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
|
||||
messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'")
|
||||
else:
|
||||
url = reverse_lazy(detail, kwargs={
|
||||
'pk': self.object.pk,
|
||||
|
||||
@@ -11,8 +11,9 @@ For setup information and other such helpful stuff check the [Wiki](https://gith
|
||||
- PyRIGS: Base app, stores 'global' information
|
||||
- RIGS: Rigboard stuff - event calendar 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.
|
||||
- users: Our custom logic for registration and profiles. Semi-modular.
|
||||
- training: SoonTM
|
||||
|
||||
|
||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||
|
||||
@@ -20,6 +20,7 @@ admin.site.register(models.VatRate, VersionAdmin)
|
||||
admin.site.register(models.Event, VersionAdmin)
|
||||
admin.site.register(models.EventItem, VersionAdmin)
|
||||
admin.site.register(models.Invoice, VersionAdmin)
|
||||
admin.site.register(models.EventCheckIn)
|
||||
|
||||
|
||||
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
||||
@@ -153,8 +154,9 @@ class AssociateAdmin(VersionAdmin):
|
||||
|
||||
@admin.register(models.Profile)
|
||||
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
||||
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
|
||||
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login')
|
||||
list_display_links = ['username']
|
||||
list_filter = UserAdmin.list_filter + ('is_approved',)
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('Personal info'), {
|
||||
@@ -206,3 +208,8 @@ class RiskAssessmentAdmin(VersionAdmin):
|
||||
@admin.register(models.EventChecklist)
|
||||
class EventChecklistAdmin(VersionAdmin):
|
||||
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)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EventForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['items_json'].initial = self._get_items_json
|
||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
||||
@@ -121,23 +121,7 @@ class EventForm(forms.ModelForm):
|
||||
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
||||
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
||||
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
||||
'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__'
|
||||
'purchase_order', 'collector', 'forum_url']
|
||||
|
||||
|
||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||
@@ -147,7 +131,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
if self.cleaned_data.get('amount') != self.instance.event.total:
|
||||
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
|
||||
return super(BaseClientEventAuthorisationForm, self).clean()
|
||||
return super().clean()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -195,7 +179,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
|
||||
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'):
|
||||
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(EventRiskAssessmentForm, self).clean()
|
||||
return super().clean()
|
||||
|
||||
class Meta:
|
||||
model = models.RiskAssessment
|
||||
@@ -211,91 +195,49 @@ class EventChecklistForm(forms.ModelForm):
|
||||
if field.__class__ == forms.NullBooleanField:
|
||||
# Only display yes/no to user, the 'none' is only ever set in the background
|
||||
field.widget = forms.CheckboxInput()
|
||||
# Parsed from incoming form data by clean, then saved into models when the form is saved
|
||||
items = {}
|
||||
|
||||
related_models = {
|
||||
'venue': models.Venue,
|
||||
}
|
||||
|
||||
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 = {
|
||||
'venue': models.Venue,
|
||||
'power_mic': models.Profile,
|
||||
}
|
||||
|
||||
# Two possible formats
|
||||
def parsedatetime(self, date_string):
|
||||
try:
|
||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
|
||||
except ValueError:
|
||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
|
||||
|
||||
# There's probably a thousand better ways to do this, but this one is mine
|
||||
def clean(self):
|
||||
vehicles = {key: val for key, val in self.data.items()
|
||||
if key.startswith('vehicle')}
|
||||
for key in vehicles:
|
||||
pk = int(key.split('_')[1])
|
||||
driver_key = 'driver_' + str(pk)
|
||||
if (self.data[driver_key] == ''):
|
||||
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
||||
else:
|
||||
try:
|
||||
item = models.EventChecklistVehicle.objects.get(pk=pk)
|
||||
except models.EventChecklistVehicle.DoesNotExist:
|
||||
item = models.EventChecklistVehicle()
|
||||
|
||||
item.vehicle = vehicles['vehicle_' + str(pk)]
|
||||
item.driver = models.Profile.objects.get(pk=self.data[driver_key])
|
||||
item.full_clean('checklist')
|
||||
|
||||
# item does not have a database pk yet as it isn't saved
|
||||
self.items['v' + str(pk)] = item
|
||||
|
||||
crewmembers = {key: val for key, val in self.data.items()
|
||||
if key.startswith('crewmember')}
|
||||
other_fields = ['start', 'role', 'end']
|
||||
for key in crewmembers:
|
||||
pk = int(key.split('_')[1])
|
||||
|
||||
for field in other_fields:
|
||||
value = self.data[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:
|
||||
model = models.EventChecklist
|
||||
model = models.PowerTestRecord
|
||||
fields = '__all__'
|
||||
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)),
|
||||
outside=bool(random.getrandbits(1)))
|
||||
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, power_mic=random.choice(self.profiles),
|
||||
models.EventChecklist.objects.create(event=new_event,
|
||||
safe_parking=bool(random.getrandbits(1)),
|
||||
safe_packing=bool(random.getrandbits(1)),
|
||||
exits=bool(random.getrandbits(1)),
|
||||
@@ -287,6 +287,4 @@ class Command(BaseCommand):
|
||||
ear_plugs=bool(random.getrandbits(1)),
|
||||
hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot",
|
||||
earthing=bool(random.getrandbits(1)),
|
||||
pat=bool(random.getrandbits(1)),
|
||||
date=timezone.now(), venue=random.choice(self.venues))
|
||||
|
||||
71
RIGS/migrations/0046_create_powertests.py
Normal file
71
RIGS/migrations/0046_create_powertests.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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),
|
||||
]
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
44
RIGS/migrations/0047_auto_20230517_0944.py
Normal file
44
RIGS/migrations/0047_auto_20230517_0944.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# 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),
|
||||
]
|
||||
156
RIGS/migrations/0048_auto_20230518_1256.py
Normal file
156
RIGS/migrations/0048_auto_20230518_1256.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# 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',
|
||||
),
|
||||
]
|
||||
53
RIGS/migrations/0049_auto_20230529_1123.py
Normal file
53
RIGS/migrations/0049_auto_20230529_1123.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
19
RIGS/migrations/0050_event_forum_url.py
Normal file
19
RIGS/migrations/0050_event_forum_url.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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]),
|
||||
),
|
||||
]
|
||||
971
RIGS/models.py
Normal file
971
RIGS/models.py
Normal file
@@ -0,0 +1,971 @@
|
||||
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).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'
|
||||
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}"
|
||||
|
||||
|
||||
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
|
||||
@@ -1,4 +0,0 @@
|
||||
from .models import *
|
||||
from .finance import *
|
||||
from .hs import *
|
||||
from .events import *
|
||||
@@ -1,467 +0,0 @@
|
||||
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')
|
||||
]
|
||||
@@ -1,170 +0,0 @@
|
||||
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}"
|
||||
@@ -1,243 +0,0 @@
|
||||
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})"
|
||||
@@ -1,173 +0,0 @@
|
||||
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})
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -30,22 +30,11 @@
|
||||
{% if perms.RIGS.add_event %}
|
||||
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
|
||||
New Event</a>
|
||||
<a class="dropdown-item" href="{% url 'subhire_create' %}"><span class="fas fa-truck"></span>
|
||||
New Subhire</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% if perms.RIGS.view_riskassessment %}
|
||||
<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>
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'hs_list' %}">H&S</a></li>
|
||||
{% endif %}
|
||||
{% if perms.RIGS.view_invoice %}
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
@@ -1,131 +1,197 @@
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'js/moment.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// set some button listeners
|
||||
$('#today-button').click(function(){ calendar.today(); });
|
||||
$('#go-to-date-input').change(function(){
|
||||
if(moment($('#go-to-date-input').val()).isValid()){
|
||||
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{
|
||||
document.getElementById('go-to-date-button').classList.add('disabled');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block title %}Calendar{% 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%;
|
||||
}
|
||||
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
|
||||
{% endblock %}
|
||||
|
||||
.day {
|
||||
display:contents;
|
||||
}
|
||||
.day-label {
|
||||
grid-row-start: 1;
|
||||
text-align: right;
|
||||
margin:0;
|
||||
font-size: 1em !important;
|
||||
height: 1em;
|
||||
}
|
||||
{% block js %}
|
||||
<script src="{% static 'js/moment.js' %}"></script>
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
<script>
|
||||
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');
|
||||
|
||||
.week-day, .day-label, .event {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
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
|
||||
}
|
||||
|
||||
.event {
|
||||
background-color: #CCC;
|
||||
font-size: 0.8em !important;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
thisEvent = {
|
||||
'start': $(this).attr('earliest'),
|
||||
'end': end,
|
||||
'className': 'modal-href',
|
||||
'title': $(this).attr('title'),
|
||||
'url': $(this).attr('url'),
|
||||
'allDay': allDay
|
||||
}
|
||||
|
||||
.event-end {
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
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);
|
||||
|
||||
.event-start {
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
.week-day {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
// 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;
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.event {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
case 'timeGridWeek':
|
||||
$month.removeClass('active');
|
||||
$week.addClass('active');
|
||||
$day.removeClass('active');
|
||||
break;
|
||||
|
||||
[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>
|
||||
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
|
||||
$('#next-button').click(function(){ calendar.next(); });
|
||||
$('#prev-button').click(function(){ calendar.prev(); });
|
||||
$('#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(){
|
||||
if(moment($('#go-to-date-input').val()).isValid()){
|
||||
$('#go-to-date-button').prop('disabled', false);
|
||||
} else{
|
||||
$('#go-to-date-button').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
$('#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>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mb-1">
|
||||
<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="form-inline col-4">
|
||||
<div class="input-group">
|
||||
<input type="date" id="go-to-date-input" placeholder="Go to date..." class="form-control">
|
||||
<span class="input-group-append">
|
||||
<a class="btn btn-success" id="go-to-date-button">Go!</a>
|
||||
</span>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="pull-left">
|
||||
<span id="calendar-header" class="h2"></span>
|
||||
</div>
|
||||
<div class="form-inline float-right btn-page my-3">
|
||||
<div class="input-group mx-2">
|
||||
<input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
|
||||
<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>
|
||||
<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 %}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{% 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,7 +15,36 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'partials/archive_form.html' %}
|
||||
<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>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% with object_list as events %}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
|
||||
{% load markdown_tags %}
|
||||
{% load button from filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3 py-3">
|
||||
@@ -25,10 +27,12 @@
|
||||
{% include 'partials/hs_details.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
||||
<div class="col-md-8 py-3">
|
||||
{% include 'partials/auth_details.html' %}
|
||||
</div>
|
||||
{% if event.is_rig %}
|
||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
||||
<div class="col-md-8 py-3">
|
||||
{% include 'partials/auth_details.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not request.is_ajax and perms.RIGS.view_event %}
|
||||
<div class="col-sm-12 text-right">
|
||||
@@ -45,17 +49,48 @@
|
||||
<hr>
|
||||
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
||||
{% endif %}
|
||||
<h4>Event Items</h4>
|
||||
<br>
|
||||
{% include 'partials/item_table.html' %}
|
||||
</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>
|
||||
{% 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 %}
|
||||
<div class="col-sm-12 text-right">
|
||||
{% include 'partials/event_detail_buttons.html' %}
|
||||
|
||||
@@ -106,10 +106,6 @@
|
||||
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>
|
||||
</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>
|
||||
@@ -213,7 +209,7 @@
|
||||
<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' %}">
|
||||
{% if venue %}
|
||||
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
|
||||
<option value="{{venue.id}}" selected="selected" data-update_url="{% url 'venue_update' venue.id %}">{{ venue }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -235,7 +231,7 @@
|
||||
<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="col-sm-10">
|
||||
<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" %}
|
||||
@@ -250,7 +246,7 @@
|
||||
<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="col-sm-10">
|
||||
<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" %}
|
||||
@@ -338,12 +334,26 @@
|
||||
|
||||
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
||||
<label for="{{ form.purchase_order.id_for_label }}"
|
||||
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</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 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>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<div class="col-12 text-right my-3">
|
||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||
{% 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' %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,21 +32,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</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>
|
||||
<p>List vehicles and their drivers</p>
|
||||
<ul>
|
||||
{% for i in object.vehicles.all %}
|
||||
<li>{{i}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,168 +70,11 @@
|
||||
</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">
|
||||
{% button 'edit' url='ec_edit' pk=object.pk %}
|
||||
{% 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' %}
|
||||
</div>
|
||||
<div class="col-12 text-right">
|
||||
|
||||
@@ -13,57 +13,19 @@
|
||||
{% block preload_js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/selects.js' %}"></script>
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<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 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-12">
|
||||
{% include 'form_errors.html' %}
|
||||
{% if edit %}
|
||||
<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 %}
|
||||
|
||||
<form role="form" method="POST" action="{% if edit %}{% url 'ec_edit' pk=object.pk %}{% else %}{% url 'event_ec' pk=event.pk %}{% endif %}">
|
||||
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
|
||||
value="{{event.pk}}"/>
|
||||
{% csrf_token %}
|
||||
@@ -94,60 +56,12 @@
|
||||
<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="form-control selectpicker col-8" 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 %}
|
||||
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
|
||||
{% elif event.venue %}
|
||||
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</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>
|
||||
@@ -175,176 +89,6 @@
|
||||
</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="col-sm-12 text-right">
|
||||
{% button 'submit' %}
|
||||
|
||||
105
RIGS/templates/hs/eventcheckin_form.html
Normal file
105
RIGS/templates/hs/eventcheckin_form.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% 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,6 +12,7 @@
|
||||
<th scope="col">Dates</th>
|
||||
<th scope="col">RA</th>
|
||||
<th scope="col">Checklists</th>
|
||||
<th scope="col">Power Records</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -35,6 +36,14 @@
|
||||
<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>
|
||||
</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>
|
||||
{% empty %}
|
||||
<tr class="bg-warning text-dark">
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
{% 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 %}
|
||||
175
RIGS/templates/hs/power_detail.html
Normal file
175
RIGS/templates/hs/power_detail.html
Normal file
@@ -0,0 +1,175 @@
|
||||
{% 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.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 %}
|
||||
199
RIGS/templates/hs/power_form.html
Normal file
199
RIGS/templates/hs/power_form.html
Normal file
@@ -0,0 +1,199 @@
|
||||
{% 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() {
|
||||
$('#{{ 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() {
|
||||
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{% 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>
|
||||
@@ -47,6 +47,6 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</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>
|
||||
|
||||
@@ -47,5 +47,15 @@
|
||||
class="fas fa-pound-sign"></span>
|
||||
<span class="d-none d-sm-inline">Invoice</span></a>
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,15 @@
|
||||
<dt class="col-sm-6">PO</dt>
|
||||
<dd class="col-sm-6">{{ object.purchase_order }}</dd>
|
||||
{% 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,14 +20,16 @@
|
||||
{% else %}
|
||||
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% if event.has_checklist %}
|
||||
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span> {% if event.checklists.count > 1 %}({{event.checklists.count}}){% endif %}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
|
||||
{% 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 %}
|
||||
{% if perms.RIGS.view_invoice %}
|
||||
{% if event.invoice %}
|
||||
|
||||
@@ -12,7 +12,21 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||
<tr class="{% 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-->
|
||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||
<!--Dates & Times-->
|
||||
@@ -42,7 +56,7 @@
|
||||
<!---Details-->
|
||||
<td id="event_details" class="w-100">
|
||||
<h4>
|
||||
<a href="{{event.get_absolute_url}}">
|
||||
<a href="{% url 'event_detail' event.pk %}">
|
||||
{{ event.name }}
|
||||
</a>
|
||||
{% if event.venue %}
|
||||
|
||||
@@ -10,10 +10,18 @@
|
||||
<hr>
|
||||
<h5>Event Checklists:</h5>
|
||||
{% 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 %}
|
||||
<br/>
|
||||
{% endfor %}
|
||||
<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>
|
||||
{% 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/>
|
||||
{% endfor %}
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<button type="button" class="btn btn-success btn-sm item-add"
|
||||
data-toggle="modal"
|
||||
data-target="#itemModal">
|
||||
<span class="fas fa-plus"></span> Add Item
|
||||
<i class="fas fa-plus"></i> Add Item
|
||||
</button>
|
||||
</th>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
{% 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>
|
||||
@@ -1,76 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,202 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% 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,13 +171,10 @@ def title_spaced(string):
|
||||
@register.filter(needs_autoescape=True)
|
||||
def namewithnotes(obj, url, autoescape=True):
|
||||
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
|
||||
return mark_safe(obj.name + " <a href='{}'><span class='fas fa-sticky-note'></span></a>".format(reverse(url, kwargs={'pk': obj.pk})))
|
||||
return mark_safe(obj.name + f" <a href='{reverse(url, kwargs={'pk': obj.pk})}'><span class='fas fa-sticky-note'></span></a>")
|
||||
else:
|
||||
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)
|
||||
def linkornone(target, namespace=None, autoescape=True):
|
||||
@@ -186,7 +183,7 @@ def linkornone(target, namespace=None, autoescape=True):
|
||||
link = namespace + "://" + target
|
||||
else:
|
||||
link = target
|
||||
return mark_safe("<a href='{}' target='_blank'><span class='overflow-ellipsis'>{}</span></a>".format(link, str(target)))
|
||||
return mark_safe(f"<a href='{link}' target='_blank'><span class='overflow-ellipsis'>{target}</span></a>")
|
||||
else:
|
||||
return "None"
|
||||
|
||||
|
||||
@@ -43,15 +43,22 @@ def venue(db):
|
||||
|
||||
@pytest.fixture # TODO parameterise with Event sizes
|
||||
def checklist(basic_event, venue, admin_user, ra):
|
||||
checklist = models.EventChecklist.objects.create(event=basic_event, power_mic=admin_user, safe_parking=False,
|
||||
checklist = models.EventChecklist.objects.create(event=basic_event, safe_parking=False,
|
||||
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
||||
ear_plugs=False, hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
|
||||
extinguishers_location="Somewhere, I forgot",
|
||||
date=timezone.now(), venue=venue)
|
||||
yield checklist
|
||||
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
|
||||
def many_events(db, admin_user, scope="class"):
|
||||
many_events = {
|
||||
|
||||
@@ -114,7 +114,7 @@ class CreateEvent(FormPage):
|
||||
}
|
||||
|
||||
def select_event_type(self, type_name):
|
||||
self.find_element(By.XPATH, '//button[.="{}"]'.format(type_name)).click()
|
||||
self.find_element(By.XPATH, f'//button[.="{type_name}"]').click()
|
||||
|
||||
def item_row(self, ID):
|
||||
return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID))
|
||||
@@ -230,11 +230,6 @@ class CreateEventChecklist(FormPage):
|
||||
URL_TEMPLATE = 'event/{event_id}/checklist'
|
||||
|
||||
_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 = {
|
||||
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
|
||||
@@ -245,6 +240,20 @@ class CreateEventChecklist(FormPage):
|
||||
'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')),
|
||||
'hs_location': (regions.TextBox, (By.ID, 'id_hs_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')),
|
||||
'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')),
|
||||
'earthing': (regions.CheckBox, (By.ID, 'id_earthing')),
|
||||
@@ -263,58 +272,10 @@ class CreateEventChecklist(FormPage):
|
||||
'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
|
||||
def power_mic(self):
|
||||
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
|
||||
def success(self):
|
||||
return '{event_id}' not in self.driver.current_url
|
||||
|
||||
@@ -6,7 +6,7 @@ from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
|
||||
|
||||
class Header(Region):
|
||||
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):
|
||||
|
||||
@@ -318,7 +318,7 @@ class TestEventDuplicate(BaseRigboardTest):
|
||||
|
||||
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
|
||||
|
||||
# Check the new items are visible
|
||||
@@ -327,26 +327,25 @@ class TestEventDuplicate(BaseRigboardTest):
|
||||
self.assertIn("Test Item 2", table.text)
|
||||
self.assertIn("Test Item 3", table.text)
|
||||
|
||||
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)
|
||||
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)
|
||||
# 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,
|
||||
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
|
||||
|
||||
# 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,
|
||||
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
|
||||
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,
|
||||
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
|
||||
table = self.page.item_table # ID number is known, see above
|
||||
@@ -677,14 +676,6 @@ def small_ec(page, admin_user):
|
||||
page.ear_plugs = True
|
||||
page.hs_location = "The Moon"
|
||||
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):
|
||||
@@ -705,14 +696,15 @@ def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra)
|
||||
page.ear_plugs = True
|
||||
page.hs_location = "Death Valley"
|
||||
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
|
||||
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.pat = True
|
||||
page.source_rcd = True
|
||||
@@ -727,56 +719,15 @@ def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra)
|
||||
page.w1_polarity = True
|
||||
page.w1_voltage = 240
|
||||
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()
|
||||
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?
|
||||
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()
|
||||
|
||||
@@ -259,7 +259,7 @@ class TestPrintPaperwork(TestCase):
|
||||
|
||||
def test_login_redirect(client, django_user_model):
|
||||
request_url = reverse('event_embed', kwargs={'pk': 1})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
expected_url = f"{reverse('login_embed')}?next={request_url}"
|
||||
|
||||
# Request the page and check it redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
@@ -372,7 +372,8 @@ def test_ra_redirect(admin_client, admin_user, ra):
|
||||
|
||||
|
||||
class TestMarkdownTemplateTags(TestCase):
|
||||
markdown = open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")).read()
|
||||
with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md"), encoding="utf-8") as f:
|
||||
markdown = f.read()
|
||||
|
||||
def test_html_safe(self):
|
||||
html = markdown_filter(self.markdown)
|
||||
|
||||
55
RIGS/urls.py
55
RIGS/urls.py
@@ -43,8 +43,12 @@ urlpatterns = [
|
||||
|
||||
# Rigboard
|
||||
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
|
||||
re_path(r'^rigboard/calendar/$', login_required()(views.WebCalendar.as_view()),
|
||||
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
|
||||
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')),
|
||||
|
||||
|
||||
@@ -66,22 +70,6 @@ urlpatterns = [
|
||||
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
|
||||
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
|
||||
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
|
||||
|
||||
@@ -91,10 +79,8 @@ urlpatterns = [
|
||||
name='ra_detail'),
|
||||
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
||||
name='ra_edit'),
|
||||
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentList.as_view()),
|
||||
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>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.MarkReviewed.as_view()),
|
||||
name='ra_review', kwargs={'model': 'RiskAssessment'}),
|
||||
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()),
|
||||
@@ -103,10 +89,29 @@ urlpatterns = [
|
||||
name='ec_detail'),
|
||||
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
||||
name='ec_edit'),
|
||||
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistList.as_view()),
|
||||
name='ec_list'),
|
||||
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.EventChecklistReview.as_view()),
|
||||
name='ec_review'),
|
||||
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.MarkReviewed.as_view()),
|
||||
name='ec_review', kwargs={'model': 'EventChecklist'}),
|
||||
|
||||
path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()),
|
||||
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
|
||||
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
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
|
||||
@@ -1,10 +0,0 @@
|
||||
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,5 +3,3 @@ from .finance import *
|
||||
from .hs import *
|
||||
from .ical import *
|
||||
from .rigboard import *
|
||||
from .subhire import *
|
||||
from .dashboards import *
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
272
RIGS/views/hs.py
272
RIGS/views/hs.py
@@ -1,16 +1,39 @@
|
||||
from django.apps import apps
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views import generic
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models, forms
|
||||
from RIGS.views.rigboard import get_related
|
||||
from PyRIGS.views import PrintView
|
||||
from PyRIGS.views import PrintView, ModalURLMixin
|
||||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
class EventRiskAssessmentCreate(generic.CreateView):
|
||||
class HSCreateView(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
|
||||
template_name = 'hs/risk_assessment_form.html'
|
||||
form_class = forms.EventRiskAssessmentForm
|
||||
@@ -23,28 +46,12 @@ class EventRiskAssessmentCreate(generic.CreateView):
|
||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||
|
||||
if ra is not None:
|
||||
return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
|
||||
return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk}))
|
||||
|
||||
return super(EventRiskAssessmentCreate, self).get(self)
|
||||
|
||||
def get_form(self, **kwargs):
|
||||
form = super(EventRiskAssessmentCreate, self).get_form(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
form.instance.event = event
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentCreate, self).get_context_data(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
context['event'] = event
|
||||
context['page_title'] = f'Create Risk Assessment for Event {event.display_id}'
|
||||
get_related(context['form'], context)
|
||||
return context
|
||||
return super().get(self)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
||||
return reverse('ra_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
||||
@@ -57,10 +64,10 @@ class EventRiskAssessmentEdit(generic.UpdateView):
|
||||
ra.reviewed_by = None
|
||||
ra.reviewed_at = None
|
||||
ra.save()
|
||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
||||
return reverse('ra_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
rpk = self.kwargs.get('pk')
|
||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
||||
context['event'] = ra.event
|
||||
@@ -75,47 +82,17 @@ class EventRiskAssessmentDetail(generic.DetailView):
|
||||
template_name = 'hs/risk_assessment_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
|
||||
context = super().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>"
|
||||
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):
|
||||
model = models.EventChecklist
|
||||
template_name = 'hs/event_checklist_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistDetail, self).get_context_data(**kwargs)
|
||||
context = super().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>"
|
||||
return context
|
||||
|
||||
@@ -130,10 +107,10 @@ class EventChecklistEdit(generic.UpdateView):
|
||||
ec.reviewed_by = None
|
||||
ec.reviewed_at = None
|
||||
ec.save()
|
||||
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
||||
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistEdit, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
pk = self.kwargs.get('pk')
|
||||
ec = models.EventChecklist.objects.get(pk=pk)
|
||||
context['event'] = ec.event
|
||||
@@ -143,7 +120,7 @@ class EventChecklistEdit(generic.UpdateView):
|
||||
return context
|
||||
|
||||
|
||||
class EventChecklistCreate(generic.CreateView):
|
||||
class EventChecklistCreate(HSCreateView):
|
||||
model = models.EventChecklist
|
||||
template_name = 'hs/event_checklist_form.html'
|
||||
form_class = forms.EventChecklistForm
|
||||
@@ -152,75 +129,95 @@ class EventChecklistCreate(generic.CreateView):
|
||||
def get(self, *args, **kwargs):
|
||||
epk = kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
# Check if RA exists
|
||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||
if ra is None:
|
||||
messages.error(self.request, 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 super().get(self)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('ec_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if context['event'].venue:
|
||||
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)
|
||||
# Check if RA exists
|
||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||
|
||||
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.')
|
||||
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
|
||||
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(EventChecklistCreate, self).get(self)
|
||||
|
||||
def get_form(self, **kwargs):
|
||||
form = super(EventChecklistCreate, self).get_form(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
form.instance.event = event
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistCreate, self).get_context_data(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
context['event'] = event
|
||||
context['page_title'] = f'Create Event Checklist for Event {event.display_id}'
|
||||
return context
|
||||
return super().get(self)
|
||||
|
||||
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')
|
||||
return reverse('pt_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistList, self).get_context_data(**kwargs)
|
||||
context['title'] = 'Event Checklist'
|
||||
context['view'] = 'ec_detail'
|
||||
context['edit'] = 'ec_edit'
|
||||
context['review'] = 'ec_review'
|
||||
context['perm'] = 'perms.RIGS.review_eventchecklist'
|
||||
context = super().get_context_data(**kwargs)
|
||||
if context['event'].venue:
|
||||
context['venue'] = context['event'].venue
|
||||
if context['event'].riskassessment.power_mic:
|
||||
context['power_mic'] = context['event'].riskassessment.power_mic
|
||||
return context
|
||||
|
||||
|
||||
class EventChecklistReview(generic.View):
|
||||
def get(self, *args, **kwargs):
|
||||
rpk = kwargs.get('pk')
|
||||
ec = models.EventChecklist.objects.get(pk=rpk)
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(self.request.user)
|
||||
ec.reviewed_by = self.request.user
|
||||
ec.reviewed_at = timezone.now()
|
||||
ec.save()
|
||||
return HttpResponseRedirect(reverse_lazy('ec_list'))
|
||||
|
||||
|
||||
class HSList(generic.ListView):
|
||||
paginate_by = 20
|
||||
model = models.Event
|
||||
template_name = 'hs/hs_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).exclude(dry_hire=True).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(HSList, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = 'H&S Overview'
|
||||
return context
|
||||
|
||||
@@ -233,3 +230,64 @@ class RAPrint(PrintView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
|
||||
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,14 +1,12 @@
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django_ical.views import ICalFeed
|
||||
|
||||
from RIGS import models
|
||||
|
||||
from itertools import chain
|
||||
|
||||
|
||||
class CalendarICS(ICalFeed):
|
||||
"""
|
||||
@@ -33,7 +31,6 @@ class CalendarICS(ICalFeed):
|
||||
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
|
||||
params['non-rig'] = request.GET.get('non-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['provisional'] = request.GET.get('provisional', 'true') == 'true'
|
||||
@@ -43,46 +40,42 @@ class CalendarICS(ICalFeed):
|
||||
|
||||
def description(self, params):
|
||||
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 '') + ('Subhires' if params['subhire'] else '') + '\n'
|
||||
desc += "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
||||
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
||||
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
||||
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
||||
|
||||
return desc
|
||||
|
||||
def items(self, params):
|
||||
# include events from up to 1 year ago
|
||||
start = timezone.now() - datetime.timedelta(days=365)
|
||||
start = datetime.datetime.now() - datetime.timedelta(days=365)
|
||||
filter = Q(start_date__gte=start)
|
||||
|
||||
type_filters = Q(pk=None) # Need something that is false for every entry
|
||||
typeFilters = Q(pk=None) # Need something that is false for every entry
|
||||
|
||||
if params['dry-hire']:
|
||||
type_filters = type_filters | Q(dry_hire=True, is_rig=True)
|
||||
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
|
||||
|
||||
if params['non-rig']:
|
||||
type_filters = type_filters | Q(is_rig=False)
|
||||
typeFilters = typeFilters | Q(is_rig=False)
|
||||
|
||||
if params['rig']:
|
||||
type_filters = type_filters | Q(is_rig=True, dry_hire=False)
|
||||
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
||||
|
||||
status_filters = Q(pk=None) # Need something that is false for every entry
|
||||
statusFilters = Q(pk=None) # Need something that is false for every entry
|
||||
|
||||
if params['cancelled']:
|
||||
status_filters = status_filters | Q(status=models.Event.CANCELLED)
|
||||
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
||||
if params['provisional']:
|
||||
status_filters = status_filters | Q(status=models.Event.PROVISIONAL)
|
||||
statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
|
||||
if params['confirmed']:
|
||||
status_filters = status_filters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||
statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||
|
||||
filter = filter & type_filters & status_filters
|
||||
filter = filter & typeFilters & statusFilters
|
||||
|
||||
events = models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||
'venue', 'mic')
|
||||
|
||||
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):
|
||||
title = ''
|
||||
|
||||
@@ -113,32 +106,30 @@ class CalendarICS(ICalFeed):
|
||||
return item.latest_time
|
||||
|
||||
def item_location(self, item):
|
||||
if hasattr(item, 'venue'):
|
||||
return item.venue
|
||||
return ""
|
||||
return item.venue
|
||||
|
||||
def item_description(self, item):
|
||||
# Create a nice information-rich description
|
||||
# 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'Event = {item.name}\n'
|
||||
if hasattr(item, 'venue'):
|
||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
||||
if item.is_rig and item.person:
|
||||
desc += 'Client = ' + item.person.name + (
|
||||
(' for ' + item.organisation.name) if item.organisation else '') + '\n'
|
||||
desc += f'Status = {item.get_status_display()}\n'
|
||||
if hasattr(item, 'mic'):
|
||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||
|
||||
desc += '\n'
|
||||
if hasattr(item, 'meet_at') and item.meet_at:
|
||||
if item.meet_at:
|
||||
desc += 'Crew Meet = ' + (
|
||||
timezone.make_aware(item.meet_at).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||
if hasattr(item, 'access_at') and item.access_at:
|
||||
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||
if item.access_at:
|
||||
desc += 'Access At = ' + (
|
||||
timezone.make_aware(item.access_at).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||
if item.start_date:
|
||||
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
|
||||
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
||||
@@ -149,6 +140,8 @@ class CalendarICS(ICalFeed):
|
||||
desc += '\n'
|
||||
if item.description:
|
||||
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()}'
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@ import copy
|
||||
import datetime
|
||||
import re
|
||||
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.contrib import messages
|
||||
@@ -11,15 +18,18 @@ from django.core.exceptions import SuspiciousOperation
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from PyRIGS import decorators
|
||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
|
||||
from RIGS import models, forms, utils
|
||||
from RIGS import models, forms
|
||||
|
||||
__author__ = 'ghost'
|
||||
|
||||
@@ -37,25 +47,14 @@ class RigboardIndex(generic.TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
class WebCalendar(generic.ListView):
|
||||
model = models.Event
|
||||
class WebCalendar(generic.TemplateView):
|
||||
template_name = 'calendar.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# use today's date for the calendar
|
||||
d = utils.get_date(self.request.GET.get('month', None))
|
||||
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")
|
||||
context['view'] = kwargs.get('view', '')
|
||||
context['date'] = kwargs.get('date', '')
|
||||
# context['page_title'] = "Calendar"
|
||||
return context
|
||||
|
||||
|
||||
@@ -69,6 +68,10 @@ class EventDetail(generic.DetailView, ModalURLMixin):
|
||||
if self.object.dry_hire:
|
||||
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
||||
context['page_title'] = title
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
else:
|
||||
context['override'] = 'base_assets.html'
|
||||
return context
|
||||
|
||||
|
||||
@@ -204,7 +207,27 @@ class EventArchive(generic.ListView):
|
||||
"Muppet! Check the dates, it has been fixed for you.")
|
||||
start, end = end, start # Stop the impending fail
|
||||
|
||||
qs = self.model.objects.event_search(self.request.GET.get('q', None), start, end, self.request.GET.get('status', ""))
|
||||
filter = Q()
|
||||
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():
|
||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||
@@ -361,3 +384,41 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||
context['to_name'] = self.request.GET.get('to_name', None)
|
||||
context['target'] = 'event_authorise_form_preview'
|
||||
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)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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
|
||||
18
assets/migrations/0028_alter_asset_length.py
Normal file
18
assets/migrations/0028_alter_asset_length.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
is_cable = models.BooleanField(default=False)
|
||||
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
|
||||
length = models.DecimalField(decimal_places=1, max_digits=10,
|
||||
length = models.DecimalField(decimal_places=2, max_digits=10,
|
||||
blank=True, null=True, help_text='m')
|
||||
csa = models.DecimalField(decimal_places=2, max_digits=10,
|
||||
blank=True, null=True, help_text='mm²')
|
||||
@@ -192,5 +192,5 @@ class Asset(models.Model, RevisionMixin):
|
||||
return str(self.asset_id)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def display_name(self):
|
||||
return f"{self.display_id} | {self.description}"
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
function onAuditClick(assetID) {
|
||||
$('#' + assetID).remove();
|
||||
}
|
||||
$('#modal').on('hidden.bs.modal', function (e) {
|
||||
searchbar = document.getElementById('id_q');
|
||||
searchbar.value = "";
|
||||
setTimeout(searchbar.focus(), 2000);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/selects.js' %}"></script>
|
||||
<script src="{% static 'js/easymde.min.js' %}"></script>
|
||||
<script src="{% static 'js/interaction.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
@@ -35,9 +34,10 @@
|
||||
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
setupMDE('#id_comments');
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</thead>
|
||||
<tbody id="asset_table_body">
|
||||
{% for item in object_list %}
|
||||
<tr class="table-{{ item.status.display_class|default:'' }} assetRow">
|
||||
<tr class="table-{{ item.status.display_class|default:'' }} assetRow" id="{{ item.asset_id }}">
|
||||
<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="assetCategory align-middle">{{ item.category }}</td>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
|
||||
{% render_field form.purchase_price|add_class:'form-control'|set_data:"toggle:tooltip" value=object.purchase_price title="Ex. VAT" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
|
||||
{% render_field form.replacement_cost|add_class:'form-control'|set_data:"toggle:tooltip" value=object.replacement_cost title="Ex. VAT" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class AssetForm(FormPage):
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||
'comments': (regions.TextBox, (By.ID, 'id_comments')),
|
||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
|
||||
@@ -195,7 +195,7 @@ class TestAssetForm(AutoLoginTest):
|
||||
# self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||
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.assertTrue(self.page.success)
|
||||
@@ -350,7 +350,7 @@ class TestAssetAudit(AutoLoginTest):
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
||||
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
|
||||
audited = models.Asset.objects.get(asset_id=asset_row.id)
|
||||
assert audited.last_audited_by is None
|
||||
|
||||
@@ -24,6 +24,7 @@ function fonts(done) {
|
||||
function styles(done) {
|
||||
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
|
||||
return gulp.src(['pipeline/source_assets/scss/**/*.scss',
|
||||
'node_modules/fullcalendar/main.css',
|
||||
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
|
||||
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
|
||||
'node_modules/easymde/dist/easymde.min.css'
|
||||
@@ -58,6 +59,7 @@ function scripts() {
|
||||
'node_modules/html5sortable/dist/html5sortable.min.js',
|
||||
'node_modules/clipboard/dist/clipboard.min.js',
|
||||
'node_modules/moment/moment.js',
|
||||
'node_modules/fullcalendar/main.js',
|
||||
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
|
||||
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
|
||||
'node_modules/easymde/dist/easymde.min.js',
|
||||
|
||||
1191
package-lock.json
generated
1191
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
||||
"clipboard": "^2.0.8",
|
||||
"cssnano": "^5.0.13",
|
||||
"easymde": "^2.16.1",
|
||||
"fullcalendar": "^5.10.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-flatten": "^0.4.0",
|
||||
|
||||
@@ -73,6 +73,7 @@ function initPicker(obj) {
|
||||
return array;
|
||||
}
|
||||
};
|
||||
//console.log(obj.data);
|
||||
if (!obj.data('noclear')) {
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
|
||||
@@ -6,11 +6,6 @@ function setupItemTable(items_json) {
|
||||
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) {
|
||||
return $('<div/>').text(str).html();
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ Date.prototype.getISOString = function () {
|
||||
var dd = this.getDate().toString();
|
||||
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
|
||||
};
|
||||
$(document).ready(function () {
|
||||
$(document).on('click', '.modal-href', function (e) {
|
||||
$link = $(this);
|
||||
jQuery(document).ready(function () {
|
||||
jQuery(document).on('click', '.modal-href', function (e) {
|
||||
$link = jQuery(this);
|
||||
// Anti modal inception
|
||||
if ($link.parents('#modal').length == 0) {
|
||||
e.preventDefault();
|
||||
modaltarget = $link.data('target');
|
||||
modalobject = "";
|
||||
$('#modal').load($link.attr('href'), function (e) {
|
||||
$('#modal').modal();
|
||||
jQuery('#modal').load($link.attr('href'), function (e) {
|
||||
jQuery('#modal').modal();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -23,6 +23,7 @@ $(document).ready(function () {
|
||||
s.type = 'text/javascript';
|
||||
document.body.appendChild(s);
|
||||
s.src = '{% static "js/asteroids.min.js"%}';
|
||||
ga('send', 'event', 'easter_egg', 'activated');
|
||||
}
|
||||
easter_egg.load();
|
||||
});
|
||||
|
||||
@@ -281,7 +281,3 @@ html.embedded {
|
||||
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
.badge-purple, .bg-purple {
|
||||
background-color: #800080 !important;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
<body>
|
||||
<a class="skip-link" href='#main'>Skip to content</a>
|
||||
{% 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">
|
||||
<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%}">
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
e.preventDefault();
|
||||
data = $(this).serialize();
|
||||
action = $(this).attr('action');
|
||||
console.log(action)
|
||||
$.post(action, data, function(resp) {
|
||||
$('#modal').html(resp);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
<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>
|
||||
<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 now! <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="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;">
|
||||
@@ -24,7 +31,6 @@
|
||||
</div>
|
||||
<div class="col-sm-4 mb-3">
|
||||
<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;">
|
||||
<h4 class="card-header">Asset Database</h4>
|
||||
<div class="list-group list-group-flush">
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load title_spaced from filters %}
|
||||
{% spaceless %}
|
||||
<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 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%}
|
||||
{% if append or prepend %}
|
||||
<div class="input-group {{col}}">
|
||||
<div class="input-group {{col}} flex-nowrap">
|
||||
{% if prepend %}
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{ prepend }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% render_field field|add_class:'form-control' %}
|
||||
{% render_field field|add_class:'form-control' style=style %}
|
||||
{% if append %}
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">{{ append }}</span>
|
||||
@@ -17,6 +17,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% render_field field|add_class:'form-control' class+=col %}
|
||||
{% render_field field|add_class:'form-control' class+=col style=style %}
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
@@ -105,6 +105,10 @@ class TrainingItem(models.Model):
|
||||
def display_id(self):
|
||||
return f"{self.category.reference_number}.{self.reference_number}"
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return f"{self.display_id} | {self.name}"
|
||||
|
||||
@display_id.filter
|
||||
@classmethod
|
||||
def display_id(cls, lookup, value):
|
||||
@@ -369,7 +373,7 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
|
||||
return str(self)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('trainee_detail', kwargs={'pk': self.trainee.pk})
|
||||
return reverse('trainee_detail', kwargs={'pk': self.trainee_id})
|
||||
|
||||
class Meta:
|
||||
unique_together = ["trainee", "level"]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<br><small>{{ item.description }}</small>
|
||||
{% if item.prerequisites.exists %}
|
||||
<div class="ml-3 font-italic">
|
||||
<p class="text-info mb-0">Passed Out Prerequisites:</p>
|
||||
<p class="text-info mb-0">Competency Assessment Prerequisites:</p>
|
||||
<ul>
|
||||
{% for p in item.prerequisites.all %}
|
||||
<li>{{p}}</li>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<spacer length="4" />
|
||||
<para>{{ item.description }}</para>
|
||||
{% if item.prerequisites.exists %}
|
||||
<h4>Prerequisites:</h4>
|
||||
<h4>Competency Assessment Prerequisites:</h4>
|
||||
<ul bulletFontSize="5">
|
||||
{% for p in item.prerequisites.all %}
|
||||
<li><para>{{p}}</para></li>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<p>Please Note:</p>
|
||||
<ul>
|
||||
<li>Technical Assistant status is automatically valid when the item requirements are met.</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>Technician status is also automatic. Notification of completion should be made at the next general meeting.</li>
|
||||
<li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li>
|
||||
</ul>
|
||||
<sup>Correct as of 3rd September 2021, check the Training Policy.</sup>
|
||||
<sup>Correct as of 24th May 2023, check the Training Policy.</sup>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for level in object_list %}
|
||||
|
||||
@@ -43,7 +43,7 @@ def confirm_button(user, trainee, level):
|
||||
if level.user_has_requirements(trainee):
|
||||
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'):
|
||||
string += "<a class='btn btn-info' href='{}'>Confirm</a>".format(reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk}))
|
||||
string += f"<a class='btn btn-info' href='{reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk})}'>Confirm</a>"
|
||||
return mark_safe(string)
|
||||
else:
|
||||
return ""
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
||||
page.submit()
|
||||
assert page.success
|
||||
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
|
||||
assert qualification.supervisor.pk == supervisor.pk
|
||||
assert qualification.supervisor_id == supervisor.pk
|
||||
assert qualification.date == date
|
||||
assert qualification.notes == "A note"
|
||||
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
|
||||
qual = models.TrainingItemQualification.objects.last()
|
||||
assert qual is not None
|
||||
assert training_item.pk == qual.item.pk
|
||||
assert training_item.pk == qual.item_id
|
||||
# Ensure only one revision has been created
|
||||
assert Revision.objects.count() == 1
|
||||
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})
|
||||
|
||||
@@ -28,6 +28,9 @@ class ItemListExport(PrintListView):
|
||||
model = models.TrainingItem
|
||||
template_name = 'item_list.xml'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(active=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['filename'] = "TrainingItemList.pdf"
|
||||
@@ -262,5 +265,5 @@ class ItemQualifications(generic.ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = f"People Qualified In {self.object_list[0].item}"
|
||||
context["page_title"] = f"People Qualified In {models.TrainingItem.objects.get(pk=self.kwargs['pk'])}"
|
||||
return context
|
||||
|
||||
@@ -50,10 +50,7 @@ class Command(BaseCommand):
|
||||
"add_supplier", "view_cabletype", "change_cabletype",
|
||||
"add_cabletype", "view_eventchecklist", "change_eventchecklist",
|
||||
"add_eventchecklist", "view_riskassessment", "change_riskassessment",
|
||||
"add_riskassessment", "add_eventchecklistcrew", "change_eventchecklistcrew",
|
||||
"delete_eventchecklistcrew", "view_eventchecklistcrew", "add_eventchecklistvehicle",
|
||||
"change_eventchecklistvehicle",
|
||||
"delete_eventchecklistvehicle", "view_eventchecklistvehicle", ]
|
||||
"add_riskassessment"]
|
||||
finance_perms = keyholder_perms + ["add_invoice", "change_invoice", "view_invoice",
|
||||
"add_payment", "change_payment", "delete_payment"]
|
||||
hs_perms = keyholder_perms + ["review_riskassessment", "review_eventchecklist"]
|
||||
|
||||
@@ -118,11 +118,6 @@
|
||||
<input type="checkbox" value="dry-hire" data-default="true" checked> Dry-Hires
|
||||
</label>
|
||||
<label class="checkbox-inline mx-lg-2">
|
||||
<input type="checkbox" value="subhire" data-default="false"> Subhires
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group d-flex flex-column flex-lg-row">
|
||||
<label class="checkbox-inline mr-lg-2">
|
||||
<input type="checkbox" value="cancelled" data-default="false" > Cancelled
|
||||
</label>
|
||||
<label class="checkbox-inline mx-lg-2">
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.core import mail
|
||||
from django.test import LiveServerTestCase
|
||||
from django.test.utils import override_settings
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from PyRIGS.tests.base import create_browser
|
||||
from RIGS import models
|
||||
@@ -24,31 +25,31 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
def test_registration(self):
|
||||
# Navigate to the registration page
|
||||
self.browser.get(self.live_server_url + '/user/register/')
|
||||
title_text = self.browser.find_element_by_tag_name('h3').text
|
||||
title_text = self.browser.find_element(By.TAG_NAME, 'h3').text
|
||||
self.assertIn("User Registration", title_text)
|
||||
|
||||
# Check the form invites correctly
|
||||
username = self.browser.find_element_by_id('id_username')
|
||||
username = self.browser.find_element(By.ID, 'id_username')
|
||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||
email = self.browser.find_element_by_id('id_email')
|
||||
email = self.browser.find_element(By.ID, 'id_email')
|
||||
self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
|
||||
# If this is correct we don't need to test it later
|
||||
self.assertEqual(email.get_attribute('type'), 'email')
|
||||
password1 = self.browser.find_element_by_id('id_password1')
|
||||
password1 = self.browser.find_element(By.ID, 'id_password1')
|
||||
self.assertEqual(password1.get_attribute('placeholder'), 'Password')
|
||||
self.assertEqual(password1.get_attribute('type'), 'password')
|
||||
password2 = self.browser.find_element_by_id('id_password2')
|
||||
password2 = self.browser.find_element(By.ID, 'id_password2')
|
||||
self.assertEqual(
|
||||
password2.get_attribute('placeholder'), 'Password confirmation')
|
||||
self.assertEqual(password2.get_attribute('type'), 'password')
|
||||
first_name = self.browser.find_element_by_id('id_first_name')
|
||||
first_name = self.browser.find_element(By.ID, 'id_first_name')
|
||||
self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
|
||||
last_name = self.browser.find_element_by_id('id_last_name')
|
||||
last_name = self.browser.find_element(By.ID, 'id_last_name')
|
||||
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
||||
initials = self.browser.find_element_by_id('id_initials')
|
||||
initials = self.browser.find_element(By.ID, 'id_initials')
|
||||
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
||||
# No longer required for new users
|
||||
# phone = self.browser.find_element_by_id('id_phone')
|
||||
# phone = self.browser.find_element(By.ID, 'id_phone')
|
||||
# self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
||||
|
||||
# Fill the form out incorrectly
|
||||
@@ -62,21 +63,20 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
initials.send_keys('JS')
|
||||
# phone.send_keys('0123456789')
|
||||
time.sleep(1)
|
||||
self.browser.switch_to.frame(self.browser.find_element_by_tag_name("iframe"))
|
||||
self.browser.find_element_by_id('anchor').click()
|
||||
self.browser.switch_to.frame(self.browser.find_element(By.TAG_NAME, "iframe"))
|
||||
self.browser.find_element(By.ID, 'anchor').click()
|
||||
self.browser.switch_to.default_content()
|
||||
time.sleep(3)
|
||||
# Submit incorrect form
|
||||
submit = self.browser.find_element_by_xpath("//input[@type='submit']")
|
||||
submit = self.browser.find_element(By.XPATH, "//input[@type='submit']")
|
||||
submit.click()
|
||||
|
||||
# Restablish error fields
|
||||
password1 = self.browser.find_element_by_id('id_password1')
|
||||
password2 = self.browser.find_element_by_id('id_password2')
|
||||
password1 = self.browser.find_element(By.ID, 'id_password1')
|
||||
password2 = self.browser.find_element(By.ID, 'id_password2')
|
||||
|
||||
# Read what the error is
|
||||
alert = self.browser.find_element_by_css_selector(
|
||||
'div.alert-danger').text
|
||||
alert = self.browser.find_element(By.CSS_SELECTOR, '.alert-danger').text
|
||||
# TODO Use regex matching to handle smart/unsmart quotes...
|
||||
self.assertIn("password fields didn", alert)
|
||||
|
||||
@@ -92,8 +92,7 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
password2.send_keys(Keys.ENTER)
|
||||
|
||||
# Check we have a success message
|
||||
alert = self.browser.find_element_by_css_selector(
|
||||
'div.alert-success').text
|
||||
alert = self.browser.find_element(By.CSS_SELECTOR, '.alert-success').text
|
||||
self.assertIn('register', alert)
|
||||
self.assertIn('email', alert)
|
||||
|
||||
@@ -111,14 +110,14 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
self.browser.get(urls[0]) # go to the first link
|
||||
|
||||
# Complete registration
|
||||
title_text = self.browser.find_element_by_tag_name('h2').text
|
||||
title_text = self.browser.find_element(By.TAG_NAME, 'h2').text
|
||||
self.assertIn('Complete', title_text)
|
||||
|
||||
# Test login
|
||||
self.browser.get(self.live_server_url + '/user/login')
|
||||
username = self.browser.find_element_by_id('id_username')
|
||||
username = self.browser.find_element(By.ID, 'id_username')
|
||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||
password = self.browser.find_element_by_id('id_password')
|
||||
password = self.browser.find_element(By.ID, 'id_password')
|
||||
self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
||||
self.assertEqual(password.get_attribute('type'), 'password')
|
||||
|
||||
@@ -132,8 +131,7 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
self.assertFalse(profileObject.is_approved)
|
||||
|
||||
# Read what the error is
|
||||
alert = self.browser.find_element_by_css_selector(
|
||||
'div.alert-danger').text
|
||||
alert = self.browser.find_element(By.CSS_SELECTOR, 'div.alert-danger').text
|
||||
self.assertIn("approved", alert)
|
||||
|
||||
# Approve the user so we can proceed
|
||||
@@ -142,14 +140,14 @@ class UserRegistrationTest(LiveServerTestCase):
|
||||
|
||||
# Retry login
|
||||
self.browser.get(self.live_server_url + '/user/login')
|
||||
username = self.browser.find_element_by_id('id_username')
|
||||
username = self.browser.find_element(By.ID, 'id_username')
|
||||
username.send_keys('TestUsername')
|
||||
password = self.browser.find_element_by_id('id_password')
|
||||
password = self.browser.find_element(By.ID, 'id_password')
|
||||
password.send_keys('correcthorsebatterystaple')
|
||||
password.send_keys(Keys.ENTER)
|
||||
|
||||
# Check we are logged in
|
||||
udd = self.browser.find_element_by_class_name('navbar').text
|
||||
udd = self.browser.find_element(By.CLASS_NAME, 'navbar').text
|
||||
self.assertIn('Hi John', udd)
|
||||
|
||||
# Check all the data actually got saved
|
||||
|
||||
@@ -30,15 +30,15 @@ for app in [apps.get_app_config(label) for label in ("RIGS", "assets", "training
|
||||
modelname = model.__name__.lower()
|
||||
if appname == 'rigboard':
|
||||
urlpatterns += [
|
||||
path('{}/<str:pk>/history/'.format(modelname),
|
||||
permission_required_with_403('{}.change_{}'.format(app.label, modelname))(
|
||||
path(f'{modelname}/<str:pk>/history/',
|
||||
permission_required_with_403(f'{app.label}.change_{modelname}')(
|
||||
views.VersionHistory.as_view()),
|
||||
name='{}_history'.format(modelname), kwargs={'model': model, 'app': appname, }),
|
||||
name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }),
|
||||
]
|
||||
else:
|
||||
urlpatterns += [
|
||||
path('{}/{}/<str:pk>/history/'.format(appname, modelname),
|
||||
permission_required_with_403('{}.change_{}'.format(app.label, modelname))(
|
||||
path(f'{appname}/{modelname}/<str:pk>/history/',
|
||||
permission_required_with_403(f'{app.label}.change_{modelname}')(
|
||||
views.VersionHistory.as_view()),
|
||||
name='{}_history'.format(modelname), kwargs={'model': model, 'app': appname, }),
|
||||
name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from diff_match_patch import diff_match_patch
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@@ -147,9 +148,9 @@ class ModelComparison:
|
||||
|
||||
@cached_property
|
||||
def item_changes(self):
|
||||
from RIGS.models import EventAuthorisation
|
||||
from training.models import TrainingLevelQualification, TrainingItemQualification
|
||||
if self.follow and self.version.object is not None:
|
||||
from RIGS.models import EventAuthorisation
|
||||
from training.models import TrainingLevelQualification, TrainingItemQualification
|
||||
item_type = ContentType.objects.get_for_model(self.version.object)
|
||||
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \
|
||||
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
|
||||
@@ -160,10 +161,14 @@ class ModelComparison:
|
||||
# Build some dicts of what we have
|
||||
item_dict = {} # build a list of items, key is the item_pk
|
||||
for version in old_item_versions: # put all the old versions in a list
|
||||
if version._model is None:
|
||||
continue
|
||||
compare = ModelComparison(old=version._object_version.object, **comparisonParams)
|
||||
item_dict[version.object_id] = compare
|
||||
|
||||
for version in new_item_versions: # go through the new versions
|
||||
if version._model is None:
|
||||
continue
|
||||
try:
|
||||
compare = item_dict[version.object_id] # see if there's a matching old version
|
||||
compare.new = version._object_version.object # then add the new version to the dictionary
|
||||
|
||||
@@ -27,10 +27,10 @@ class VersionHistory(generic.ListView):
|
||||
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(VersionHistory, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['object'] = self.get_object()
|
||||
if self.kwargs['app'] != 'rigboard':
|
||||
context['override'] = 'base_{}.html'.format(self.kwargs['app'])
|
||||
context['override'] = f'base_{self.kwargs["app"]}.html'
|
||||
|
||||
return context
|
||||
|
||||
@@ -59,10 +59,10 @@ class ActivityTable(generic.ListView):
|
||||
return RIGSVersion.objects.get_for_multiple_models(filter_models(self.kwargs.get('models'), self.request.user)).order_by("-revision__date_created")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ActivityTable, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "{} Activity Stream".format(title(self.kwargs['app']))
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = f"{title(self.kwargs['app'])} Activity Stream"
|
||||
if self.kwargs['app'] != 'rigboard':
|
||||
context['override'] = 'base_{}.html'.format(self.kwargs['app'])
|
||||
context['override'] = f'base_{self.kwargs["app"]}.html'
|
||||
|
||||
return context
|
||||
|
||||
@@ -77,7 +77,7 @@ class ActivityFeed(generic.ListView): # Appears on homepage
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# Call the base implementation first to get a context
|
||||
context = super(ActivityFeed, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = "Activity Feed"
|
||||
maxTimeDelta = datetime.timedelta(hours=1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user