Compare commits

..

4 Commits

Author SHA1 Message Date
676cee164b Very initial version of checkin form 2023-05-09 21:37:25 +01:00
cccb84e4dc Remove old 'vehicle/crew' stuff 2023-05-09 20:47:41 +01:00
742836486e Revamp H&S overview, remove individual lists.
They were not a good thing.
2023-05-08 19:59:58 +01:00
e774b06ac4 Split power related parts of event checklist into a seperate form 2023-05-08 19:46:35 +01:00
81 changed files with 2679 additions and 2656 deletions

View File

@@ -12,7 +12,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYTHONDONTWRITEBYTECODE: 1
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
@@ -42,8 +41,8 @@ jobs:
pipenv run python3 manage.py makemigrations --check --dry-run pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput pipenv run python3 manage.py collectstatic --noinput
- name: Run Tests - name: Run Tests
run: pipenv run pytest -n auto --cov run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v2
if: failure() if: failure()
with: with:
name: failure-screenshots ${{ matrix.test-group }} name: failure-screenshots ${{ matrix.test-group }}

12
Pipfile
View File

@@ -19,7 +19,7 @@ cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0" dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6" dj-static = "~=0.0.6"
Django = "~=3.2" Django = "~=3.2"
django-debug-toolbar = "~=4.0.0" django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0" django-filter = "~=2.4.0"
django-ical = "~=1.7.1" django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3" django-recurrence = "~=1.10.3"
@@ -39,7 +39,7 @@ premailer = "~=3.7.0"
progress = "~=1.5" progress = "~=1.5"
psutil = "~=5.8.0" psutil = "~=5.8.0"
psycopg2 = "~=2.8.6" psycopg2 = "~=2.8.6"
Pygments = "~=2.15.0" Pygments = "~=2.7.4"
pyparsing = "~=2.4.7" pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5" PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4" PyPOM = "~=2.2.4"
@@ -47,7 +47,7 @@ python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21" pytoml = "~=0.1.21"
pytz = "~=2020.5" pytz = "~=2020.5"
reportlab = "*" reportlab = "*"
requests = "~=2.31.0" requests = "~=2.25.1"
retrying = "~=1.3.3" retrying = "~=1.3.3"
simplejson = "~=3.17.2" simplejson = "~=3.17.2"
six = "~=1.15.0" six = "~=1.15.0"
@@ -56,7 +56,7 @@ sqlparse = "~=0.4.2"
static3 = "~=0.7.0" static3 = "~=0.7.0"
svg2rlg = "~=0.3" svg2rlg = "~=0.3"
tini = "~=3.0.1" tini = "~=3.0.1"
tornado = "~=6.3" tornado = "~=6.1"
urllib3 = "~=1.26.5" urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0" whitenoise = "~=5.2.0"
yolk = "~=0.4.3" yolk = "~=0.4.3"
@@ -79,7 +79,7 @@ django-hcaptcha = "*"
pikepdf = "*" pikepdf = "*"
django-queryable-properties = "*" django-queryable-properties = "*"
django-mass-edit = "*" django-mass-edit = "*"
selenium = "~=4.9.1" selenium = "~=3.141.0"
[dev-packages] [dev-packages]
pycodestyle = "~=2.9.1" pycodestyle = "~=2.9.1"
@@ -93,7 +93,7 @@ pytest = "*"
pytest-reverse = "*" pytest-reverse = "*"
[requires] [requires]
python_version = "3.10" python_version = "3.9"
[dev-packages.pytest-xdist] [dev-packages.pytest-xdist]
extras = [ "psutil",] extras = [ "psutil",]

1142
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -121,7 +121,3 @@ def nottinghamtec_address_required(function):
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
return wrap return wrap
def not_estates():
return user_passes_test_with_403(lambda u: not u.email.endswith('@nottingham.ac.uk'))

0
PyRIGS/forms.py Normal file
View File

View File

@@ -35,9 +35,6 @@ if DEBUG:
ALLOWED_HOSTS.append('localhost') ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com') ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1') ALLOWED_HOSTS.append('127.0.0.1')
ALLOWED_HOSTS.append('.github.dev')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG: if not DEBUG:
@@ -67,7 +64,7 @@ INSTALLED_APPS = (
'assets', 'assets',
'training', 'training',
# 'debug_toolbar', 'debug_toolbar',
'registration', 'registration',
'reversion', 'reversion',
'widget_tweaks', 'widget_tweaks',
@@ -78,7 +75,7 @@ INSTALLED_APPS = (
MIDDLEWARE = ( MIDDLEWARE = (
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
# 'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
'reversion.middleware.RevisionMiddleware', 'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@@ -221,6 +218,8 @@ TIME_ZONE = 'Europe/London'
FORMAT_MODULE_PATH = 'PyRIGS.formats' FORMAT_MODULE_PATH = 'PyRIGS.formats'
USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
@@ -265,10 +264,3 @@ TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk' AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
SECURE_HSTS_PRELOAD = True

View File

@@ -63,7 +63,7 @@ def screenshot_failure(func):
if not pathlib.Path("screenshots").is_dir(): if not pathlib.Path("screenshots").is_dir():
os.mkdir("screenshots") os.mkdir("screenshots")
self.driver.save_screenshot(screenshot_file) self.driver.save_screenshot(screenshot_file)
print(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr) print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
raise e raise e
return wrapper_func return wrapper_func

View File

@@ -59,8 +59,8 @@ class TestSampleDataGenerator(TestCase):
assert Asset.objects.all().count() > 50 assert Asset.objects.all().count() > 50
assert Event.objects.all().count() > 100 assert Event.objects.all().count() > 100
call_command('deleteSampleData') call_command('deleteSampleData')
assert not Asset.objects.all().exists() assert Asset.objects.all().count() == 0
assert not Event.objects.all().exists() assert Event.objects.all().count() == 0
@override_settings(DEBUG=True) @override_settings(DEBUG=True)
@@ -76,9 +76,9 @@ def test_unauthenticated(client): # Nothing should be available to the unauthen
assertTemplateUsed(response, 'login_redirect.html') assertTemplateUsed(response, 'login_redirect.html')
else: else:
if "embed" in str(url): if "embed" in str(url):
expected_url = f"{reverse('login_embed')}?next={request_url}" expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
else: else:
expected_url = f"{reverse('login')}?next={request_url}" expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url) assertRedirects(response, expected_url)
call_command('deleteSampleData') call_command('deleteSampleData')

View File

@@ -6,8 +6,6 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from PyRIGS.decorators import not_estates
from PyRIGS import views from PyRIGS import views
urlpatterns = [ urlpatterns = [
@@ -16,17 +14,17 @@ urlpatterns = [
path('assets/', include('assets.urls')), path('assets/', include('assets.urls')),
path('training/', include('training.urls')), path('training/', include('training.urls')),
path('', not_estates()(views.Index.as_view()), name='index'), path('', login_required(views.Index.as_view()), name='index'),
# API # API
path('api/<str:model>/', not_estates()(views.SecureAPIRequest.as_view()), path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"), name="api_secure"),
path('api/<str:model>/<int:pk>/', not_estates()(views.SecureAPIRequest.as_view()), path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"), name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'), path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search/', not_estates()(views.Search.as_view()), name='search'), path('search/', login_required(views.Search.as_view()), name='search'),
path('search_help/', not_estates()(views.SearchHelp.as_view()), name='search_help'), path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')), path('', include('users.urls')),

View File

@@ -48,7 +48,6 @@ class Index(generic.TemplateView): # Displays the current rig count along with
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['rig_count'] = models.Event.objects.rig_count() context['rig_count'] = models.Event.objects.rig_count()
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(status=models.Event.CANCELLED).filter(is_rig=True, dry_hire=False)
return context return context
@@ -134,15 +133,11 @@ class SecureAPIRequest(generic.View):
results = [] results = []
query = reduce(operator.and_, queries) query = reduce(operator.and_, queries)
objects = self.models[model].objects.filter(query) objects = self.models[model].objects.filter(query)
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
if model == "profile":
objects = objects.filter(is_active=True, is_approved=True)
for o in objects: for o in objects:
name = o.display_name if hasattr(o, 'display_name') else o.name
data = { data = {
'pk': o.pk, 'pk': o.pk,
'value': o.pk, 'value': o.pk,
'text': name, 'text': o.name,
} }
try: # See if there is a valid update URL try: # See if there is a valid update URL
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk}) data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
@@ -187,7 +182,7 @@ class ModalURLMixin:
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk})) update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'") messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
else: else:
url = reverse_lazy(detail, kwargs={ url = reverse_lazy(detail, kwargs={
'pk': self.object.pk, 'pk': self.object.pk,

View File

@@ -11,9 +11,8 @@ For setup information and other such helpful stuff check the [Wiki](https://gith
- PyRIGS: Base app, stores 'global' information - PyRIGS: Base app, stores 'global' information
- RIGS: Rigboard stuff - event calendar etc - RIGS: Rigboard stuff - event calendar etc
- assets: Database of our kit, testing data etc - assets: Database of our kit, testing data etc
- training: Logs in-house training within various "departments" (sound, lighting etc).
- versioning: Our custom logic built on top of django-reversion. Semi-modular. - versioning: Our custom logic built on top of django-reversion. Semi-modular.
- users: Our custom logic for registration and profiles. Semi-modular. - users: Our custom logic for registration and profiles. Semi-modular.
- training: SoonTM
[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com)

View File

@@ -20,7 +20,6 @@ admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin) admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin) admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice, VersionAdmin) admin.site.register(models.Invoice, VersionAdmin)
admin.site.register(models.EventCheckIn)
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary. @transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
@@ -154,9 +153,8 @@ class AssociateAdmin(VersionAdmin):
@admin.register(models.Profile) @admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin): class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login') list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
list_display_links = ['username'] list_display_links = ['username']
list_filter = UserAdmin.list_filter + ('is_approved',)
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), { (_('Personal info'), {

View File

@@ -121,7 +121,7 @@ class EventForm(forms.ModelForm):
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date', fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic', 'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status', 'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
'purchase_order', 'collector', 'forum_url'] 'purchase_order', 'collector']
class BaseClientEventAuthorisationForm(forms.ModelForm): class BaseClientEventAuthorisationForm(forms.ModelForm):
@@ -131,7 +131,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
def clean(self): def clean(self):
if self.cleaned_data.get('amount') != self.instance.event.total: if self.cleaned_data.get('amount') != self.instance.event.total:
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).') self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
return super().clean() return super(BaseClientEventAuthorisationForm, self).clean()
class Meta: class Meta:
abstract = True abstract = True
@@ -179,7 +179,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>") unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'): if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers') raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
return super().clean() return super(EventRiskAssessmentForm, self).clean()
class Meta: class Meta:
model = models.RiskAssessment model = models.RiskAssessment
@@ -195,11 +195,90 @@ class EventChecklistForm(forms.ModelForm):
if field.__class__ == forms.NullBooleanField: if field.__class__ == forms.NullBooleanField:
# Only display yes/no to user, the 'none' is only ever set in the background # Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput() field.widget = forms.CheckboxInput()
# Parsed from incoming form data by clean, then saved into models when the form is saved
items = {}
related_models = { related_models = {
'venue': models.Venue, '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: class Meta:
model = models.EventChecklist model = models.EventChecklist
fields = '__all__' fields = '__all__'
@@ -214,11 +293,6 @@ class PowerTestRecordForm(forms.ModelForm):
# Only display yes/no to user, the 'none' is only ever set in the background # Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput() field.widget = forms.CheckboxInput()
related_models = {
'venue': models.Venue,
'power_mic': models.Profile,
}
class Meta: class Meta:
model = models.PowerTestRecord model = models.PowerTestRecord
fields = '__all__' fields = '__all__'
@@ -229,15 +303,7 @@ class EventCheckInForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['time'].initial = timezone.now() self.fields['time'].initial = timezone.now()
self.fields['role'].initial = "Crew"
class Meta: class Meta:
model = models.EventCheckIn model = models.EventCheckIn
fields = '__all__' fields = '__all__'
exclude = ['end_time']
class EditCheckInForm(forms.ModelForm):
class Meta:
model = models.EventCheckIn
fields = '__all__'

View File

@@ -10,7 +10,7 @@ def migrate_old_data(apps, schema_editor):
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord') PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
for ec in EventChecklist.objects.all(): for ec in EventChecklist.objects.all():
# New highscore for the most pythonic BS I've ever written. # 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")}) PowerTestRecord.objects.create(event=ec.event, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created)})
def revert(apps, schema_editor): def revert(apps, schema_editor):

View File

@@ -1,24 +1,15 @@
# Generated by Django 3.2.19 on 2023-05-18 11:56 # Generated by Django 3.2.16 on 2023-05-08 18:46
from django.db import migrations
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('RIGS', '0047_auto_20230517_0944'), ('RIGS', '0046_create_powertests'),
] ]
operations = [ operations = [
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='checklist',
),
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='driver',
),
migrations.RemoveField( migrations.RemoveField(
model_name='eventchecklist', model_name='eventchecklist',
name='all_rcds_tested', name='all_rcds_tested',
@@ -59,10 +50,6 @@ class Migration(migrations.Migration):
model_name='eventchecklist', model_name='eventchecklist',
name='pat', name='pat',
), ),
migrations.RemoveField(
model_name='eventchecklist',
name='power_mic',
),
migrations.RemoveField( migrations.RemoveField(
model_name='eventchecklist', model_name='eventchecklist',
name='public_sockets_tested', name='public_sockets_tested',
@@ -127,30 +114,4 @@ class Migration(migrations.Migration):
model_name='eventchecklist', model_name='eventchecklist',
name='w3_voltage', 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',
),
] ]

View File

@@ -1,44 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-17 08:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.core.exceptions import ObjectDoesNotExist
def migrate_old_data(apps, schema_editor):
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
EventCheckIn = apps.get_model('RIGS', 'EventCheckIn')
for ec in EventChecklist.objects.all():
for crew in ec.crew.all():
try:
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=ec.vehicles.get(driver=crew.crewmember).vehicle)
except ObjectDoesNotExist:
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end)
def revert(apps, schema_editor):
apps.get_model('RIGS', 'EventCheckIn').objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0046_create_powertests'),
]
operations = [
migrations.CreateModel(
name='EventCheckIn',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()),
('role', models.CharField(blank=True, max_length=50)),
('vehicle', models.CharField(blank=True, max_length=100)),
('end_time', models.DateTimeField(blank=True, null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.event')),
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to=settings.AUTH_USER_MODEL)),
],
),
migrations.RunPython(migrate_old_data, reverse_code=revert),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.18 on 2023-05-09 19:43
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0047_auto_20230508_1946'),
]
operations = [
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='checklist',
),
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='driver',
),
migrations.RemoveField(
model_name='eventchecklist',
name='power_mic',
),
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.DeleteModel(
name='EventChecklistCrew',
),
migrations.DeleteModel(
name='EventChecklistVehicle',
),
]

View File

@@ -1,53 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-29 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0048_auto_20230518_1256'),
]
operations = [
migrations.AlterField(
model_name='powertestrecord',
name='fd_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='fd_pssc',
field=models.IntegerField(blank=True, help_text='Prospective Short Circuit Current / A', null=True, verbose_name='PSCC'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w1_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w1_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='w2_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w2_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='w3_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w3_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 3.2.18 on 2023-05-09 20:12
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0048_auto_20230509_2043'),
]
operations = [
migrations.CreateModel(
name='EventCheckOut',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()),
('vehicle', models.CharField(max_length=100)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.event')),
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkouts', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='EventCheckIn',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()),
('vehicle', models.CharField(max_length=100)),
('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)),
],
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-27 11:28
import RIGS.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0049_auto_20230529_1123'),
]
operations = [
migrations.AddField(
model_name='event',
name='forum_url',
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-07-09 21:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0050_event_forum_url'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='method',
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('T', 'TEC Adjustment')], default='', max_length=2),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.21 on 2023-09-05 22:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0051_alter_payment_method'),
]
operations = [
migrations.AddField(
model_name='venue',
name='on_campus',
field=models.BooleanField(default=False, verbose_name='Is this venue on a UoN campus?'),
),
]

View File

@@ -76,16 +76,11 @@ class Profile(AbstractUser):
@classmethod @classmethod
def users_awaiting_approval_count(cls): 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(models.Q(is_approved=False)).count()
return Profile.objects.filter(is_approved=False, last_login=None).count()
def __str__(self): def __str__(self):
return self.name 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): class ContactableManager(models.Manager):
def search(self, query=None): def search(self, query=None):
@@ -213,7 +208,6 @@ class Venue(models.Model, RevisionMixin):
phone = models.CharField(max_length=15, blank=True, default='') phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, default='')
three_phase_available = models.BooleanField(default=False) three_phase_available = models.BooleanField(default=False)
on_campus = models.BooleanField(default=False, verbose_name="Is this venue on a UoN campus?")
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, default='')
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, default='')
@@ -310,14 +304,6 @@ class EventManager(models.Manager):
return qs 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']) @reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin): class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database # Done to make it much nicer on the database
@@ -367,8 +353,6 @@ class Event(models.Model, RevisionMixin):
auth_request_at = models.DateTimeField(null=True, blank=True) auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(blank=True, default='') auth_request_to = models.EmailField(blank=True, default='')
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
@property @property
def display_id(self): def display_id(self):
if self.pk: if self.pk:
@@ -421,15 +405,7 @@ class Event(models.Model, RevisionMixin):
@property @property
def hs_done(self): def hs_done(self):
return self.riskassessment is not None and self.has_checklist and self.has_power return self.riskassessment is not None and len(self.checklists.all()) > 0
@property
def has_checklist(self):
return self.checklists.exists()
@property
def has_power(self):
return self.power_tests.exists()
@property @property
def has_start_time(self): def has_start_time(self):
@@ -502,22 +478,13 @@ class Event(models.Model, RevisionMixin):
else: else:
return bool(self.purchase_order) 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() objects = EventManager()
def get_absolute_url(self): def get_absolute_url(self):
return reverse('event_detail', kwargs={'pk': self.pk}) return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return f"{self.display_id} | {self.name}" return f"{self.display_id}: {self.name}"
def clean(self): def clean(self):
errdict = {} errdict = {}
@@ -689,11 +656,13 @@ class Payment(models.Model, RevisionMixin):
CASH = 'C' CASH = 'C'
INTERNAL = 'I' INTERNAL = 'I'
EXTERNAL = 'E' EXTERNAL = 'E'
SUCORE = 'SU'
ADJUSTMENT = 'T' ADJUSTMENT = 'T'
METHODS = ( METHODS = (
(CASH, 'Cash'), (CASH, 'Cash'),
(INTERNAL, 'Internal'), (INTERNAL, 'Internal'),
(EXTERNAL, 'External'), (EXTERNAL, 'External'),
(SUCORE, 'SU Core'),
(ADJUSTMENT, 'TEC Adjustment'), (ADJUSTMENT, 'TEC Adjustment'),
) )
@@ -721,17 +690,17 @@ def validate_url(value):
class ReviewableModel(models.Model): class ReviewableModel(models.Model):
reviewed_at = models.DateTimeField(null=True, blank=True) reviewed_at = models.DateTimeField(null=True)
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
verbose_name="Reviewer", on_delete=models.CASCADE) verbose_name="Reviewer", on_delete=models.CASCADE)
class Meta:
abstract = True
@cached_property @cached_property
def fieldz(self): 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] 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]
class Meta:
abstract = True
@reversion.register @reversion.register
class RiskAssessment(ReviewableModel, RevisionMixin): class RiskAssessment(ReviewableModel, RevisionMixin):
@@ -831,12 +800,6 @@ class RiskAssessment(ReviewableModel, RevisionMixin):
def get_event_size_display(self): def get_event_size_display(self):
return self.SIZES[self.event_size][1] + " Event" 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 @property
def activity_feed_string(self): def activity_feed_string(self):
return str(self.event) return str(self.event)
@@ -845,8 +808,14 @@ class RiskAssessment(ReviewableModel, RevisionMixin):
def name(self): def name(self):
return str(self) return str(self)
def get_absolute_url(self):
return reverse('ra_detail', kwargs={'pk': self.pk})
@reversion.register def __str__(self):
return f"{self.pk} | {self.event}"
@reversion.register(follow=['vehicles', 'crew'])
class EventChecklist(ReviewableModel, RevisionMixin): class EventChecklist(ReviewableModel, RevisionMixin):
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE) event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
@@ -872,9 +841,6 @@ class EventChecklist(ReviewableModel, RevisionMixin):
('review_eventchecklist', 'Can review Event Checklists') ('review_eventchecklist', 'Can review Event Checklists')
] ]
def __str__(self):
return f"{self.pk} - {self.event}"
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return str(self.event) return str(self.event)
@@ -882,15 +848,15 @@ class EventChecklist(ReviewableModel, RevisionMixin):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ec_detail', kwargs={'pk': self.pk}) return reverse('ec_detail', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.pk} - {self.event}"
@reversion.register @reversion.register
class PowerTestRecord(ReviewableModel, RevisionMixin): 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) 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', 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?") verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
venue = models.ForeignKey('Venue', on_delete=models.CASCADE) venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, default='')
# Small Electrical Checks # Small Electrical Checks
@@ -909,62 +875,44 @@ class PowerTestRecord(ReviewableModel, RevisionMixin):
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - 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_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_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_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=pssc_text) fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
# Worst case points # Worst case points
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") 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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V") w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
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) 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_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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V") w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
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) 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_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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V") w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
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) 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>") 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>") public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
def __str__(self):
return f"{self.pk} - {self.event}"
class Meta: class Meta:
ordering = ['event'] ordering = ['event']
permissions = [ permissions = [
('review_power', 'Can review Power Test Records') ('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): class EventCheckIn(models.Model):
event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE) 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) person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkins', on_delete=models.CASCADE)
time = models.DateTimeField() time = models.DateTimeField()
role = models.CharField(max_length=50, blank=True) vehicle = models.CharField(max_length=100)
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): class EventCheckOut(models.Model):
sass = " Please invent time travel and retry." event = models.ForeignKey('Event', on_delete=models.CASCADE)
if self.time > timezone.now(): person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkouts', on_delete=models.CASCADE)
raise ValidationError("May not check in in the future." + sass) time = models.DateTimeField() # TODO Validate may not check in in future
if self.end_time and self.end_time < self.time: vehicle = models.CharField(max_length=100)
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

View File

@@ -1,5 +0,0 @@
{% extends 'base_client.html' %}
{% block content %}
{% include 'estates/estates_event_table.html' %}
{% endblock %}

View File

@@ -1,78 +0,0 @@
{% load namewithnotes 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">Event Details</th>
<th scope="col">Status</th>
<th scope="col">Member In Charge</th>
<th scope="col">Power Plan</th>
</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>
{{ event.name }}
{% if event.venue %}
<small>at {{ event.venue }}</small>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
{{ event.person.name }}
{% if event.organisation %}
for {{ event.organisation.name }}
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
</td>
<td>
{{ event.get_status_display }}
</td>
<!---MIC-->
<td id="event_mic" class="text-nowrap">
{% if event.mic %}
{{ event.mic }}
{% elif event.is_rig %}
<span class="fas fa-user-slash"></span>
{% endif %}
</td>
<td>
{{ event.riskassessment.power_plan|default:"Pending" }}
</td>
</tr>
{% empty %}
<tr class="bg-warning">
<td colspan="4">No events found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -1,8 +1,6 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %} {% load markdown_tags %}
{% load button from filters %}
{% load static %}
{% block content %} {% block content %}
<div class="row my-3 py-3"> <div class="row my-3 py-3">
@@ -54,43 +52,6 @@
</div> </div>
</div> </div>
</div> </div>
{% if event.can_check_in %}
<div class="col-sm-12">
<div class="card mt-3">
<div class="card-header">Crew Record</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Vehicle</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
<th scope="col">{% if request.user.pk is event.mic.pk %}<a href="{% url 'event_checkin_override' event.pk %}" class="btn btn-sm btn-success"><span class="fas fa-plus"></span> Add</a>{% endif %}</th>
</tr>
</thead>
<tbody id="crewmembers">
{% for crew in object.crew.all %}
<tr>
<td>{{crew.person}}</td>
<td>{{crew.vehicle|default:"None"}}</td>
<td>{{crew.time}}</td>
<td>{{crew.role}}</td>
<td>{% if crew.end_time %}{{crew.end_time}}{% else %}<span class="text-success fas fa-clock" data-toggle="tooltip" title="This person is currently checked into this event"></span>{% endif %}</td>
<td>{% if crew.end_time %}{% if crew.person.pk == request.user.pk or event.mic.pk == request.user.pk %}{% button 'edit' 'edit_checkin' crew.pk clazz='btn-sm modal-href' %}{% endif %}{%endif%}</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center bg-warning">Apparently this event happened by magic...</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %} {% include 'partials/event_detail_buttons.html' %}

