Compare commits

..

11 Commits

Author SHA1 Message Date
90ae87b1b2 Implement some suggestions from the Doctor 2023-05-20 17:02:33 +01:00
0b22669a29 Default power MIC to MIC 2023-05-20 16:41:35 +01:00
020d43d5f8 Do not set power plan field to required on RA
"This might be a problem if the risk assessment is being done by one person and the power plan by another."
2023-05-20 16:39:21 +01:00
b4c1ada05c Add link to create power test from EC detail 2023-05-20 16:28:04 +01:00
aff911493f Fix population of initial venue values for EC/PT 2023-05-20 16:25:48 +01:00
020b08f9b0 Default venue to event venue in EC/PT 2023-05-20 15:24:24 +01:00
aae9a45d82 Allow a higher value in PSSC fields 2023-05-20 15:20:34 +01:00
773c07af81 Add units to power test record detail and form
I'm a bad scientist (coz I'm an engineer)
2023-05-20 10:40:22 +00:00
0cbba9d436 Return user to current page when clicking 'mark reviewed' 2023-05-19 18:20:49 +00:00
d47322a959 Update to target Python 3.10 2023-05-19 18:20:44 +00:00
74b8948daa Add absolute URL to power tests 2023-05-19 18:08:35 +00:00
114 changed files with 9681 additions and 6530 deletions

View File

@@ -1,49 +0,0 @@
*.sqlite3
*.md
**/tests
conftest.py
pytest.ini
Dockerfile
node_modules
npm-debug.log
.git
.env
__pycache__
*.pyc
*.pyo
*.pyd
.Python
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.gitignore
.mypy_cache
.pytest_cache
.hypothesis
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.vscode
.idea
.DS_Store
*.swp
*.swo
*~
docs/
tests/
*.md
docker-compose*.yml
Dockerfile*
.dockerignore

View File

@@ -12,48 +12,41 @@ jobs:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYTHONDONTWRITEBYTECODE: 1
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get install libcairo2-dev
- name: "Set up Python"
uses: actions/setup-python@v5
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@v6
python-version: 3.9
cache: 'pipenv'
- name: Install Dependencies
run: uv sync --locked --all-extras --dev
run: |
python3 -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
- name: Cache Static Files
id: static-cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
- uses: bahmutov/npm-install@v1
if: steps.static-cache.outputs.cache-hit != 'true'
- run: node node_modules/gulp/bin/gulp build
if: steps.static-cache.outputs.cache-hit != 'true'
- name: Basic Checks
run: |
uv run pycodestyle . --exclude=.venv,migrations,node_modules
uv run python3 manage.py check
uv run python3 manage.py makemigrations --check --dry-run
uv run python3 manage.py collectstatic --noinput
pipenv run pycodestyle . --exclude=migrations,node_modules
pipenv run python3 manage.py check
pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput
- name: Run Tests
run: uv run pytest -n auto --cov
- uses: actions/upload-artifact@v4
run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v2
if: failure()
with:
name: failure-screenshots ${{ matrix.test-group }}
path: screenshots/
retention-days: 5
- name: Coveralls
run: uv run coveralls --service=github
run: pipenv run coveralls --service=github

4
.gitignore vendored
View File

@@ -101,7 +101,3 @@ crashlytics.properties
crashlytics-build.properties
.vscode/
screenshots/
# Virutal Environments
.venv/
/.env

View File

@@ -1 +0,0 @@
3.10

6
.slugignore Normal file
View File

@@ -0,0 +1,6 @@
*.sqlite3
*.md
**/tests
conftest.py
pytest.ini
Dockerfile

View File

@@ -1,41 +0,0 @@
# Stage 1: Base build stage
FROM combos/python_node:3.10_22 AS base
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
FROM base AS builder
# Set up environment
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
# Create non-root user
RUN addgroup --system app && adduser --system --group app
WORKDIR /app
# Copy uv project files first (for better caching)
COPY pyproject.toml uv.lock ./
WORKDIR /app
# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --all-groups
# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --all-groups
FROM python:3.10-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
COPY --from=builder /app /app
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["uv", "run", "gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "PyRIGS.wsgi"]

104
Pipfile Normal file
View File

@@ -0,0 +1,104 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ansicolors = "~=1.1.8"
asgiref = "~=3.3.1"
"backports.tempfile" = "~=1.0"
"backports.weakref" = "~=1.0.post1"
beautifulsoup4 = "~=4.9.3"
Brotli = "~=1.0.9"
cachetools = "~=4.2.1"
chardet = "~=4.0.0"
configparser = "~=5.0.1"
contextlib2 = "~=0.6.0.post1"
cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.2"
django-debug-toolbar = "~=4.0.0"
django-filter = "~=2.4.0"
django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9"
django-widget-tweaks = "~=1.4.8"
django-htmlmin = "~=0.11.0"
envparse = "*"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=9.3.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "*"
requests = "~=2.25.1"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
soupsieve = "~=2.1"
sqlparse = "~=0.4.2"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.1"
urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
zipp = "~=3.4.0"
"zope.component" = "~=4.6.2"
"zope.deferredimport" = "~=4.3.1"
"zope.deprecation" = "~=4.4.0"
"zope.event" = "~=4.5.0"
"zope.hookable" = "~=5.0.1"
"zope.interface" = "~=5.2.0"
"zope.proxy" = "~=4.3.5"
"zope.schema" = "~=6.0.1"
sentry-sdk = "*"
diff-match-patch = "*"
python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
"z3c.rml" = "*"
pikepdf = "*"
django-queryable-properties = "*"
django-mass-edit = "*"
selenium = "~=4.9.1"
[dev-packages]
pycodestyle = "~=2.9.1"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.10"
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

1924
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

2
Procfile Normal file
View File

@@ -0,0 +1,2 @@
release: python manage.py migrate
web: gunicorn PyRIGS.wsgi --log-file -

View File

@@ -79,9 +79,7 @@ def api_key_required(function):
"""
Decorator for views that checks api_pk and api_key.
Failed users will be given a 403 error.
Should only be used for urls which include <api_pk> and <api_key> kwargs.
Will update the kwargs to include the user object if successful (under the key 'user').
Should only be used for urls which include <api_pk> and <api_key> kwargs
"""
def wrap(request, *args, **kwargs):
@@ -99,7 +97,6 @@ def api_key_required(function):
try:
user_object = models.Profile.objects.get(pk=userid)
kwargs = {**kwargs, 'user': user_object}
except models.Profile.DoesNotExist:
return error_resp

View File

@@ -26,23 +26,19 @@ DEBUG = env('DEBUG', cast=bool, default=True)
STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS", default="rigs.nottinghamtec.co.uk").split(",")
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
if STAGING:
ALLOWED_HOSTS.append('.herokuapp.com')
if DEBUG:
CRSF_TRUSTED_ORIGINS = ALLOWED_HOSTS.copy()
CRSF_TRUSTED_ORIGINS.append("http://localhost:8000")
CRSF_TRUSTED_ORIGINS.append("http://localhost:8001")
ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1')
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG:
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
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
INTERNAL_IPS = ['127.0.0.1']
@@ -97,18 +93,17 @@ WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.{}'.format(
env('DATABASE_ENGINE', default='sqlite3')
),
'NAME': env('DATABASE_NAME', default='rigs'),
'USER': env('DATABASE_USERNAME', default='rigs'),
'PASSWORD': env('DATABASE_PASSWORD', default='rigs'),
'HOST': env('DATABASE_HOST', default='127.0.0.1'),
'PORT': env('DATABASE_PORT', 5432),
}
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'),
}
}
if not DEBUG:
import dj_database_url
DATABASES['default'] = dj_database_url.config()
# Logging
LOGGING = {
'version': 1,
@@ -227,8 +222,6 @@ USE_L10N = True
USE_TZ = True
USE_THOUSAND_SEPARATOR = False
# Need to allow seconds as datetime-local input type spits out a time that has seconds
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
@@ -257,7 +250,6 @@ TEMPLATES = [
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
"RIGS.views.is_ajax",
],
'debug': DEBUG
},
@@ -270,3 +262,10 @@ TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
SECURE_HSTS_PRELOAD = True

View File

