Compare commits

..

57 Commits

Author SHA1 Message Date
ce3d1ff685 Optimise dockerfile a little lot a bit 2026-01-25 22:42:55 +00:00
bd8ead54f3 Version 1 dockerfile
Makes a VERY big image, I suspect we can optimise this a lot...
2026-01-11 23:21:13 +00:00
b9f37555c2 Port a few more tests to pytest proper
Having two distinct test flavours is giving me a headache
2025-10-05 22:38:47 +01:00
7d9185e155 Update view logic for is_ajax being changed to a template context processor 2025-10-05 20:03:50 +01:00
93c576fad8 Update for premailer changed default 2025-10-05 19:50:26 +01:00
949fbfd5fb Pin pluggy to 1.2.0
Any newer and the mystery importlib metadata error appears. Weird! >_>
2025-10-05 19:49:16 +01:00
e8355dba90 Port to Django 5.2 2025-10-05 19:40:15 +01:00
Joe Banks
fa60c4421f Merge pull request #627 from nottinghamtec/jb3/pipenv-to-uv
Migrate from pipenv to uv
2025-09-28 19:35:06 +01:00
Joe Banks
97b3663909 Add .venv to pycodestyle ignore 2025-09-28 19:28:54 +01:00
Joe Banks
50f6ff4bfd Migrate CI flows to using UV 2025-09-28 19:20:24 +01:00
Joe Banks
0945a893d5 Add uv files 2025-09-28 19:20:11 +01:00
Joe Banks
bc3611db9e Remove pipenv files 2025-09-28 19:20:03 +01:00
Joe Banks
a6c76ee24f no harm in a cheeky CSS animation 2025-09-25 23:29:56 +01:00
Joe Banks
a4ab2992a4 FrankenRIGS 2025-09-25 23:18:42 +01:00
Joe Banks
33ac604d10 Add override functionality to database url via FRANKENRIGS_DATABASE_URL 2025-09-25 23:07:55 +01:00
Joe Banks
9f25fe7bf0 Upgrade psycopg2 and switch to psycopg2-binary 2025-09-25 20:49:56 +01:00
Joe Banks
68b28a6df2 Supposedly fix the zope.interface problem 2025-09-24 20:52:01 +01:00
Joe Banks
b022a0541e Attempt to improve list stylings 2025-09-24 20:25:39 +01:00
Joe Banks
51deadc192 Merge pull request #624 from jamesatjaminit/master
fix: use earliest time instead of start time
2025-09-22 00:59:35 +01:00
James Cook
444f64ddc1 fix: use earliest time instead of start time 2025-09-21 22:40:44 +01:00
Joe Banks
3f38ce77e0 Filter Rigboard context data with new cancelled query parameter 2025-03-30 15:34:21 +01:00
Joe Banks
39a2401ec9 Add button to toggle cancelled event filtering 2025-03-30 15:34:02 +01:00
Joe Banks
032768d3b0 Add day of week to rigboard 2025-03-27 01:16:08 +00:00
Joe Banks
007571fab6 Attempt to fix some of the failing tests 2025-03-17 00:16:44 +00:00
Joe Banks
311189470d Remove erroneous whitespace 2025-03-16 22:59:21 +00:00
Joe Banks
db387c768a Potentially support cmd+P on macOS for printing events 2025-03-16 22:58:16 +00:00
Joe Banks
3c4901f86a Hide MIC when not required 2025-03-16 22:55:17 +00:00
Joe Banks
385a3bd979 Update colours for rigboard 2025-03-16 22:55:07 +00:00
Joe Banks
ded8925ef5 Add needs_mic property to events 2025-03-16 22:54:58 +00:00
Joe Banks
0bb7418f17 Merge pull request #621 from nottinghamtec/jb3/bunch-o-fixes
Bunch o' Fixes
2025-03-16 22:25:05 +00:00
Joe Banks
30a053b813 Update some locked NPM packages (npm audit) 2025-03-16 22:17:49 +00:00
Joe Banks
bd5af7c390 Update interaction tests for only MIC filter 2025-03-16 22:11:00 +00:00
Joe Banks
22bba7e8af Introduce new rigboard template 2025-03-16 22:06:06 +00:00
Joe Banks
dcf19ff773 Don't show full POs in event status 2025-03-16 22:05:54 +00:00
Joe Banks
6e1ac4e203 Create template for each row of new rigboard 2025-03-16 22:05:45 +00:00
Joe Banks
899f958aa3 Store legacy rigboard in new template file 2025-03-16 22:05:35 +00:00
Joe Banks
ee83fd2064 Add link to legacy rigboard on rigboard template 2025-03-16 22:05:27 +00:00
Joe Banks
87dd32da6c Add shortcut to event page for opening PDF with Ctrl+P
Closes #457. You can still use browser default print if you are so
inclined (however it looks trash). This is a quick way to export the
PDF.
2025-03-16 20:05:03 +00:00
Joe Banks
25593f3e6c Surface only_mic in frontend ICS URl generator 2025-03-16 20:00:24 +00:00
Joe Banks
89d50fbd49 Update ICS feed with ?only_mic filter 2025-03-16 19:59:24 +00:00
Joe Banks
7313c905ea Attach user object when using api_key_required decorator 2025-03-16 19:58:56 +00:00
Joe Banks
074d40ea03 Select current user for supervisor field
(Closes #615 and #546)
2025-03-16 19:47:58 +00:00
Joe Banks
1496a75826 Add .venv to gitignore 2025-03-16 19:33:16 +00:00
Joe Banks
1f22470bfd Add missing endif 2025-01-25 23:47:57 +00:00
Joe Banks
47babff6d4 Notes on power test export 2025-01-25 23:43:39 +00:00
Joe Banks
ad8ffa69de Fix indentation of crew list partial
VSCode auto-format and DTL do not play nicely
2024-12-13 23:29:57 +00:00
Joe Banks
99fdca6a2d Show crew list on event checklist view (#613)
* Split crew list into partial

* Use new crew list partial on event checklist
2024-12-13 16:43:12 +00:00
Joe Banks
245a92f8e6 Fix vertical centering of navbar logo (#614) 2024-12-08 21:46:20 +00:00
Joe Banks
89d7c5140e Add fields to rig info for additional access requirements (#611)
* Add fields to rig info for additional access requirements

* Add form field for additional access requirements

* Display access requirements in rig info

* Make access requirements field non-required

Oops...

* Add event access field to risk assessment

* Allow for modification and display risk assessment venue access item

* Correct tests for RAs with new parking fields

* Add note to new venue access component of RA

* Correct div boundaries for non-rig access requirements

* Fill parking and access field in sample data generator

* Set parking and access field to false in RA creation test

* Hopefully the final correction of the RA test suite
2024-11-20 22:44:11 +00:00
Joe Banks
6b4a1ce6bf Add quick link to log session on homepage for supervisors 2024-11-14 01:58:53 +00:00
Joe Banks
93762fe198 Allow sorting and filtering by Profile.date_joined in Django admin (#606) 2024-11-01 23:59:22 +00:00
Joe Banks
2559c92927 Power test export refactoring (#607) 2024-11-01 23:49:17 +00:00
3afa4deb52 Tweak finance dashboard to ignore 'non-rigs' when counting 'total events'
You can't invoice for a 'non-rig' after all.
2024-10-30 18:00:32 +00:00
Joe Banks
9f910af7eb Change ANTIALIAS to LANCZOS in asset label generator (#604) 2024-10-29 22:48:19 +00:00
Joe Banks
6c32db3998 Fix issues caused by thousands separator (#602)
* Disable thousands separation in locale settings that caused issues updating rigs

* Update invoice dashboard to use "g" suffixed floatformats for thousands separation
2024-10-27 15:09:04 +00:00
Joe Banks
c2ef469d5d Finance dashboard (#600)
* Enable commas for float thousands separation

* Add new invoice dashboard template

* Add new view controller for finance dashboard

* Add finance dashboard to dropdown

* Update finance URLs to put dashboard at index route

* Add payment methods to generated sample data

* Flip 'outstanding' and 'waiting' cards on dashboard to match order in dropdown

Also made them link to their respective lists and fixed low text contrast

---------

Co-authored-by: FreneticScribbler <aj@aronajones.com>
2024-10-27 13:11:42 +00:00
Joe Banks
e5c7e24941 Link to event details from check-in notice (#595) 2024-10-26 17:54:51 +01:00
79 changed files with 3495 additions and 3153 deletions

49
.dockerignore Normal file
View File

@@ -0,0 +1,49 @@
*.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

@@ -18,34 +18,37 @@ jobs:
- name: Install build dependencies
run: |
sudo apt-get install libcairo2-dev
- name: Set up Python
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: 'pipenv'
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install Dependencies
run: |
python3 -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
run: uv sync --locked --all-extras --dev
- name: Cache Static Files
id: static-cache
uses: actions/cache@v4
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: |
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
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
- name: Run Tests
run: pipenv run pytest -n auto --cov
run: uv run pytest -n auto --cov
- uses: actions/upload-artifact@v4
if: failure()
with:
@@ -53,4 +56,4 @@ jobs:
path: screenshots/
retention-days: 5
- name: Coveralls
run: pipenv run coveralls --service=github
run: uv run coveralls --service=github

4
.gitignore vendored
View File

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

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.10

View File

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

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# 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
View File

@@ -1,104 +0,0 @@
[[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 = "~=22.0.0"
icalendar = "~=4.0.7"
idna = "~=3.7"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=10.0.1"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.15.0"
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.32.3"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
soupsieve = "~=2.1"
sqlparse = "~=0.5.0"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.3"
urllib3 = "~=1.26.19"
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 = "*"

2309
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -79,7 +79,9 @@ 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
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').
"""
def wrap(request, *args, **kwargs):
@@ -97,6 +99,7 @@ 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,21 +26,23 @@ DEBUG = env('DEBUG', cast=bool, default=True)
STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
if STAGING:
ALLOWED_HOSTS.append('.herokuapp.com')
ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS", default="rigs.nottinghamtec.co.uk").split(",")
if DEBUG:
ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1')
ALLOWED_HOSTS.append('.app.github.dev')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
CRSF_TRUSTED_ORIGINS = ALLOWED_HOSTS.copy()
CRSF_TRUSTED_ORIGINS.append("http://localhost:8000")
CRSF_TRUSTED_ORIGINS.append("http://localhost:8001")
ALLOWED_HOSTS = ['*']
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']
@@ -95,17 +97,18 @@ WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'),
}
'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),
}
}
if not DEBUG:
import dj_database_url
DATABASES['default'] = dj_database_url.config()
# Logging
LOGGING = {
'version': 1,
@@ -224,6 +227,8 @@ 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')
@@ -252,6 +257,7 @@ TEMPLATES = [
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
"RIGS.views.is_ajax",
],
'debug': DEBUG
},
@@ -264,10 +270,3 @@ TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
SECURE_HSTS_PRELOAD = True

View File

@@ -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 PdfFileMerger, PdfFileReader
from PyPDF2 import PdfMerger, PdfReader
from z3c.rml import rml2pdf
from django.conf import settings
@@ -30,9 +30,11 @@ 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 request.headers.get('x-requested-with') == 'XMLHttpRequest'
return {"is_ajax": 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.
@@ -183,7 +185,7 @@ class SecureAPIRequest(generic.View):
class ModalURLMixin:
def get_close_url(self, update, detail):
if is_ajax(self.request):
if is_ajax(self.request).get('is_ajax'):
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]))
@@ -202,7 +204,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):
if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html"
return context
@@ -221,7 +223,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):
if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html"
return context
@@ -232,7 +234,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):
if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html"
return context
@@ -243,7 +245,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):
if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html"
return context
@@ -333,10 +335,10 @@ def get_info_string(user):
def render_pdf_response(template, context, append_terms):
merger = PdfFileMerger()
merger = PdfMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
merger.append(PdfReader(buffer))
buffer.close()
if append_terms:

View File

@@ -154,9 +154,9 @@ class AssociateAdmin(VersionAdmin):
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login')
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login', 'date_joined')
list_display_links = ['username']
list_filter = UserAdmin.list_filter + ('is_approved',)
list_filter = UserAdmin.list_filter + ('is_approved', 'date_joined')
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {

View File

@@ -22,6 +22,7 @@ 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()
@@ -38,6 +39,8 @@ 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)
@@ -125,7 +128,7 @@ class EventForm(forms.ModelForm):
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
'purchase_order', 'collector', 'forum_url']
'purchase_order', 'collector', 'forum_url', 'parking_and_access']
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())
date=datetime.date.today(), method=random.choice(models.Payment.METHODS)[0])
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,6 +276,7 @@ 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,6 +13,7 @@ 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):
@@ -33,6 +34,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).transform()
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css, allow_loading_external_files=True).transform()
msg.attach_alternative(html, 'text/html')
msg.send()

View File

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,19 @@
# 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

@@ -351,6 +351,9 @@ 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)
@@ -376,6 +379,10 @@ 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
@@ -505,7 +512,7 @@ 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.start_date, datetime.time(00, 00))
earliest = datetime.datetime.combine(self.earliest_time, 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()
@@ -779,6 +786,9 @@ 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)
@@ -803,6 +813,7 @@ 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()

View File

@@ -5,7 +5,7 @@ import urllib.request
from io import BytesIO
import datetime
from PyPDF2 import PdfFileReader, PdfFileMerger
from PyPDF2 import PdfReader, PdfMerger
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.cache import cache
@@ -31,12 +31,12 @@ def send_eventauthorisation_success_email(instance):
}
template = get_template('event_print.xml')
merger = PdfFileMerger()
merger = PdfMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
merger.append(PdfReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
@@ -66,7 +66,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).transform()
external_styles=css, allow_loading_external_files=True).transform()
client_email.attach_alternative(html, 'text/html')
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
@@ -124,7 +124,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).transform()
external_styles=css, allow_loading_external_files=True).transform()
email.attach_alternative(html, 'text/html')
email.send()

View File

@@ -87,6 +87,8 @@
<listStyle name="ul"
start="bulletchar"
leftIndent="0"
bulletDedent="10"
bulletFontSize="10"/>
</stylesheet>

View File

@@ -6,7 +6,36 @@
{% load total_invoices_todo from filters %}
{% block titleheader %}
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
<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>
{% endblock %}
{% block titleelements %}
@@ -45,6 +74,7 @@
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

@@ -1,5 +1,5 @@
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
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}}.
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}}.
The TEC Rig Information Gathering System

View File

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

View File

@@ -281,8 +281,15 @@
{{ 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">

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends 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 request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends 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 request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends 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,8 +69,11 @@
</div>
</div>
</div>
{% include 'partials/crew_list.html' with event=object.event %}
</div>
<div class="col-12 text-right">
<div class="col-12 text-right mt-4">
{% 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 request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends 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 request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends 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 request.is_ajax and self.request.user.pk is form.event.mic.pk %}
{% if not 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 request.is_ajax %}
{% if not is_ajax %}
<div class="row mt-3">
<div class="col-sm-12 text-right">
{% button 'submit' %}

View File

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

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load yesnoi from filters %}

View File

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

View File

@@ -2,8 +2,13 @@
{% load filters %}
{% block extrastyles %}
<paraStyle name="style.powerReviewed" alignment="center" backColor="green" textColor="white"/>
<paraStyle name="style.powerUnreviewed" alignment="center" backColor="red" textColor="white"/>
<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"/>
@@ -12,6 +17,10 @@
<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 %}
@@ -34,6 +43,16 @@
<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>
@@ -51,38 +70,69 @@
</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">Power Test Results</h2>
<h2 fontSize="16">Event Power Checklist</h2>
<spacer length="15"/>
<para><strong>Source RCD protected?</strong> {{ object.source_rcd|yesno|capfirst }}</para>
<para><sub>(If cable is more than 3 metres long)</sub></para>
<spacer length="5"/>
<para><strong>Appropriate and clear labelling on distribution and cabling?</strong> {{ object.labelling|yesno|capfirst }}</para>
<spacer length="5"/>
<para><strong>Equipment appropriately earthed?</strong> {{ object.source_rcd|yesno|capfirst }}</para>
<para><sub>(truss, stage, generators etc.)</sub></para>
<spacer length="5"/>
<para><strong>All equipment in PAT period?</strong> {{ object.pat|yesno|capfirst }}</para>
<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">Tests at first distro</h2>
<h2 fontSize="14">Power tests (First Distro)</h2>
<spacer length="5"/>
<blockTable colWidths="100,410">
<blockTable colWidths="100,410" style="voltageTable">
<tr>
<td><para><strong>Voltage<br/><sub>(cube meter) / V</sub></strong></para></td>
<td><para><strong>Voltage</strong></para><para style="style.smallText">(cube meter) / V</para></td>
<td>
<blockTable colWidths="100,100,100" style="powerTable">
<tr>
@@ -100,11 +150,11 @@
</tr>
</blockTable>
<spacer length="15"/>
<spacer length="10"/>
<blockTable colWidths="100,100,190,120">
<blockTable colWidths="100,100,190,120" style="voltageTable">
<tr>
<td><para><strong>Phase Rotation<br/><sub>(if required)</sub></strong></para></td>
<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>
@@ -113,11 +163,11 @@
<spacer length="15"/>
<para><strong>Prospective Short Circuit Current / A</strong> {{ object.fd_pssc }}</para>
<para><strong>Prospective Short Circuit Current (PSCC)</strong> {{ object.fd_pssc }} A</para>
<spacer length="15"/>
<h2 fontSize="14">Tests 'Worst Case' points (at least 1 required)</h2>
<h2 fontSize="14">Power Tests (Worst Case Points)</h2>
<spacer length="15"/>
<blockTable colWidths="100,100,190,120" style="powerTable">
@@ -153,18 +203,33 @@
{% endif %}
</blockTable>
<spacer length="15"/>
<h2 fontSize="14">Generic Tests</h2>
{% 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>All circuit RCDs tested?</strong><br/>(using test button)</para></td>
<td><para>{{ object.all_rcds_tested|yesno|capfirst }}</para></td>
<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>Public/performer accessible circuits tested?</strong><br/>(using socket tester)</para></td>
<td><para>{{ object.public_sockets_tested|yesno|capfirst }}</para></td>
<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,6 +124,13 @@
<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 request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load filters %}
{% block content %}
@@ -151,8 +151,19 @@
</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 request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}
@@ -162,6 +162,17 @@
</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

@@ -0,0 +1,106 @@
{% 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

@@ -23,7 +23,7 @@
</div>
{% endif %}
{% if object.organisation %}
<div class="card card-default">
<div class="card card-default mb-3">
<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

@@ -0,0 +1,50 @@
{% 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

@@ -11,6 +11,7 @@
{{ 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>

View File

@@ -0,0 +1,147 @@
{% 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

@@ -3,7 +3,7 @@
{% if event.is_rig %}
{% if event.sum_total > 0 %}
{% if event.purchase_order %}
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
<span class="badge badge-success">PO: Received</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,5 +44,8 @@
<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,195 +1,70 @@
{% 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;
.light-link {
color: #ebf5ff !important;
}
.eventgrid {
grid-column: 1/1 !important;
padding: 0.5em;
.dark-link {
color: #4495ff !important;
}
.grid-header {
display: none;
.link-on-green {
color: #ffffff !important;
}
#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
<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 %}
table-info
{% with border_class="border-primary" header_bg="bg-primary" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif not event.mic %}
table-danger
{% 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 %}
table-success
{% with border_class="border-success" header_bg="bg-success" header_text="link-on-green" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% else %}
table-warning
{% with border_class="border-warning" header_bg="bg-warning" header_text="dark-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% 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>
{% 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>
<!---Details-->
<div id="event_details" class="w-100">
<h4>
<a href="{% url 'event_detail' event.pk %}">
<span class="c-inline c-lg-none">{{ event }}</span><span class="c-none c-lg-inline">{{ event.name }}</span>
</a>
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
<br class="c-none c-lg-inline">
{% if event.venue %}
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
</div>
{% include 'partials/event_status.html' %}
<!---MIC-->
<div id="event_mic" class="text-nowrap">
<span class="c-md-none align-middle">MIC:</span>
{% if event.mic %}
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
{{ event.mic }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
{% elif event.is_rig %}
<span class="fas fa-exclamation"></span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}

View File

@@ -0,0 +1,195 @@
{% 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

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

View File

@@ -4,15 +4,44 @@
{% 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>
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>
{% 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 %}
<div style="container-type: inline-size;">
{% include 'partials/event_table.html' %}
{% 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)
suspended_structures=False, outside=False, parking_and_access=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.ID, 'event_row')
_event_row_locator = (By.CLASS_NAME, "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.ID, "event_dates")
_event_dates_locator = (By.CLASS_NAME, "event-dates")
_event_details_locator = (By.ID, "event_details")
_event_mic_locator = (By.ID, "event_mic")
@@ -207,6 +207,7 @@ 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('%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)
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)
def test_add_button(self):
self.page.add()
@@ -530,10 +530,11 @@ 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",
"rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false&only_mic=true",
self.page.cal_url)
# Awesome - all seems to work
@@ -759,6 +760,7 @@ 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

@@ -115,7 +115,8 @@ urlpatterns = [
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
# Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
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()),
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 Q
from django.db.models import Sum
from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
@@ -18,8 +18,76 @@ from RIGS import models
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
TIME_FILTERS = ["all", "year", "month", "week"]
class InvoiceIndex(generic.ListView):
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):
model = models.Invoice
template_name = 'invoice_list.html'

View File

@@ -24,6 +24,7 @@ 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 = {}
@@ -35,6 +36,9 @@ 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
@@ -73,6 +77,9 @@ 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

@@ -41,8 +41,13 @@ 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'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['events'] = objects.select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['page_title'] = "Rigboard"
return context
@@ -355,7 +360,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).transform()
external_styles=css, allow_loading_external_files=True).transform()
msg.attach_alternative(html, 'text/html')
msg.send()
@@ -371,7 +376,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).transform()
response.content = premailer.Premailer(response.rendered_content, external_styles=css, allow_loading_external_files=True).transform()
return response
def get_context_data(self, **kwargs):

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% extends is_ajax|yesno:'base_ajax.html,base_assets.html' %}
{% load widget_tweaks %}
{% block content %}
@@ -79,7 +79,7 @@
</div>
</div>
</div>
{% if not request.is_ajax %}
{% if not 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

@@ -1,4 +1,4 @@
{% extends 'base_assets.html' %}
{% extends is_ajax|yesno:"base_ajax.html,base_assets.html" %}
{% load widget_tweaks %}
{% load static %}

View File

@@ -52,3 +52,10 @@ 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

@@ -7,13 +7,12 @@ 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, screenshot_failure_cls, assert_times_almost_equal
from PyRIGS.tests.base import AutoLoginTest, 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()
@@ -144,7 +143,6 @@ 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()
@@ -211,7 +209,6 @@ class TestAssetForm(AutoLoginTest):
self.assertEqual(asset.date_acquired, acquired)
@screenshot_failure_cls
class TestSupplierList(AutoLoginTest):
def setUp(self):
super().setUp()
@@ -244,39 +241,35 @@ class TestSupplierList(AutoLoginTest):
self.page.set_query("")
self.page.search()
time.sleep(1)
self.assertTrue(len(self.page.suppliers) == 7)
self.page.set_query("This is not a supplier")
self.page.set_query("NOTFOUND")
self.page.search()
self.assertTrue(len(self.page.suppliers) == 0)
@screenshot_failure_cls
class TestSupplierCreateAndEdit(AutoLoginTest):
def setUp(self):
super().setUp()
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
def test_supplier_create(logged_in_browser, live_server):
page = pages.SupplierCreate(logged_in_browser.driver, live_server.url).open()
def test_supplier_create(self):
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
page.remove_all_required()
page.submit()
assert !self.page.success
assert "This field is required." in self.page.errors["Name"]
self.page.remove_all_required()
self.page.submit()
self.assertFalse(self.page.success)
self.assertIn("This field is required.", self.page.errors["Name"])
page.name = "Optican Health Supplies"
page.submit()
assert page.success
self.page.name = "Optican Health Supplies"
self.page.submit()
self.assertTrue(self.page.success)
def test_supplier_edit(self):
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
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()
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)
assert test_supplier.name == page.name
new_name = "Cyberdyne Systems"
page.name = new_name
page.submit()
assert page.success
def test_audit_search(logged_in_browser, live_server, test_asset):
@@ -311,47 +304,30 @@ def test_audit_success(logged_in_browser, admin_user, live_server, test_asset):
assert test_asset.asset_id not in page.assets
@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(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"]
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(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
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

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 PdfFileMerger, PdfFileReader
from PyPDF2 import PdfMerger, PdfReader
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):
if is_ajax(self.request).get('is_ajax'):
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):
if is_ajax(self.request).get('is_ajax'):
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):
if is_ajax(self.request).get('is_ajax'):
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):
if is_ajax(self.request).get('is_ajax'):
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):
if is_ajax(self.request).get('is_ajax'):
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.ANTIALIAS), box=(5, 5))
image.paste(logo.resize(logo_size, Image.LANCZOS), 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 = PdfFileMerger()
merger = PdfMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
merger.append(PdfReader(buffer))
buffer.close()
merged = BytesIO()

52
compose.yml Normal file
View File

@@ -0,0 +1,52 @@
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:

650
package-lock.json generated

File diff suppressed because it is too large Load Diff

92
pyproject.toml Normal file
View File

@@ -0,0 +1,92 @@
[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,2 +1,3 @@
[pycodestyle]
max-line-length = 320
exclude = .venv

View File

@@ -13,6 +13,10 @@
<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' %}">
@@ -36,7 +40,7 @@
<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; position: absolute; left: 0.5em; top: 0;" alt="TEC's Logo: Serif 'TEC' vertically next to a blue box with the words 'PA and Lighting', surrounded by graduated rings" id="logo">
<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>
{% block titleheader %}
{% endblock %}
@@ -72,7 +76,7 @@
{% endfor %}
{% endif %}
{% endblock %}
{% if page_title and not request.is_ajax %}
{% if page_title and not is_ajax %}
<h2>{{page_title|safe}}</h2>
{% endif %}
{% block content %}{% endblock %}

View File

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

View File

@@ -12,7 +12,7 @@
<div class="col-sm-12">
{% for event in now %}
<div class="alert alert-primary rounded-0">
Event {{ event }} is happening today! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/>
Event <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>
{% endfor %}
</div>
@@ -56,6 +56,9 @@
<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 %}
{% ifequal page page_obj.number %}
{% if 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>
{% endifequal %}
{% endif %}
{% endfor %}
{% if show_last %}

View File

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

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_training.html' %}
{% extends 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 request.is_ajax %}
{% if not is_ajax %}
<button type="submit" class="btn btn-primary">Save</button>
{% endif %}
</form>

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_training.html' %}
{% extends is_ajax|yesno:'base_ajax.html,base_training.html' %}
{% load static %}
{% load widget_tweaks %}
@@ -51,7 +51,7 @@
{% render_field form.notes|add_class:'form-control' rows=3 %}
</div>
</div>
{% if not request.is_ajax %}
{% if not is_ajax %}
<div class="col-sm-12 text-right pr-0">
{% button 'submit' %}
</div>

View File

@@ -1,6 +1,9 @@
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required data-noclear="true">
{% if supervisor %}
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true"
data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required
data-noclear="true">
{% if supervisor %}
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
{% endif %}
{% endif %}
<option value="{{request.user.pk}}" selected>{{ request.user }}</option>
</select>

View File

@@ -17,8 +17,8 @@ def select_super(page, supervisor):
assert page.supervisor_selector.is_open
page.supervisor_selector.search(supervisor.name[:-6])
time.sleep(2) # Slow down for javascript
page.supervisor_selector.set_option(supervisor.name, True)
assert page.supervisor_selector.options[0].selected
page.supervisor_selector.toggle()
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
@@ -40,6 +40,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
page.item_selector.toggle()
select_super(page, supervisor)
page.supervisor_selector.toggle()
page.submit()
assert page.success

View File

@@ -126,7 +126,7 @@ class AddQualification(generic.CreateView, ModalURLMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["depths"] = models.TrainingItemQualification.CHOICES
if is_ajax(self.request):
if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_training.html'

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load static %}
{% load linkornone from filters %}
@@ -41,7 +41,7 @@
{% endblock %}
{% block content %}
{% if not request.is_ajax and object.pk == user.pk %}
{% if not is_ajax and object.pk == user.pk %}
<div class="row py-3">
<div class="col text-right">
<div class="btn-group">
@@ -85,7 +85,7 @@
</div>
</div>
</div>
{% if not request.is_ajax and object.pk == user.pk %}
{% if not is_ajax and object.pk == user.pk %}
<div class="col-lg-8 col-12">
<div class="card">
<div class="card-header">Personal iCal Details</div>
@@ -126,6 +126,9 @@
<label class="checkbox-inline ml-lg-2">
<input type="checkbox" value="confirmed" data-default="true" checked> Confirmed/Booked
</label>
<label class="checkbox-inline ml-lg-2">
<input type="checkbox" value="only_mic" data-default="false" > Only MIC
</label>
</div>
</form>
</dd>

View File

@@ -1,10 +1,10 @@
from django.contrib import messages
from django.contrib.auth.views import LoginView
from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from RIGS import models
from django.conf import settings
# This view should be exempt from requiring CSRF token.
@@ -28,7 +28,7 @@ class LoginEmbed(LoginView):
class ProfileDetail(generic.DetailView):
template_name = "profile_detail.html"
model = models.Profile
model = get_user_model()
def get_queryset(self):
try:
@@ -48,7 +48,7 @@ class ProfileDetail(generic.DetailView):
class ProfileUpdateSelf(generic.UpdateView):
template_name = "profile_form.html"
model = models.Profile
model = get_user_model()
fields = ['first_name', 'last_name', 'email', 'initials', 'phone', 'dark_theme']
def get_queryset(self):

1676
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax_nomodal.html,base_rigs.html" %}
{% extends is_ajax|yesno:"base_ajax_nomodal.html,base_rigs.html" %}
{% load static %}
{% load humanize %}