View File

@@ -209,7 +209,7 @@
<div class="col-sm-9 col-md-7 col-lg-8"> <div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}"> <select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %} {% if venue %}
<option value="{{venue.id}}" selected="selected" data-update_url="{% url 'venue_update' venue.id %}">{{ venue }}</option> <option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>
@@ -231,7 +231,7 @@
<label for="{{ form.start_date.id_for_label }}" <label for="{{ form.start_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label> class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
<div class="col-sm-10"> <div class="col-sm-8">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required"> <div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
{% render_field form.start_date class+="form-control" %} {% render_field form.start_date class+="form-control" %}
@@ -246,7 +246,7 @@
<label for="{{ form.end_date.id_for_label }}" <label for="{{ form.end_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label> class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
<div class="col-sm-10"> <div class="col-sm-8">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date"> <div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
{% render_field form.end_date class+="form-control" %} {% render_field form.end_date class+="form-control" %}
@@ -334,26 +334,12 @@
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)"> <div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
<label for="{{ form.purchase_order.id_for_label }}" <label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label> class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8"> <div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %} {% render_field form.purchase_order class+="form-control" %}
</div> </div>
</div> </div>
<div class="form-group" data-toggle="tooltip" title="The thread for this event on the TEC Forum">
<label for="{{ form.forum_url.id_for_label }}"
class="col-sm-4 col-form-label">Forum Thread</label>
<div class="col-sm-12">
<p class="small mb-0">Paste URL</p>
{% render_field form.forum_url class+="form-control" %}
{% if object.pk %}
<p class="small mb-0">or</p>
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank">
<span class="fas fa-plus"></span> <span class="hidden-xs">Create Forum Thread</span></a>
{% endif %}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -9,8 +9,6 @@
<div class="col-12 text-right my-3"> <div class="col-12 text-right my-3">
{% button 'edit' url='ec_edit' pk=object.pk %} {% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %} {% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %} {% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div> </div>
</div> </div>
@@ -33,6 +31,12 @@
{% endif %} {% endif %}
</dd> </dd>
</dl> </dl>
<p>List vehicles and their drivers</p>
<ul>
{% for i in object.vehicles.all %}
<li>{{i}}</li>
{% endfor %}
</ul>
</div> </div>
</div> </div>
</div> </div>
@@ -70,11 +74,168 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card mb-3">
<div class="card-header">Crew Record</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Crewmember</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
</tr>
</thead>
<tbody id="crewmemberst">
{% for crew in object.crew.all %}
<tr>
<td>{{crew.crewmember}}</td>
<td>{{crew.start}}</td>
<td>{{crew.role}}</td>
<td>{{crew.end}}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
<div class="card-body">
{% if object.event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">
{{ object.rcds|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
<dd class="col-2">
{{ object.supply_test|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
{% else %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
<dd class="col-2">
{{ object.source_rcd|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'labelling'|safe }}</dt>
<dd class="col-2">
{{ object.labelling|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
<hr>
<p>Tests at first distro</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th>{{ object|help_text:'fd_voltage_l1' }}</th>
<th>{{ object|help_text:'fd_voltage_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
</tr>
<tr>
<td>{{ object.fd_voltage_l1 }}</td>
<td>{{ object.fd_voltage_l2 }}</td>
<td>{{ object.fd_voltage_l3 }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_phase_rotation'|safe }}</th>
<td colspan="3">{{ object.fd_phase_rotation|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_earth_fault'|safe}}</th>
<td colspan="3">{{ object.fd_earth_fault }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_pssc'}}</th>
<td colspan="3">{{ object.fd_pssc }}</td>
</tr>
</tbody>
</table>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{ object|help_text:'w1_description'|safe}}</th>
<td>{{ object.w1_description }}</td>
<td>{{ object.w2_description|default:'' }}</td>
<td>{{ object.w3_description|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_polarity'|safe}}</th>
<td>{{ object.w1_polarity|yesnoi }}</td>
<td>{{ object.w2_polarity|default:''|yesnoi }}</td>
<td>{{ object.w3_polarity|default:''|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_voltage'|safe}}</th>
<td>{{ object.w1_voltage }}</td>
<td>{{ object.w2_voltage|default:'' }}</td>
<td>{{ object.w3_voltage|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_earth_fault'|safe}}</th>
<td>{{ object.w1_earth_fault }}</td>
<td>{{ object.w2_earth_fault|default:'' }}</td>
<td>{{ object.w3_earth_fault|default:'' }}</td>
</tr>
</tbody>
</table>
<hr>
<dl class="row">
<dt class="col-10">{{ object|help_text:'all_rcds_tested'|safe }}</dt>
<dd class="col-2">
{{ object.all_rcds_tested|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'public_sockets_tested'|safe }}</dt>
<dd class="col-2">
{{ object.public_sockets_tested|yesnoi }}
</dd>
</dl>
<hr>
{% include 'partials/ec_power_info.html' %}
{% endif %}
</div>
</div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %} {% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %} {% button 'view' url='event_detail' pk=object.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %} {% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">

View File

@@ -13,19 +13,22 @@
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/selects.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script> <script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="col-12"> <div class="col-12">
{% include 'form_errors.html' %} {% include 'form_errors.html' %}
{% if edit %}
<form role="form" method="POST" action="{% if edit %}{% url 'ec_edit' pk=object.pk %}{% else %}{% url 'event_ec' pk=event.pk %}{% endif %}"> <form role="form" method="POST" action="{% url 'ec_edit' pk=object.pk %}">
{% else %}
<form role="form" method="POST" action="{% url 'event_ec' pk=event.pk %}">
{% endif %}
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}" <input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/> value="{{event.pk}}"/>
{% csrf_token %} {% csrf_token %}
@@ -56,9 +59,11 @@
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group"> <div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}" <label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label> class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}"> <select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %} {% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option> <option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>

View File

@@ -1,105 +1,56 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %} {% load static %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load button from filters %} {% load button from filters %}
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/> <link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/selects.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script> <script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script> <script src="{% static 'js/tooltip.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="col-12"> <div class="col-12">
{% include 'form_errors.html' %} {% include 'form_errors.html' %}
<form id="checkin" role="form" method="POST" action="{{ form.action|default:request.path }}"> <form role="form" method="POST" action="{% url 'event_checkin' pk=event.pk %}">
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}" <input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/> value="{{event.pk}}"/>
{% 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 }}" <input type="hidden" name="{{ form.person.name }}" id="{{ form.person.id_for_label }}"
value="{{request.user.pk}}"/> value="{{request.user.pk}}"/>
{% endif %}
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.time.id_for_label }}" <label for="{{ form.time.id_for_label }}"
class="col-sm-4 col-form-label">Start Time</label> class="col-sm-4 col-form-label">{{ form.time.label }}</label>
<div class="col-sm-8"> <div class="col-sm-8">
{% render_field form.time class+="form-control" %} {% render_field form.time class+="form-control" %}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.role.id_for_label }}" class="col col-form-label">Role</label> <label for="{{ form.vehicle.id_for_label }}" class="col-sm-4 col-form-label">Did you drive? If so, what did you drive?</label>
<div class="row pl-3"> <br><button type="button" class="btn btn-primary" onclick="document.getElementById('id_vehicle').value='Virgil'"><span class="fas fa-truck-moving"></span> Virgil</button>
<div class="col-md-6 col-sm-12"> <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>
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_role').value='MIC'">MIC</button> <br>Other (enter text)
<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"> <div class="col-sm-8">
{% render_field form.end_time class+="form-control" %} {% render_field form.vehicle class+="form-control" %}
</div> </div>
</div> </div>
{% endif %}
{% if not request.is_ajax %}
<div class="row mt-3"> <div class="row mt-3">
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% button 'submit' %} {% button 'submit' %}
</div> </div>
</div> </div>
{% endif %}
</form> </form>
</div> </div>
{% endblock %} {% 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 %}

View File

@@ -86,7 +86,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th> <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_l1' }}</th>
<th>{{ object|help_text:'fd_voltage_l2' }}</th> <th>{{ object|help_text:'fd_voltage_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th> <th>{{ object|help_text:'fd_voltage_l3' }}</th>
@@ -165,11 +165,11 @@
</div> </div>
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% button 'edit' url='pt_edit' pk=object.pk %} {% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %} {% button 'view' url='event_detail' pk=object.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %} {% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="powertestrecord_history" %} {% include 'partials/last_edited.html' with target="eventchecklist_history" %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -51,18 +51,22 @@
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group"> <div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
<label for="{{ form.power_mic.id_for_label }}" <label for="{{ form.power_mic.id_for_label }}"
class="col-4 col-form-label">{{ form.power_mic.help_text }}</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"> <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 %} {% if power_mic %}
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option> <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 %} {% endif %}
</select> </select>
</div> </div>
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group"> <div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}" <label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label> class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker 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="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %} {% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option> <option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>
@@ -115,7 +119,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th> <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_l1.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l2.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> <th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>

View File

@@ -25,7 +25,7 @@
}); });
$('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() { $('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() {
$('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value)); $('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value));
//$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value)); $('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
}); });
$('input[type=radio][name=special_structures]').change(function() { $('input[type=radio][name=special_structures]').change(function() {
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value)); $('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));

View File

@@ -49,13 +49,6 @@
{% endif %} {% endif %}
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a> <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>
<a href="{% url 'event_checkin' event.pk %}" class="btn btn-success"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a>
{% if event.can_check_in %}
{% if request.user.current_event %}
<a href="{% url 'event_checkout' %}" class="btn btn-warning">Check Out</a>
{% else %}
<a href="{% url 'event_checkin' event.pk %}" class="btn btn-success modal-href"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a>
{% endif %}
{% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -77,15 +77,6 @@
<dt class="col-sm-6">PO</dt> <dt class="col-sm-6">PO</dt>
<dd class="col-sm-6">{{ object.purchase_order }}</dd> <dd class="col-sm-6">{{ object.purchase_order }}</dd>
{% endif %} {% endif %}
<dt class="col-6">Forum Thread</dt>
{% if object.forum_url %}
<dd class="col-6"><a href="{{object.forum_url}}">{{object.forum_url}}</a></dd>
{% else %}
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank"><span
class="fas fa-plus"></span> <span
class="hidden-xs">Create Forum Thread</span></a>
{% endif %}
</dl> </dl>
</div> </div>
</div> </div>

View File

@@ -20,16 +20,14 @@
{% else %} {% else %}
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span> <span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
{% endif %} {% endif %}
{% if event.has_checklist %} {% endif %}
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span> {% if event.checklists.count > 1 %}({{event.checklists.count}}){% endif %}</span> {% if not event.dry_hire %}
{% if event.hs_done %}
{# TODO Display status of all checklists #}
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span></span>
{% else %} {% else %}
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span> <span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
{% endif %} {% endif %}
{% if event.has_power %}
<span class="badge badge-success">Power Record: <span class="fas fa-check"></span> {% if event.power_tests.count > 1 %}({{event.power_tests.count}}){% endif %}</span>
{% else %}
<span class="badge badge-danger">Power Record: <span class="fas fa-times"></span></span>
{% endif %}
{% endif %} {% endif %}
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
{% if event.invoice %} {% if event.invoice %}

View File

@@ -29,15 +29,7 @@
</div> </div>
<div class="row pt-3"> <div class="row pt-3">
<label class="col-sm-4 col-form-label" <label class="col-sm-4 col-form-label"
for="{{ form.method.id_for_label }}">{{ form.method.label }} for="{{ form.method.id_for_label }}">{{ form.method.label }}</label>
<span class="fas fa-info-circle text-info" data-toggle="collapse" data-target="#collapse" aria-expanded="false" aria-controls="collapse"></span>
<ul class="collapse" id="collapse">
<li>Cash - Self Explanatory</li>
<li>Internal - Transfers within the Students' Union only</li>
<li>External - All other transfers (<em>including</em> the University)</li>
<li>TEC Adjustment - Manual corrections</li>
</ul>
</label>
<div class="col-sm-8"> <div class="col-sm-8">
{% render_field form.method class+="form-control" %} {% render_field form.method class+="form-control" %}
</div> </div>

View File

@@ -171,7 +171,7 @@ def title_spaced(string):
@register.filter(needs_autoescape=True) @register.filter(needs_autoescape=True)
def namewithnotes(obj, url, autoescape=True): def namewithnotes(obj, url, autoescape=True):
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0: if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
return mark_safe(obj.name + f" <a href='{reverse(url, kwargs={'pk': obj.pk})}'><span class='fas fa-sticky-note'></span></a>") return mark_safe(obj.name + " <a href='{}'><span class='fas fa-sticky-note'></span></a>".format(reverse(url, kwargs={'pk': obj.pk})))
else: else:
return obj.name return obj.name
@@ -183,7 +183,7 @@ def linkornone(target, namespace=None, autoescape=True):
link = namespace + "://" + target link = namespace + "://" + target
else: else:
link = target link = target
return mark_safe(f"<a href='{link}' target='_blank'><span class='overflow-ellipsis'>{target}</span></a>") return mark_safe("<a href='{}' target='_blank'><span class='overflow-ellipsis'>{}</span></a>".format(link, str(target)))
else: else:
return "None" return "None"

View File

@@ -43,22 +43,15 @@ def venue(db):
@pytest.fixture # TODO parameterise with Event sizes @pytest.fixture # TODO parameterise with Event sizes
def checklist(basic_event, venue, admin_user, ra): def checklist(basic_event, venue, admin_user, ra):
checklist = models.EventChecklist.objects.create(event=basic_event, safe_parking=False, checklist = models.EventChecklist.objects.create(event=basic_event, power_mic=admin_user, safe_parking=False,
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False, safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
ear_plugs=False, hs_location="Locked away safely", ear_plugs=False, hs_location="Locked away safely",
extinguishers_location="Somewhere, I forgot", extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
date=timezone.now(), venue=venue) date=timezone.now(), venue=venue)
yield checklist yield checklist
checklist.delete() checklist.delete()
@pytest.fixture
def power_test(basic_event, venue, admin_user, ra):
power_test = models.PowerTestRecord.objects.create(event=basic_event, venue=venue)
yield power_test
power_test.delete()
@pytest.fixture @pytest.fixture
def many_events(db, admin_user, scope="class"): def many_events(db, admin_user, scope="class"):
many_events = { many_events = {

View File

@@ -114,7 +114,7 @@ class CreateEvent(FormPage):
} }
def select_event_type(self, type_name): def select_event_type(self, type_name):
self.find_element(By.XPATH, f'//button[.="{type_name}"]').click() self.find_element(By.XPATH, '//button[.="{}"]'.format(type_name)).click()
def item_row(self, ID): def item_row(self, ID):
return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID)) return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID))
@@ -230,6 +230,11 @@ class CreateEventChecklist(FormPage):
URL_TEMPLATE = 'event/{event_id}/checklist' URL_TEMPLATE = 'event/{event_id}/checklist'
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]") _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
_power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]")
_add_vehicle_locator = (By.XPATH, "//button[contains(., 'Vehicle')]")
_add_crew_locator = (By.XPATH, "//button[contains(., 'Crew')]")
_vehicle_row_locator = ('xpath', "//tr[@id[starts-with(., 'vehicle') and not(contains(.,'new'))]]")
_crew_row_locator = ('xpath', "//tr[@id[starts-with(., 'crew') and not(contains(.,'new'))]]")
form_items = { form_items = {
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')), 'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
@@ -240,20 +245,6 @@ class CreateEventChecklist(FormPage):
'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')), 'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')),
'hs_location': (regions.TextBox, (By.ID, 'id_hs_location')), 'hs_location': (regions.TextBox, (By.ID, 'id_hs_location')),
'extinguishers_location': (regions.TextBox, (By.ID, 'id_extinguishers_location')), 'extinguishers_location': (regions.TextBox, (By.ID, 'id_extinguishers_location')),
}
@property
def success(self):
return '{event_id}' not in self.driver.current_url
class CreatePowerTestRecord(FormPage):
URL_TEMPLATE = 'event/{event_id}/power'
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
_power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]")
form_items = {
'rcds': (regions.CheckBox, (By.ID, 'id_rcds')), 'rcds': (regions.CheckBox, (By.ID, 'id_rcds')),
'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')), 'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')),
'earthing': (regions.CheckBox, (By.ID, 'id_earthing')), 'earthing': (regions.CheckBox, (By.ID, 'id_earthing')),
@@ -272,10 +263,58 @@ class CreatePowerTestRecord(FormPage):
'w1_earth_fault': (regions.TextBox, (By.ID, 'id_w1_earth_fault')), 'w1_earth_fault': (regions.TextBox, (By.ID, 'id_w1_earth_fault')),
} }
def add_vehicle(self):
self.find_element(*self._add_vehicle_locator).click()
def add_crew(self):
self.find_element(*self._add_crew_locator).click()
@property @property
def power_mic(self): def power_mic(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector)) return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector))
@property
def vehicles(self):
return [self.VehicleRow(self, el) for el in self.find_elements(*self._vehicle_row_locator)]
class VehicleRow(Region):
_name_locator = ('xpath', ".//input")
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
@property
def name(self):
return regions.TextBox(self, self.root.find_element(*self._name_locator))
@property
def vehicle(self):
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
@property
def crew(self):
return [self.CrewRow(self, el) for el in self.find_elements(*self._crew_row_locator)]
class CrewRow(Region):
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
_start_time_locator = ('xpath', ".//input[@name[starts-with(., 'start') and not(contains(.,'new'))]]")
_end_time_locator = ('xpath', ".//input[@name[starts-with(., 'end') and not(contains(.,'new'))]]")
_role_locator = ('xpath', ".//input[@name[starts-with(., 'role') and not(contains(.,'new'))]]")
@property
def crewmember(self):
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
@property
def start_time(self):
return regions.DateTimePicker(self, self.root.find_element(*self._start_time_locator))
@property
def end_time(self):
return regions.DateTimePicker(self, self.root.find_element(*self._end_time_locator))
@property
def role(self):
return regions.TextBox(self, self.root.find_element(*self._role_locator))
@property @property
def success(self): def success(self):
return '{event_id}' not in self.driver.current_url return '{event_id}' not in self.driver.current_url

View File

@@ -6,7 +6,7 @@ from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
class Header(Region): class Header(Region):
def find_link(self, link_text): def find_link(self, link_text):
return self.driver.find_element(By.PARTIAL_LINK_TEXT, link_text) return self.driver.find_element_by_partial_link_text(link_text)
class ItemRow(Region): class ItemRow(Region):

View File

@@ -318,7 +318,7 @@ class TestEventDuplicate(BaseRigboardTest):
self.assertFalse(newEvent.authorised) self.assertFalse(newEvent.authorised)
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element(By.XPATH, '//h2').text) self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element_by_xpath('//h2').text)
self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible
# Check the new items are visible # Check the new items are visible
@@ -327,25 +327,26 @@ class TestEventDuplicate(BaseRigboardTest):
self.assertIn("Test Item 2", table.text) self.assertIn("Test Item 2", table.text)
self.assertIn("Test Item 3", table.text) self.assertIn("Test Item 3", table.text)
infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..') infoPanel = self.driver.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
self.assertIn("N%05d" % self.testEvent.pk, infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) self.assertIn("N%05d" % self.testEvent.pk,
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the PO hasn't carried through # Check the PO hasn't carried through
self.assertNotIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text) self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
self.assertIn("N%05d" % self.testEvent.pk, self.assertIn("N%05d" % self.testEvent.pk,
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
self.driver.get(self.live_server_url + '/event/' + str(self.testEvent.pk)) # Go back to the old event self.driver.get(self.live_server_url + '/event/' + str(self.testEvent.pk)) # Go back to the old event
# Check that based-on hasn't crept into the old event # Check that based-on hasn't crept into the old event
infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..') infoPanel = self.driver.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
self.assertNotIn("N%05d" % self.testEvent.pk, self.assertNotIn("N%05d" % self.testEvent.pk,
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the PO remains on the old event # Check the PO remains on the old event
self.assertIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text) self.assertIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
self.assertNotIn("N%05d" % self.testEvent.pk, self.assertNotIn("N%05d" % self.testEvent.pk,
infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the items are as they were # Check the items are as they were
table = self.page.item_table # ID number is known, see above table = self.page.item_table # ID number is known, see above
@@ -676,6 +677,14 @@ def small_ec(page, admin_user):
page.ear_plugs = True page.ear_plugs = True
page.hs_location = "The Moon" page.hs_location = "The Moon"
page.extinguishers_location = "With the rest of the fire" page.extinguishers_location = "With the rest of the fire"
# If we do this first the search fails, for ... reasons
page.power_mic.search(admin_user.name)
page.power_mic.toggle()
assert not page.power_mic.is_open
page.earthing = True
page.rcds = True
page.supply_test = True
page.pat = True
def test_ec_create_small(logged_in_browser, live_server, admin_user, ra): def test_ec_create_small(logged_in_browser, live_server, admin_user, ra):
@@ -696,15 +705,14 @@ def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra)
page.ear_plugs = True page.ear_plugs = True
page.hs_location = "Death Valley" page.hs_location = "Death Valley"
page.extinguishers_location = "With the rest of the fire" page.extinguishers_location = "With the rest of the fire"
# If we do this first the search fails, for ... reasons
page.power_mic.search(admin_user.name)
page.power_mic.toggle()
assert not page.power_mic.is_open
# Gotta scroll to make the button clickable # Gotta scroll to make the button clickable
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
page.submit()
assert page.success
def test_power_checklist(logged_in_browser, live_server, admin_user, power_test, medium_ra):
page = pages.CreatePowerTestRecord(logged_in_browser.driver, live_server.url, event_id=medium_ra.event.pk).open()
page.earthing = True page.earthing = True
page.pat = True page.pat = True
page.source_rcd = True page.source_rcd = True
@@ -719,15 +727,56 @@ def test_power_checklist(logged_in_browser, live_server, admin_user, power_test,
page.w1_polarity = True page.w1_polarity = True
page.w1_voltage = 240 page.w1_voltage = 240
page.w1_earth_fault = "0.42" page.w1_earth_fault = "0.42"
# If we do this first the search fails, for ... reasons
page.power_mic.search(admin_user.name)
page.power_mic.toggle()
assert not page.power_mic.is_open
page.submit() page.submit()
assert page.success assert page.success
def test_ec_create_vehicle(logged_in_browser, live_server, admin_user, checklist):
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
small_ec(page, admin_user)
page.add_vehicle()
assert len(page.vehicles) == 1
vehicle_name = 'Brian'
page.vehicles[0].name.set_value(vehicle_name)
# Appears we're moving too fast for javascript...
t.sleep(1)
page.vehicles[0].vehicle.search(admin_user.first_name)
t.sleep(1)
page.submit()
assert page.success
# Check data is correct
checklist.refresh_from_db()
vehicle = models.EventChecklistVehicle.objects.get(checklist=checklist.pk)
assert vehicle_name == vehicle.vehicle
# TODO Test validation of end before start
def test_ec_create_crew(logged_in_browser, live_server, admin_user, checklist):
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
small_ec(page, admin_user)
page.add_crew()
assert len(page.crew) == 1
role = "MIC"
start_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 9, 0))
end_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 10, 30))
crew = page.crew[0]
t.sleep(2)
crew.crewmember.search(admin_user.first_name)
t.sleep(2)
crew.role.set_value(role)
crew.start_time.set_value(start_time)
crew.end_time.set_value(end_time)
page.submit()
assert page.success
# Check data is correct
crew_obj = models.EventChecklistCrew.objects.get(checklist=checklist.pk)
assert admin_user.pk == crew_obj.crewmember.pk
assert role == crew_obj.role
assert start_time == crew_obj.start
assert end_time == crew_obj.end
# TODO Can I loop through all the boolean fields and test them at once? # TODO Can I loop through all the boolean fields and test them at once?
def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event): def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event):
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open() page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open()

View File

@@ -259,7 +259,7 @@ class TestPrintPaperwork(TestCase):
def test_login_redirect(client, django_user_model): def test_login_redirect(client, django_user_model):
request_url = reverse('event_embed', kwargs={'pk': 1}) request_url = reverse('event_embed', kwargs={'pk': 1})
expected_url = f"{reverse('login_embed')}?next={request_url}" expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
# Request the page and check it redirects # Request the page and check it redirects
response = client.get(request_url, follow=True) response = client.get(request_url, follow=True)
@@ -372,8 +372,7 @@ def test_ra_redirect(admin_client, admin_user, ra):
class TestMarkdownTemplateTags(TestCase): class TestMarkdownTemplateTags(TestCase):
with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md"), encoding="utf-8") as f: markdown = open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")).read()
markdown = f.read()
def test_html_safe(self): def test_html_safe(self):
html = markdown_filter(self.markdown) html = markdown_filter(self.markdown)

View File

@@ -4,7 +4,7 @@ from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import RedirectView from django.views.generic import RedirectView
from PyRIGS.decorators import (api_key_required, has_oembed, from PyRIGS.decorators import (api_key_required, has_oembed,
permission_required_with_403, not_estates) permission_required_with_403)
from . import views from . import views
urlpatterns = [ urlpatterns = [
@@ -42,22 +42,21 @@ urlpatterns = [
name='venue_update'), name='venue_update'),
# Rigboard # Rigboard
path('rigboard/', not_estates()(views.RigboardIndex.as_view()), name='rigboard'), path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', not_estates()(views.WebCalendar.as_view()), path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
name='web_calendar'), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$', re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
not_estates()(views.WebCalendar.as_view()), name='web_calendar'), 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}))/$', re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
not_estates()(views.WebCalendar.as_view()), name='web_calendar'), login_required()(views.WebCalendar.as_view()), name='web_calendar'),
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')), path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
path('estates/', login_required()(views.EstatesEventList.as_view()), name='estates'),
path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()), path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()),
name='event_detail'), name='event_detail'),
path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()), path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()),
name='event_create'), name='event_create'),
path('event/archive/', not_estates()(views.EventArchive.as_view()), path('event/archive/', login_required()(views.EventArchive.as_view()),
name='event_archive'), name='event_archive'),
path('event/<int:pk>/embed/', path('event/<int:pk>/embed/',
xframe_options_exempt(login_required(login_url='/user/login/embed/')(views.EventEmbed.as_view())), xframe_options_exempt(login_required(login_url='/user/login/embed/')(views.EventEmbed.as_view())),
@@ -76,7 +75,7 @@ urlpatterns = [
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()), path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
name='event_ra'), name='event_ra'),
path('event/ra/<int:pk>/', not_estates()(views.EventRiskAssessmentDetail.as_view()), path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()),
name='ra_detail'), name='ra_detail'),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()), path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
name='ra_edit'), name='ra_edit'),
@@ -86,7 +85,7 @@ urlpatterns = [
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()), path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
name='event_ec'), name='event_ec'),
path('event/checklist/<int:pk>/', not_estates()(views.EventChecklistDetail.as_view()), path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()),
name='ec_detail'), name='ec_detail'),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()), path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
name='ec_edit'), name='ec_edit'),
@@ -95,24 +94,15 @@ urlpatterns = [
path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()), path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()),
name='event_pt'), name='event_pt'),
path('event/power/<int:pk>/', not_estates()(views.PowerTestDetail.as_view()), path('event/power/<int:pk>/', login_required(views.PowerTestDetail.as_view()),
name='pt_detail'), name='pt_detail'),
path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()), path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()),
name='pt_edit'), name='pt_edit'),
path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()), path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()),
name='pt_review', kwargs={'model': 'PowerTestRecord'}), name='pt_review', kwargs={'model': 'PowerTestRecord'}),
path('event/<int:pk>/checkin/', not_estates()(views.EventCheckIn.as_view()), path('event/<int:pk>/checkin/', permission_required_with_403('RIGS.add_eventcheckin')(views.EventCheckIn.as_view()),
name='event_checkin'), name='event_checkin'),
path('event/checkout/', not_estates()(views.EventCheckOut.as_view()),
name='event_checkout'),
path('event/<int:pk>/checkin/edit/', not_estates()(views.EventCheckInEdit.as_view()),
name='edit_checkin'),
path('event/<int:pk>/checkin/add/', not_estates()(views.EventCheckInOverride.as_view()),
name='event_checkin_override'),
path('event/<int:pk>/thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'),
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
# Finance # Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()), path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),

View File

@@ -115,7 +115,7 @@ class VenueDetail(GenericDetailView):
class VenueCreate(GenericCreateView, ModalURLMixin): class VenueCreate(GenericCreateView, ModalURLMixin):
model = models.Venue model = models.Venue
fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available', 'on_campus'] fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
def get_success_url(self): def get_success_url(self):
return self.get_close_url('venue_update', 'venue_detail') return self.get_close_url('venue_update', 'venue_detail')
@@ -123,7 +123,7 @@ class VenueCreate(GenericCreateView, ModalURLMixin):
class VenueUpdate(GenericUpdateView, ModalURLMixin): class VenueUpdate(GenericUpdateView, ModalURLMixin):
model = models.Venue model = models.Venue
fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available', 'on_campus'] fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
def get_success_url(self): def get_success_url(self):
return self.get_close_url('venue_update', 'venue_detail') return self.get_close_url('venue_update', 'venue_detail')

View File

@@ -1,36 +1,42 @@
from django.apps import apps from django.apps import apps
from django.contrib import messages from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.views import generic from django.views import generic
from reversion import revisions as reversion from reversion import revisions as reversion
from RIGS import models, forms from RIGS import models, forms
from RIGS.views.rigboard import get_related from RIGS.views.rigboard import get_related
from PyRIGS.views import PrintView, ModalURLMixin from PyRIGS.views import PrintView
from django.shortcuts import redirect
class HSCreateView(generic.CreateView): class HSCreateView(generic.CreateView):
def get_form(self, **kwargs):
form = super().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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
event = models.Event.objects.get(pk=self.kwargs.get('pk')) epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event context['event'] = event
context['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}' context['page_title'] = f'Create {self} for Event {event.display_id}'
get_related(context['form'], context)
return context return context
class MarkReviewed(generic.RedirectView): class MarkReviewed(generic.View):
def get_redirect_url(self, *args, **kwargs): def get(self, *args, **kwargs):
obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk')) obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk'))
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(self.request.user) reversion.set_user(self.request.user)
obj.reviewed_by = self.request.user obj.reviewed_by = self.request.user
obj.reviewed_at = timezone.now() obj.reviewed_at = timezone.now()
obj.save() obj.save()
return self.request.META.get('HTTP_REFERER', reverse('hs_list')) return HttpResponseRedirect(reverse_lazy('hs_list'))
class EventRiskAssessmentCreate(HSCreateView): class EventRiskAssessmentCreate(HSCreateView):
@@ -46,12 +52,12 @@ class EventRiskAssessmentCreate(HSCreateView):
ra = models.RiskAssessment.objects.filter(event=event).first() ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is not None: if ra is not None:
return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk})) return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
return super().get(self) return super(EventRiskAssessmentCreate, self).get(self)
def get_success_url(self): def get_success_url(self):
return reverse('ra_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
class EventRiskAssessmentEdit(generic.UpdateView): class EventRiskAssessmentEdit(generic.UpdateView):
@@ -64,10 +70,10 @@ class EventRiskAssessmentEdit(generic.UpdateView):
ra.reviewed_by = None ra.reviewed_by = None
ra.reviewed_at = None ra.reviewed_at = None
ra.save() ra.save()
return reverse('ra_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
rpk = self.kwargs.get('pk') rpk = self.kwargs.get('pk')
ra = models.RiskAssessment.objects.get(pk=rpk) ra = models.RiskAssessment.objects.get(pk=rpk)
context['event'] = ra.event context['event'] = ra.event
@@ -82,7 +88,7 @@ class EventRiskAssessmentDetail(generic.DetailView):
template_name = 'hs/risk_assessment_detail.html' template_name = 'hs/risk_assessment_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>" context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context return context
@@ -92,7 +98,7 @@ class EventChecklistDetail(generic.DetailView):
template_name = 'hs/event_checklist_detail.html' template_name = 'hs/event_checklist_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventChecklistDetail, self).get_context_data(**kwargs)
context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>" context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context return context
@@ -107,10 +113,10 @@ class EventChecklistEdit(generic.UpdateView):
ec.reviewed_by = None ec.reviewed_by = None
ec.reviewed_at = None ec.reviewed_at = None
ec.save() ec.save()
return reverse('ec_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventChecklistEdit, self).get_context_data(**kwargs)
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
ec = models.EventChecklist.objects.get(pk=pk) ec = models.EventChecklist.objects.get(pk=pk)
context['event'] = ec.event context['event'] = ec.event
@@ -129,21 +135,18 @@ class EventChecklistCreate(HSCreateView):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
epk = kwargs.get('pk') epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
# Check if RA exists # Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first() ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None: if ra is None:
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.') messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk})) return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
return super().get(self)
return super(EventChecklistCreate, self).get(self)
def get_success_url(self): def get_success_url(self):
return reverse('ec_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if context['event'].venue:
context['venue'] = context['event'].venue
return context
class PowerTestDetail(generic.DetailView): class PowerTestDetail(generic.DetailView):
@@ -166,7 +169,7 @@ class PowerTestEdit(generic.UpdateView):
ec.reviewed_by = None ec.reviewed_by = None
ec.reviewed_at = None ec.reviewed_at = None
ec.save() ec.save()
return reverse('pt_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -175,7 +178,7 @@ class PowerTestEdit(generic.UpdateView):
context['event'] = ec.event context['event'] = ec.event
context['edit'] = True context['edit'] = True
context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}' context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}'
get_related(context['form'], context) # get_related(context['form'], context)
return context return context
@@ -187,25 +190,18 @@ class PowerTestCreate(HSCreateView):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
epk = kwargs.get('pk') epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
# Check if RA exists # Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first() ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None: if ra is None:
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.') 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 HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
return super().get(self) return super().get(self)
def get_success_url(self): def get_success_url(self):
return reverse('pt_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('pt_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
if context['event'].riskassessment.power_mic:
context['power_mic'] = context['event'].riskassessment.power_mic
return context
class HSList(generic.ListView): class HSList(generic.ListView):
@@ -214,10 +210,10 @@ class HSList(generic.ListView):
template_name = 'hs/hs_list.html' template_name = 'hs/hs_list.html'
def get_queryset(self): def get_queryset(self):
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).exclude(dry_hire=True).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists') return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(HSList, self).get_context_data(**kwargs)
context['page_title'] = 'H&S Overview' context['page_title'] = 'H&S Overview'
return context return context
@@ -232,62 +228,14 @@ class RAPrint(PrintView):
return context return context
class EventCheckIn(generic.CreateView, ModalURLMixin): class EventCheckIn(generic.CreateView):
model = models.EventCheckIn model = models.EventCheckIn
template_name = 'hs/eventcheckin_form.html' template_name = 'hs/eventcheckin_form.html'
form_class = forms.EventCheckInForm 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk')) context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk'))
context['page_title'] = f'Check In to Event {context["event"].display_id}' context['page_title'] = f'Check In to Event {context["event"].display_id}'
# get_related(context['form'], context) # get_related(context['form'], context)
return 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', '/')

View File

@@ -3,12 +3,6 @@ import datetime
import re import re
import premailer import premailer
import simplejson import simplejson
import urllib
import hmac
import hashlib
from envparse import env
from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@@ -25,8 +19,6 @@ from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import generic from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.mixins import UserPassesTestMixin
from PyRIGS import decorators from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
@@ -385,55 +377,3 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
context['to_name'] = self.request.GET.get('to_name', None) context['to_name'] = self.request.GET.get('to_name', None)
context['target'] = 'event_authorise_form_preview' context['target'] = 'event_authorise_form_preview'
return context return context
class CreateForumThread(generic.base.RedirectView):
permanent = False
def get_redirect_url(self, *args, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk'])
if event.forum_url:
return event.forum_url
params = {
'title': str(event),
'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}',
'category': 'rig-info'
}
return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}'
class RecieveForumWebhook(generic.View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}"
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed):
return HttpResponseForbidden('Invalid signature header')
# Check if this is the right kind of event. The webhook filters by category on the forum side
if request.headers.get('X-Discourse-Event') == "topic_created":
body = simplejson.loads(request.body.decode('utf-8'))
event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros
event = models.Event.objects.filter(pk=event_id).first()
if event:
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}"
event.save()
return HttpResponse(status=202)
return HttpResponse(status=204)
class EstatesEventList(UserPassesTestMixin, generic.TemplateView):
template_name = 'estates/estates_event_list.html'
def get_context_data(self, **kwargs):
# get super context
context = super().get_context_data(**kwargs)
# call out method to get current events
context['events'] = models.Event.objects.current_events().filter(venue__on_campus=True, dry_hire=False, is_rig=True)
context['page_title'] = "Upcoming Campus Events"
return context
def test_func(self):
return self.request.user.email.endswith('@nottingham.ac.uk')

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-24 22:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0027_asset_nickname'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='length',
field=models.DecimalField(blank=True, decimal_places=2, help_text='m', max_digits=10, null=True),
),
]

View File

@@ -135,7 +135,7 @@ class Asset(models.Model, RevisionMixin):
# Cable assets # Cable assets
is_cable = models.BooleanField(default=False) is_cable = models.BooleanField(default=False)
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL) cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
length = models.DecimalField(decimal_places=2, max_digits=10, length = models.DecimalField(decimal_places=1, max_digits=10,
blank=True, null=True, help_text='m') blank=True, null=True, help_text='m')
csa = models.DecimalField(decimal_places=2, max_digits=10, csa = models.DecimalField(decimal_places=2, max_digits=10,
blank=True, null=True, help_text='mm²') blank=True, null=True, help_text='mm²')
@@ -192,5 +192,5 @@ class Asset(models.Model, RevisionMixin):
return str(self.asset_id) return str(self.asset_id)
@property @property
def display_name(self): def name(self):
return f"{self.display_id} | {self.description}" return f"{self.display_id} | {self.description}"

View File

@@ -35,11 +35,6 @@
function onAuditClick(assetID) { function onAuditClick(assetID) {
$('#' + assetID).remove(); $('#' + assetID).remove();
} }
$('#modal').on('hidden.bs.modal', function (e) {
searchbar = document.getElementById('id_q');
searchbar.value = "";
setTimeout(searchbar.focus(), 2000);
})
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -12,6 +12,7 @@
{{ block.super }} {{ block.super }}
<script src="{% static 'js/selects.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script> <script src="{% static 'js/easymde.min.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
@@ -34,10 +35,9 @@
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))}); $(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
}); });
</script> </script>
<script src="{% static "js/tooltip.js" %}"></script>
<script> <script>
$(function () { $(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip(); setupMDE('#id_comments');
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -12,7 +12,7 @@
</thead> </thead>
<tbody id="asset_table_body"> <tbody id="asset_table_body">
{% for item in object_list %} {% for item in object_list %}
<tr class="table-{{ item.status.display_class|default:'' }} assetRow" id="{{ item.asset_id }}"> <tr class="table-{{ item.status.display_class|default:'' }} assetRow">
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th> <th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td> <td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
<td class="assetCategory align-middle">{{ item.category }}</td> <td class="assetCategory align-middle">{{ item.category }}</td>

View File

@@ -1,15 +1,15 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load title_spaced from filters %} {% load title_spaced from filters %}
{% spaceless %} {% spaceless %}
{% if not nolabel %}<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>{%endif%} <label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
{% if append or prepend %} {% if append or prepend %}
<div class="input-group {{col}} flex-nowrap"> <div class="input-group {{col}}">
{% if prepend %} {% if prepend %}
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">{{ prepend }}</span> <span class="input-group-text">{{ prepend }}</span>
</div> </div>
{% endif %} {% endif %}
{% render_field field|add_class:'form-control' style=style %} {% render_field field|add_class:'form-control' %}
{% if append %} {% if append %}
<div class="input-group-append"> <div class="input-group-append">
<span class="input-group-text">{{ append }}</span> <span class="input-group-text">{{ append }}</span>
@@ -17,6 +17,6 @@
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
{% render_field field|add_class:'form-control' class+=col style=style %} {% render_field field|add_class:'form-control' class+=col %}
{% endif %} {% endif %}
{% endspaceless %} {% endspaceless %}

View File

@@ -34,7 +34,7 @@
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label> <label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div> <div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.purchase_price|add_class:'form-control'|set_data:"toggle:tooltip" value=object.purchase_price title="Ex. VAT" %} {% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
</div> </div>
</div> </div>
@@ -42,7 +42,7 @@
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label> <label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div> <div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.replacement_cost|add_class:'form-control'|set_data:"toggle:tooltip" value=object.replacement_cost title="Ex. VAT" %} {% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
</div> </div>
</div> </div>

View File

@@ -38,17 +38,3 @@ def test_asset(db, category, status):
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100) asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
yield asset yield asset
asset.delete() asset.delete()
@pytest.fixture
def test_status_2(db):
status = models.AssetStatus.objects.create(name="Lost", should_show=False)
yield status
status.delete()
@pytest.fixture
def test_asset_2(db, category, test_status_2):
asset, created = models.Asset.objects.get_or_create(asset_id="10", description="Working Mic", status=test_status_2, category=category, date_acquired=datetime.date(2001, 10, 20), replacement_cost=1000)
yield asset
asset.delete()

View File

@@ -77,7 +77,7 @@ class AssetForm(FormPage):
'description': (regions.TextBox, (By.ID, 'id_description')), 'description': (regions.TextBox, (By.ID, 'id_description')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')), 'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')), 'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'comments': (regions.TextBox, (By.ID, 'id_comments')), 'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')), 'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')), 'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')), 'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),

View File

@@ -1,6 +1,5 @@
import time import time
import datetime import datetime
import pytest
from django.utils import timezone from django.utils import timezone
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
@@ -54,45 +53,45 @@ class TestAssetList(AutoLoginTest):
self.assertEqual("10", asset_ids[2]) self.assertEqual("10", asset_ids[2])
self.assertEqual("C1", asset_ids[3]) self.assertEqual("C1", asset_ids[3])
def test_search(self):
self.page.set_query("10")
self.page.search()
self.assertTrue(len(self.page.assets) == 1)
self.assertEqual("Working Mic", self.page.assets[0].description)
self.assertEqual("10", self.page.assets[0].id)
@pytest.mark.xfail(reason="Fails on CI for unknown reason", raises=AssertionError) self.page.set_query("light")
def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type): self.page.search()
page = pages.AssetList(logged_in_browser.driver, live_server.url).open() self.assertTrue(len(self.page.assets) == 1)
page.set_query(test_asset.asset_id) self.assertEqual("A light", self.page.assets[0].description)
page.search()
assert len(page.assets) == 1
assert page.assets[0].description == test_asset.description
assert page.assets[0].id == test_asset.asset_id
page.set_query(test_asset.description) self.page.set_query("Random string")
page.search() self.page.search()
assert len(page.assets) == 1 self.assertTrue(len(self.page.assets) == 0)
assert page.assets[0].description == test_asset.description
page.set_query("Random string") self.page.set_query("")
page.search() self.page.search()
assert len(page.assets) == 0 # Only working stuff shown by default
self.assertTrue(len(self.page.assets) == 2)
page.set_query("") self.page.status_selector.toggle()
page.search() self.assertTrue(self.page.status_selector.is_open)
# Only working stuff shown by default self.page.status_selector.select_all()
assert len(page.assets) == 1 self.page.status_selector.toggle()
self.assertFalse(self.page.status_selector.is_open)
self.page.filter()
self.assertTrue(len(self.page.assets) == 4)
page.status_selector.toggle() self.page.category_selector.toggle()
assert page.status_selector.is_open self.assertTrue(self.page.category_selector.is_open)
page.status_selector.select_all() self.page.category_selector.set_option("Sound", True)
page.status_selector.toggle() self.page.category_selector.close()
assert not page.status_selector.is_open self.assertFalse(self.page.category_selector.is_open)
page.filter() self.page.filter()
assert len(page.assets) == 2 self.assertTrue(len(self.page.assets) == 2)
asset_ids = list(map(lambda x: x.id, self.page.assets))
page.category_selector.toggle() self.assertEqual("1", asset_ids[0])
assert page.category_selector.is_open self.assertEqual("10", asset_ids[1])
page.category_selector.set_option(category.name, True)
page.category_selector.close()
assert not page.category_selector.is_open
page.filter()
assert len(page.assets) == 2
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type): def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):
@@ -196,7 +195,7 @@ class TestAssetForm(AutoLoginTest):
# self.assertTrue(self.page.parent_selector.options[0].selected) # self.assertTrue(self.page.parent_selector.options[0].selected)
self.page.parent_selector.toggle() self.page.parent_selector.toggle()
self.assertFalse(self.driver.find_element(By.ID, 'cable-table').is_displayed()) self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
self.page.submit() self.page.submit()
self.assertTrue(self.page.success) self.assertTrue(self.page.success)
@@ -351,7 +350,7 @@ class TestAssetAudit(AutoLoginTest):
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal'))) self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
self.assertEqual(self.page.modal.asset_id, asset_row.id) self.assertEqual(self.page.modal.asset_id, asset_row.id)
self.page.modal.close() self.page.modal.close()
self.assertFalse(self.driver.find_element(By.ID, 'modal').is_displayed()) self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
# Make sure audit log was NOT filled out # Make sure audit log was NOT filled out
audited = models.Asset.objects.get(asset_id=asset_row.id) audited = models.Asset.objects.get(asset_id=asset_row.id)
assert audited.last_audited_by is None assert audited.last_audited_by is None

View File

