Compare commits

..

19 Commits

Author SHA1 Message Date
f72b611e77 pep8 2023-05-17 12:23:17 +01:00
8da90ba670 Implement codedoctor suggestions 2023-05-17 09:45:46 +01:00
0d1beeaead Minor fixes 2023-05-16 17:22:42 +01:00
51ff06fadd Add power record status chip, checklist status chip displays number of checklists 2023-05-16 16:01:27 +01:00
06137ab180 Register check in model with the admin site 2023-05-16 14:45:54 +01:00
e7dd8f4cf9 Clicking check out does not redirect the user 2023-05-16 14:42:56 +01:00
2fb8b8c81b UI work 2023-05-15 13:40:15 +01:00
dd02df4a17 Checkin button turns into checkout button where applicable 2023-05-15 01:31:56 +01:00
8cc40fe9fa Minor improvement to homepage UI 2023-05-15 01:28:50 +01:00
904ef1f180 Add homepage checkin for events happening now 2023-05-15 01:26:54 +01:00
417ec8ff3d Basic checkin/out logic complete 2023-05-15 01:12:04 +01:00
97dac51a52 Add ability to edit checkins, more validation 2023-05-15 00:36:57 +01:00
fc8bb2dc4b Fix tests after form split 2023-05-14 22:26:58 +01:00
67d2f80815 Merge branch 'master' into checkin 2023-05-14 22:14:04 +01:00
15a9a03200 Further work on checkin, add role field etc 2023-05-10 18:24:56 +01:00
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
69 changed files with 1530 additions and 1769 deletions

View File

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

View File

@@ -39,7 +39,7 @@ premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.15.0"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4"
@@ -47,7 +47,7 @@ python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "*"
requests = "~=2.31.0"
requests = "~=2.25.1"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
@@ -56,7 +56,7 @@ sqlparse = "~=0.4.2"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.3"
tornado = "~=6.1"
urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
@@ -93,7 +93,7 @@ pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.10"
python_version = "3.9"
[dev-packages.pytest-xdist]
extras = [ "psutil",]

866
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 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('example.com')
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')
if not DEBUG:
@@ -221,6 +218,8 @@ TIME_ZONE = 'Europe/London'
FORMAT_MODULE_PATH = 'PyRIGS.formats'
USE_I18N = True
USE_L10N = 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'
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():
os.mkdir("screenshots")
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
return wrapper_func

View File

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

View File

@@ -6,8 +6,6 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path
from django.views.generic import TemplateView
from PyRIGS.decorators import not_estates
from PyRIGS import views
urlpatterns = [
@@ -16,17 +14,17 @@ urlpatterns = [
path('assets/', include('assets.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
path('api/<str:model>/', not_estates()(views.SecureAPIRequest.as_view()),
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
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"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search/', not_estates()(views.Search.as_view()), name='search'),
path('search_help/', not_estates()(views.SearchHelp.as_view()), name='search_help'),
path('search/', login_required(views.Search.as_view()), name='search'),
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')),

View File

@@ -48,7 +48,7 @@ class Index(generic.TemplateView): # Displays the current rig count along with
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['rig_count'] = models.Event.objects.rig_count()
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(status=models.Event.CANCELLED).filter(is_rig=True, dry_hire=False)
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now())
return context
@@ -134,15 +134,11 @@ class SecureAPIRequest(generic.View):
results = []
query = reduce(operator.and_, queries)
objects = self.models[model].objects.filter(query)
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
if model == "profile":
objects = objects.filter(is_active=True, is_approved=True)
for o in objects:
name = o.display_name if hasattr(o, 'display_name') else o.name
data = {
'pk': o.pk,
'value': o.pk,
'text': name,
'text': o.name,
}
try: # See if there is a valid update URL
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
@@ -187,7 +183,7 @@ class ModalURLMixin:
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'")
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
else:
url = reverse_lazy(detail, kwargs={
'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
- RIGS: Rigboard stuff - event calendar etc
- assets: Database of our kit, testing data etc
- training: Logs in-house training within various "departments" (sound, lighting etc).
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
- users: Our custom logic for registration and profiles. Semi-modular.
- training: SoonTM
[![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

@@ -154,9 +154,8 @@ class AssociateAdmin(VersionAdmin):
@admin.register(models.Profile)
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_filter = UserAdmin.list_filter + ('is_approved',)
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {

View File

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

View File

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

View File

@@ -3,22 +3,6 @@
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):
@@ -40,5 +24,146 @@ class Migration(migrations.Migration):
('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),
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='checklist',
),
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='driver',
),
migrations.RemoveField(
model_name='eventchecklist',
name='all_rcds_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='earthing',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_phase_rotation',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_pssc',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l1',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l2',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l3',
),
migrations.RemoveField(
model_name='eventchecklist',
name='labelling',
),
migrations.RemoveField(
model_name='eventchecklist',
name='pat',
),
migrations.RemoveField(
model_name='eventchecklist',
name='power_mic',
),
migrations.RemoveField(
model_name='eventchecklist',
name='public_sockets_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='rcds',
),
migrations.RemoveField(
model_name='eventchecklist',
name='source_rcd',
),
migrations.RemoveField(
model_name='eventchecklist',
name='supply_test',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_voltage',
),
migrations.AddField(
model_name='powertestrecord',
name='power_mic',
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
),
migrations.AlterField(
model_name='eventchecklist',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='riskassessment',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.DeleteModel(
name='EventChecklistCrew',
),
migrations.DeleteModel(
name='EventChecklistVehicle',
),
]

View File

@@ -1,156 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-18 11:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0047_auto_20230517_0944'),
]
operations = [
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='checklist',
),
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='driver',
),
migrations.RemoveField(
model_name='eventchecklist',
name='all_rcds_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='earthing',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_phase_rotation',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_pssc',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l1',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l2',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l3',
),
migrations.RemoveField(
model_name='eventchecklist',
name='labelling',
),
migrations.RemoveField(
model_name='eventchecklist',
name='pat',
),
migrations.RemoveField(
model_name='eventchecklist',
name='power_mic',
),
migrations.RemoveField(
model_name='eventchecklist',
name='public_sockets_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='rcds',
),
migrations.RemoveField(
model_name='eventchecklist',
name='source_rcd',
),
migrations.RemoveField(
model_name='eventchecklist',
name='supply_test',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_voltage',
),
migrations.AddField(
model_name='powertestrecord',
name='power_mic',
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
),
migrations.AlterField(
model_name='eventchecklist',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='riskassessment',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.DeleteModel(
name='EventChecklistCrew',
),
migrations.DeleteModel(
name='EventChecklistVehicle',
),
]

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

@@ -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,8 +76,7 @@ class Profile(AbstractUser):
@classmethod
def users_awaiting_approval_count(cls):
# last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity
return Profile.objects.filter(is_approved=False, last_login=None).count()
return Profile.objects.filter(models.Q(is_approved=False)).count()
def __str__(self):
return self.name
@@ -213,7 +212,6 @@ class Venue(models.Model, RevisionMixin):
phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='')
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='')
address = models.TextField(blank=True, default='')
@@ -310,14 +308,6 @@ class EventManager(models.Manager):
return qs
def validate_forum_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
if obj.hostname not in ('forum.nottinghamtec.co.uk'):
raise ValidationError('URL must point to a location on the TEC Forum')
@reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database
@@ -367,8 +357,6 @@ class Event(models.Model, RevisionMixin):
auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(blank=True, default='')
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
@property
def display_id(self):
if self.pk:
@@ -509,7 +497,7 @@ class Event(models.Model, RevisionMixin):
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()
return not self.dry_hire and earliest <= timezone.now()
objects = EventManager()
@@ -517,7 +505,7 @@ class Event(models.Model, RevisionMixin):
return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.display_id} | {self.name}"
return f"{self.display_id}: {self.name}"
def clean(self):
errdict = {}
@@ -689,11 +677,13 @@ class Payment(models.Model, RevisionMixin):
CASH = 'C'
INTERNAL = 'I'
EXTERNAL = 'E'
SUCORE = 'SU'
ADJUSTMENT = 'T'
METHODS = (
(CASH, 'Cash'),
(INTERNAL, 'Internal'),
(EXTERNAL, 'External'),
(SUCORE, 'SU Core'),
(ADJUSTMENT, 'TEC Adjustment'),
)
@@ -885,9 +875,6 @@ class EventChecklist(ReviewableModel, RevisionMixin):
@reversion.register
class PowerTestRecord(ReviewableModel, RevisionMixin):
earth_fault_text = "Earth Fault Loop Impedance (Z<small>S</small>) / Ω"
pssc_text = "Prospective Short Circuit Current / A"
event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE)
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
@@ -909,21 +896,21 @@ 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_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text=pssc_text)
fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
# Worst case points
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
@@ -937,9 +924,6 @@ class PowerTestRecord(ReviewableModel, RevisionMixin):
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)

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

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

View File

@@ -9,8 +9,6 @@
<div class="col-12 text-right my-3">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
</div>
@@ -73,8 +71,6 @@
<div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
<div class="col-12 text-right">

View File

@@ -59,6 +59,8 @@
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %}
</select>
</div>

View File

@@ -86,7 +86,7 @@
</thead>
<tbody>
<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_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
@@ -165,11 +165,11 @@
</div>
</div>
<div class="col-12 text-right">
{% button 'edit' url='pt_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
<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>
{% endblock %}

View File

@@ -51,18 +51,22 @@
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
<label for="{{ form.power_mic.id_for_label }}"
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
{% if power_mic %}
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
{% elif event.riskassessment.power_mic %}
<option value="{{event.riskassessment.power_mic.pk}}" selected="selected">{{ event.riskassessment.power_mic.name }}</option>
{% endif %}
</select>
</div>
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
<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 %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %}
</select>
</div>
@@ -115,7 +119,7 @@
</thead>
<tbody>
<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_l2.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() {
$('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value));
//$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
});
$('input[type=radio][name=special_structures]').change(function() {
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));

View File

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

View File

@@ -15,7 +15,7 @@
{% endif %}
{% endif %}
{% if not event.dry_hire %}
{% if event.riskassessment %}
{% if event.has_checklist %}
<a href="{{ event.riskassessment.get_absolute_url }}"><span class="badge badge-success">RA: <span class="fas fa-check{% if event.riskassessment.reviewed_by %}-double{%endif%}"></span></a>
{% else %}
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>

View File

@@ -29,15 +29,7 @@
</div>
<div class="row pt-3">
<label class="col-sm-4 col-form-label"
for="{{ form.method.id_for_label }}">{{ form.method.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>
for="{{ form.method.id_for_label }}">{{ form.method.label }}</label>
<div class="col-sm-8">
{% render_field form.method class+="form-control" %}
</div>

View File

@@ -171,7 +171,7 @@ def title_spaced(string):
@register.filter(needs_autoescape=True)
def namewithnotes(obj, url, autoescape=True):
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
return mark_safe(obj.name + 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:
return obj.name
@@ -183,7 +183,7 @@ def linkornone(target, namespace=None, autoescape=True):
link = namespace + "://" + target
else:
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:
return "None"

View File

@@ -114,7 +114,7 @@ class CreateEvent(FormPage):
}
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):
return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID))

View File

@@ -259,7 +259,7 @@ class TestPrintPaperwork(TestCase):
def test_login_redirect(client, django_user_model):
request_url = reverse('event_embed', kwargs={'pk': 1})
expected_url = 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
response = client.get(request_url, follow=True)
@@ -372,8 +372,7 @@ def test_ra_redirect(admin_client, admin_user, ra):
class TestMarkdownTemplateTags(TestCase):
with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md"), encoding="utf-8") as f:
markdown = f.read()
markdown = open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")).read()
def test_html_safe(self):
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 PyRIGS.decorators import (api_key_required, has_oembed,
permission_required_with_403, not_estates)
permission_required_with_403)
from . import views
urlpatterns = [
@@ -42,22 +42,21 @@ urlpatterns = [
name='venue_update'),
# Rigboard
path('rigboard/', not_estates()(views.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', not_estates()(views.WebCalendar.as_view()),
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
name='web_calendar'),
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}))/$',
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('estates/', login_required()(views.EstatesEventList.as_view()), name='estates'),
path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()),
name='event_detail'),
path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()),
name='event_create'),
path('event/archive/', not_estates()(views.EventArchive.as_view()),
path('event/archive/', login_required()(views.EventArchive.as_view()),
name='event_archive'),
path('event/<int:pk>/embed/',
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()),
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'),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
name='ra_edit'),
@@ -86,7 +85,7 @@ urlpatterns = [
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
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'),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
name='ec_edit'),
@@ -95,25 +94,22 @@ urlpatterns = [
path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()),
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'),
path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()),
name='pt_edit'),
path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()),
name='pt_review', kwargs={'model': 'PowerTestRecord'}),
path('event/<int:pk>/checkin/', 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'),
path('event/checkout/', not_estates()(views.EventCheckOut.as_view()),
path('event/checkout/', permission_required_with_403('RIGS.change_eventcheckin')(views.EventCheckOut.as_view()),
name='event_checkout'),
path('event/<int:pk>/checkin/edit/', not_estates()(views.EventCheckInEdit.as_view()),
path('event/<int:pk>/checkin/edit/', permission_required_with_403('RIGS.change_eventcheckin')(views.EventCheckInEdit.as_view()),
name='edit_checkin'),
path('event/<int:pk>/checkin/add/', not_estates()(views.EventCheckInOverride.as_view()),
path('event/<int:pk>/checkin/add/', permission_required_with_403('RIGS.add_eventcheckin')(views.EventCheckInOverride.as_view()),
name='event_checkin_override'),
path('event/<int:pk>/thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'),
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
# Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
name='invoice_list'),

View File

@@ -115,7 +115,7 @@ class VenueDetail(GenericDetailView):
class VenueCreate(GenericCreateView, ModalURLMixin):
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):
return self.get_close_url('venue_update', 'venue_detail')
@@ -123,7 +123,7 @@ class VenueCreate(GenericCreateView, ModalURLMixin):
class VenueUpdate(GenericUpdateView, ModalURLMixin):
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):
return self.get_close_url('venue_update', 'venue_detail')

View File

@@ -13,24 +13,31 @@ from django.shortcuts import redirect
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):
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['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}'
get_related(context['form'], context)
return context
class MarkReviewed(generic.RedirectView):
def get_redirect_url(self, *args, **kwargs):
class MarkReviewed(generic.View):
def get(self, *args, **kwargs):
obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk'))
with reversion.create_revision():
reversion.set_user(self.request.user)
obj.reviewed_by = self.request.user
obj.reviewed_at = timezone.now()
obj.save()
return self.request.META.get('HTTP_REFERER', reverse('hs_list'))
return HttpResponseRedirect(reverse('hs_list'))
class EventRiskAssessmentCreate(HSCreateView):
@@ -48,7 +55,7 @@ class EventRiskAssessmentCreate(HSCreateView):
if ra is not None:
return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk}))
return super().get(self)
return super(EventRiskAssessmentCreate, self).get(self)
def get_success_url(self):
return reverse('ra_detail', kwargs={'pk': self.object.pk})
@@ -67,7 +74,7 @@ class EventRiskAssessmentEdit(generic.UpdateView):
return reverse('ra_detail', kwargs={'pk': self.object.pk})
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')
ra = models.RiskAssessment.objects.get(pk=rpk)
context['event'] = ra.event
@@ -82,7 +89,7 @@ class EventRiskAssessmentDetail(generic.DetailView):
template_name = 'hs/risk_assessment_detail.html'
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>"
return context
@@ -92,7 +99,7 @@ class EventChecklistDetail(generic.DetailView):
template_name = 'hs/event_checklist_detail.html'
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>"
return context
@@ -110,7 +117,7 @@ class EventChecklistEdit(generic.UpdateView):
return reverse('ec_detail', kwargs={'pk': self.object.pk})
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')
ec = models.EventChecklist.objects.get(pk=pk)
context['event'] = ec.event
@@ -129,22 +136,19 @@ class EventChecklistCreate(HSCreateView):
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
# Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None:
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
return super().get(self)
return super(EventChecklistCreate, self).get(self)
def get_success_url(self):
return reverse('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if context['event'].venue:
context['venue'] = context['event'].venue
return context
class PowerTestDetail(generic.DetailView):
model = models.PowerTestRecord
@@ -166,7 +170,7 @@ class PowerTestEdit(generic.UpdateView):
ec.reviewed_by = None
ec.reviewed_at = None
ec.save()
return reverse('pt_detail', kwargs={'pk': self.object.pk})
return reverse('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -175,7 +179,7 @@ class PowerTestEdit(generic.UpdateView):
context['event'] = ec.event
context['edit'] = True
context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}'
get_related(context['form'], context)
# get_related(context['form'], context)
return context
@@ -187,6 +191,7 @@ class PowerTestCreate(HSCreateView):
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
# Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first()
@@ -199,14 +204,6 @@ class PowerTestCreate(HSCreateView):
def get_success_url(self):
return reverse('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):
paginate_by = 20
@@ -214,10 +211,10 @@ class HSList(generic.ListView):
template_name = 'hs/hs_list.html'
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):
context = super().get_context_data(**kwargs)
context = super(HSList, self).get_context_data(**kwargs)
context['page_title'] = 'H&S Overview'
return context
@@ -266,12 +263,6 @@ class EventCheckInEdit(generic.UpdateView, ModalURLMixin):
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...!

View File

@@ -3,12 +3,6 @@ import datetime
import re
import premailer
import simplejson
import urllib
import hmac
import hashlib
from envparse import env
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib import messages
@@ -25,8 +19,6 @@ from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.mixins import UserPassesTestMixin
from PyRIGS import decorators
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['target'] = 'event_authorise_form_preview'
return context
class CreateForumThread(generic.base.RedirectView):
permanent = False
def get_redirect_url(self, *args, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk'])
if event.forum_url:
return event.forum_url
params = {
'title': str(event),
'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}',
'category': 'rig-info'
}
return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}'
class RecieveForumWebhook(generic.View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}"
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed):
return HttpResponseForbidden('Invalid signature header')
# Check if this is the right kind of event. The webhook filters by category on the forum side
if request.headers.get('X-Discourse-Event') == "topic_created":
body = simplejson.loads(request.body.decode('utf-8'))
event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros
event = models.Event.objects.filter(pk=event_id).first()
if event:
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}"
event.save()
return HttpResponse(status=202)
return HttpResponse(status=204)
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
is_cable = models.BooleanField(default=False)
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
length = models.DecimalField(decimal_places=2, max_digits=10,
length = models.DecimalField(decimal_places=1, max_digits=10,
blank=True, null=True, help_text='m')
csa = models.DecimalField(decimal_places=2, max_digits=10,
blank=True, null=True, help_text='mm²')
@@ -192,5 +192,5 @@ class Asset(models.Model, RevisionMixin):
return str(self.asset_id)
@property
def display_name(self):
def name(self):
return f"{self.display_id} | {self.description}"

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
</thead>
<tbody id="asset_table_body">
{% 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>
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
<td class="assetCategory align-middle">{{ item.category }}</td>

View File

@@ -1,15 +1,15 @@
{% load widget_tweaks %}
{% load title_spaced from filters %}
{% 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 %}
<div class="input-group {{col}} flex-nowrap">
<div class="input-group {{col}}">
{% if prepend %}
<div class="input-group-prepend">
<span class="input-group-text">{{ prepend }}</span>
</div>
{% endif %}
{% render_field field|add_class:'form-control' style=style %}
{% render_field field|add_class:'form-control' %}
{% if append %}
<div class="input-group-append">
<span class="input-group-text">{{ append }}</span>
@@ -17,6 +17,6 @@
{% endif %}
</div>
{% else %}
{% render_field field|add_class:'form-control' class+=col style=style %}
{% render_field field|add_class:'form-control' class+=col %}
{% endif %}
{% endspaceless %}

View File

@@ -34,7 +34,7 @@
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
<div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.purchase_price|add_class:'form-control'|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>
@@ -42,7 +42,7 @@
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
<div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.replacement_cost|add_class:'form-control'|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>

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)
yield asset
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')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'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')),
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),

View File

@@ -1,6 +1,5 @@
import time
import datetime
import pytest
from django.utils import timezone
from selenium.webdriver.common.by import By
@@ -54,45 +53,45 @@ class TestAssetList(AutoLoginTest):
self.assertEqual("10", asset_ids[2])
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)
def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type):
page = pages.AssetList(logged_in_browser.driver, live_server.url).open()
page.set_query(test_asset.asset_id)
page.search()
assert len(page.assets) == 1
assert page.assets[0].description == test_asset.description
assert page.assets[0].id == test_asset.asset_id
self.page.set_query("light")
self.page.search()
self.assertTrue(len(self.page.assets) == 1)
self.assertEqual("A light", self.page.assets[0].description)
page.set_query(test_asset.description)
page.search()
assert len(page.assets) == 1
assert page.assets[0].description == test_asset.description
self.page.set_query("Random string")
self.page.search()
self.assertTrue(len(self.page.assets) == 0)
page.set_query("Random string")
page.search()
assert len(page.assets) == 0
page.set_query("")
page.search()
self.page.set_query("")
self.page.search()
# Only working stuff shown by default
assert len(page.assets) == 1
self.assertTrue(len(self.page.assets) == 2)
page.status_selector.toggle()
assert page.status_selector.is_open
page.status_selector.select_all()
page.status_selector.toggle()
assert not page.status_selector.is_open
page.filter()
assert len(page.assets) == 2
self.page.status_selector.toggle()
self.assertTrue(self.page.status_selector.is_open)
self.page.status_selector.select_all()
self.page.status_selector.toggle()
self.assertFalse(self.page.status_selector.is_open)
self.page.filter()
self.assertTrue(len(self.page.assets) == 4)
page.category_selector.toggle()
assert page.category_selector.is_open
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
self.page.category_selector.toggle()
self.assertTrue(self.page.category_selector.is_open)
self.page.category_selector.set_option("Sound", True)
self.page.category_selector.close()
self.assertFalse(self.page.category_selector.is_open)
self.page.filter()
self.assertTrue(len(self.page.assets) == 2)
asset_ids = list(map(lambda x: x.id, self.page.assets))
self.assertEqual("1", asset_ids[0])
self.assertEqual("10", asset_ids[1])
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):

