Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ screenshots/

# Virutal Environments
.venv/
/.env
6 changes: 0 additions & 6 deletions .slugignore

This file was deleted.

41 changes: 41 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
2 changes: 0 additions & 2 deletions Procfile

This file was deleted.

50 changes: 22 additions & 28 deletions PyRIGS/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,23 @@
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']

Expand Down Expand Up @@ -95,20 +97,18 @@

# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'),
}
'default': {
'ENGINE': 'django.db.backends.{}'.format(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f-string is easier to read, write, and less computationally expensive than legacy string formatting. Explained here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh shut up you

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

if env("FRANKENRIGS_DATABASE_URL") is not None:
DATABASES['default'] = dj_database_url.config(env="FRANKENRIGS_DATABASE_URL")
else:
DATABASES['default'] = dj_database_url.config()

# Logging
LOGGING = {
'version': 1,
Expand Down Expand Up @@ -257,6 +257,7 @@
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
"RIGS.views.is_ajax",
],
'debug': DEBUG
},
Expand All @@ -269,10 +270,3 @@
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
4 changes: 2 additions & 2 deletions PyRIGS/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
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")),
]
20 changes: 11 additions & 9 deletions PyRIGS/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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
Expand All @@ -30,9 +30,11 @@
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.
Expand Down Expand Up @@ -183,7 +185,7 @@ def get(self, request, model, pk=None, param=None):

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]))
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions RIGS/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,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)
Expand Down
3 changes: 2 additions & 1 deletion RIGS/management/commands/send_reminders.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@


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):
Expand All @@ -33,6 +34,6 @@ def handle(self, *args, **options):
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()
10 changes: 5 additions & 5 deletions RIGS/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion RIGS/templates/email/eventauthorisation_mic_success.txt
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading