Compare commits

...

22 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
52 changed files with 2188 additions and 2603 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 - name: Install build dependencies
run: | run: |
sudo apt-get install libcairo2-dev sudo apt-get install libcairo2-dev
- name: Set up Python
- name: "Set up Python"
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version-file: ".python-version"
cache: 'pipenv'
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install Dependencies - name: Install Dependencies
run: | run: uv sync --locked --all-extras --dev
python3 -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
- name: Cache Static Files - name: Cache Static Files
id: static-cache id: static-cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: 'pipeline/built_assets' path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }} key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
if: steps.static-cache.outputs.cache-hit != 'true' if: steps.static-cache.outputs.cache-hit != 'true'
- run: node node_modules/gulp/bin/gulp build - run: node node_modules/gulp/bin/gulp build
if: steps.static-cache.outputs.cache-hit != 'true' if: steps.static-cache.outputs.cache-hit != 'true'
- name: Basic Checks - name: Basic Checks
run: | run: |
pipenv run pycodestyle . --exclude=migrations,node_modules uv run pycodestyle . --exclude=.venv,migrations,node_modules
pipenv run python3 manage.py check uv run python3 manage.py check
pipenv run python3 manage.py makemigrations --check --dry-run uv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput uv run python3 manage.py collectstatic --noinput
- name: Run Tests - name: Run Tests
run: pipenv run pytest -n auto --cov run: uv run pytest -n auto --cov
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
@@ -53,4 +56,4 @@ jobs:
path: screenshots/ path: screenshots/
retention-days: 5 retention-days: 5
- name: Coveralls - name: Coveralls
run: pipenv run coveralls --service=github run: uv run coveralls --service=github

1
.gitignore vendored
View File

@@ -104,3 +104,4 @@ screenshots/
# Virutal Environments # Virutal Environments
.venv/ .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

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

View File

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

View File

@@ -9,7 +9,7 @@ from functools import reduce
from itertools import chain from itertools import chain
from io import BytesIO from io import BytesIO
from PyPDF2 import PdfFileMerger, PdfFileReader from PyPDF2 import PdfMerger, PdfReader
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from django.conf import settings from django.conf import settings
@@ -30,9 +30,11 @@ from RIGS import models
from assets import models as asset_models from assets import models as asset_models
from training import models as training_models from training import models as training_models
# Template context processor
def is_ajax(request): 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. 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: class ModalURLMixin:
def get_close_url(self, update, detail): def get_close_url(self, update, detail):
if is_ajax(self.request): if is_ajax(self.request).get('is_ajax'):
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk})) update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
@@ -202,7 +204,7 @@ class GenericListView(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s" 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" context['override'] = "base_ajax.html"
return context return context
@@ -221,7 +223,7 @@ class GenericDetailView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = f"{self.model.__name__} | {self.object.name}" 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" context['override'] = "base_ajax.html"
return context return context
@@ -232,7 +234,7 @@ class GenericUpdateView(generic.UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = f"Edit {self.model.__name__}" 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" context['override'] = "base_ajax.html"
return context return context
@@ -243,7 +245,7 @@ class GenericCreateView(generic.CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = f"Create {self.model.__name__}" 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" context['override'] = "base_ajax.html"
return context return context
@@ -333,10 +335,10 @@ def get_info_string(user):
def render_pdf_response(template, context, append_terms): def render_pdf_response(template, context, append_terms):
merger = PdfFileMerger() merger = PdfMerger()
rml = template.render(context) rml = template.render(context)
buffer = rml2pdf.parseString(rml) buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer)) merger.append(PdfReader(buffer))
buffer.close() buffer.close()
if append_terms: if append_terms:

View File

@@ -39,6 +39,8 @@ class EventForm(forms.ModelForm):
@property @property
def _get_items_json(self): def _get_items_json(self):
items = {} items = {}
if self.instance.pk is None:
return items
for item in self.instance.items.all(): for item in self.instance.items.all():
data = serializers.serialize('json', [item]) data = serializers.serialize('json', [item])
struct = simplejson.loads(data) struct = simplejson.loads(data)

View File

@@ -13,6 +13,7 @@ from RIGS import models
class Command(BaseCommand): 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.' help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
def handle(self, *args, **options): def handle(self, *args, **options):
@@ -33,6 +34,6 @@ class Command(BaseCommand):
reply_to=[f"h.s.manager@{settings.DOMAIN}"], reply_to=[f"h.s.manager@{settings.DOMAIN}"],
) )
css = finders.find('css/email.css') 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.attach_alternative(html, 'text/html')
msg.send() msg.send()

View File

@@ -512,7 +512,7 @@ class Event(models.Model, RevisionMixin):
def can_check_in(self): def can_check_in(self):
earliest = self.earliest_time earliest = self.earliest_time
if isinstance(self.earliest_time, datetime.date): 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) tz = pytz.timezone(settings.TIME_ZONE)
earliest = tz.localize(earliest) earliest = tz.localize(earliest)
return not self.dry_hire and not self.status == Event.CANCELLED and earliest <= timezone.now() return not self.dry_hire and not self.status == Event.CANCELLED and earliest <= timezone.now()

View File

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

View File

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

View File

@@ -6,7 +6,36 @@
{% load total_invoices_todo from filters %} {% load total_invoices_todo from filters %}
{% block titleheader %} {% 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 %} {% endblock %}
{% block titleelements %} {% block titleelements %}

View File

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

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 markdown_tags %} {% load markdown_tags %}
{% load static %} {% load static %}
@@ -18,7 +18,7 @@
{% block content %} {% block content %}
<div class="row my-3 py-3"> <div class="row my-3 py-3">
{% if not request.is_ajax %} {% if not is_ajax %}
{% if perms.RIGS.view_event %} {% if perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %} {% include 'partials/event_detail_buttons.html' %}
@@ -49,7 +49,7 @@
</div> </div>
{% endif %} {% endif %}
{% 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"> <div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %} {% include 'partials/event_detail_buttons.html' %}
</div> </div>
@@ -72,13 +72,13 @@
{% include 'partials/crew_list.html' %} {% include 'partials/crew_list.html' %}
{% 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"> <div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %} {% include 'partials/event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% 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"> <div class="col-sm-12 text-right">
{% include 'partials/last_edited.html' with target="event_history" %} {% include 'partials/last_edited.html' with target="event_history" %}
</div> </div>
@@ -86,7 +86,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% if request.is_ajax %} {% if is_ajax %}
{% block footer %} {% block footer %}
{% if perms.RIGS.view_event %} {% if perms.RIGS.view_event %}
{% include 'partials/last_edited.html' with target="event_history" %} {% include 'partials/last_edited.html' with target="event_history" %}

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 widget_tweaks %}
{% load static %} {% load static %}
{% load button from filters %} {% 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 %} {% load widget_tweaks %}
{% block title %}TEC Email Address Required{% endblock %} {% 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 help_text from filters %}
{% load profile_by_index from filters %} {% load profile_by_index from filters %}
{% load yesnoi 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 widget_tweaks %}
{% load static %} {% load static %}
{% load help_text from filters %} {% 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 widget_tweaks %}
{% load static %} {% load static %}
{% load button from filters %} {% load button from filters %}
@@ -28,7 +28,7 @@
<form id="checkin" role="form" method="POST" action="{{ form.action|default:request.path }}"> <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 }}" <input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/> value="{{event.pk}}"/>
{% if not request.is_ajax and self.request.user.pk is form.event.mic.pk %} {% if not is_ajax and self.request.user.pk is form.event.mic.pk %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.person.id_for_label }}" <label for="{{ form.person.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.person.label }}</label> class="col-sm-4 col-form-label">{{ form.person.label }}</label>
@@ -86,7 +86,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if not request.is_ajax %} {% if not is_ajax %}
<div class="row mt-3"> <div class="row mt-3">
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% button 'submit' %} {% button 'submit' %}

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 help_text from filters %}
{% load profile_by_index from filters %} {% load profile_by_index from filters %}
{% load yesnoi 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 widget_tweaks %}
{% load static %} {% load static %}
{% load help_text from filters %} {% 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 filters %} {% load filters %}
{% block content %} {% block content %}

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 widget_tweaks %}
{% load static %} {% load static %}
{% load help_text from filters %} {% load help_text from filters %}

View File

@@ -1,7 +1,7 @@
<div class="row"> <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> 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 %} {% for radio in formitem %}
<div class="custom-control custom-radio"> <div class="custom-control custom-radio">
{{ radio.tag }} {{ radio.tag }}

View File

@@ -16,9 +16,16 @@
</div> </div>
{% endif %} {% endif %}
{% if not request.GET.legacy %} {% if not request.GET.legacy %}
<a href="?legacy=true" class="btn btn-secondary">View legacy rigboard</a>
{% if not request.GET.hide_cancelled %}
<a href="?hide_cancelled=true" class="btn btn-primary mr-3">Hide cancelled</a>
{% else %} {% else %}
<a href="." class="btn btn-secondary">Go to new rigboard</a> <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 %} {% endif %}
</div> </div>

View File

@@ -41,8 +41,13 @@ class RigboardIndex(generic.TemplateView):
# get super context # get super context
context = super().get_context_data(**kwargs) 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 # 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" context['page_title'] = "Rigboard"
return context return context
@@ -355,7 +360,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
) )
css = finders.find('css/email.css') css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context), 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.attach_alternative(html, 'text/html')
msg.send() msg.send()
@@ -371,7 +376,7 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
css = finders.find('css/email.css') css = finders.find('css/email.css')
response = super().render_to_response(context, **response_kwargs) response = super().render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse) 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 return response
def get_context_data(self, **kwargs): 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 %} {% load widget_tweaks %}
{% block content %} {% block content %}
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
</div> </div>
{% if not request.is_ajax %} {% if not is_ajax %}
<div class="form-group form-row pull-right"> <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> <button class="btn btn-success" type="submit" form="asset_audit_form" id="id_mark_audited">Mark Audited</button>
</div> </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 widget_tweaks %}
{% load static %} {% 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) 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 yield asset
asset.delete() 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 import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait 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 PyRIGS.tests.pages import animation_is_finished
from assets import models from assets import models
from . import pages from . import pages
@screenshot_failure_cls
class TestAssetList(AutoLoginTest): class TestAssetList(AutoLoginTest):
def setUp(self): def setUp(self):
super().setUp() 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 assert models.Asset.objects.last().description == test_asset.description
@screenshot_failure_cls
class TestAssetForm(AutoLoginTest): class TestAssetForm(AutoLoginTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@@ -211,7 +209,6 @@ class TestAssetForm(AutoLoginTest):
self.assertEqual(asset.date_acquired, acquired) self.assertEqual(asset.date_acquired, acquired)
@screenshot_failure_cls
class TestSupplierList(AutoLoginTest): class TestSupplierList(AutoLoginTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@@ -247,37 +244,32 @@ class TestSupplierList(AutoLoginTest):
time.sleep(1) time.sleep(1)
self.assertTrue(len(self.page.suppliers) == 7) 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.page.search()
self.assertTrue(len(self.page.suppliers) == 0) self.assertTrue(len(self.page.suppliers) == 0)
@screenshot_failure_cls def test_supplier_create(logged_in_browser, live_server):
class TestSupplierCreateAndEdit(AutoLoginTest): page = pages.SupplierCreate(logged_in_browser.driver, live_server.url).open()
def setUp(self):
super().setUp()
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
def test_supplier_create(self): page.remove_all_required()
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open() page.submit()
assert !self.page.success
assert "This field is required." in self.page.errors["Name"]
self.page.remove_all_required() page.name = "Optican Health Supplies"
self.page.submit() page.submit()
self.assertFalse(self.page.success) assert page.success
self.assertIn("This field is required.", self.page.errors["Name"])
self.page.name = "Optican Health Supplies"
self.page.submit()
self.assertTrue(self.page.success)
def test_supplier_edit(self): def test_supplier_edit(logged_in_browser, live_server, test_supplier):
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open() page = pages.SupplierEdit(logged_in_browser.driver, live_server.url, supplier_id=test_supplier.pk).open()
self.assertEqual("Fullmetal Heavy Industry", self.page.name) assert test_supplier.name == page.name
new_name = "Cyberdyne Systems" new_name = "Cyberdyne Systems"
self.page.name = new_name page.name = new_name
self.page.submit() page.submit()
self.assertTrue(self.page.success) assert page.success
def test_audit_search(logged_in_browser, live_server, test_asset): def test_audit_search(logged_in_browser, live_server, test_asset):
@@ -312,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 assert test_asset.asset_id not in page.assets
@screenshot_failure_cls def test_audit_fail(logged_in_browser, admin_user, live_server, test_asset):
class TestAssetAudit(AutoLoginTest): page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
def setUp(self): wait = WebDriverWait(logged_in_browser.driver, 20)
super().setUp() page.set_query(test_asset.asset_id)
self.category = models.AssetCategory.objects.create(name="Haulage") page.search()
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True) wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
self.supplier = models.Supplier.objects.create(name="The Bazaar") # Do it wrong on purpose to check error display
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1, page.modal.remove_all_required()
voltage_rating=40, num_pins=13) page.modal.description = ""
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status, page.modal.submit()
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10) wait.until(animation_is_finished())
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status, assert "This field is required." in self.page.modal.errors["Description"]
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
category=self.category,
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
self.wait = WebDriverWait(self.driver, 20)
def test_audit_fail(self):
self.page.set_query(self.asset.asset_id)
self.page.search()
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
# Do it wrong on purpose to check error display
self.page.modal.remove_all_required()
self.page.modal.description = ""
self.page.modal.submit()
self.wait.until(animation_is_finished())
self.driver.implicitly_wait(4)
self.assertIn("This field is required.", self.page.modal.errors["Description"])
def test_audit_list(self): def test_audit_list(logged_in_browser, admin_user, live_server, test_asset):
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets)) page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
asset_row = self.page.assets[0] wait = WebDriverWait(logged_in_browser.driver, 20)
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click() assert models.Asset.objects.filter(last_audited_at=None).count() == len(self.page.assets)
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal'))) asset_row = page.assets[0]
self.assertEqual(self.page.modal.asset_id, asset_row.id) logged_in_browser.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
self.page.modal.close() wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
self.assertFalse(self.driver.find_element(By.ID, 'modal').is_displayed()) assert self.page.modal.asset_id == asset_row.id
# Make sure audit log was NOT filled out page.modal.close()
audited = models.Asset.objects.get(asset_id=asset_row.id) assert !logged_in_browser.driver.find_element(By.ID, 'modal').is_displayed()
assert audited.last_audited_by is None # 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.shortcuts import get_object_or_404
from django.template.loader import get_template 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 PIL import Image, ImageDraw, ImageFont, ImageOps
from barcode import Code39 from barcode import Code39
from barcode.writer import ImageWriter from barcode.writer import ImageWriter
@@ -127,7 +127,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
return context return context
def get_success_url(self): def get_success_url(self):
if is_ajax(self.request): if is_ajax(self.request).get('is_ajax'):
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk})) update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
@@ -233,7 +233,7 @@ class SupplierList(GenericListView):
context['edit'] = 'supplier_update' context['edit'] = 'supplier_update'
context['can_edit'] = self.request.user.has_perm('assets.change_supplier') context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
context['detail'] = 'supplier_detail' context['detail'] = 'supplier_detail'
if is_ajax(self.request): if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
else: else:
context['override'] = 'base_assets.html' context['override'] = 'base_assets.html'
@@ -250,7 +250,7 @@ class SupplierDetail(GenericDetailView):
context['detail_link'] = 'supplier_detail' context['detail_link'] = 'supplier_detail'
context['associated'] = 'partials/associated_assets.html' context['associated'] = 'partials/associated_assets.html'
context['associated2'] = '' context['associated2'] = ''
if is_ajax(self.request): if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
else: else:
context['override'] = 'base_assets.html' context['override'] = 'base_assets.html'
@@ -264,7 +264,7 @@ class SupplierCreate(GenericCreateView, ModalURLMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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" context['override'] = "base_ajax.html"
else: else:
context['override'] = 'base_assets.html' context['override'] = 'base_assets.html'
@@ -280,7 +280,7 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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" context['override'] = "base_ajax.html"
else: else:
context['override'] = 'base_assets.html' context['override'] = 'base_assets.html'
@@ -417,11 +417,11 @@ class GenerateLabels(generic.View):
# 'images3': images[3::4], # 'images3': images[3::4],
'filename': name 'filename': name
} }
merger = PdfFileMerger() merger = PdfMerger()
rml = template.render(context) rml = template.render(context)
buffer = rml2pdf.parseString(rml) buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer)) merger.append(PdfReader(buffer))
buffer.close() buffer.close()
merged = BytesIO() 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:

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] [pycodestyle]
max-line-length = 320 max-line-length = 320
exclude = .venv

View File

@@ -13,6 +13,10 @@
<meta name="theme-color" content="#3853a4"> <meta name="theme-color" content="#3853a4">
<meta name="color-scheme" content="light dark"> <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="icon" type="image/png" href="{% static 'imgs/pyrigs-avatar.png' %}">
<link rel="apple-touch-icon" href="{% static 'imgs/pyrigs-avatar.png' %}"> <link rel="apple-touch-icon" href="{% static 'imgs/pyrigs-avatar.png' %}">
@@ -72,7 +76,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% if page_title and not request.is_ajax %} {% if page_title and not is_ajax %}
<h2>{{page_title|safe}}</h2> <h2>{{page_title|safe}}</h2>
{% endif %} {% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

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

View File

@@ -17,11 +17,11 @@
{% endif %} {% endif %}
{% for page in page_numbers %} {% 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> <li class="page-item active"><a class="page-link" href="#">{{ page }}</a></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="?{% url_replace request 'page' page %}" class="page">{{ page }}</a></li> <li class="page-item"><a class="page-link" href="?{% url_replace request 'page' page %}" class="page">{{ page }}</a></li>
{% endifequal %} {% endif %}
{% endfor %} {% endfor %}
{% if show_last %} {% 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 title %}Search Help{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
{% if not request.is_ajax %} {% if not is_ajax %}
<div class="col-sm-12"> <div class="col-sm-12">
<h1>Search Help</h1> <h1>Search Help</h1>
</div> </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 static %}
{% load widget_tweaks %} {% load widget_tweaks %}
@@ -37,7 +37,7 @@
<label for="depth" class="col-sm-2 col-form-label">Depth</label> <label for="depth" class="col-sm-2 col-form-label">Depth</label>
{% render_field form.depth|add_class:'form-control col-sm'|attr:'required' %} {% render_field form.depth|add_class:'form-control col-sm'|attr:'required' %}
</div> </div>
{% if not request.is_ajax %} {% if not is_ajax %}
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
{% endif %} {% endif %}
</form> </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 static %}
{% load widget_tweaks %} {% load widget_tweaks %}
@@ -51,7 +51,7 @@
{% render_field form.notes|add_class:'form-control' rows=3 %} {% render_field form.notes|add_class:'form-control' rows=3 %}
</div> </div>
</div> </div>
{% if not request.is_ajax %} {% if not is_ajax %}
<div class="col-sm-12 text-right pr-0"> <div class="col-sm-12 text-right pr-0">
{% button 'submit' %} {% button 'submit' %}
</div> </div>

View File

@@ -126,7 +126,7 @@ class AddQualification(generic.CreateView, ModalURLMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["depths"] = models.TrainingItemQualification.CHOICES context["depths"] = models.TrainingItemQualification.CHOICES
if is_ajax(self.request): if is_ajax(self.request).get('is_ajax'):
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
else: else:
context['override'] = 'base_training.html' 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 static %}
{% load linkornone from filters %} {% load linkornone from filters %}
@@ -41,7 +41,7 @@
{% endblock %} {% endblock %}
{% block content %} {% 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="row py-3">
<div class="col text-right"> <div class="col text-right">
<div class="btn-group"> <div class="btn-group">
@@ -85,7 +85,7 @@
</div> </div>
</div> </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="col-lg-8 col-12">
<div class="card"> <div class="card">
<div class="card-header">Personal iCal Details</div> <div class="card-header">Personal iCal Details</div>

View File

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