View File

@@ -2,7 +2,7 @@ from django.contrib.auth.decorators import login_required
from django.urls import path, register_converter
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 . import views, converters
@@ -10,8 +10,8 @@ register_converter(converters.AssetIDConverter, 'asset')
register_converter(converters.ListConverter, 'list')
urlpatterns = [
path('', not_estates()(views.AssetList.as_view()), name='asset_index'),
path('asset/list/', not_estates()(views.AssetList.as_view()), name='asset_list'),
path('', login_required(views.AssetList.as_view()), name='asset_index'),
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/create/', permission_required_with_403('assets.add_asset')
(views.AssetCreate.as_view()), name='asset_create'),
@@ -19,14 +19,14 @@ urlpatterns = [
(views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(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('cables/list/', not_estates()(views.CableList.as_view()), name='cable_list'),
path('cabletype/list/', not_estates()(views.CableTypeList.as_view()), name='cable_type_list'),
path('cables/list/', login_required(views.CableList.as_view()), name='cable_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/<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/',
xframe_options_exempt(
@@ -37,8 +37,8 @@ urlpatterns = [
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('supplier/list/', not_estates()(views.SupplierList.as_view()), name='supplier_list'),
path('supplier/<int:pk>/', not_estates()(views.SupplierDetail.as_view()), name='supplier_detail'),
path('supplier/list/', login_required(views.SupplierList.as_view()), name='supplier_list'),
path('supplier/<int:pk>/', login_required(views.SupplierDetail.as_view()), name='supplier_detail'),
path('supplier/create/', permission_required_with_403('assets.add_supplier')
(views.SupplierCreate.as_view()), name='supplier_create'),
path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier')

1295
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,20 +78,6 @@
</div>
</div>
{% 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 %}
<div class="form-group form-row">
<div class="col-sm-10 col-sm-offset-2">

View File

@@ -11,7 +11,7 @@
{% 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/>
Event {{ event }} is happening now! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/>
{% endfor %}
</div>
{% endif %}

View File

@@ -105,10 +105,6 @@ class TrainingItem(models.Model):
def display_id(self):
return f"{self.category.reference_number}.{self.reference_number}"
@property
def display_name(self):
return f"{self.display_id} | {self.name}"
@display_id.filter
@classmethod
def display_id(cls, lookup, value):
@@ -373,7 +369,7 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
return str(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:
unique_together = ["trainee", "level"]

View File

@@ -78,6 +78,11 @@
</tr>
{% endfor %}
<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>
</table>
</div>

View File

@@ -8,10 +8,10 @@
<p>Please Note:</p>
<ul>
<li>Technical Assistant status is automatically valid when the item requirements are met.</li>
<li>Technician status is also automatic. 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>
</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>
{% endif %}
{% for level in object_list %}

View File

@@ -43,7 +43,7 @@ def confirm_button(user, trainee, level):
if level.user_has_requirements(trainee):
string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>"
if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'):
string += 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)
else:
return ""

View File

@@ -44,7 +44,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
page.submit()
assert page.success
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
assert qualification.supervisor_id == supervisor.pk
assert qualification.supervisor.pk == supervisor.pk
assert qualification.date == date
assert qualification.notes == "A note"
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
qual = models.TrainingItemQualification.objects.last()
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
assert Revision.objects.count() == 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.contrib.auth.decorators import login_required
from training.decorators import is_supervisor
from training import views, models
from versioning.views import VersionHistory
from PyRIGS.decorators import not_estates
urlpatterns = [
path('items/', not_estates()(views.ItemList.as_view()), name='item_list'),
path('items/export/', not_estates()(views.ItemListExport.as_view()), name='item_list_export'),
path('item/<int:pk>/qualified_users/', not_estates()(views.ItemQualifications.as_view()), name='item_qualification'),
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
path('items/export/', login_required(views.ItemListExport.as_view()), name='item_list_export'),
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/<int:pk>/', not_estates()(views.TraineeDetail.as_view()),
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
path('trainee/<int:pk>/', login_required(views.TraineeDetail.as_view()),
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()),
name='add_qualification'),
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
name='edit_qualification'),
path('levels/', not_estates()(views.LevelList.as_view()), name='level_list'),
path('level/<int:pk>/', not_estates()(views.LevelDetail.as_view()), name='level_detail'),
path('level/<int:pk>/user/<int:u>/', not_estates()(views.LevelDetail.as_view()), name='level_detail'),
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
path('level/<int:pk>/', login_required(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/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>/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'),
]

View File

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

View File

@@ -5,7 +5,7 @@ from django.urls import path
from django.views.decorators.clickjacking import xframe_options_exempt
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
urlpatterns = [
@@ -14,11 +14,11 @@ urlpatterns = [
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'),
# User editing
path('user/edit/', not_estates()(views.ProfileUpdateSelf.as_view()),
path('user/edit/', login_required(views.ProfileUpdateSelf.as_view()),
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'),
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>/',
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
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()
if appname == 'rigboard':
urlpatterns += [
path(f'{modelname}/<str:pk>/history/',
permission_required_with_403(f'{app.label}.change_{modelname}')(
path('{}/<str:pk>/history/'.format(modelname),
permission_required_with_403('{}.change_{}'.format(app.label, modelname))(
views.VersionHistory.as_view()),
name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }),
name='{}_history'.format(modelname), kwargs={'model': model, 'app': appname, }),
]
else:
urlpatterns += [
path(f'{appname}/{modelname}/<str:pk>/history/',
permission_required_with_403(f'{app.label}.change_{modelname}')(
path('{}/{}/<str:pk>/history/'.format(appname, modelname),
permission_required_with_403('{}.change_{}'.format(app.label, modelname))(
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 django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
@@ -148,9 +147,9 @@ class ModelComparison:
@cached_property
def item_changes(self):
if self.follow and self.version.object is not None:
from RIGS.models import EventAuthorisation
from training.models import TrainingLevelQualification, TrainingItemQualification
if self.follow and self.version.object is not None:
item_type = ContentType.objects.get_for_model(self.version.object)
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
@@ -161,14 +160,10 @@ class ModelComparison:
# Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk
for version in old_item_versions: # put all the old versions in a list
if version._model is None:
continue
compare = ModelComparison(old=version._object_version.object, **comparisonParams)
item_dict[version.object_id] = compare
for version in new_item_versions: # go through the new versions
if version._model is None:
continue
try:
compare = item_dict[version.object_id] # see if there's a matching old version
compare.new = version._object_version.object # then add the new version to the dictionary

View File

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