@@ -36,8 +36,8 @@ urlpatterns = [
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
# import debug_toolbar
import debug_toolbar
urlpatterns += [
# path('__debug__/', include(debug_toolbar.urls)),
path('__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
]

View File

@@ -9,7 +9,7 @@ from functools import reduce
from itertools import chain
from io import BytesIO
from PyPDF2 import PdfMerger, PdfReader
from PyPDF2 import PdfFileMerger, PdfFileReader
from z3c.rml import rml2pdf
from django.conf import settings
@@ -30,11 +30,9 @@ from RIGS import models
from assets import models as asset_models
from training import models as training_models
# Template context processor
def is_ajax(request):
return {"is_ajax": request.headers.get('x-requested-with') == 'XMLHttpRequest'}
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
@@ -50,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
@@ -136,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})
@@ -185,11 +179,11 @@ class SecureAPIRequest(generic.View):
class ModalURLMixin:
def get_close_url(self, update, detail):
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, 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,
@@ -204,7 +198,7 @@ class GenericListView(generic.ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s"
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -223,7 +217,7 @@ class GenericDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"{self.model.__name__} | {self.object.name}"
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -234,7 +228,7 @@ class GenericUpdateView(generic.UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Edit {self.model.__name__}"
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -245,7 +239,7 @@ class GenericCreateView(generic.CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Create {self.model.__name__}"
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -335,10 +329,10 @@ def get_info_string(user):
def render_pdf_response(template, context, append_terms):
merger = PdfMerger()
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfReader(buffer))
merger.append(PdfFileReader(buffer))
buffer.close()
if append_terms:

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', 'date_joined')
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', 'date_joined')
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {

View File

@@ -1,11 +1,10 @@
from datetime import datetime, timedelta
from datetime import datetime
import simplejson
from django import forms
from django.conf import settings
from django.core import serializers
from django.utils import timezone
from django.utils.html import format_html
from reversion import revisions as reversion
from RIGS import models
@@ -22,7 +21,6 @@ class EventForm(forms.ModelForm):
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
parking_and_access = forms.BooleanField(label="Additional parking or access requirements (i.e. campus parking permits, wristbands)?", required=False)
items_json = forms.CharField()
@@ -39,8 +37,6 @@ class EventForm(forms.ModelForm):
@property
def _get_items_json(self):
items = {}
if self.instance.pk is None:
return items
for item in self.instance.items.all():
data = serializers.serialize('json', [item])
struct = simplejson.loads(data)
@@ -101,9 +97,6 @@ class EventForm(forms.ModelForm):
raise forms.ValidationError(
'You haven\'t provided any client contact details. Please add a person or organisation.',
code='contact')
access = self.cleaned_data.get("access_at")
if 'warn-access' not in self.data and access is not None and access.date() < (self.cleaned_data.get("start_date") - timedelta(days=7)):
raise forms.ValidationError(format_html("Are you sure about that? Your access time seems a bit optimistic. If you're sure, save again. <input type='hidden' id='warn-access' name='warn-access' value='0'/>"), code='access_sanity')
return super().clean()
def save(self, commit=True):
@@ -128,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', 'parking_and_access']
'purchase_order', 'collector']
class BaseClientEventAuthorisationForm(forms.ModelForm):

View File

@@ -254,7 +254,7 @@ class Command(BaseCommand):
new_invoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
date=datetime.date.today(), method=random.choice(models.Payment.METHODS)[0])
date=datetime.date.today())
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
nonstandard_equipment=bool(random.getrandbits(1)),
@@ -276,7 +276,6 @@ class Command(BaseCommand):
nonstandard_emergency_procedure=bool(random.getrandbits(1)),
special_structures=bool(random.getrandbits(1)),
suspended_structures=bool(random.getrandbits(1)),
parking_and_access=bool(random.getrandbits(1)),
outside=bool(random.getrandbits(1)))
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
models.EventChecklist.objects.create(event=new_event,

View File

@@ -13,7 +13,6 @@ from RIGS import models
class Command(BaseCommand):
# FIXME This needs a different implementation when moved off heroku
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
def handle(self, *args, **options):
@@ -34,6 +33,6 @@ class Command(BaseCommand):
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css, allow_loading_external_files=True).transform()
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
msg.send()

View File