@@ -2,7 +2,7 @@ from django.contrib.auth.decorators import login_required
from django.urls import path, register_converter from django.urls import path, register_converter
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from PyRIGS.decorators import has_oembed, permission_required_with_403, not_estates from PyRIGS.decorators import has_oembed, permission_required_with_403
from PyRIGS.views import OEmbedView from PyRIGS.views import OEmbedView
from . import views, converters from . import views, converters
@@ -10,8 +10,8 @@ register_converter(converters.AssetIDConverter, 'asset')
register_converter(converters.ListConverter, 'list') register_converter(converters.ListConverter, 'list')
urlpatterns = [ urlpatterns = [
path('', not_estates()(views.AssetList.as_view()), name='asset_index'), path('', login_required(views.AssetList.as_view()), name='asset_index'),
path('asset/list/', not_estates()(views.AssetList.as_view()), name='asset_list'), path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'),
path('asset/id/<asset:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'), path('asset/id/<asset:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
path('asset/create/', permission_required_with_403('assets.add_asset') path('asset/create/', permission_required_with_403('assets.add_asset')
(views.AssetCreate.as_view()), name='asset_create'), (views.AssetCreate.as_view()), name='asset_create'),
@@ -19,26 +19,26 @@ urlpatterns = [
(views.AssetEdit.as_view()), name='asset_update'), (views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<asset:pk>/label', not_estates()(views.GenerateLabel.as_view()), name='generate_label'), path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'), path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
path('cables/list/', not_estates()(views.CableList.as_view()), name='cable_list'), path('cables/list/', login_required(views.CableList.as_view()), name='cable_list'),
path('cabletype/list/', not_estates()(views.CableTypeList.as_view()), name='cable_type_list'), path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'), path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'), path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),
path('cabletype/<int:pk>/detail/', not_estates()(views.CableTypeDetail.as_view()), name='cable_type_detail'), path('cabletype/<int:pk>/detail/', login_required(views.CableTypeDetail.as_view()), name='cable_type_detail'),
path('asset/id/<str:pk>/embed/', path('asset/id/<str:pk>/embed/',
xframe_options_exempt( xframe_options_exempt(
login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())), login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())),
name='asset_embed'), name='asset_embed'),
path('asset/id/<str:pk>/oembed_json/', views.AssetOEmbed.as_view(), name='asset_oembed'), path('asset/id/<str:pk>/oembed_json/', views.AssetOEmbed.as_view(), name='asset_oembed'),
path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'), path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'),
path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'), path('asset/id/<str:pk>/audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'),
path('supplier/list/', not_estates()(views.SupplierList.as_view()), name='supplier_list'), path('supplier/list/', login_required(views.SupplierList.as_view()), name='supplier_list'),
path('supplier/<int:pk>/', not_estates()(views.SupplierDetail.as_view()), name='supplier_detail'), path('supplier/<int:pk>/', login_required(views.SupplierDetail.as_view()), name='supplier_detail'),
path('supplier/create/', permission_required_with_403('assets.add_supplier') path('supplier/create/', permission_required_with_403('assets.add_supplier')
(views.SupplierCreate.as_view()), name='supplier_create'), (views.SupplierCreate.as_view()), name='supplier_create'),
path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier') path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier')

2426
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@
"jquery": "^3.6.0", "jquery": "^3.6.0",
"konami": "^1.6.3", "konami": "^1.6.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"node-sass": "^9.0.0", "node-sass": "^7.0.3",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"uglify-js": "^3.14.5" "uglify-js": "^3.14.5"

View File

@@ -73,7 +73,7 @@ function initPicker(obj) {
return array; return array;
} }
}; };
//console.log(obj.data); console.log(obj.data);
if (!obj.data('noclear')) { if (!obj.data('noclear')) {
obj.prepend($("<option></option>") obj.prepend($("<option></option>")
.attr("value",'') .attr("value",'')

View File

@@ -6,6 +6,11 @@ function setupItemTable(items_json) {
newitem = -1; newitem = -1;
} }
function nl2br(str, is_xhtml) {
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
}
function escapeHtml(str) { function escapeHtml(str) {
return $('<div/>').text(str).html(); return $('<div/>').text(str).html();
} }

View File

@@ -30,9 +30,6 @@
<body> <body>
<a class="skip-link" href='#main'>Skip to content</a> <a class="skip-link" href='#main'>Skip to content</a>
{% block navbar %} {% block navbar %}
{% if request.user.current_event %}
<div class="bg-primary d-flex justify-content-between align-items-center"><span class="ml-2">You are currently checked in to <a href="{{request.user.current_event.event.get_absolute_url}}" class="text-white">{{request.user.current_event.event}}</a></span><a href="{% url 'event_checkout'%}" class="btn btn-warning">Check Out</a></div>
{% endif %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
<div class="container"> <div class="container">
<a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}"> <a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">

View File

@@ -17,6 +17,7 @@
e.preventDefault(); e.preventDefault();
data = $(this).serialize(); data = $(this).serialize();
action = $(this).attr('action'); action = $(this).attr('action');
console.log(action)
$.post(action, data, function(resp) { $.post(action, data, function(resp) {
$('#modal').html(resp); $('#modal').html(resp);
}); });

View File

@@ -1,7 +1,6 @@
{% extends override|default:"base_rigs.html" %} {% extends override|default:"base_rigs.html" %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load button from filters %} {% load button from filters %}
{% load verbose_name from filters %}
{% load markdown_tags %} {% load markdown_tags %}
{% block content %} {% block content %}
@@ -31,11 +30,6 @@
<dd>{{ object.three_phase_available|yesno|capfirst }}</dd> <dd>{{ object.three_phase_available|yesno|capfirst }}</dd>
{% endif%} {% endif%}
{% if object.on_campus is not None %}
<dt>{{ object|verbose_name:"on_campus" }}</dt>
<dd>{{ object.on_campus|yesno|capfirst }}</dd>
{% endif%}
{% if object.union_account is not None %} {% if object.union_account is not None %}
<dt>Union Account</dt> <dt>Union Account</dt>
<dd>{{ object.union_account|yesno|capfirst }}</dd> <dd>{{ object.union_account|yesno|capfirst }}</dd>

View File

@@ -78,20 +78,6 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if form.on_campus is not None %}
<div class="form-group form-row">
<div class="col-sm-10 col-sm-offset-2">
<div class="checkbox">
<label>
{% render_field form.on_campus %} {{ form.on_campus.label }}
</label>
</div>
</div>
<div class="alert alert-danger">
<span class="fas fa-exclamation"></span> Selecting this option will add <em>all</em> events at this venue to the calendar viewable by UoN Estates.
</div>
</div>
{% endif %}
{% if form.union_account is not None %} {% if form.union_account is not None %}
<div class="form-group form-row"> <div class="form-group form-row">
<div class="col-sm-10 col-sm-offset-2"> <div class="col-sm-10 col-sm-offset-2">

View File

@@ -8,13 +8,6 @@
<div class="row"> <div class="row">
<h1 class="col-sm-12 pb-3">R<small class="text-muted">ig</small> I<small class="text-muted">nformation</small> G<small class="text-muted">athering</small> S<small class="text-muted">ystem</small></h1> <h1 class="col-sm-12 pb-3">R<small class="text-muted">ig</small> I<small class="text-muted">nformation</small> G<small class="text-muted">athering</small> S<small class="text-muted">ystem</small></h1>
<h2 class="col-sm-12 pb-3">Welcome back {{ user.get_full_name }}, there {%if rig_count == 1 %}is one rig coming up{%else%}are {{ rig_count|apnumber }} rigs coming up.{%endif%}</h2> <h2 class="col-sm-12 pb-3">Welcome back {{ user.get_full_name }}, there {%if rig_count == 1 %}is one rig coming up{%else%}are {{ rig_count|apnumber }} rigs coming up.{%endif%}</h2>
{% if now %}
<div class="col-sm-12 alert alert-primary rounded-0 mx-auto">
{% for event in now %}
Event {{ event }} is happening today! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/>
{% endfor %}
</div>
{% endif %}
<div class="col-sm-4 mb-3"> <div class="col-sm-4 mb-3">
<div class="card"> <div class="card">
<img class="card-img-top d-none d-sm-block" src="{% static 'imgs/rigs.jpg' %}" alt="Some lights and haze, very purple" style="height: 150px; object-fit: cover;"> <img class="card-img-top d-none d-sm-block" src="{% static 'imgs/rigs.jpg' %}" alt="Some lights and haze, very purple" style="height: 150px; object-fit: cover;">
@@ -31,6 +24,7 @@
</div> </div>
<div class="col-sm-4 mb-3"> <div class="col-sm-4 mb-3">
<div class="card"> <div class="card">
{% now "m-d" as todays_date %}
<img class="card-img-top d-none d-sm-block" src="{% if todays_date == '04-01' %}{% static 'imgs/tappytaptap.gif' %}{%else%}{% static 'imgs/assets.jpg' %}{%endif%}" alt="M32 sound desk close up of the faders" style="height: 150px; object-fit: cover;"> <img class="card-img-top d-none d-sm-block" src="{% if todays_date == '04-01' %}{% static 'imgs/tappytaptap.gif' %}{%else%}{% static 'imgs/assets.jpg' %}{%endif%}" alt="M32 sound desk close up of the faders" style="height: 150px; object-fit: cover;">
<h4 class="card-header">Asset Database</h4> <h4 class="card-header">Asset Database</h4>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">

View File

@@ -105,10 +105,6 @@ class TrainingItem(models.Model):
def display_id(self): def display_id(self):
return f"{self.category.reference_number}.{self.reference_number}" return f"{self.category.reference_number}.{self.reference_number}"
@property
def display_name(self):
return f"{self.display_id} | {self.name}"
@display_id.filter @display_id.filter
@classmethod @classmethod
def display_id(cls, lookup, value): def display_id(cls, lookup, value):
@@ -373,7 +369,7 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
return str(self) return str(self)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('trainee_detail', kwargs={'pk': self.trainee_id}) return reverse('trainee_detail', kwargs={'pk': self.trainee.pk})
class Meta: class Meta:
unique_together = ["trainee", "level"] unique_together = ["trainee", "level"]

View File

@@ -78,6 +78,11 @@
</tr> </tr>
{% endfor %} {% endfor %}
<tr><th colspan="3" class="text-center">{{object}}</th></tr> <tr><th colspan="3" class="text-center">{{object}}</th></tr>
<tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -8,10 +8,10 @@
<p>Please Note:</p> <p>Please Note:</p>
<ul> <ul>
<li>Technical Assistant status is automatically valid when the item requirements are met.</li> <li>Technical Assistant status is automatically valid when the item requirements are met.</li>
<li>Technician status is also automatic. Notification of completion should be made at the next general meeting.</li> <li>Technician status is also automatic, but notification of status should be made at the next general meeting, at which point 'approval' should be granted on the system.</li>
<li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li> <li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li>
</ul> </ul>
<sup>Correct as of 24th May 2023, check the Training Policy.</sup> <sup>Correct as of 3rd September 2021, check the Training Policy.</sup>
</div> </div>
{% endif %} {% endif %}
{% for level in object_list %} {% for level in object_list %}

View File

@@ -43,7 +43,7 @@ def confirm_button(user, trainee, level):
if level.user_has_requirements(trainee): if level.user_has_requirements(trainee):
string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>" string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>"
if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'): if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'):
string += f"<a class='btn btn-info' href='{reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk})}'>Confirm</a>" string += "<a class='btn btn-info' href='{}'>Confirm</a>".format(reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk}))
return mark_safe(string) return mark_safe(string)
else: else:
return "" return ""

View File

@@ -44,7 +44,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
page.submit() page.submit()
assert page.success assert page.success
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item) qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
assert qualification.supervisor_id == supervisor.pk assert qualification.supervisor.pk == supervisor.pk
assert qualification.date == date assert qualification.date == date
assert qualification.notes == "A note" assert qualification.notes == "A note"
assert qualification.depth == models.TrainingItemQualification.STARTED assert qualification.depth == models.TrainingItemQualification.STARTED

View File

@@ -29,7 +29,7 @@ def test_add_qualification_reversion(admin_client, trainee, training_item, super
assert response.status_code == 302 assert response.status_code == 302
qual = models.TrainingItemQualification.objects.last() qual = models.TrainingItemQualification.objects.last()
assert qual is not None assert qual is not None
assert training_item.pk == qual.item_id assert training_item.pk == qual.item.pk
# Ensure only one revision has been created # Ensure only one revision has been created
assert Revision.objects.count() == 1 assert Revision.objects.count() == 1
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1}) response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})

View File

@@ -1,34 +1,33 @@
from django.urls import path from django.urls import path
from django.contrib.auth.decorators import login_required
from training.decorators import is_supervisor from training.decorators import is_supervisor
from training import views, models from training import views, models
from versioning.views import VersionHistory from versioning.views import VersionHistory
from PyRIGS.decorators import not_estates
urlpatterns = [ urlpatterns = [
path('items/', not_estates()(views.ItemList.as_view()), name='item_list'), path('items/', login_required(views.ItemList.as_view()), name='item_list'),
path('items/export/', not_estates()(views.ItemListExport.as_view()), name='item_list_export'), path('items/export/', login_required(views.ItemListExport.as_view()), name='item_list_export'),
path('item/<int:pk>/qualified_users/', not_estates()(views.ItemQualifications.as_view()), name='item_qualification'), path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
path('trainee/list/', not_estates()(views.TraineeList.as_view()), name='trainee_list'), path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
path('trainee/<int:pk>/', not_estates()(views.TraineeDetail.as_view()), path('trainee/<int:pk>/', login_required(views.TraineeDetail.as_view()),
name='trainee_detail'), name='trainee_detail'),
path('trainee/<int:pk>/history', not_estates()(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think) path('trainee/<int:pk>/history', login_required(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()), path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
name='add_qualification'), name='add_qualification'),
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()), path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
name='edit_qualification'), name='edit_qualification'),
path('levels/', not_estates()(views.LevelList.as_view()), name='level_list'), path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
path('level/<int:pk>/', not_estates()(views.LevelDetail.as_view()), name='level_detail'), path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
path('level/<int:pk>/user/<int:u>/', not_estates()(views.LevelDetail.as_view()), name='level_detail'), path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'), path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'),
path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'), path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'),
path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'), path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'),
path('trainee/<int:pk>/item_record', not_estates()(views.TraineeItemDetail.as_view()), name='trainee_item_detail'), path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'),
path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'), path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'),
] ]

View File

@@ -265,5 +265,5 @@ class ItemQualifications(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = f"People Qualified In {models.TrainingItem.objects.get(pk=self.kwargs['pk'])}" context["page_title"] = f"People Qualified In {self.object_list[0].item}"
return context return context

View File

@@ -50,7 +50,10 @@ class Command(BaseCommand):
"add_supplier", "view_cabletype", "change_cabletype", "add_supplier", "view_cabletype", "change_cabletype",
"add_cabletype", "view_eventchecklist", "change_eventchecklist", "add_cabletype", "view_eventchecklist", "change_eventchecklist",
"add_eventchecklist", "view_riskassessment", "change_riskassessment", "add_eventchecklist", "view_riskassessment", "change_riskassessment",
"add_riskassessment"] "add_riskassessment", "add_eventchecklistcrew", "change_eventchecklistcrew",
"delete_eventchecklistcrew", "view_eventchecklistcrew", "add_eventchecklistvehicle",
"change_eventchecklistvehicle",
"delete_eventchecklistvehicle", "view_eventchecklistvehicle", ]
finance_perms = keyholder_perms + ["add_invoice", "change_invoice", "view_invoice", finance_perms = keyholder_perms + ["add_invoice", "change_invoice", "view_invoice",
"add_payment", "change_payment", "delete_payment"] "add_payment", "change_payment", "delete_payment"]
hs_perms = keyholder_perms + ["review_riskassessment", "review_eventchecklist"] hs_perms = keyholder_perms + ["review_riskassessment", "review_eventchecklist"]

View File

@@ -6,7 +6,6 @@ from django.core import mail
from django.test import LiveServerTestCase from django.test import LiveServerTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from PyRIGS.tests.base import create_browser from PyRIGS.tests.base import create_browser
from RIGS import models from RIGS import models
@@ -25,31 +24,31 @@ class UserRegistrationTest(LiveServerTestCase):
def test_registration(self): def test_registration(self):
# Navigate to the registration page # Navigate to the registration page
self.browser.get(self.live_server_url + '/user/register/') 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) self.assertIn("User Registration", title_text)
# Check the form invites correctly # 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') 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') self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
# If this is correct we don't need to test it later # If this is correct we don't need to test it later
self.assertEqual(email.get_attribute('type'), 'email') 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('placeholder'), 'Password')
self.assertEqual(password1.get_attribute('type'), '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( self.assertEqual(
password2.get_attribute('placeholder'), 'Password confirmation') password2.get_attribute('placeholder'), 'Password confirmation')
self.assertEqual(password2.get_attribute('type'), 'password') 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') 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') 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') self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
# No longer required for new users # 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') # self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
# Fill the form out incorrectly # Fill the form out incorrectly
@@ -63,20 +62,21 @@ class UserRegistrationTest(LiveServerTestCase):
initials.send_keys('JS') initials.send_keys('JS')
# phone.send_keys('0123456789') # phone.send_keys('0123456789')
time.sleep(1) time.sleep(1)
self.browser.switch_to.frame(self.browser.find_element(By.TAG_NAME, "iframe")) self.browser.switch_to.frame(self.browser.find_element_by_tag_name("iframe"))
self.browser.find_element(By.ID, 'anchor').click() self.browser.find_element_by_id('anchor').click()
self.browser.switch_to.default_content() self.browser.switch_to.default_content()
time.sleep(3) time.sleep(3)
# Submit incorrect form # 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() submit.click()
# Restablish error fields # Restablish error fields
password1 = self.browser.find_element(By.ID, 'id_password1') password1 = self.browser.find_element_by_id('id_password1')
password2 = self.browser.find_element(By.ID, 'id_password2') password2 = self.browser.find_element_by_id('id_password2')
# Read what the error is # Read what the error is
alert = self.browser.find_element(By.CSS_SELECTOR, '.alert-danger').text alert = self.browser.find_element_by_css_selector(
'div.alert-danger').text
# TODO Use regex matching to handle smart/unsmart quotes... # TODO Use regex matching to handle smart/unsmart quotes...
self.assertIn("password fields didn", alert) self.assertIn("password fields didn", alert)
@@ -92,7 +92,8 @@ class UserRegistrationTest(LiveServerTestCase):
password2.send_keys(Keys.ENTER) password2.send_keys(Keys.ENTER)
# Check we have a success message # Check we have a success message
alert = self.browser.find_element(By.CSS_SELECTOR, '.alert-success').text alert = self.browser.find_element_by_css_selector(
'div.alert-success').text
self.assertIn('register', alert) self.assertIn('register', alert)
self.assertIn('email', alert) self.assertIn('email', alert)
@@ -110,14 +111,14 @@ class UserRegistrationTest(LiveServerTestCase):
self.browser.get(urls[0]) # go to the first link self.browser.get(urls[0]) # go to the first link
# Complete registration # 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) self.assertIn('Complete', title_text)
# Test login # Test login
self.browser.get(self.live_server_url + '/user/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') 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('placeholder'), 'Password')
self.assertEqual(password.get_attribute('type'), 'password') self.assertEqual(password.get_attribute('type'), 'password')
@@ -131,7 +132,8 @@ class UserRegistrationTest(LiveServerTestCase):
self.assertFalse(profileObject.is_approved) self.assertFalse(profileObject.is_approved)
# Read what the error is # 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) self.assertIn("approved", alert)
# Approve the user so we can proceed # Approve the user so we can proceed
@@ -140,14 +142,14 @@ class UserRegistrationTest(LiveServerTestCase):
# Retry login # Retry login
self.browser.get(self.live_server_url + '/user/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') 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('correcthorsebatterystaple')
password.send_keys(Keys.ENTER) password.send_keys(Keys.ENTER)
# Check we are logged in # 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) self.assertIn('Hi John', udd)
# Check all the data actually got saved # Check all the data actually got saved

View File

@@ -5,7 +5,7 @@ from django.urls import path
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from registration.backends.default.views import RegistrationView from registration.backends.default.views import RegistrationView
from PyRIGS.decorators import permission_required_with_403, not_estates from PyRIGS.decorators import permission_required_with_403
from users import forms, views from users import forms, views
urlpatterns = [ urlpatterns = [
@@ -14,11 +14,11 @@ urlpatterns = [
path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'), path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'),
path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'), path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'),
# User editing # User editing
path('user/edit/', not_estates()(views.ProfileUpdateSelf.as_view()), path('user/edit/', login_required(views.ProfileUpdateSelf.as_view()),
name='profile_update_self'), name='profile_update_self'),
path('user/reset_api_key', not_estates()(views.ResetApiKey.as_view(permanent=False)), path('user/reset_api_key', login_required(views.ResetApiKey.as_view(permanent=False)),
name='reset_api_key'), name='reset_api_key'),
path('user/', not_estates()(views.ProfileDetail.as_view()), name='profile_detail'), path('user/', login_required(views.ProfileDetail.as_view()), name='profile_detail'),
path('user/<int:pk>/', path('user/<int:pk>/',
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()), permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
name='profile_detail'), name='profile_detail'),

View File

@@ -30,15 +30,15 @@ for app in [apps.get_app_config(label) for label in ("RIGS", "assets", "training
modelname = model.__name__.lower() modelname = model.__name__.lower()
if appname == 'rigboard': if appname == 'rigboard':
urlpatterns += [ urlpatterns += [
path(f'{modelname}/<str:pk>/history/', path('{}/<str:pk>/history/'.format(modelname),
permission_required_with_403(f'{app.label}.change_{modelname}')( permission_required_with_403('{}.change_{}'.format(app.label, modelname))(
views.VersionHistory.as_view()), views.VersionHistory.as_view()),
name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }), name='{}_history'.format(modelname), kwargs={'model': model, 'app': appname, }),
] ]
else: else:
urlpatterns += [ urlpatterns += [
path(f'{appname}/{modelname}/<str:pk>/history/', path('{}/{}/<str:pk>/history/'.format(appname, modelname),
permission_required_with_403(f'{app.label}.change_{modelname}')( permission_required_with_403('{}.change_{}'.format(app.label, modelname))(
views.VersionHistory.as_view()), views.VersionHistory.as_view()),
name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }), name='{}_history'.format(modelname), kwargs={'model': model, 'app': appname, }),
] ]

View File

@@ -1,4 +1,3 @@
import logging
from diff_match_patch import diff_match_patch from diff_match_patch import diff_match_patch
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -148,9 +147,9 @@ class ModelComparison:
@cached_property @cached_property
def item_changes(self): 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: 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) 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)) \ 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)) .exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
@@ -161,14 +160,10 @@ class ModelComparison:
# Build some dicts of what we have # Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk 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 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) compare = ModelComparison(old=version._object_version.object, **comparisonParams)
item_dict[version.object_id] = compare item_dict[version.object_id] = compare
for version in new_item_versions: # go through the new versions for version in new_item_versions: # go through the new versions
if version._model is None:
continue
try: try:
compare = item_dict[version.object_id] # see if there's a matching old version 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 compare.new = version._object_version.object # then add the new version to the dictionary

View File

@@ -27,10 +27,10 @@ class VersionHistory(generic.ListView):
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk']) return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(VersionHistory, self).get_context_data(**kwargs)
context['object'] = self.get_object() context['object'] = self.get_object()
if self.kwargs['app'] != 'rigboard': if self.kwargs['app'] != 'rigboard':
context['override'] = f'base_{self.kwargs["app"]}.html' context['override'] = 'base_{}.html'.format(self.kwargs['app'])
return context 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") 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(ActivityTable, self).get_context_data(**kwargs)
context['page_title'] = f"{title(self.kwargs['app'])} Activity Stream" context['page_title'] = "{} Activity Stream".format(title(self.kwargs['app']))
if self.kwargs['app'] != 'rigboard': if self.kwargs['app'] != 'rigboard':
context['override'] = f'base_{self.kwargs["app"]}.html' context['override'] = 'base_{}.html'.format(self.kwargs['app'])
return context return context
@@ -77,7 +77,7 @@ class ActivityFeed(generic.ListView): # Appears on homepage
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# Call the base implementation first to get a context # Call the base implementation first to get a context
context = super().get_context_data(**kwargs) context = super(ActivityFeed, self).get_context_data(**kwargs)
context['page_title'] = "Activity Feed" context['page_title'] = "Activity Feed"
maxTimeDelta = datetime.timedelta(hours=1) maxTimeDelta = datetime.timedelta(hours=1)