Port to Django 5.2 (#635)

* Port to Django 5.2

* Pin pluggy to 1.2.0

Any newer and the mystery importlib metadata error appears. Weird! >_>

* Update for premailer changed default

* Update view logic for is_ajax being changed to a template context processor

* Port a few more tests to pytest proper

Having two distinct test flavours is giving me a headache

* Version 1 dockerfile

Makes a VERY big image, I suspect we can optimise this a lot...

* Optimise dockerfile a little lot a bit

* fix(users): change logout link to POST request

* fix(tests): fix some syntax errors in test code

still got lots of failed tests :(

* fix: replace deprecated Django APIs

* ci: update Dockerfile

* ci: update Dockerfile

* fix(dependencies+tests): update EoL/vulnerable dependencies and improve test stability

* Upgrade Python from 3.10 to 3.12
* Update frontend dependencies, replace node-sass with sass

* chore(logging): ignore dangling obj reference warning from pypdf

* ci: update compose.yml for prod deployment

- Add Nginx as a reverse proxy
- Add cert-selfsign.sh for generating self-signed certificates
- Add certbot-issue.sh for Let's Encrypt certificate issuance and renewal
- Add cron-install.sh and cron-uninstall.sh for system cron management
- Add .env.example as an environment variable template

* style: reformat code

* ci: add more sleep trying to pass tests

* ci: ignore browser-based tests during ci testing

* fix: create home for the new user during Dockerfile building

* chore: remove heroku conf file

* fix: RIGS not franken anymore

* fix: restore is_ajax as a boolean and split out the context processor

* test: remove Selenium interaction tests and their CI workarounds

* fix: harden production settings (CSRF origins, env casts, ADMINS) and drop dead debug toolbar code

* deploy: serve static files from nginx, pin PGDATA, and make cert scripts loud on failure

* build: switch .dockerignore to an allowlist

* build: drop unused dependencies, soft-pin the rest and target Python 3.14

* build: move image to Python 3.14 / Node 24 and force DEBUG off

* ci: add dependabot config for uv, npm, docker and actions

* build: narrow Sass deprecation silencing to @import and require Node 24

* build: provide placeholder env for collectstatic now that DEBUG is off in the image

* deploy: add plain-HTTP nginx config for local development and use it in compose

* deploy: remove self-signed cert script

* build: make the image multi-arch with official node and python base images

* ci: lint and test on PRs, build the image on PRs and push it to GHCR on master

* docs: add local running and sample data instructions

* fix: report a form error instead of crashing when big power has no Power MIC

* fix: upgrade pypdf and urllib3 to patched releases

* fix: apply non-breaking npm audit fixes

* fix: strip whitespace in ALLOWED_HOSTS and drop misleading HSTS preload

* deploy: add a pyrigs healthcheck and make nginx wait for it

* build: pin the uv image version

* fix: correct the check-in person picker condition and use BeautifulSoup's string argument

* refactor: replace unique_together with UniqueConstraint

* build: replace pycodestyle with ruff and fix what it found

Removes unused imports and variables, and fixes a few real problems it
surfaced:
- EventCheckIn.active() referenced an undefined name and raised NameError;
  it now returns whether the check-in has no end time
- RIGS.admin defined EventChecklistAdmin twice; the second is now
  PowerTestRecordAdmin
- RIGS/tests/conftest.py used date/timedelta without importing them
- the signal-registering imports in apps.py are kept with noqa

pycodestyle config in setup.cfg is dropped.

* style: format the codebase with ruff

* style: normalise line endings, trailing whitespace and end-of-file newlines

* ci: run ruff and file hygiene through prek, and document it

---------

Co-authored-by: Hang <me@hangxu.me>
Co-authored-by: Joe Banks <joe@jb3.dev>
This commit is contained in:
2026-10-02 00:43:41 +02:00
committed by GitHub
parent 4cd68f5a0f
commit 08171767a0
148 changed files with 6573 additions and 9514 deletions

View File

@@ -7,11 +7,11 @@ class PersonList(GenericListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "People"
context['create'] = 'person_create'
context['edit'] = 'person_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_person')
context['detail'] = 'person_detail'
context["page_title"] = "People"
context["create"] = "person_create"
context["edit"] = "person_update"
context["can_edit"] = self.request.user.has_perm("RIGS.change_person")
context["detail"] = "person_detail"
return context
@@ -20,29 +20,29 @@ class PersonDetail(GenericDetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['history_link'] = 'person_history'
context['detail_link'] = 'person_detail'
context['update_link'] = 'person_update'
context['associated'] = 'partials/associated_organisations.html'
context['associated2'] = 'partials/associated_events.html'
context['can_edit'] = self.request.user.has_perm('RIGS.change_person')
context["history_link"] = "person_history"
context["detail_link"] = "person_detail"
context["update_link"] = "person_update"
context["associated"] = "partials/associated_organisations.html"
context["associated2"] = "partials/associated_events.html"
context["can_edit"] = self.request.user.has_perm("RIGS.change_person")
return context
class PersonCreate(GenericCreateView, ModalURLMixin):
model = models.Person
fields = ['name', 'phone', 'email', 'address', 'notes']
fields = ["name", "phone", "email", "address", "notes"]
def get_success_url(self):
return self.get_close_url('person_update', 'person_detail')
return self.get_close_url("person_update", "person_detail")
class PersonUpdate(GenericUpdateView, ModalURLMixin):
model = models.Person
fields = ['name', 'phone', 'email', 'address', 'notes']
fields = ["name", "phone", "email", "address", "notes"]
def get_success_url(self):
return self.get_close_url('person_update', 'person_detail')
return self.get_close_url("person_update", "person_detail")
class OrganisationList(GenericListView):
@@ -50,11 +50,11 @@ class OrganisationList(GenericListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['create'] = 'organisation_create'
context['edit'] = 'organisation_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation')
context['detail'] = 'organisation_detail'
context['union_account'] = True
context["create"] = "organisation_create"
context["edit"] = "organisation_update"
context["can_edit"] = self.request.user.has_perm("RIGS.change_organisation")
context["detail"] = "organisation_detail"
context["union_account"] = True
return context
@@ -63,29 +63,29 @@ class OrganisationDetail(GenericDetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['history_link'] = 'organisation_history'
context['detail_link'] = 'organisation_detail'
context['update_link'] = 'organisation_update'
context['associated'] = 'partials/associated_people.html'
context['associated2'] = 'partials/associated_events.html'
context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation')
context["history_link"] = "organisation_history"
context["detail_link"] = "organisation_detail"
context["update_link"] = "organisation_update"
context["associated"] = "partials/associated_people.html"
context["associated2"] = "partials/associated_events.html"
context["can_edit"] = self.request.user.has_perm("RIGS.change_organisation")
return context
class OrganisationCreate(GenericCreateView, ModalURLMixin):
model = models.Organisation
fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
fields = ["name", "phone", "email", "address", "notes", "union_account"]
def get_success_url(self):
return self.get_close_url('organisation_update', 'organisation_detail')
return self.get_close_url("organisation_update", "organisation_detail")
class OrganisationUpdate(GenericUpdateView, ModalURLMixin):
model = models.Organisation
fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
fields = ["name", "phone", "email", "address", "notes", "union_account"]
def get_success_url(self):
return self.get_close_url('organisation_update', 'organisation_detail')
return self.get_close_url("organisation_update", "organisation_detail")
class VenueList(GenericListView):
@@ -93,10 +93,10 @@ class VenueList(GenericListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['create'] = 'venue_create'
context['edit'] = 'venue_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_venue')
context['detail'] = 'venue_detail'
context["create"] = "venue_create"
context["edit"] = "venue_update"
context["can_edit"] = self.request.user.has_perm("RIGS.change_venue")
context["detail"] = "venue_detail"
return context
@@ -105,25 +105,25 @@ class VenueDetail(GenericDetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['history_link'] = 'venue_history'
context['detail_link'] = 'venue_detail'
context['update_link'] = 'venue_update'
context['associated2'] = 'partials/associated_events.html'
context['can_edit'] = self.request.user.has_perm('RIGS.change_venue')
context["history_link"] = "venue_history"
context["detail_link"] = "venue_detail"
context["update_link"] = "venue_update"
context["associated2"] = "partials/associated_events.html"
context["can_edit"] = self.request.user.has_perm("RIGS.change_venue")
return context
class VenueCreate(GenericCreateView, ModalURLMixin):
model = models.Venue
fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
fields = ["name", "phone", "email", "address", "notes", "three_phase_available"]
def get_success_url(self):
return self.get_close_url('venue_update', 'venue_detail')
return self.get_close_url("venue_update", "venue_detail")
class VenueUpdate(GenericUpdateView, ModalURLMixin):
model = models.Venue
fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
fields = ["name", "phone", "email", "address", "notes", "three_phase_available"]
def get_success_url(self):
return self.get_close_url('venue_update', 'venue_detail')
return self.get_close_url("venue_update", "venue_detail")

View File

@@ -16,7 +16,7 @@ from z3c.rml import rml2pdf
from RIGS import models
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
forms.DateField.widget = forms.DateInput(attrs={"type": "date"})
TIME_FILTERS = ["all", "year", "month", "week"]
@@ -27,53 +27,73 @@ def days_between(d1, d2):
class InvoiceDashboard(generic.TemplateView):
template_name = 'invoice_dashboard.html'
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."
context["page_title"] = "Invoice Dashboard"
context["description"] = "Overview of financial status of TEC rigs."
time_filter = self.request.GET.get('time_filter', 'all')
time_filter = self.request.GET.get("time_filter", "all")
if time_filter not in TIME_FILTERS:
time_filter = 'all'
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))
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'])
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')
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']})")
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']])
context["total_income"] = sum([i["total"] for i in context["payment_methods"]])
payments = context['payments']
payments = context["payments"]
mean_duration = 0
for payment in payments:
@@ -82,23 +102,25 @@ class InvoiceDashboard(generic.TemplateView):
if len(payments) > 0:
mean_duration /= len(payments)
context['mean_invoice_to_payment'] = mean_duration
context["mean_invoice_to_payment"] = mean_duration
return context
class InvoiceOutstanding(generic.ListView):
model = models.Invoice
template_name = 'invoice_list.html'
template_name = "invoice_list.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
total = 0
for i in context['object_list']:
for i in context["object_list"]:
total += i.balance
event_count = len(list(context['object_list']))
context['page_title'] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})"
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
event_count = len(list(context["object_list"]))
context["page_title"] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})"
context["description"] = (
"Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
)
return context
def get_queryset(self):
@@ -107,18 +129,18 @@ class InvoiceOutstanding(generic.ListView):
class InvoiceDetail(generic.DetailView):
model = models.Invoice
template_name = 'invoice_detail.html'
template_name = "invoice_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})"
context["page_title"] = f"Invoice {self.object.display_id} ({invoice_date})"
if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
context["page_title"] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed:
context['page_title'] += "<span class='badge badge-success float-right'>PAID</span>"
context["page_title"] += "<span class='badge badge-success float-right'>PAID</span>"
else:
context['page_title'] += "<span class='badge badge-info float-right'>OUTSTANDING</span>"
context["page_title"] += "<span class='badge badge-info float-right'>OUTSTANDING</span>"
return context
@@ -126,17 +148,12 @@ class InvoicePrint(generic.View):
def get(self, request, pk):
invoice = get_object_or_404(models.Invoice, pk=pk)
object = invoice.event
template = get_template('event_print.xml')
template = get_template("event_print.xml")
name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
name = re.sub(r"[^a-zA-Z0-9 \n\.]", "", object.name)
filename = f"Invoice {invoice.display_id} for {object.display_id} {name}.pdf"
context = {
'object': object,
'invoice': invoice,
'current_user': request.user,
'filename': filename
}
context = {"object": object, "invoice": invoice, "current_user": request.user, "filename": filename}
rml = template.render(context)
@@ -144,65 +161,65 @@ class InvoicePrint(generic.View):
pdfData = buffer.read()
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'filename="{filename}"'
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = f'filename="{filename}"'
response.write(pdfData)
return response
class InvoiceVoid(generic.View):
def get(self, *args, **kwargs):
pk = kwargs.get('pk')
pk = kwargs.get("pk")
object = get_object_or_404(models.Invoice, pk=pk)
object.void = not object.void
object.save()
if object.void:
return HttpResponseRedirect(reverse('invoice_list'))
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk}))
return HttpResponseRedirect(reverse("invoice_list"))
return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": object.pk}))
class InvoiceDelete(generic.DeleteView):
model = models.Invoice
template_name = 'invoice_confirm_delete.html'
template_name = "invoice_confirm_delete.html"
def get(self, request, pk):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
messages.info(self.request, "To delete an invoice, delete the payments first.")
return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": obj.pk}))
return super(InvoiceDelete, self).get(pk)
def post(self, request, pk):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
messages.info(self.request, "To delete an invoice, delete the payments first.")
return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": obj.pk}))
return super(InvoiceDelete, self).post(pk)
def get_success_url(self):
return self.request.POST.get('next')
return self.request.POST.get("next")
class InvoiceArchive(generic.ListView):
model = models.Invoice
template_name = 'invoice_list_archive.html'
template_name = "invoice_list_archive.html"
paginate_by = 25
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void"
context["page_title"] = "Invoice Archive"
context["description"] = "This page displays all invoices: outstanding, paid, and void"
return context
def get_queryset(self):
return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date')
return self.model.objects.search(self.request.GET.get("q")).order_by("-invoice_date")
class InvoiceWaiting(generic.ListView):
model = models.Event
paginate_by = 25
template_name = 'invoice_list_waiting.html'
template_name = "invoice_list_waiting.html"
def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
@@ -210,7 +227,7 @@ class InvoiceWaiting(generic.ListView):
objects = self.get_queryset()
for obj in objects:
total += obj.sum_total
context['page_title'] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})"
context["page_title"] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})"
return context
def get_queryset(self):
@@ -222,51 +239,51 @@ class InvoiceEvent(generic.View):
@reversion.create_revision()
def get(self, *args, **kwargs):
reversion.set_user(self.request.user)
epk = kwargs.get('pk')
epk = kwargs.get("pk")
event = models.Event.objects.get(pk=epk)
invoice, created = models.Invoice.objects.get_or_create(event=event)
if created:
invoice.invoice_date = datetime.date.today()
messages.success(self.request, 'Invoice created successfully')
messages.success(self.request, "Invoice created successfully")
if kwargs.get('void'):
if kwargs.get("void"):
invoice.void = not invoice.void
invoice.save()
messages.warning(self.request, 'Invoice voided')
messages.warning(self.request, "Invoice voided")
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk}))
return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": invoice.pk}))
class PaymentCreate(generic.CreateView):
model = models.Payment
fields = ['invoice', 'date', 'amount', 'method']
template_name = 'payment_form.html'
fields = ["invoice", "date", "amount", "method"]
template_name = "payment_form.html"
def get_initial(self):
initial = super().get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
invoicepk = self.request.GET.get("invoice", self.request.POST.get("invoice", None))
if invoicepk is None:
raise Http404()
invoice = get_object_or_404(models.Invoice, pk=invoicepk)
initial.update({'invoice': invoice})
initial.update({"invoice": invoice})
return initial
@transaction.atomic()
@reversion.create_revision()
def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['invoice'])
reversion.add_to_revision(form.cleaned_data["invoice"])
reversion.set_comment("Payment added")
return super().form_valid(form, *args, **kwargs)
def get_success_url(self):
messages.info(self.request, "location.reload()")
return reverse('closemodal')
return reverse("closemodal")
class PaymentDelete(generic.DeleteView):
model = models.Payment
template_name = 'payment_confirm_delete.html'
template_name = "payment_confirm_delete.html"
@transaction.atomic()
@reversion.create_revision()
@@ -276,4 +293,4 @@ class PaymentDelete(generic.DeleteView):
return super().delete(*args, **kwargs)
def get_success_url(self):
return self.request.POST.get('next')
return self.request.POST.get("next")

View File

@@ -15,48 +15,48 @@ from django.shortcuts import redirect
class HSCreateView(generic.CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
event = models.Event.objects.get(pk=self.kwargs.get('pk'))
context['event'] = event
context['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}'
get_related(context['form'], context)
event = models.Event.objects.get(pk=self.kwargs.get("pk"))
context["event"] = event
context["page_title"] = f"Create {self.model.__name__} for Event {event.display_id}"
get_related(context["form"], context)
return context
class MarkReviewed(generic.RedirectView):
def get_redirect_url(self, *args, **kwargs):
obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk'))
obj = apps.get_model("RIGS", kwargs.get("model")).objects.get(pk=kwargs.get("pk"))
with reversion.create_revision():
reversion.set_user(self.request.user)
obj.reviewed_by = self.request.user
obj.reviewed_at = timezone.now()
obj.save()
return self.request.META.get('HTTP_REFERER', reverse('hs_list'))
return self.request.META.get("HTTP_REFERER", reverse("hs_list"))
class EventRiskAssessmentCreate(HSCreateView):
model = models.RiskAssessment
template_name = 'hs/risk_assessment_form.html'
template_name = "hs/risk_assessment_form.html"
form_class = forms.EventRiskAssessmentForm
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
epk = kwargs.get("pk")
event = models.Event.objects.get(pk=epk)
# Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is not None:
return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk}))
return HttpResponseRedirect(reverse("ra_edit", kwargs={"pk": ra.pk}))
return super().get(self)
def get_success_url(self):
return reverse('ra_detail', kwargs={'pk': self.object.pk})
return reverse("ra_detail", kwargs={"pk": self.object.pk})
class EventRiskAssessmentEdit(generic.UpdateView):
model = models.RiskAssessment
template_name = 'hs/risk_assessment_form.html'
template_name = "hs/risk_assessment_form.html"
form_class = forms.EventRiskAssessmentForm
def get_success_url(self):
@@ -64,42 +64,46 @@ class EventRiskAssessmentEdit(generic.UpdateView):
ra.reviewed_by = None
ra.reviewed_at = None
ra.save()
return reverse('ra_detail', kwargs={'pk': self.object.pk})
return reverse("ra_detail", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
rpk = self.kwargs.get('pk')
rpk = self.kwargs.get("pk")
ra = models.RiskAssessment.objects.get(pk=rpk)
context['event'] = ra.event
context['edit'] = True
context['page_title'] = f'Edit Risk Assessment for Event {ra.event.display_id}'
get_related(context['form'], context)
context["event"] = ra.event
context["edit"] = True
context["page_title"] = f"Edit Risk Assessment for Event {ra.event.display_id}"
get_related(context["form"], context)
return context
class EventRiskAssessmentDetail(generic.DetailView):
model = models.RiskAssessment
template_name = 'hs/risk_assessment_detail.html'
template_name = "hs/risk_assessment_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
context["page_title"] = (
f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
)
return context
class EventChecklistDetail(generic.DetailView):
model = models.EventChecklist
template_name = 'hs/event_checklist_detail.html'
template_name = "hs/event_checklist_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
context["page_title"] = (
f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
)
return context
class EventChecklistEdit(generic.UpdateView):
model = models.EventChecklist
template_name = 'hs/event_checklist_form.html'
template_name = "hs/event_checklist_form.html"
form_class = forms.EventChecklistForm
def get_success_url(self):
@@ -107,58 +111,63 @@ class EventChecklistEdit(generic.UpdateView):
ec.reviewed_by = None
ec.reviewed_at = None
ec.save()
return reverse('ec_detail', kwargs={'pk': self.object.pk})
return reverse("ec_detail", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pk = self.kwargs.get('pk')
pk = self.kwargs.get("pk")
ec = models.EventChecklist.objects.get(pk=pk)
context['event'] = ec.event
context['edit'] = True
context['page_title'] = f'Edit Event Checklist for Event {ec.event.display_id}'
get_related(context['form'], context)
context["event"] = ec.event
context["edit"] = True
context["page_title"] = f"Edit Event Checklist for Event {ec.event.display_id}"
get_related(context["form"], context)
return context
class EventChecklistCreate(HSCreateView):
model = models.EventChecklist
template_name = 'hs/event_checklist_form.html'
template_name = "hs/event_checklist_form.html"
form_class = forms.EventChecklistForm
# From both business logic and programming POVs, RAs must exist before ECs!
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
epk = kwargs.get("pk")
event = models.Event.objects.get(pk=epk)
# Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None:
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
messages.error(
self.request,
f"A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.",
)
return HttpResponseRedirect(reverse("event_ra", kwargs={"pk": epk}))
return super().get(self)
def get_success_url(self):
return reverse('ec_detail', kwargs={'pk': self.object.pk})
return reverse("ec_detail", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if context['event'].venue:
context['venue'] = context['event'].venue
if context["event"].venue:
context["venue"] = context["event"].venue
return context
class PowerTestDetail(generic.DetailView):
model = models.PowerTestRecord
template_name = 'hs/power_detail.html'
template_name = "hs/power_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Power Test Record for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
context["page_title"] = (
f"Power Test Record for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
)
return context
class PowerTestEdit(generic.UpdateView):
model = models.PowerTestRecord
template_name = 'hs/power_form.html'
template_name = "hs/power_form.html"
form_class = forms.PowerTestRecordForm
def get_success_url(self):
@@ -166,130 +175,140 @@ class PowerTestEdit(generic.UpdateView):
ec.reviewed_by = None
ec.reviewed_at = None
ec.save()
return reverse('pt_detail', kwargs={'pk': self.object.pk})
return reverse("pt_detail", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pk = self.kwargs.get('pk')
pk = self.kwargs.get("pk")
ec = models.PowerTestRecord.objects.get(pk=pk)
context['event'] = ec.event
context['edit'] = True
context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}'
get_related(context['form'], context)
context["event"] = ec.event
context["edit"] = True
context["page_title"] = f"Edit Power Test Record for Event {ec.event.display_id}"
get_related(context["form"], context)
return context
class PowerTestCreate(HSCreateView):
model = models.PowerTestRecord
template_name = 'hs/power_form.html'
template_name = "hs/power_form.html"
form_class = forms.PowerTestRecordForm
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
epk = kwargs.get("pk")
event = models.Event.objects.get(pk=epk)
# Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None:
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.')
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
messages.error(
self.request,
f"A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.",
)
return HttpResponseRedirect(reverse("event_ra", kwargs={"pk": epk}))
return super().get(self)
def get_success_url(self):
return reverse('pt_detail', kwargs={'pk': self.object.pk})
return reverse("pt_detail", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if context['event'].venue:
context['venue'] = context['event'].venue
if context['event'].riskassessment.power_mic:
context['power_mic'] = context['event'].riskassessment.power_mic
if context["event"].venue:
context["venue"] = context["event"].venue
if context["event"].riskassessment.power_mic:
context["power_mic"] = context["event"].riskassessment.power_mic
return context
class HSList(generic.ListView):
paginate_by = 20
model = models.Event
template_name = 'hs/hs_list.html'
template_name = "hs/hs_list.html"
def get_queryset(self):
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).exclude(dry_hire=True).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
return (
models.Event.objects.all()
.exclude(status=models.Event.CANCELLED)
.exclude(dry_hire=True)
.order_by("-start_date")
.select_related("riskassessment")
.prefetch_related("checklists")
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = 'H&S Overview'
context["page_title"] = "H&S Overview"
return context
class RAPrint(PrintView):
model = models.RiskAssessment
template_name = 'hs/ra_print.xml'
template_name = "hs/ra_print.xml"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
context["filename"] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
return context
class PowerPrint(PrintView):
model = models.PowerTestRecord
template_name = 'hs/power_print.xml'
template_name = "hs/power_print.xml"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filename'] = f"PowerTestRecord_for_{context['object'].event.display_id}.pdf"
context["filename"] = f"PowerTestRecord_for_{context['object'].event.display_id}.pdf"
return context
class EventCheckIn(generic.CreateView, ModalURLMixin):
model = models.EventCheckIn
template_name = 'hs/eventcheckin_form.html'
template_name = "hs/eventcheckin_form.html"
form_class = forms.EventCheckInForm
def get_success_url(self):
return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...!
return self.get_close_url("event_detail", "event_detail") # Well, that's one way of doing that...!
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk'))
context['page_title'] = f'Check In to Event {context["event"].display_id}'
context["event"] = models.Event.objects.get(pk=self.kwargs.get("pk"))
context["page_title"] = f"Check In to Event {context['event'].display_id}"
# get_related(context['form'], context)
return context
class EventCheckInOverride(generic.CreateView):
model = models.EventCheckIn
template_name = 'hs/eventcheckin_form.html'
template_name = "hs/eventcheckin_form.html"
form_class = forms.EditCheckInForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk'))
context['page_title'] = f'Manually add Check In to Event {context["event"].display_id}'
context['manual'] = True
context["event"] = models.Event.objects.get(pk=self.kwargs.get("pk"))
context["page_title"] = f"Manually add Check In to Event {context['event'].display_id}"
context["manual"] = True
return context
class EventCheckInEdit(generic.UpdateView, ModalURLMixin):
model = models.EventCheckIn
template_name = 'hs/eventcheckin_form.html'
template_name = "hs/eventcheckin_form.html"
form_class = forms.EditCheckInForm
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
if not obj.person == self.request.user and not obj.event.mic == self.request.user:
return redirect(self.request.META.get('HTTP_REFERER', '/'))
return redirect(self.request.META.get("HTTP_REFERER", "/"))
return super().dispatch(request)
def get_success_url(self):
return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...!
return self.get_close_url("event_detail", "event_detail") # Well, that's one way of doing that...!
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['event'] = self.object.event
context['page_title'] = f'Edit Check In for Event {context["event"].display_id}'
context['edit'] = True
context["event"] = self.object.event
context["page_title"] = f"Edit Check In for Event {context['event'].display_id}"
context["edit"] = True
# get_related(context['form'], context)
return context
@@ -300,4 +319,4 @@ class EventCheckOut(generic.RedirectView):
if checkin:
checkin.end_time = timezone.now()
checkin.save()
return self.request.META.get('HTTP_REFERER', '/')
return self.request.META.get("HTTP_REFERER", "/")

View File

@@ -12,9 +12,10 @@ class CalendarICS(ICalFeed):
"""
A simple event calender
"""
# Metadata which is passed on to clients
product_id = 'RIGS'
title = 'RIGS Calendar'
product_id = "RIGS"
title = "RIGS Calendar"
timezone = settings.TIME_ZONE
file_name = "rigs.ics"
@@ -29,24 +30,34 @@ class CalendarICS(ICalFeed):
def get_object(self, request, *args, **kwargs):
params = {}
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
params['rig'] = request.GET.get('rig', 'true') == 'true'
params["dry-hire"] = request.GET.get("dry-hire", "true") == "true"
params["non-rig"] = request.GET.get("non-rig", "true") == "true"
params["rig"] = request.GET.get("rig", "true") == "true"
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["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']
params["user"] = kwargs["user"]
return params
def description(self, params):
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
desc = (
"Calendar generated by RIGS system. This includes event types: "
+ ("Rig, " if params["rig"] else "")
+ ("Non-rig, " if params["non-rig"] else "")
+ ("Dry Hire " if params["dry-hire"] else "")
+ "\n"
)
desc = (
desc
+ "Includes events with status: "
+ ("Cancelled, " if params["cancelled"] else "")
+ ("Provisional, " if params["provisional"] else "")
+ ("Confirmed/Booked, " if params["confirmed"] else "")
)
return desc
@@ -57,50 +68,53 @@ class CalendarICS(ICalFeed):
typeFilters = Q(pk=None) # Need something that is false for every entry
if params['dry-hire']:
if params["dry-hire"]:
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
if params['non-rig']:
if params["non-rig"]:
typeFilters = typeFilters | Q(is_rig=False)
if params['rig']:
if params["rig"]:
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
statusFilters = Q(pk=None) # Need something that is false for every entry
if params['cancelled']:
if params["cancelled"]:
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
if params['provisional']:
if params["provisional"]:
statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
if params['confirmed']:
if params["confirmed"]:
statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
filter = filter & typeFilters & statusFilters
if params['only_mic']:
filter = filter & Q(mic=params['user'])
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')
return (
models.Event.objects.filter(filter)
.order_by("-start_date")
.select_related("person", "organisation", "venue", "mic")
)
def item_title(self, item):
title = ''
title = ""
# Prefix title with status (if it's a critical status)
if item.cancelled:
title += 'CANCELLED: '
title += "CANCELLED: "
if not item.is_rig:
title += 'NON-RIG: '
title += "NON-RIG: "
if item.dry_hire:
title += 'DRY HIRE: '
title += "DRY HIRE: "
# Add the rig name
title += item.name
# Add the status
title += f' ({item.get_status_display()})'
title += f" ({item.get_status_display()})"
return title
@@ -121,36 +135,54 @@ class CalendarICS(ICalFeed):
tz = pytz.timezone(self.timezone)
desc = f'Rig ID = {item.display_id}\n'
desc += f'Event = {item.name}\n'
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
desc = f"Rig ID = {item.display_id}\n"
desc += f"Event = {item.name}\n"
desc += "Venue = " + (item.venue.name if item.venue else "---") + "\n"
if item.is_rig and item.person:
desc += 'Client = ' + item.person.name + (
(' for ' + item.organisation.name) if item.organisation else '') + '\n'
desc += f'Status = {item.get_status_display()}\n'
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += (
"Client = "
+ item.person.name
+ ((" for " + item.organisation.name) if item.organisation else "")
+ "\n"
)
desc += f"Status = {item.get_status_display()}\n"
desc += "MIC = " + (item.mic.name if item.mic else "---") + "\n"
desc += '\n'
desc += "\n"
if item.meet_at:
desc += 'Crew Meet = ' + (
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
desc += (
"Crew Meet = "
+ (item.meet_at.astimezone(tz).strftime("%Y-%m-%d %H:%M") if item.meet_at else "---")
+ "\n"
)
if item.access_at:
desc += 'Access At = ' + (
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
desc += (
"Access At = "
+ (item.access_at.astimezone(tz).strftime("%Y-%m-%d %H:%M") if item.access_at else "---")
+ "\n"
)
if item.start_date:
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
desc += (
"Event Start = "
+ item.start_date.strftime("%Y-%m-%d")
+ ((" " + item.start_time.strftime("%H:%M")) if item.has_start_time else "")
+ "\n"
)
if item.end_date:
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + (
(' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
desc += (
"Event End = "
+ item.end_date.strftime("%Y-%m-%d")
+ ((" " + item.end_time.strftime("%H:%M")) if item.has_end_time else "")
+ "\n"
)
desc += '\n'
desc += "\n"
if item.description:
desc += f'Event Description:\n{item.description}\n\n'
desc += f"Event Description:\n{item.description}\n\n"
# if item.notes: // Need to add proper keyholder checks before this gets put back
# desc += 'Notes:\n'+item.notes+'\n\n'
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
desc += f"URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}"
return desc

View File

@@ -8,7 +8,6 @@ import hmac
import hashlib
from envparse import env
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib import messages
@@ -17,10 +16,9 @@ from django.core import signing
from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives
from django.db.models import Q
from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
@@ -31,11 +29,11 @@ from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
from RIGS import models, forms
__author__ = 'ghost'
__author__ = "ghost"
class RigboardIndex(generic.TemplateView):
template_name = 'rigboard.html'
template_name = "rigboard.html"
def get_context_data(self, **kwargs):
# get super context
@@ -43,28 +41,28 @@ class RigboardIndex(generic.TemplateView):
objects = models.Event.objects.current_events()
if self.request.GET.get('hide_cancelled', False):
if self.request.GET.get("hide_cancelled", False):
objects = objects.exclude(status=models.Event.CANCELLED)
# call out method to get current events
context['events'] = objects.select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['page_title'] = "Rigboard"
context["events"] = objects.select_related("riskassessment", "invoice").prefetch_related("checklists")
context["page_title"] = "Rigboard"
return context
class WebCalendar(generic.TemplateView):
template_name = 'calendar.html'
template_name = "calendar.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '')
context["view"] = kwargs.get("view", "")
context["date"] = kwargs.get("date", "")
# context['page_title'] = "Calendar"
return context
class EventDetail(generic.DetailView, ModalURLMixin):
template_name = 'event_detail.html'
template_name = "event_detail.html"
model = models.Event
def get_context_data(self, **kwargs):
@@ -72,36 +70,36 @@ class EventDetail(generic.DetailView, ModalURLMixin):
title = f"{self.object.display_id} | {self.object.name}"
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title
context["page_title"] = title
if is_ajax(self.request):
context['override'] = "base_ajax.html"
context["override"] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
context["override"] = "base_assets.html"
return context
class EventEmbed(EventDetail):
template_name = 'event_embed.html'
template_name = "event_embed.html"
class EventOEmbed(OEmbedView):
model = models.Event
url_name = 'event_embed'
url_name = "event_embed"
class EventCreate(generic.CreateView):
model = models.Event
form_class = forms.EventForm
template_name = 'event_form.html'
template_name = "event_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "New Event"
context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate()
context["page_title"] = "New Event"
context["edit"] = True
context["currentVAT"] = models.VatRate.objects.current_rate()
form = context['form']
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
form = context["form"]
if hasattr(form, "items_json") and re.search(r'"-\d+"', form["items_json"].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
get_related(form, context)
@@ -109,39 +107,43 @@ class EventCreate(generic.CreateView):
return context
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
return reverse_lazy("event_detail", kwargs={"pk": self.object.pk})
class EventUpdate(generic.UpdateView):
model = models.Event
form_class = forms.EventForm
template_name = 'event_form.html'
template_name = "event_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Event {self.object.display_id}"
context['edit'] = True
context["page_title"] = f"Event {self.object.display_id}"
context["edit"] = True
form = context['form']
form = context["form"]
get_related(form, context)
return context
def render_to_response(self, context, **response_kwargs):
if hasattr(context, 'duplicate') and not context['duplicate']:
if hasattr(context, "duplicate") and not context["duplicate"]:
# If this event has already been emailed to a client, show a warning
if self.object.auth_request_at is not None:
messages.info(self.request,
'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
messages.info(
self.request,
"This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.",
)
if hasattr(self.object, 'authorised'):
messages.warning(self.request,
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
if hasattr(self.object, "authorised"):
messages.warning(
self.request,
"This event has already been authorised by the client, any changes to the price will require reauthorisation.",
)
return super().render_to_response(context, **response_kwargs)
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
return reverse_lazy("event_detail", kwargs={"pk": self.object.pk})
class EventDuplicate(EventUpdate):
@@ -158,34 +160,38 @@ class EventDuplicate(EventUpdate):
new.collector = None
# Remove all the authorisation information from the new event
new.auth_request_to = ''
new.auth_request_to = ""
new.auth_request_by = None
new.auth_request_at = None
if self.request.method in (
'POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
"POST",
"PUT",
): # This only happens on save (otherwise items won't display in editor)
new.pk = None # This means a new event will be created on save, and all items will be re-created
else:
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
messages.info(self.request, "Event data duplicated but not yet saved. Click save to complete operation.")
return new
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Duplicate of Event {self.object.display_id}"
context["page_title"] = f"Duplicate of Event {self.object.display_id}"
context["duplicate"] = True
return context
class EventPrint(PrintView):
model = models.Event
template_name = 'event_print.xml'
template_name = "event_print.xml"
append_terms = True
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['quote'] = True
context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf"
context["quote"] = True
context["filename"] = (
f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf"
)
return context
@@ -196,20 +202,19 @@ class EventArchive(generic.ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
context['page_title'] = 'Event Archive'
context["start"] = self.request.GET.get("start", None)
context["end"] = self.request.GET.get("end", datetime.date.today().strftime("%Y-%m-%d"))
context["statuses"] = models.Event.EVENT_STATUS_CHOICES
context["page_title"] = "Event Archive"
return context
def get_queryset(self):
start = self.request.GET.get('start', None)
end = self.request.GET.get('end', datetime.date.today())
start = self.request.GET.get("start", None)
end = self.request.GET.get("end", datetime.date.today())
# Assume idiots, always check
if start and start > end:
messages.add_message(self.request, messages.INFO,
"Muppet! Check the dates, it has been fixed for you.")
messages.add_message(self.request, messages.INFO, "Muppet! Check the dates, it has been fixed for you.")
start, end = end, start # Stop the impending fail
filter = Q()
@@ -218,21 +223,21 @@ class EventArchive(generic.ListView):
if start:
filter &= Q(start_date__gte=start)
q = self.request.GET.get('q', "")
q = self.request.GET.get("q", "")
objects = self.model.objects.all()
if q:
objects = self.model.objects.search(q)
status = self.request.GET.getlist('status', "")
status = self.request.GET.getlist("status", "")
if len(status) > 0:
filter &= Q(status__in=status)
qs = objects.filter(filter).order_by('-start_date')
qs = objects.filter(filter).order_by("-start_date")
# Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic')
qs.select_related("person", "organisation", "venue", "mic")
if not qs.exists():
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
@@ -241,49 +246,57 @@ class EventArchive(generic.ListView):
class EventAuthorise(generic.UpdateView):
template_name = 'eventauthorisation_form.html'
success_template = 'eventauthorisation_success.html'
template_name = "eventauthorisation_form.html"
success_template = "eventauthorisation_success.html"
preview = False
def form_valid(self, form):
self.object = form.save()
self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' +
f'You will also receive email confirmation to {self.object.email}.')
messages.add_message(
self.request,
messages.SUCCESS,
"Success! Your event has been authorised. "
+ f"You will also receive email confirmation to {self.object.email}.",
)
return self.render_to_response(self.get_context_data())
@property
def event(self):
return models.Event.objects.select_related('organisation', 'person', 'venue').get(pk=self.kwargs['pk'])
return models.Event.objects.select_related("organisation", "person", "venue").get(pk=self.kwargs["pk"])
def get_object(self, queryset=None):
return getattr(self.event, 'authorisation', None)
return getattr(self.event, "authorisation", None)
def get_form_class(self):
return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL
context['page_title'] = f"{self.event.display_id}: {self.event.name}"
context["event"] = self.event
context["tos_url"] = settings.TERMS_OF_HIRE_URL
context["page_title"] = f"{self.event.display_id}: {self.event.name}"
if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
context["page_title"] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context["preview"] = self.preview
return context
def get(self, request, *args, **kwargs):
if self.get_object() is not None and self.get_object().pk is not None:
if self.event.authorised:
messages.add_message(self.request, messages.WARNING,
"This event has already been authorised. "
"Reauthorising is not necessary at this time.")
messages.add_message(
self.request,
messages.WARNING,
"This event has already been authorised. Reauthorising is not necessary at this time.",
)
else:
messages.add_message(self.request, messages.WARNING,
"This event has already been authorised, but the amount has changed. " +
"Please check the amount and reauthorise.")
messages.add_message(
self.request,
messages.WARNING,
"This event has already been authorised, but the amount has changed. "
+ "Please check the amount and reauthorise.",
)
return super().get(request, *args, **kwargs)
def get_form(self, **kwargs):
@@ -296,20 +309,19 @@ class EventAuthorise(generic.UpdateView):
def dispatch(self, request, *args, **kwargs):
# Verify our signature matches up and all is well with the integrity of the URL
try:
data = signing.loads(kwargs.get('hmac'))
assert int(kwargs.get('pk')) == int(data.get('pk'))
request.email = data['email']
request.sent_by = models.Profile.objects.get(pk=data['sent_by'])
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
raise SuspiciousOperation(
"This URL is invalid. Please ask your TEC contact for a new URL")
data = signing.loads(kwargs.get("hmac"))
assert int(kwargs.get("pk")) == int(data.get("pk"))
request.email = data["email"]
request.sent_by = models.Profile.objects.get(pk=data["sent_by"])
except signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist:
raise SuspiciousOperation("This URL is invalid. Please ask your TEC contact for a new URL")
return super().dispatch(request, *args, **kwargs)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
model = models.Event
form_class = forms.EventAuthorisationRequestForm
template_name = 'eventauthorisation_request.html'
template_name = "eventauthorisation_request.html"
@method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs):
@@ -321,17 +333,20 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
def get_success_url(self):
if is_ajax(self.request):
url = reverse_lazy('closemodal')
url = reverse_lazy("closemodal")
messages.info(self.request, "location.reload()")
else:
url = reverse_lazy('event_detail', kwargs={
'pk': self.object.pk,
})
url = reverse_lazy(
"event_detail",
kwargs={
"pk": self.object.pk,
},
)
messages.add_message(self.request, messages.SUCCESS, "Authorisation request successfully sent.")
return url
def form_valid(self, form):
email = form.cleaned_data['email']
email = form.cleaned_data["email"]
event = self.object
event.auth_request_by = self.request.user
event.auth_request_at = timezone.now()
@@ -339,18 +354,20 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
event.save()
context = {
'object': self.object,
'request': self.request,
'hmac': signing.dumps({
'pk': self.object.pk,
'email': email,
'sent_by': self.request.user.pk,
}),
"object": self.object,
"request": self.request,
"hmac": signing.dumps(
{
"pk": self.object.pk,
"email": email,
"sent_by": self.request.user.pk,
}
),
}
if event.person is not None and email == event.person.email:
context['to_name'] = event.person.name
context["to_name"] = event.person.name
elif event.organisation is not None and email == event.organisation.email:
context['to_name'] = event.organisation.name
context["to_name"] = event.organisation.name
msg = EmailMultiAlternatives(
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
@@ -358,10 +375,13 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
to=[email],
reply_to=[self.request.user.email],
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context),
external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
css = finders.find("css/email.css")
html = premailer.Premailer(
get_template("email/eventauthorisation_client_request.html").render(context),
external_styles=css,
allow_loading_external_files=True,
).transform()
msg.attach_alternative(html, "text/html")
msg.send()
@@ -373,21 +393,25 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
model = models.Event
def render_to_response(self, context, **response_kwargs):
css = finders.find('css/email.css')
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):
context = super().get_context_data(**kwargs)
context['hmac'] = signing.dumps({
'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'),
'sent_by': self.request.user.pk,
})
context['to_name'] = self.request.GET.get('to_name', None)
context['target'] = 'event_authorise_form_preview'
context["hmac"] = signing.dumps(
{
"pk": self.object.pk,
"email": self.request.GET.get("email", "hello@world.test"),
"sent_by": self.request.user.pk,
}
)
context["to_name"] = self.request.GET.get("to_name", None)
context["target"] = "event_authorise_form_preview"
return context
@@ -395,17 +419,17 @@ class CreateForumThread(generic.base.RedirectView):
permanent = False
def get_redirect_url(self, *args, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk'])
event = get_object_or_404(models.Event, pk=kwargs["pk"])
if event.forum_url:
return event.forum_url
params = {
'title': str(event),
'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}',
'category': 'rig-info'
"title": str(event),
"body": f"https://rigs.nottinghamtec.co.uk/event/{event.pk}",
"category": "rig-info",
}
return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}'
return f"https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}"
class RecieveForumWebhook(generic.View):
@@ -415,12 +439,14 @@ class RecieveForumWebhook(generic.View):
def post(self, request, *args, **kwargs):
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}"
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed):
return HttpResponseForbidden('Invalid signature header')
if not hmac.compare_digest(request.headers.get("X-Discourse-Event-Signature"), computed):
return HttpResponseForbidden("Invalid signature header")
# Check if this is the right kind of event. The webhook filters by category on the forum side
if request.headers.get('X-Discourse-Event') == "topic_created":
body = simplejson.loads(request.body.decode('utf-8'))
event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros
if request.headers.get("X-Discourse-Event") == "topic_created":
body = simplejson.loads(request.body.decode("utf-8"))
event_id = int(
body["topic"]["title"][1:6]
) # find the ID, force convert it to an int to eliminate leading zeros
event = models.Event.objects.filter(pk=event_id).first()
if event:
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}"