@@ -1,4 +1,4 @@
# Generated by Django 3.2.19 on 2023-05-29 10:23
# Generated by Django 3.2.19 on 2023-05-20 10:36
from django.db import migrations, models
@@ -13,7 +13,7 @@ class Migration(migrations.Migration):
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'),
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
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'),
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
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'),
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
@@ -43,7 +43,7 @@ class Migration(migrations.Migration):
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'),
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.2.19 on 2023-05-20 14:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0049_auto_20230520_1136'),
]
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='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='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='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'),
),
]

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.25 on 2024-11-20 20:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0051_alter_payment_method'),
]
operations = [
migrations.AddField(
model_name='event',
name='parking_and_access',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.2.25 on 2024-11-20 21:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0052_event_parking_and_access'),
]
operations = [
migrations.AddField(
model_name='riskassessment',
name='parking_and_access',
field=models.BooleanField(default=False, help_text='Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)'),
preserve_default=False,
),
]

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, date_joined_date=timezone.now().date()).count()
return Profile.objects.filter(models.Q(is_approved=False)).count()
def __str__(self):
return self.name
@@ -309,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
@@ -351,9 +342,6 @@ class Event(models.Model, RevisionMixin):
access_at = models.DateTimeField(blank=True, null=True)
meet_at = models.DateTimeField(blank=True, null=True)
# Venue requirements
parking_and_access = models.BooleanField(default=False)
# Crew management
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
on_delete=models.CASCADE)
@@ -369,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:
@@ -379,10 +365,6 @@ class Event(models.Model, RevisionMixin):
return self.pk
return "????"
@property
def needs_mic(self):
return self.is_rig and not self.dry_hire
# Calculated values
"""
EX Vat
@@ -512,10 +494,10 @@ class Event(models.Model, RevisionMixin):
def can_check_in(self):
earliest = self.earliest_time
if isinstance(self.earliest_time, datetime.date):
earliest = datetime.datetime.combine(self.earliest_time, datetime.time(00, 00))
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()
@@ -523,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 = {}
@@ -695,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'),
)
@@ -786,9 +770,6 @@ class RiskAssessment(ReviewableModel, RevisionMixin):
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
# Venue Access
parking_and_access = models.BooleanField(help_text="Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)")
# Blimey that was a lot of options
supervisor_consulted = models.BooleanField(null=True)
@@ -813,7 +794,6 @@ class RiskAssessment(ReviewableModel, RevisionMixin):
'nonstandard_emergency_procedure': False,
'special_structures': False,
'suspended_structures': False,
'parking_and_access': False
}
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
@@ -954,10 +934,6 @@ class PowerTestRecord(ReviewableModel, RevisionMixin):
def activity_feed_string(self):
return str(self.event)
@property
def name(self):
return f"Power Test Record - {self.event}"
class EventCheckIn(models.Model):
event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)

View File

@@ -3,9 +3,8 @@ import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
import datetime
from PyPDF2 import PdfReader, PdfMerger
from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.cache import cache
@@ -31,12 +30,12 @@ def send_eventauthorisation_success_email(instance):
}
template = get_template('event_print.xml')
merger = PdfMerger()
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfReader(buffer))
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
@@ -66,7 +65,7 @@ def send_eventauthorisation_success_email(instance):
css = finders.find('css/email.css')
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
external_styles=css, allow_loading_external_files=True).transform()
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
@@ -111,7 +110,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
context = {
'request': request,
'link_suffix': reverse("admin:RIGS_profile_changelist") + f'?is_approved__exact=0&date_joined__date={timezone.now().date()}',
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
'number_of_users': models.Profile.users_awaiting_approval_count(),
'to_name': admin.first_name
}
@@ -124,7 +123,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
)
css = finders.find('css/email.css')
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
external_styles=css, allow_loading_external_files=True).transform()
external_styles=css).transform()
email.attach_alternative(html, 'text/html')
email.send()

View File

@@ -22,9 +22,6 @@
<paraStyle name="center" alignment="center"/>
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
{% block extrastyles %}
{% endblock %}
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
@@ -87,8 +84,6 @@
<listStyle name="ul"
start="bulletchar"
leftIndent="0"
bulletDedent="10"
bulletFontSize="10"/>
</stylesheet>
@@ -142,7 +137,6 @@
<nextFrame/>
{% block content %}
{% endblock %}
<namedString id="lastPage"><pageNumber/></namedString>
</story>
</document>

View File

@@ -6,36 +6,7 @@
{% load total_invoices_todo from filters %}
{% block titleheader %}
<style>
.franken {
font-family: Fontdiner Swanky;
color: #00ff00;
animation: glow 1.5s infinite alternate;
}
@keyframes glow {
0% {
text-shadow: 0 0 5px #00ff00,
0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 40px #00ff00;
}
50% {
text-shadow: 0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 30px #00ff00,
0 0 60px #00ff00;
}
100% {
text-shadow: 0 0 5px #00ff00,
0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 40px #00ff00;
}
}
</style>
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/"><span class="franken">Franken</span>RIGS</a>
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
{% endblock %}
{% block titleelements %}
@@ -74,7 +45,6 @@
Invoices <span class="badge {% if todo == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ todo }}</span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
<a class="dropdown-item" href="{% url 'invoice_dashboard' %}"><span class="fas fa-chart-line"></span> Dashboard</a>
{% if perms.RIGS.add_invoice %}
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ waiting }}</span></a>
{% endif %}

View File

@@ -26,7 +26,6 @@
var calendarEl = document.getElementById('calendar');
calendar = new FullCalendar.Calendar(calendarEl, {
firstDay: 1,
themeSystem: 'bootstrap',
aspectRatio: 1.5,
eventTimeFormat: {

View File

@@ -1,5 +1,5 @@
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
Just to let you know your event N{{object.eventdisplay_id}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
The TEC Rig Information Gathering System

View File

@@ -46,7 +46,7 @@
</div>
</div>
<div class="row">
<div class="col-sm-12" style="container-type: inline-size;">
<div class="col-sm-12">
{% with object_list as events %}
{% include 'partials/event_table.html' %}
{% endwith %}

View File

@@ -1,24 +1,12 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %}
{% load button from filters %}
{% load static %}
{% block js %}
{{ block.super }}
<script>
$(document).keydown(function(e) {
if ((e.ctrlKey || e.metaKey) && e.keyCode == 80) {
window.open("{% url 'event_print' object.pk %}", '_blank');
return false;
}
});
</script>
{% endblock %}
{% block content %}
<div class="row my-3 py-3">
{% if not is_ajax %}
{% if not request.is_ajax %}
{% if perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %}
@@ -27,11 +15,8 @@
{% endif %}
{% if object.is_rig and perms.RIGS.view_event %}
{# only need contact details for a rig #}
<div class="col-md-6 mb-3">
<div class="col-md-6">
{% include 'partials/contact_details.html' %}
{% if object.parking_and_access or object.riskassessment.parking_and_access %}
{% include 'partials/parking_and_access.html' %}
{% endif %}
</div>
{% endif %}
<div class="col-md-6">
@@ -49,7 +34,7 @@
</div>
{% endif %}
{% endif %}
{% if not is_ajax and perms.RIGS.view_event %}
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %}
</div>
@@ -69,16 +54,50 @@
</div>
</div>
</div>
{% include 'partials/crew_list.html' %}
{% if not is_ajax and perms.RIGS.view_event %}
{% if event.can_check_in %}
<div class="col-sm-12">
<div class="card mt-3">
<div class="card-header">Crew Record</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Vehicle</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
<th scope="col">{% if request.user.pk is event.mic.pk %}<a href="{% url 'event_checkin_override' event.pk %}" class="btn btn-sm btn-success"><span class="fas fa-plus"></span> Add</a>{% endif %}</th>
</tr>
</thead>
<tbody id="crewmembers">
{% for crew in object.crew.all %}
<tr>
<td>{{crew.person}}</td>
<td>{{crew.vehicle|default:"None"}}</td>
<td>{{crew.time}}</td>
<td>{{crew.role}}</td>
<td>{% if crew.end_time %}{{crew.end_time}}{% else %}<span class="text-success fas fa-clock" data-toggle="tooltip" title="This person is currently checked into this event"></span>{% endif %}</td>
<td>{% if crew.end_time %}{% if crew.person.pk == request.user.pk or event.mic.pk == request.user.pk %}{% button 'edit' 'edit_checkin' crew.pk clazz='btn-sm modal-href' %}{% endif %}{%endif%}</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center bg-warning">Apparently this event happened by magic...</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %}
</div>
{% endif %}
{% endif %}
{% if not is_ajax and perms.RIGS.view_event %}
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/last_edited.html' with target="event_history" %}
</div>
@@ -86,7 +105,7 @@
</div>
{% endblock %}
{% if is_ajax %}
{% if request.is_ajax %}
{% block footer %}
{% if perms.RIGS.view_event %}
{% include 'partials/last_edited.html' with target="event_history" %}

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="{{venue.value}}" selected="selected" data-update_url="{% url 'venue_update' 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" %}
@@ -281,15 +281,8 @@
{{ form.dry_hire.label }} {% render_field form.dry_hire %}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<label data-toggle="tooltip" title="Do we need to secure campus parking permits, wristbands for backstage access or other non-standard requirements?">
{{ form.parking_and_access.label }} {% render_field form.parking_and_access %}
</label>
</div>
</div>
</div>
</div>
{# Status is needed on all events types and it looks good here in the form #}
<div class="form-group" data-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
@@ -341,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

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load button from filters %}

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}TEC Email Address Required{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load yesnoi from filters %}
@@ -69,11 +69,8 @@
</div>
</div>
</div>
{% include 'partials/crew_list.html' with event=object.event %}
</div>
<div class="col-12 text-right mt-4">
<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

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load button from filters %}
@@ -28,7 +28,7 @@
<form id="checkin" role="form" method="POST" action="{{ form.action|default:request.path }}">
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/>
{% if not is_ajax and self.request.user.pk is form.event.mic.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>
@@ -86,7 +86,7 @@
</div>
</div>
{% endif %}
{% if not is_ajax %}
{% if not request.is_ajax %}
<div class="row mt-3">
<div class="col-sm-12 text-right">
{% button 'submit' %}

View File

@@ -1,59 +0,0 @@
{% extends 'base_rigs.html' %}
{% load paginator from filters %}
{% load help_text from filters %}
{% load verbose_name from filters %}
{% load get_field from filters %}
{% block title %}{{ title }} List{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h2>{{title}} List</h2>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="table-responsive">
<table class="table mb-0 table-sm">
<thead>
<tr>
<th scope="col">Event</th>
{# mmm hax #}
{% if object_list.0 != None %}
{% for field in object_list.0.fieldz %}
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
{% endfor %}
{% endif %}
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr class="{% if object.reviewed_by %}table-success{%endif%}">
{# General #}
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
{% for field in object_list.0.fieldz %}
<td>{{ object|get_field:field }}</td>
{% endfor %}
{# Buttons #}
<td>
{% include 'partials/hs_status.html' %}
</td>
</tr>
{% empty %}
<tr class="bg-warning">
<td colspan="6">Nothing found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% if is_paginated %}
<div class="row justify-content-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load yesnoi from filters %}
@@ -165,12 +165,11 @@
</div>
</div>
<div class="col-12 text-right">
{% button 'print' 'pt_print' object.pk %}
{% 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

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}

View File

@@ -1,235 +0,0 @@
{% extends 'base_print.xml' %}
{% load filters %}
{% block extrastyles %}
<paraStyle name="style.powerReviewed" borderPadding="3" alignment="center" backColor="green" textColor="white"/>
<paraStyle name="style.powerUnreviewed" borderPadding="3" alignment="center" backColor="red" textColor="white"/>
<paraStyle name="style.smallText" fontSize="8"/>
<paraStyle leftIndent="2in" rightIndent="2in" name="style.smallEvent" fontSize="10" alignment="center" backColor="green" textColor="white" borderPadding="4" borderColor="black"/>
<paraStyle leftIndent="2in" rightIndent="2in" name="style.mediumEvent" fontSize="10" alignment="center" backColor="orange" textColor="white" borderPadding="4" borderColor="black"/>
<paraStyle leftIndent="2in" rightIndent="2in" name="style.largeEvent" fontSize="10" alignment="center" backColor="red" textColor="white" borderPadding="4" borderColor="black"/>
<blockTableStyle id="powerTable">
<blockValign value="middle"/>
<lineStyle kind="LINEABOVE" colorName="black" thickness="1"/>
<lineStyle kind="LINEBELOW" colorName="black" thickness="1"/>
<lineStyle kind="LINEAFTER" colorName="black" thickness="1"/>
<lineStyle kind="LINEBEFORE" colorName="black" thickness="1"/>
</blockTableStyle>
<blockTableStyle id="voltageTable">
<blockValign value="middle"/>
</blockTableStyle>
{% endblock %}
{% block content %}
<spacer length="15"/>
<h1>Power Test Record for <strong>{{ object.event }}</strong></h1>
<spacer length="15"/>
<h2>Client: {{ object.event.person|default:object.event.organisation }} | Venue: {{ object.event.venue }} | MIC: {{ object.event.mic }}</h2>
<spacer length="15"/>
<hr/>
<spacer length="15"/>
{% if object.reviewed_by %}
<para style="style.powerReviewed"><strong>Reviewed by: {{ object.reviewed_by }} at {{ object.reviewed_at|date:"D d/m/Y" }}</strong></para>
{% else %}
<para style="style.powerUnreviewed"><strong>Power test results not yet reviewed</strong></para>
{% endif %}
<spacer length="15"/>
<hr/>
<spacer length="15"/>
<h2 fontSize="16">Power Plan Information</h2>
<spacer length="15"/>
{% if object.event.riskassessment.event_size == 0 %}
<para style="style.smallEvent"><strong>Small Event</strong></para>
{% elif object.event.riskassessment.event_size == 1 %}
<para style="style.mediumEvent"><strong>Medium Event</strong></para>
{% elif object.event.riskassessment.event_size == 2 %}
<para style="style.largeEvent"><strong>Large Event</strong></para>
{% endif %}
<spacer length="15"/>
<blockTable colWidths="250,250">
<tr>
<td><para><strong>Power MIC:</strong> {{ object.power_mic }}</para></td>
<td><para><strong>Venue:</strong> {{ object.event.venue }}</para></td>
</tr>
<tr>
<td><para><strong>Event Date:</strong> {{ object.event.start_date |date:"D d/m/Y" }}</para></td>
<td><para><strong>Generators:</strong> {{ object.event.riskassessment.generators|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Power Test taken at:</strong> {{ object.date_created|date:"D d/m/Y H:i" }}</para></td>
<td><para><strong>Other Companies Power:</strong> {{ object.event.riskassessment.other_companies_power|yesno|capfirst }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>
{% if object.notes %}
<hr/>
<spacer length="15"/>
<para><strong>Additional Notes:</strong></para>
<spacer length="15"/>
<para>{{ object.notes }}</para>
<spacer length="15"/>
{% endif %}
<hr/>
<spacer length="15"/>
{% comment %}
0 - Small event
1 - Medium event (extra power records)
2 - Large event (extra power records)
{% endcomment %}
{% if object.event.riskassessment.event_size >= 1 %}
<para alignment="center"><strong>Power Test results enclosed on next page</strong></para>
<condPageBreak height="10in"/>
<h2 fontSize="16">Event Power Checklist</h2>
<spacer length="15"/>
<blockTable colWidths="250,270" style="powerTable">
<tr>
<td><para><strong>All circuit RCDs tested?</strong></para><para style="style.smallText">(using test button)</para></td>
<td><para>{{ object.all_rcds_tested|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Public/performer accessible circuits tested?</strong></para><para style="style.smallText">(using socket tester)</para></td>
<td><para>{{ object.public_sockets_tested|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Source RCD protected?</strong></para><para style="style.smallText">(if cable is more than 3m long)</para></td>
<td><para>{{ object.source_rcd|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Appropriate and clear labelling on distribution and cabling?</strong></para></td>
<td><para>{{ object.labelling|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Equipment appropriately earthed?</strong></para><para style="style.smallText">(truss, stage, generators, etc.)</para></td>
<td><para>{{ object.earthing|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>All equipment in PAT period?</strong><br/><br/></para></td>
<td><para>{{ object.pat|yesno|capfirst }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>
<h2 fontSize="14">Power tests (First Distro)</h2>
<spacer length="5"/>
<blockTable colWidths="100,410" style="voltageTable">
<tr>
<td><para><strong>Voltage</strong></para><para style="style.smallText">(cube meter) / V</para></td>
<td>
<blockTable colWidths="100,100,100" style="powerTable">
<tr>
<td><para><strong>L1 - N</strong></para></td>
<td><para><strong>L2 - N</strong></para></td>
<td><para><strong>L3 - N</strong></para></td>
</tr>
<tr>
<td>{{ object.fd_voltage_l1}}</td>
<td>{{ object.fd_voltage_l2}}</td>
<td>{{ object.fd_voltage_l3}}</td>
</tr>
</blockTable>
</td>
</tr>
</blockTable>
<spacer length="10"/>
<blockTable colWidths="100,100,190,120" style="voltageTable">
<tr>
<td><para><strong>Phase Rotation</strong></para><para style="style.smallText">(if required)</para></td>
<td><para>{{ object.fd_phase_rotation|yesno|capfirst }}</para></td>
<td><para><strong>Earth Fault Loop Impedance (Z<sub>s</sub>) / Ω</strong></para></td>
<td><para>{{ object.fd_earth_fault }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>
<para><strong>Prospective Short Circuit Current (PSCC)</strong> {{ object.fd_pssc }} A</para>
<spacer length="15"/>
<h2 fontSize="14">Power Tests (Worst Case Points)</h2>
<spacer length="15"/>
<blockTable colWidths="100,100,190,120" style="powerTable">
<tr>
<td><para><strong>Description</strong></para></td>
<td><para><strong>Polarity checked?</strong></para></td>
<td><para><strong>Voltage / V</strong></para></td>
<td><para><strong>Earth Fault Loop Impedance (Z<sub>s</sub>) / Ω</strong></para></td>
</tr>
{% if object.w1_description %}
<tr>
<td><para><strong>{{ object.w1_description }}</strong></para></td>
<td><para>{{ object.w1_polarity|yesno|capfirst }}</para></td>
<td><para>{{ object.w1_voltage }} V</para></td>
<td><para>{{ object.w1_earth_fault }}</para></td>
</tr>
{% endif %}
{% if object.w2_description %}
<tr>
<td><para><strong>{{ object.w2_description }}</strong></para></td>
<td><para>{{ object.w2_polarity|yesno|capfirst }}</para></td>
<td><para>{{ object.w2_voltage }} V</para></td>
<td><para>{{ object.w2_earth_fault }}</para></td>
</tr>
{% endif %}
{% if object.w3_description %}
<tr>
<td><para><strong>{{ object.w3_description }}</strong></para></td>
<td><para>{{ object.w3_polarity|yesno|capfirst }}</para></td>
<td><para>{{ object.w3_voltage }} V</para></td>
<td><para>{{ object.w3_earth_fault }}</para></td>
</tr>
{% endif %}
</blockTable>
{% else %}
{% comment %}
Small power test
{% endcomment %}
<h2 fontSize="16">Power Checklist</h2>
<spacer length="15"/>
<blockTable colWidths="250,270" style="powerTable">
<tr>
<td><para><strong>RCDs installed where needed and tested?</strong><br/><br/></para></td>
<td><para>{{ object.rcds|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Electrical supplies tested?</strong><br/><br/></para></td>
<td><para>{{ object.supply_test|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Equipment appropriately earthed?</strong></para><para style="style.smallText">(truss, stage, generators, etc.)</para></td>
<td><para>{{ object.earthing|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>All equipment in PAT period?</strong><br/><br/></para></td>
<td><para>{{ object.pat|yesno|capfirst }}</para></td>
</tr>
</blockTable>
{% endif %}
{% endblock %}

View File

@@ -124,13 +124,6 @@
<td><para>{{ object|help_text:'persons_responsible_structures'|striptags }}</para></td>
<td><para>{{ object.persons_responsible_structures|default:'N/A' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Venue Access</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'parking_and_access'|striptags }}</para></td>
<td><para>{{ object.parking_and_access|yesno|capfirst }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>\
<hr/>

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load filters %}
{% block content %}
@@ -151,19 +151,8 @@
</dl>
</div>
</div>
<div class="card card-default mb-3">
<div class="card-header">Venue Access</div>
<div class="card-body">
<dl class="row">
<dt class="col-10">{{ object|help_text:'parking_and_access' }}</dt>
<dd class="col-2">
{{ object.parking_and_access|yesnoi:'invert' }}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 text-right">
{% button 'print' 'ra_print' object.pk %}

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}
@@ -162,17 +162,6 @@
</div>
</div>
</div>
<div class="row my-3">
<div class="col-12">
<div class="card">
<div class="card-header">Venue Access</div>
<div class="card-body">
<p><strong>If yes to the below, ensure you have communicated with the client and secured all necessary access prior to the event commencing.</strong></p>
{% include 'partials/yes_no_radio.html' with formitem=form.parking_and_access %}
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-sm-12 text-right">
<div class="btn-group">

View File

@@ -1,106 +0,0 @@
{% extends 'base_rigs.html' %}
{% load humanize %}
{% block content %}
<form method="GET" action="{% url 'invoice_dashboard' %}">
<div class="form-row">
<div class="form-group col-md-4">
<label for="time_filter">Time Filter</label>
<select id="time_filter" name="time_filter" class="form-control">
<option value="week" {% if time_filter == 'week' %}selected{% endif %}>Last Week (7 days)</option>
<option value="month" {% if time_filter == 'month' %}selected{% endif %}>Last Month (30 days)</option>
<option value="year" {% if time_filter == 'year' %}selected{% endif %}>Last Year</option>
<option value="all" {% if time_filter == 'all' %}selected{% endif %}>All Time</option>
</select>
</div>
</div>
</form>
<script>
$('#time_filter').change(function () {
$(this).closest('form').submit();
});
</script>
<h3>Overview</h3>
<!-- big cards in 2x2 grid with total_outstanding, total_events, total_invoices and total_payments, different backgrounds -->
<div class="card-deck">
<div class="card">
<a href="{% url 'invoice_waiting' %}" class="text-decoration-none text-white">
<div class="card-body bg-primary">
<h5 class="card-title text-center">Total Waiting</h5>
<p class="card-text text-center h3"><strong>£{{ total_waiting|floatformat:"2g" }}</strong></p>
</div>
</a>
</div>
<div class="card">
<a href="{% url 'invoice_list' %}" class="text-decoration-none text-dark">
<div class="card-body bg-info">
<h5 class="card-title text-center">Total Outstanding</h5>
<p class="card-text text-center h3"><strong>£{{ total_outstanding|floatformat:"2g" }}</strong></p>
</div>
</a>
</div>
<div class="card">
<div class="card-body bg-danger">
<h5 class="card-title text-center">Total Events</h5>
<p class="card-text text-center h3"><strong>{{ total_events }}</strong></p>
</div>
</div>
<div class="card">
<div class="card-body bg-success">
<h5 class="card-title text-center">Total Invoices</h5>
<p class="card-text text-center h3"><strong>{{ total_invoices }}</strong></p>
</div>
</div>
</div>
<br />
<h3>Payments</h3>
<br/>
<h4>Sources</h4>
<br/>
{% for source in payment_methods %}
<div class="card">
<div class="card-body">
<h5 class="card-title"><strong>{{ source.method }}</strong></h5>
<p class="card-text h3">£{{ source.total|floatformat:"2g" }}</p>
</div>
</div>
{% endfor %}
<br/>
<h4>Total</h4>
<br/>
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">Total Income</h5>
<p class="card-text text-center h3"><strong>£{{ total_income|floatformat:"2g" }}</strong></p>
</div>
</div>
<br/>
<h4>Invoice Payment Time</h4>
<br/>
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">Average Time to Pay</h5>
<p class="card-text text-center h3"><strong>{{ mean_invoice_to_payment|floatformat:"2g" }} days</strong></p>
</div>
</div>
{% endblock %}

View File

@@ -31,7 +31,7 @@
{% for event in object_list %}
<tr class="{{event.status_color}}">
<th scope="row"><a href="{% url 'event_detail' event.pk %}">{{ event.display_id }}</a><br>
<span class="{% if event.get_status_display == 'Cancelled' %}text-danger{% endif %}">{{ event.get_status_display }}</span></th>
<span class="text-muted">{{ event.get_status_display }}</span></th>
<td>{{ event.start_date }}</td>
<td>
{{ event.name }}

View File

@@ -23,7 +23,7 @@
</div>
{% endif %}
{% if object.organisation %}
<div class="card card-default mb-3">
<div class="card card-default">
<div class="card-header">Organisation Details</div>
<div class="card-body">
<dl class="row">
@@ -44,4 +44,4 @@
</dl>
</div>
</div>
{% endif %}
{% endif %}

View File

@@ -1,50 +0,0 @@
{% load button from filters %}
{% 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 event.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>
</div>
{% endif %}

View File

@@ -1,6 +1,6 @@
<h5 class="py-3"><a class="btn btn-info" data-toggle="collapse" href="#values" aria-expanded="false" aria-controls="values">View Threshold Values</a></h5>
<div class="row collapse" id="values">
<div class="col-md-6 col-sm-12">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
@@ -33,20 +33,17 @@
<thead>
<tr>
<th scope="row">Distro</th>
<th scope="row">Max PSCC with Single Phase Supply (kA)</th>
<th scope="row">Max PSCC with Three Phase Supply (kA)</th>
<th scope="row">Max PSSC (kA)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Intel & Toblerone distros</td>
<td>6</td>
<td>3</td>
</tr>
<tr>
<td>All other distros</td>
<td>10</td>
<td>5</td>
</tr>
</tbody>
</table>

View File

@@ -11,7 +11,6 @@
{{ object.venue|namewithnotes:'venue_detail' }}
</a>
{% endif %}
{% if object.parking_and_access or object.riskassessment.parking_and_access %}<span class="badge badge-warning">Additional Access Requirements</span>{% endif %}
</dd>
{% if object.venue %}
<dt class="col-sm-6">Venue Notes</dt>
@@ -78,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

@@ -1,147 +0,0 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="card h-100 border-3 {{ border_class }} event-row">
<div class="card-header {{ header_bg }} {{ header_text }} py-3">
<div class="d-flex justify-content-between align-items-center">
<span class="d-flex align-items-center">
<h5 class="mb-0 mr-3">
<a href="{% url 'event_detail' event.pk %}"
class="{{ header_text }} text-decoration-underline fw-bold">
<strong>{{ event.display_id }}</strong> - {{ event.name }}
</a>
</h5>
{% if event.dry_hire %}
<span class="badge px-3 py-2 rounded-pill fs-6 text-dark bg-light">Dry Hire</span>
{% endif %}
</span>
<span class="badge fs-6 px-3 py-2 bg-light text-dark rounded-pill">{{ event.get_status_display }}</span>
</div>
</div>
<div class="card-body">
<div class="row align-items-start">
<div class="col-md-2 border-end event-dates">
<div class="mb-2">
<small class="text-muted">Meet at:</small>
{% if event.meet_at %}
<p class="mb-1">{{ event.meet_at|date:"D j M Y, H:i" }}</p>
{% else %}
<p class="mb-1">Not specified</p>
{% endif %}
</div>
<div class="mb-2">
<small class="text-muted">Access from:</small>
{% if event.access_at %}
<p class="mb-1">{{ event.access_at|date:"D j M Y, H:i" }}</p>
{% else %}
<p class="mb-1">Not specified</p>
{% endif %}
</div>
<div class="mb-2">
<small class="text-muted">Start:</small>
<p class="mb-1">
{% if event.start_date and event.start_time %}
{{ event.start_date|date:"D j M Y" }}, {{ event.start_time|date:"H:i" }}
{% elif event.start_date %}
{{ event.start_date|date:"D j M Y" }}
{% elif event.start_time %}
{{ event.start_time|date:"H:i" }}
{% else %}
Not specified
{% endif %}
</p>
</div>
<div class="mb-2">
<small class="text-muted">End:</small>
<p class="mb-1">
{% if event.end_date and event.end_time %}
{{ event.end_date|date:"D j M Y" }}, {{ event.end_time|date:"H:i" }}
{% elif event.end_date %}
{{ event.end_date|date:"D j M Y" }}
{% elif event.end_time %}
{{ event.end_time|date:"H:i" }}
{% else %}
Not specified
{% endif %}
</p>
</div>
</div>
<div class="col-md-10">
<div class="row">
<div class="col-md-6">
{% if event.venue %}
<div class="mb-3">
<small class="text-muted">Venue:</small>
<p class="mb-1">{{ event.venue|namewithnotes:'venue_detail' }}</p>
</div>
{% endif %}
{% if event.is_rig %}
<div class="mb-3">
<small class="text-muted">Client:</small>
<p class="mb-1">
{% if event.person %}
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation }}</a>
{% endif %}
{% elif event.organisation %}
<a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation }}</a>
{% else %}
No client specified
{% endif %}
</p>
</div>
{% endif %}
{% if event.mic or event.needs_mic %}
<div class="mb-3">
<small class="text-muted">Member in Charge (MIC):</small>
<div class="d-flex align-items-center mt-1">
{% if event.mic %}
<img src="{{ event.mic.profile_picture }}" alt="{{ event.mic.name }}"
class="rounded-circle mr-1" width="32" height="32">
<span>
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
{{ event.mic.name }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
</span>
{% else %}
<span class="text-danger">No MIC assigned</span>
{% endif %}
</div>
</div>
{% endif %}
</div>
<div class="col-md-6">
<div class="mb-3">
<small class="text-muted">Description:</small>
<p class="mb-1">{{ event.description|markdown }}</p>
</div>
<div class="mb-3">
<small class="text-muted">Status:</small>
<div class="mt-1">
{% include "partials/event_status.html" with status=event.status %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<div id="event_status">
<div>
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
{% if event.is_rig %}
{% if event.sum_total > 0 %}
{% if event.purchase_order %}
<span class="badge badge-success">PO: Received</span>
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
{% elif event.authorised %}
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
@@ -44,8 +44,5 @@
<span class="badge badge-info">Invoice: Not Generated</span>
{% endif %}
{% endif %}
{% if event.parking_and_access %}
<span class="badge badge-warning">Addititional Access Requirements</span>
{% endif %}
{% endif %}
</div>

View File

@@ -1,70 +1,105 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<style>
.light-link {
color: #ebf5ff !important;
}
.dark-link {
color: #4495ff !important;
}
.link-on-green {
color: #ffffff !important;
}
</style>
<div class="row">
{% for event in events %}
<div class="col-12 mb-4">
{% comment %} Determine card style based on event status {% endcomment %}
{% if event.cancelled %}
{% with border_class="border-secondary" header_bg="bg-secondary" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif not event.is_rig %}
{% with border_class="border-primary" header_bg="bg-primary" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif not event.mic %}
{% with border_class="border-danger" header_bg="bg-danger" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
{% with border_class="border-success" header_bg="bg-success" header_text="link-on-green" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% else %}
{% with border_class="border-warning" header_bg="bg-warning" header_text="dark-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% endif %}
{% else %}
{% with border_class="border-warning" header_bg="bg-warning" header_text="dark-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% endif %}
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info">
No events currently scheduled.
</div>
</div>
{% endfor %}
<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">MIC</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr class="{% if event.cancelled %}
table-secondary
{% elif not event.is_rig %}
table-info
{% elif not event.mic %}
table-danger
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% else %}
table-warning
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times-->
<td id="event_dates" style="text-align: justify;">
{% if not event.cancelled %}
{% if event.meet_at %}
<span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% if event.access_at %}
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% endif %}
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %}
{{ event.start_time|date:"H:i" }}
{% endif %}</strong>
</span>
{% if event.end_date %}
<br>
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
{% if event.has_end_time %}
{{ event.end_time|date:"H:i" }}
{% endif %}</strong>
</span>
{% endif %}
</td>
<!---Details-->
<td id="event_details" class="w-100">
<h4>
<a href="{% url 'event_detail' event.pk %}">
{{ event.name }}
</a>
{% if event.venue %}
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
{% endif %}
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
{% include 'partials/event_status.html' %}
</td>
<!---MIC-->
<td id="event_mic" class="text-nowrap">
{% if event.mic %}
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
{{ event.mic }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
{% elif event.is_rig %}
<span class="fas fa-user-slash"></span>
{% endif %}
</td>
</tr>
{% empty %}
<tr class="bg-warning">
<td colspan="4">No events found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -1,195 +0,0 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<style>
#event_table {
display: grid;
grid-template-columns: max-content min-content minmax(max-content, 1fr) max-content;
column-gap: 1em;
}
.eventgrid {
display: inherit;
grid-column: 1/5;
grid-template-columns: subgrid;
padding: 1em;
dt, dd { display: block; float: left; }
dt { clear: both; }
dd { float: right; }
}
.grid-header {
border-bottom: 1px solid grey;
border-top: 1px solid grey;
}
#event_status {
grid-column-start: 3;
}
#event_mic {
grid-row-start: 1;
grid-column-start: 4;
}
.c-none {
display: none;
}
.c-inline {
display: inline;
}
@container (width <= 500px) {
#event_table {
grid-template-columns: 1fr !important;
}
.eventgrid {
grid-column: 1/1 !important;
padding: 0.5em;
}
.grid-header {
display: none;
}
#event_dates {
order: 2;
}
#event_status {
order: 3;
}
#event_mic {
grid-row-start: auto;
grid-column-start: 4;
}
}
@container (width <= 700px) {
#event_table {
grid-template-columns: max-content;
column-gap: 0.5em;
}
.eventgrid {
grid-column: 1/3;
border: 1px solid grey;
}
#event_dates {
grid-row: 2;
grid-column: 1;
}
#event_number {
grid-row: 1;
grid-column: 1;
}
#event_mic {
grid-column: 2;
}
#event_status {
grid-column: span 2;
}
.grid-header, .c-md-none {
display: none;
}
}
@container (width > 700px) {
.c-lg-block {
display: block;
}
.c-lg-inline {
display: inline;
}
.c-lg-none, .c-md-none {
display: none;
}
}
</style>
<div id="event_table">
<div class="eventgrid grid-header font-weight-bold">
<div id="event_number">#</div>
<div id="event_dates">Dates & Times</div>
<div>Event Details</div>
<div id="event_mic">MIC</div>
</div>
{% for event in events %}
<div class="eventgrid {% if event.cancelled %}
table-secondary
{% elif not event.is_rig %}
table-info
{% elif not event.mic %}
table-danger
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% else %}
table-warning
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<div class="font-weight-bold c-none c-lg-block" id="event_number">{{ event.display_id }}</div>
<!--Dates & Times-->
<div id="event_dates" style="min-width: 180px;">
<dl>
{% if not event.cancelled %}
{% if event.meet_at %}
<dt class="font-weight-normal">Meet:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.meet_at|date:"D d/m/Y H:i" }}</dd>
{% endif %}
{% if event.access_at %}
<dt class="font-weight-normal">Access:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.access_at|date:"D d/m/Y H:i" }}</dd>
{% endif %}
{% endif %}
<dt class="font-weight-normal">Start:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %}
{{ event.start_time|date:"H:i" }}
{% endif %}
</dd>
{% if event.end_date %}
<dt class="font-weight-normal">End:</dt>
<dd class="text-nowrap font-weight-bold text-lg-right">{{ event.end_date|date:"D d/m/Y" }}
{% if event.has_end_time %}
{{ event.end_time|date:"H:i" }}
{% endif %}
</dd>
{% endif %}
</dl>
</div>
<!---Details-->
<div id="event_details" class="w-100">
<h4>
<a href="{% url 'event_detail' event.pk %}">
<span class="c-inline c-lg-none">{{ event }}</span><span class="c-none c-lg-inline">{{ event.name }}</span>
</a>
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
<br class="c-none c-lg-inline">
{% if event.venue %}
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
</div>
{% include 'partials/event_status.html' %}
<!---MIC-->
<div id="event_mic" class="text-nowrap">
<span class="c-md-none align-middle">MIC:</span>
{% if event.mic %}
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
{{ event.mic }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
{% elif event.is_rig %}
<span class="fas fa-exclamation"></span>
{% endif %}
</div>
</div>
{% endfor %}
</div>

View File

@@ -1,22 +0,0 @@
<div class="card card-default">
<div class="card-header">Parking and Access</div>
<div class="card-body">
<p>This venue has additional parking and/or access requirements.</p>
<p>Ensure the MIC has:</p>
<ul>
<li>Details of where to park</li>
<li>Details of how to access the venue</li>
<li>Details of any access restrictions</li>
<li>If on campus, sorted parking permits</li>
</ul>
{% if object.parking_and_access and object.riskassessment.parking_and_access %}
<small>Additional parking marked on both rig details and risk assessment.</small>
{% elif object.parking_and_access %}
<small>Additional parking marked on rig details.</small>
{% elif object.riskassessment.parking_and_access %}
<small>Additional parking marked on risk assessment.</small>
{% endif %}
</div>
</div>

View File

@@ -1,7 +1,7 @@
<div class="row">
<label for="{{ formitem.0.id_for_label }}"
<label for="{{ formitem.id_for_label }}"
class="col-8 control-label">{{ formitem.help_text|safe }}</label>
<div class="col-4 pb-3" id="{{ formitem.0.id_for_label|slice:'-2' }}">
<div class="col-4 pb-3" id="{{ formitem.id_for_label|slice:'-2' }}">
{% for radio in formitem %}
<div class="custom-control custom-radio">
{{ radio.tag }}

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

@@ -3,45 +3,16 @@
{% block content %}
<div class="row align-items-center justify-content-between py-2 align-middle">
<div class="col-sm-12 col-md align-middle d-flex flex-wrap">
Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span
class="table-warning mr-1 px-2 rounded text-nowrap">Action Required</span><span
class="table-danger mr-1 px-2 rounded text-nowrap">Needs MIC</span><span
class="table-secondary mr-1 px-2 rounded">Cancelled</span><span
class="table-info px-2 rounded text-nowrap">Non-Rig</span>
<div class="col-sm-12 col-md align-middle">
Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span class="table-warning mr-1 px-2 rounded">Action Required</span><span class="table-danger mr-1 px-2 rounded">Needs MIC</span><span class="table-secondary mr-1 px-2 rounded">Cancelled</span><span class="table-info px-2 rounded">Non-Rig</span>
</div>
{% if perms.RIGS.add_event %}
<div class="col text-right">
{% button 'new' 'event_create' %}
</div>
{% endif %}
{% if not request.GET.legacy %}
{% if not request.GET.hide_cancelled %}
<a href="?hide_cancelled=true" class="btn btn-primary mr-3">Hide cancelled</a>
{% else %}
<a href="." class="btn btn-primary mr-3">Show cancelled</a>
{% endif %}
<a href="?legacy=true" class="btn btn-secondary">Legacy rigboard</a>
{% else %}
<a href="." class="btn btn-secondary">New rigboard</a>
{% endif %}
</div>
{% if request.GET.legacy %}
<div class="alert alert-warning">
<strong>Warning:</strong> The legacy rigboard is being deprecated and will be removed in the future. Please use the
new rigboard.
</div>
{% endif %}
{% include 'partials/event_table.html' %}
<div style="container-type: inline-size;">
{% if request.GET.legacy %}
{% include 'partials/legacy_event_table.html' %}
{% else %}
{% include 'partials/event_table.html' %}
{% endif %}
</div>
{% endblock %}

View File

@@ -20,7 +20,7 @@ def ra(basic_event, admin_user):
known_venue=True, safe_loading=True, safe_storage=True,
area_outside_of_control=True, barrier_required=True,
nonstandard_emergency_procedure=True, special_structures=False,
suspended_structures=False, outside=False, parking_and_access=False)
suspended_structures=False, outside=False)
yield ra
ra.delete()

View File

@@ -16,14 +16,14 @@ class Rigboard(BasePage):
URL_TEMPLATE = reverse('rigboard')
_add_item_selector = (By.XPATH, "//a[contains(@class,'btn-primary') and contains(., 'New')]")
_event_row_locator = (By.CLASS_NAME, "event-row")
_event_row_locator = (By.ID, 'event_row')
def add(self):
self.find_element(*self._add_item_selector).click()
class EventListRow(Region):
_event_number_locator = (By.ID, "event_number")
_event_dates_locator = (By.CLASS_NAME, "event-dates")
_event_dates_locator = (By.ID, "event_dates")
_event_details_locator = (By.ID, "event_details")
_event_mic_locator = (By.ID, "event_mic")
@@ -207,7 +207,6 @@ class CreateRiskAssessment(FormPage):
'suspended_structures': (regions.RadioSelect, (By.ID, 'id_suspended_structures')),
'supervisor_consulted': (regions.CheckBox, (By.ID, 'id_supervisor_consulted')),
'rigging_plan': (regions.TextBox, (By.ID, 'id_rigging_plan')),
'parking_and_access': (regions.RadioSelect, (By.ID, 'id_parking_and_access')),
}
@property

View File

@@ -91,8 +91,8 @@ class TestRigboard(BaseRigboardTest):
# self.live_server_url + '/event/create/', self.driver.current_url)
def test_event_order(self):
self.assertIn(self.testEvent.start_date.strftime('%-d %b %Y'), self.page.events[0].dates)
self.assertIn(self.testEvent2.start_date.strftime('%-d %b %Y'), self.page.events[1].dates)
self.assertIn(self.testEvent.start_date.strftime('%a %d/%m/%Y'), self.page.events[0].dates)
self.assertIn(self.testEvent2.start_date.strftime('%a %d/%m/%Y'), self.page.events[1].dates)
def test_add_button(self):
self.page.add()
@@ -127,7 +127,7 @@ class TestEventCreate(BaseRigboardTest):
# Fix it
self.page.end_date = datetime.date(2020, 1, 11)
self.page.access_at = datetime.datetime(2020, 1, 8, 9)
self.page.access_at = datetime.datetime(2020, 1, 1, 9)
self.page.dry_hire = True
self.page.status = "Booked"
self.page.collected_by = "Fred"
@@ -530,11 +530,10 @@ class TestCalendar(BaseRigboardTest):
self.page.toggle_filter('cancelled')
self.page.toggle_filter('provisional')
self.page.toggle_filter('confirmed')
self.page.toggle_filter('only_mic')
# and then check the url is correct
self.assertIn(
"rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false&only_mic=true",
"rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false",
self.page.cal_url)
# Awesome - all seems to work
@@ -760,7 +759,6 @@ def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event):
page.barrier_required = False
page.nonstandard_emergency_procedure = False
page.special_structures = False
page.parking_and_access = False
# self.page.persons_responsible_structures = "Nobody and her cat, She"
page.suspended_structures = True

View File

@@ -372,7 +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:
with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")) as f:
markdown = f.read()
def test_html_safe(self):

View File

@@ -100,7 +100,6 @@ urlpatterns = [
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/power/<int:pk>/print/', permission_required_with_403('RIGS.view_powertestrecord')(views.PowerPrint.as_view()), name='pt_print'),
path('event/<int:pk>/checkin/', login_required(views.EventCheckIn.as_view()),
name='event_checkin'),
@@ -111,12 +110,8 @@ urlpatterns = [
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()),
name='event_checkin_override'),
path('event/<int:pk>/thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'),
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
# Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceDashboard.as_view()), name='invoice_dashboard'),
path('invoice/outstanding', permission_required_with_403('RIGS.view_invoice')(views.InvoiceOutstanding.as_view()),
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
name='invoice_list'),
path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceArchive.as_view()),
name='invoice_archive'),

View File

@@ -5,7 +5,7 @@ import reversion
from django import forms
from django.contrib import messages
from django.db import transaction
from django.db.models import Sum
from django.db.models import Q
from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
@@ -18,76 +18,8 @@ from RIGS import models
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
TIME_FILTERS = ["all", "year", "month", "week"]
def days_between(d1, d2):
diff = d2 - d1
return diff.total_seconds() / datetime.timedelta(days=1).total_seconds()
class InvoiceDashboard(generic.TemplateView):
template_name = 'invoice_dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice Dashboard"
context['description'] = "Overview of financial status of TEC rigs."
time_filter = self.request.GET.get('time_filter', 'all')
if time_filter not in TIME_FILTERS:
time_filter = 'all'
if time_filter == 'all':
context['events'] = models.Event.objects.filter(is_rig=True)
context['invoices'] = models.Invoice.objects.all()
context['payments'] = models.Payment.objects.all()
elif time_filter == 'year':
context['events'] = models.Event.objects.filter(is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=365))
context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=365))
context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=365))
elif time_filter == 'month':
context['events'] = models.Event.objects.filter(is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=30))
context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=30))
context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=30))
elif time_filter == 'week':
context['events'] = models.Event.objects.filter(is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=7))
context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=7))
context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=7))
context["time_filter"] = time_filter
context['total_outstanding'] = sum([i.balance for i in models.Invoice.objects.outstanding_invoices()])
context['total_waiting'] = sum([i.sum_total for i in models.Event.objects.waiting_invoices()])
context['total_events'] = len(context['events'])
context['total_invoices'] = len(context['invoices'])
context['total_payments'] = len(context['payments'])
payment_methods = dict(models.Payment.METHODS)
context['payment_methods'] = context["payments"].values('method').annotate(total=Sum('amount')).order_by('method')
for method in context['payment_methods']:
method['method'] = payment_methods.get(method['method'], f"Unknown method ({method['method']})")
context["total_income"] = sum([i['total'] for i in context['payment_methods']])
payments = context['payments']
mean_duration = 0
for payment in payments:
mean_duration += days_between(payment.invoice.invoice_date, payment.date)
if len(payments) > 0:
mean_duration /= len(payments)
context['mean_invoice_to_payment'] = mean_duration
return context
class InvoiceOutstanding(generic.ListView):
class InvoiceIndex(generic.ListView):
model = models.Invoice
template_name = 'invoice_list.html'

View File

@@ -53,6 +53,12 @@ class EventRiskAssessmentCreate(HSCreateView):
def get_success_url(self):
return reverse('ra_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if context['event'].mic:
context['power_mic'] = context['event'].mic
return context
class EventRiskAssessmentEdit(generic.UpdateView):
model = models.RiskAssessment
@@ -166,7 +172,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)
@@ -214,7 +220,7 @@ 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)
@@ -232,16 +238,6 @@ class RAPrint(PrintView):
return context
class PowerPrint(PrintView):
model = models.PowerTestRecord
template_name = 'hs/power_print.xml'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filename'] = f"PowerTestRecord_for_{context['object'].event.display_id}.pdf"
return context
class EventCheckIn(generic.CreateView, ModalURLMixin):
model = models.EventCheckIn
template_name = 'hs/eventcheckin_form.html'

View File

@@ -24,7 +24,6 @@ class CalendarICS(ICalFeed):
# Rig = 'rig' = True
# Provisional = 'provisional' = True
# Confirmed/Booked = 'confirmed' = True
# Only MIC = 'mic' = False
def get_object(self, request, *args, **kwargs):
params = {}
@@ -36,9 +35,6 @@ class CalendarICS(ICalFeed):
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
params['confirmed'] = request.GET.get('confirmed', 'true') == 'true'
params['only_mic'] = request.GET.get('only_mic', 'false') == 'true'
params['user'] = kwargs['user']
return params
@@ -77,9 +73,6 @@ class CalendarICS(ICalFeed):
filter = filter & typeFilters & statusFilters
if params['only_mic']:
filter = filter & Q(mic=params['user'])
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
'venue', 'mic')

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,7 +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 PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
@@ -41,13 +34,8 @@ class RigboardIndex(generic.TemplateView):
# get super context
context = super().get_context_data(**kwargs)
objects = models.Event.objects.current_events()
if self.request.GET.get('hide_cancelled', False):
objects = objects.exclude(status=models.Event.CANCELLED)
# call out method to get current events
context['events'] = objects.select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['page_title'] = "Rigboard"
return context
@@ -360,7 +348,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context),
external_styles=css, allow_loading_external_files=True).transform()
external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
msg.send()
@@ -376,7 +364,7 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
css = finders.find('css/email.css')
response = super().render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css, allow_loading_external_files=True).transform()
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
return response
def get_context_data(self, **kwargs):
@@ -389,41 +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)

View File

@@ -4,7 +4,7 @@
"scripts": {
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
},
"stack": "heroku-22",
"stack": "heroku-20",
"env": {
"DEBUG": {
"required": true
@@ -51,7 +51,7 @@
"url": "heroku/nodejs"
},
{
"url": "heroku/python"
"url": "https://github.com/nottinghamtec/heroku-buildpack-python"
}
]
}

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

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% load widget_tweaks %}
{% block content %}
@@ -79,7 +79,7 @@
</div>
</div>
</div>
{% if not is_ajax %}
{% if not request.is_ajax %}
<div class="form-group form-row pull-right">
<button class="btn btn-success" type="submit" form="asset_audit_form" id="id_mark_audited">Mark Audited</button>
</div>

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

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:"base_ajax.html,base_assets.html" %}
{% extends 'base_assets.html' %}
{% load widget_tweaks %}
{% load static %}
@@ -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

@@ -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,24 +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()
@pytest.fixture
def test_supplier(db):
supplier, created = models.Supplier.objects.get_or_create(name="Fullmetal Heavy Industry")
yield supplier
supplier.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,18 +1,18 @@
import time
import datetime
import pytest
from django.utils import timezone
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from PyRIGS.tests.base import AutoLoginTest, assert_times_almost_equal
from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal
from PyRIGS.tests.pages import animation_is_finished
from assets import models
from . import pages
@screenshot_failure_cls
class TestAssetList(AutoLoginTest):
def setUp(self):
super().setUp()
@@ -53,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
self.page.set_query("")
self.page.search()
# Only working stuff shown by default
self.assertTrue(len(self.page.assets) == 2)
page.set_query("")
page.search()
# Only working stuff shown by default
assert len(page.assets) == 1
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.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
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):
@@ -143,6 +143,7 @@ def test_asset_duplicate(logged_in_browser, admin_user, live_server, test_asset)
assert models.Asset.objects.last().description == test_asset.description
@screenshot_failure_cls
class TestAssetForm(AutoLoginTest):
def setUp(self):
super().setUp()
@@ -209,6 +210,7 @@ class TestAssetForm(AutoLoginTest):
self.assertEqual(asset.date_acquired, acquired)
@screenshot_failure_cls
class TestSupplierList(AutoLoginTest):
def setUp(self):
super().setUp()
@@ -241,35 +243,39 @@ class TestSupplierList(AutoLoginTest):
self.page.set_query("")
self.page.search()
time.sleep(1)
self.assertTrue(len(self.page.suppliers) == 7)
self.page.set_query("NOTFOUND")
self.page.set_query("This is not a supplier")
self.page.search()
self.assertTrue(len(self.page.suppliers) == 0)
def test_supplier_create(logged_in_browser, live_server):
page = pages.SupplierCreate(logged_in_browser.driver, live_server.url).open()
@screenshot_failure_cls
class TestSupplierCreateAndEdit(AutoLoginTest):
def setUp(self):
super().setUp()
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
page.remove_all_required()
page.submit()
assert !self.page.success
assert "This field is required." in self.page.errors["Name"]
def test_supplier_create(self):
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
page.name = "Optican Health Supplies"
page.submit()
assert page.success
self.page.remove_all_required()
self.page.submit()
self.assertFalse(self.page.success)
self.assertIn("This field is required.", self.page.errors["Name"])
self.page.name = "Optican Health Supplies"
self.page.submit()
self.assertTrue(self.page.success)
def test_supplier_edit(logged_in_browser, live_server, test_supplier):
page = pages.SupplierEdit(logged_in_browser.driver, live_server.url, supplier_id=test_supplier.pk).open()
def test_supplier_edit(self):
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
assert test_supplier.name == page.name
new_name = "Cyberdyne Systems"
page.name = new_name
page.submit()
assert page.success
self.assertEqual("Fullmetal Heavy Industry", self.page.name)
new_name = "Cyberdyne Systems"
self.page.name = new_name
self.page.submit()
self.assertTrue(self.page.success)
def test_audit_search(logged_in_browser, live_server, test_asset):
@@ -304,30 +310,47 @@ def test_audit_success(logged_in_browser, admin_user, live_server, test_asset):
assert test_asset.asset_id not in page.assets
def test_audit_fail(logged_in_browser, admin_user, live_server, test_asset):
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
wait = WebDriverWait(logged_in_browser.driver, 20)
page.set_query(test_asset.asset_id)
page.search()
wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
# Do it wrong on purpose to check error display
page.modal.remove_all_required()
page.modal.description = ""
page.modal.submit()
wait.until(animation_is_finished())
assert "This field is required." in self.page.modal.errors["Description"]
@screenshot_failure_cls
class TestAssetAudit(AutoLoginTest):
def setUp(self):
super().setUp()
self.category = models.AssetCategory.objects.create(name="Haulage")
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
self.supplier = models.Supplier.objects.create(name="The Bazaar")
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
voltage_rating=40, num_pins=13)
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
category=self.category,
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
self.wait = WebDriverWait(self.driver, 20)
def test_audit_fail(self):
self.page.set_query(self.asset.asset_id)
self.page.search()
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
# Do it wrong on purpose to check error display
self.page.modal.remove_all_required()
self.page.modal.description = ""
self.page.modal.submit()
self.wait.until(animation_is_finished())
self.driver.implicitly_wait(4)
self.assertIn("This field is required.", self.page.modal.errors["Description"])
def test_audit_list(logged_in_browser, admin_user, live_server, test_asset):
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
wait = WebDriverWait(logged_in_browser.driver, 20)
assert models.Asset.objects.filter(last_audited_at=None).count() == len(self.page.assets)
asset_row = page.assets[0]
logged_in_browser.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
assert self.page.modal.asset_id == asset_row.id
page.modal.close()
assert !logged_in_browser.driver.find_element(By.ID, 'modal').is_displayed()
# Make sure audit log was NOT filled out
audited = models.Asset.objects.get(asset_id=asset_row.id)
assert audited.last_audited_by is None
def test_audit_list(self):
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
asset_row = self.page.assets[0]
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
self.assertEqual(self.page.modal.asset_id, asset_row.id)
self.page.modal.close()
self.assertFalse(self.driver.find_element(By.ID, 'modal').is_displayed())
# Make sure audit log was NOT filled out
audited = models.Asset.objects.get(asset_id=asset_row.id)
assert audited.last_audited_by is None

View File

@@ -16,7 +16,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from PyPDF2 import PdfMerger, PdfReader
from PyPDF2 import PdfFileMerger, PdfFileReader
from PIL import Image, ImageDraw, ImageFont, ImageOps
from barcode import Code39
from barcode.writer import ImageWriter
@@ -127,7 +127,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
return context
def get_success_url(self):
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
@@ -233,7 +233,7 @@ class SupplierList(GenericListView):
context['edit'] = 'supplier_update'
context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
context['detail'] = 'supplier_detail'
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
@@ -250,7 +250,7 @@ class SupplierDetail(GenericDetailView):
context['detail_link'] = 'supplier_detail'
context['associated'] = 'partials/associated_assets.html'
context['associated2'] = ''
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
@@ -264,7 +264,7 @@ class SupplierCreate(GenericCreateView, ModalURLMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
@@ -280,7 +280,7 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if is_ajax(self.request).get('is_ajax'):
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
@@ -374,7 +374,7 @@ def generate_label(pk):
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
logo_size = (200, 200)
image.paste(logo.resize(logo_size, Image.LANCZOS), box=(5, 5))
image.paste(logo.resize(logo_size, Image.ANTIALIAS), box=(5, 5))
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
width, height = barcode_image.size
image.paste(barcode_image.crop((0, 0, width, 100)), (int(((size[0] + logo_size[0]) - width) / 2), 40))
@@ -417,11 +417,11 @@ class GenerateLabels(generic.View):
# 'images3': images[3::4],
'filename': name
}
merger = PdfMerger()
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfReader(buffer))
merger.append(PdfFileReader(buffer))
buffer.close()
merged = BytesIO()

View File

@@ -1,52 +0,0 @@
services:
db:
image: postgres:17
environment:
POSTGRES_DB: ${DATABASE_NAME}
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- .env
pyrigs:
build: .
container_name: pyrigs
ports:
- "8000:8000"
depends_on:
- db
environment:
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
DEBUG: ${DEBUG}
DJANGO_LOGLEVEL: ${DJANGO_LOGLEVEL}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS}
DATABASE_ENGINE: ${DATABASE_ENGINE}
DATABASE_NAME: ${DATABASE_NAME}
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
DATABASE_HOST: ${DATABASE_HOST}
DATABASE_PORT: ${DATABASE_PORT}
env_file:
- .env
develop:
# Create a `watch` configuration to update the app
#
watch:
# Sync the working directory with the `/app` directory in the container
- action: sync
path: .
target: /app
# Exclude the project virtual environment
ignore:
- .venv/
# Rebuild the image on changes to the `pyproject.toml`
- action: rebuild
path: ./pyproject.toml
volumes:
postgres_data:

View File

@@ -16,7 +16,7 @@ const con = require('gulp-concat');
const gulpif = require('gulp-if');
function fonts(done) {
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*', { encoding: false })
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*')
.pipe(gulp.dest('pipeline/built_assets/fonts'))
.pipe(browsersync.stream());
}
@@ -70,16 +70,16 @@ function scripts() {
.pipe(gulpif(function(file) { return interaction.includes(file.relative);}, con('interaction.js')))
.pipe(gulpif(function(file) { return jpop.includes(file.relative);}, con('jpop.js')))
.pipe(flatten())
// Only minify if filename does not already denote it as minified
.pipe(gulpif(function(file) { return file.path.indexOf("min") == -1;},terser()))
.pipe(terser())
.pipe(gulp.dest(dest))
.pipe(browsersync.stream());
}
function browserSync(done) {
spawn('python', ['manage.py', 'runserver'], {stdio: 'inherit'});
// TODO Wait for Django server to come up before browsersync, it seems inconsistent
browsersync.init({
notify: true,
notify: false,
open: false,
port: 8001,
proxy: '127.0.0.1:8000'

10034
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@
"cssnano": "^5.0.13",
"easymde": "^2.16.1",
"fullcalendar": "^5.10.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-flatten": "^0.4.0",
"gulp-if": "^3.0.0",
@@ -27,14 +28,13 @@
"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.31",
"postcss": "^8.4.5",
"uglify-js": "^3.14.5"
},
"devDependencies": {
"browser-sync": "^3.0.2",
"gulp": "^5.0.0"
"browser-sync": "^2.27.11"
},
"scripts": {
"gulp": "gulp",

View File

@@ -1,11 +1,16 @@
function changeSelectedValue(obj,pk,text,update_url) { //Pass in JQuery object and new parameters
//console.log('Changing selected value');
obj.find('option').remove(); //Remove all the available options
obj[0].add(new Option(text, pk, true, true)); // Add new option
//obj.selectpicker('val', pk); //Set the new value to be selected
obj.selectpicker('refresh');
obj.append( //Add the new option
$("<option></option>")
.attr("value",pk)
.text(text)
.data('update_url',update_url)
);
obj.selectpicker('render'); //Re-render the UI
obj.selectpicker('refresh'); //Re-render the UI
obj.selectpicker('val', pk); //Set the new value to be selected
obj.change(); //Trigger the change function manually
//console.log(obj);
}
function refreshUpdateHref(obj) {

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,12 +17,14 @@ jQuery(document).ready(function () {
});
}
});
var easter_egg = new Konami(function () {
var easter_egg = new Konami();
easter_egg.code = function () {
var s = document.createElement('script');
s.type = 'text/javascript';
document.body.appendChild(s);
s.src = '/static/js/asteroids.min.js';
});
s.src = '{% static "js/asteroids.min.js"%}';
ga('send', 'event', 'easter_egg', 'activated');
}
easter_egg.load();
});
//CTRL-Enter form submission

View File

@@ -9,4 +9,3 @@ $theme-colors: (
"primary": #3A52A2
) !default;
$enable-shadows: true;
$alert-color-level: 10;

View File

@@ -77,8 +77,17 @@
border-collapse: separate !important;
border-spacing: 0;
}
#event_table tr th {
border-right: 0 !important;
}
#event_table tr td {
border-left: 0 !important;
}
#event_table tr td:not(:last-child) {
border-right: 0 !important;
}
@each $color, $value in $theme-colors {
table.table-#{$color} {
.table-#{$color} {
> td,th {
border: 0.3em solid theme-color-level($color, -6) !important;
}
@@ -87,11 +96,6 @@
background-color: #222 !important;
}
}
#event_row.table-#{$color} {
border: 0.3em solid theme-color-level($color, -6) !important;
background-color: #222 !important;
color: white !important;
}
}
del {
color: black;
@@ -152,7 +156,4 @@
.modal {
overflow-y: auto !important; //Bootstrap Dark Theme overrides this to none for some insane reason so we need to change it back
}
.text-muted {
color: #c9c9c9 !important;
}
}

View File

@@ -281,12 +281,3 @@ html.embedded {
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
padding-right: 1rem !important;
}
// New implementation of class dropped in Bootstrap 3
.dl-horizontal {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.7rem 0;
}
.dl-horizontal > dd, .dl-horizontal .markdown > p {
margin-bottom: 0 !important;
}

View File

@@ -1,92 +0,0 @@
[project]
name = "pyrigs"
version = "0.1.0"
description = "A Django-based event booking system designed for use by TEC PA and Lighting"
readme = "README.md"
requires-python = "~=3.10.0"
dependencies = [
"ansicolors",
"asgiref",
"beautifulsoup4",
"Brotli",
"cachetools",
"chardet",
"configparser",
"contextlib2",
"cssselect",
"cssutils",
"dj-database-url",
"dj-static",
"Django~=5.2",
"django-filter",
"django-ical",
"django-recurrence",
"django-registration-redux",
"django-reversion",
"django-widget-tweaks",
"django-htmlmin",
"envparse",
"gunicorn",
"icalendar",
"idna",
"Markdown",
"msgpack",
"pep517",
"Pillow",
"premailer",
"progress",
"psutil",
"psycopg2-binary",
"Pygments",
"pyparsing",
"PyPDF2",
"pytoml",
"pytz",
"reportlab",
"retrying",
"simplejson",
"soupsieve",
"sqlparse",
"static3",
"svg2rlg",
"tornado",
"urllib3",
"whitenoise",
"yolk",
"zipp",
"zope.component",
"zope.deferredimport",
"zope.deprecation",
"zope.event",
"zope.hookable",
"zope.proxy",
"zope.schema",
"sentry-sdk",
"diff-match-patch",
"python-barcode",
"django-hCaptcha",
"django-hcaptcha",
"z3c.rml",
"pikepdf",
"django-queryable-properties",
"django-mass-edit",
"selenium",
"zope.interface",
]
[dependency-groups]
dev = [
"PyPOM",
"pycodestyle",
"coveralls",
"django-coverage-plugin",
"pytest-cov",
"pytest-django",
"pluggy~=1.2.0",
"pytest-splinter",
"pytest",
"pytest-reverse",
"pytest-xdist[psutil]",
"PyPOM[splinter]",
"autopep8>=2.3.2",
]

View File

@@ -1,3 +1,2 @@
[pycodestyle]
max-line-length = 320
exclude = .venv

View File

@@ -13,10 +13,6 @@
<meta name="theme-color" content="#3853a4">
<meta name="color-scheme" content="light dark">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fontdiner+Swanky&display=swap" rel="stylesheet">
<link rel="icon" type="image/png" href="{% static 'imgs/pyrigs-avatar.png' %}">
<link rel="apple-touch-icon" href="{% static 'imgs/pyrigs-avatar.png' %}">
@@ -39,8 +35,8 @@
{% endif %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
<div class="container">
<a class="navbar-brand" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
<img src="{% static 'imgs/logo.webp' %}" class="mr-auto" style="max-height: 40px;" alt="TEC's Logo: Serif 'TEC' vertically next to a blue box with the words 'PA and Lighting', surrounded by graduated rings" id="logo">
<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%}">
<img src="{% static 'imgs/logo.webp' %}" width="40" height="40" alt="TEC's Logo: Serif 'TEC' vertically next to a blue box with the words 'PA and Lighting', surrounded by graduated rings" id="logo">
</a>
{% block titleheader %}
{% endblock %}
@@ -76,7 +72,7 @@
{% endfor %}
{% endif %}
{% endblock %}
{% if page_title and not is_ajax %}
{% if page_title and not request.is_ajax %}
<h2>{{page_title|safe}}</h2>
{% endif %}
{% block content %}{% endblock %}

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

@@ -46,7 +46,7 @@
{% include associated2|safe %}
{% endif %}
{% if not is_ajax %}
{% if not request.is_ajax %}
<div class="row py-2">
<div class="col-sm-12 text-right">
{% if can_edit %}
@@ -59,7 +59,7 @@
</div>
{% endif %}
{% endblock %}
{% if is_ajax %}
{% if request.is_ajax %}
{% block footer %}
<div class="row py-2">
<div class="col-sm-12 text-right">

View File

@@ -9,11 +9,9 @@
<h1 class="col-sm-12 pb-3">R<small class="text-muted">ig</small> I<small class="text-muted">nformation</small> G<small class="text-muted">athering</small> S<small class="text-muted">ystem</small></h1>
<h2 class="col-sm-12 pb-3">Welcome back {{ user.get_full_name }}, there {%if rig_count == 1 %}is one rig coming up{%else%}are {{ rig_count|apnumber }} rigs coming up.{%endif%}</h2>
{% if now %}
<div class="col-sm-12">
<div class="col-sm-12 alert alert-primary rounded-0 mx-auto">
{% for event in now %}
<div class="alert alert-primary rounded-0">
Event <a href="{% url 'event_detail' event.pk %}" class="text-danger">{{ event }}</a> 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/>
</div>
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 %}
@@ -56,9 +54,6 @@
<a class="list-group-item list-group-item-action" href="{% url 'trainee_list' %}"><span class="fas fa-users"></span><span class="align-middle"> Trainee List</span></a>
<a class="list-group-item list-group-item-action" href="{% url 'level_list' %}"><span class="fas fa-layer-group"></span> <span class="align-middle">Level List</span></a>
<a class="list-group-item list-group-item-action" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> <span class="align-middle">Item List</span></a>
{% if request.user.is_supervisor %}
<a class="list-group-item list-group-item-action" href="{% url 'session_log' %}"><span class="fas fa-users"></span> <span class="align-middle">Log Session</span></a>
{% endif %}
</div>
</div>
</div>

View File

@@ -17,11 +17,11 @@
{% endif %}
{% for page in page_numbers %}
{% if page == page_obj.number %}
{% ifequal page page_obj.number %}
<li class="page-item active"><a class="page-link" href="#">{{ page }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="?{% url_replace request 'page' page %}" class="page">{{ page }}</a></li>
{% endif %}
{% endifequal %}
{% endfor %}
{% if show_last %}

View File

@@ -1,10 +1,10 @@
{% extends is_ajax|yesno:"base_ajax.html,base.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %}
{% block title %}Search Help{% endblock %}
{% block content %}
<div class="row">
{% if not is_ajax %}
{% if not request.is_ajax %}
<div class="col-sm-12">
<h1>Search Help</h1>
</div>

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):

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_training.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_training.html' %}
{% load static %}
{% load widget_tweaks %}
@@ -37,7 +37,7 @@
<label for="depth" class="col-sm-2 col-form-label">Depth</label>
{% render_field form.depth|add_class:'form-control col-sm'|attr:'required' %}
</div>
{% if not is_ajax %}
{% if not request.is_ajax %}
<button type="submit" class="btn btn-primary">Save</button>
{% endif %}
</form>

Some files were not shown because too many files have changed in this diff Show More