From 16874073e9f01acf952e8f59eb8bd5bff2bfb003 Mon Sep 17 00:00:00 2001 From: Arona Jones Date: Tue, 27 Jun 2023 13:00:51 +0100 Subject: [PATCH] "Create forum post" button on a rig page (#551) * Add button for creating forum thread draft from event detail TODO: Allow RIGS to ingest POST requests sent from the forum on new posts in RIG info to link up the forum thread RE https://forum.nottinghamtec.co.uk/t/rigs-discourse-integration/15592/21 * Mockup webhook recieving view * Correct method of CRSF exemption for webhook reciever * Use f-strings correctly, not like a big dumb * That was also dumb, fix that too * Second shot at webhook reciever * Oops * >.< * Third shot * Try again at signing * What if I gave it the right arguments. That might be a good start. * More fiddling with auth * Add debug print * Okay, put that back where it was because I inavertently overloaded my import Flashbacks to my java days... * Different header access method * Fix import, again * Fix ommited json parsing wotsit * Fix url * Fix string index * Correct template logic * Allow manual adding/editing of URLs * Filter by right flavour of event * Amend event str conversion for consistency * Oops * Make migration Will be squashed later * Fix logic when creating events * Squash migration * Implement codedoctor suggestion --- RIGS/forms.py | 2 +- RIGS/migrations/0050_event_forum_url.py | 19 +++++++++ RIGS/models.py | 12 +++++- RIGS/templates/event_form.html | 20 ++++++++-- RIGS/templates/partials/event_details.html | 9 +++++ RIGS/urls.py | 3 ++ RIGS/views/rigboard.py | 45 ++++++++++++++++++++++ 7 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 RIGS/migrations/0050_event_forum_url.py diff --git a/RIGS/forms.py b/RIGS/forms.py index 9a5f60b6..8a840166 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -121,7 +121,7 @@ class EventForm(forms.ModelForm): fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date', 'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic', 'person', 'organisation', 'dry_hire', 'checked_in_by', 'status', - 'purchase_order', 'collector'] + 'purchase_order', 'collector', 'forum_url'] class BaseClientEventAuthorisationForm(forms.ModelForm): diff --git a/RIGS/migrations/0050_event_forum_url.py b/RIGS/migrations/0050_event_forum_url.py new file mode 100644 index 00000000..b0589a9c --- /dev/null +++ b/RIGS/migrations/0050_event_forum_url.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.19 on 2023-06-27 11:28 + +import RIGS.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0049_auto_20230529_1123'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='forum_url', + field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]), + ), + ] diff --git a/RIGS/models.py b/RIGS/models.py index ea18e95e..b11eecc5 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -308,6 +308,14 @@ class EventManager(models.Manager): return qs +def validate_forum_url(value): + if not value: + return # Required error is done the field + obj = urlparse(value) + if obj.hostname not in ('forum.nottinghamtec.co.uk'): + raise ValidationError('URL must point to a location on the TEC Forum') + + @reversion.register(follow=['items']) class Event(models.Model, RevisionMixin): # Done to make it much nicer on the database @@ -357,6 +365,8 @@ class Event(models.Model, RevisionMixin): auth_request_at = models.DateTimeField(null=True, blank=True) auth_request_to = models.EmailField(blank=True, default='') + forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url]) + @property def display_id(self): if self.pk: @@ -505,7 +515,7 @@ class Event(models.Model, RevisionMixin): return reverse('event_detail', kwargs={'pk': self.pk}) def __str__(self): - return f"{self.display_id}: {self.name}" + return f"{self.display_id} | {self.name}" def clean(self): errdict = {} diff --git a/RIGS/templates/event_form.html b/RIGS/templates/event_form.html index 108ed4e5..fd1b462b 100644 --- a/RIGS/templates/event_form.html +++ b/RIGS/templates/event_form.html @@ -231,7 +231,7 @@ -
+
{% render_field form.start_date class+="form-control" %} @@ -246,7 +246,7 @@ -
+
{% render_field form.end_date class+="form-control" %} @@ -334,12 +334,26 @@
+ class="col-sm-4 col-form-label">{{ form.purchase_order.label }}
{% render_field form.purchase_order class+="form-control" %}
+ +
+ +
+

Paste URL

+ {% render_field form.forum_url class+="form-control" %} + {% if object.pk %} +

or

+ + + {% endif %} +
+
diff --git a/RIGS/templates/partials/event_details.html b/RIGS/templates/partials/event_details.html index 1b5a4e7d..6d7a249d 100644 --- a/RIGS/templates/partials/event_details.html +++ b/RIGS/templates/partials/event_details.html @@ -77,6 +77,15 @@
PO
{{ object.purchase_order }}
{% endif %} + +
Forum Thread
+ {% if object.forum_url %} +
{{object.forum_url}}
+ {% else %} + + {% endif %}
diff --git a/RIGS/urls.py b/RIGS/urls.py index 9a5a1f7c..8793b741 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -110,6 +110,9 @@ urlpatterns = [ path('event//checkin/add/', login_required(views.EventCheckInOverride.as_view()), name='event_checkin_override'), + path('event//thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'), + path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'), + # Finance path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()), name='invoice_list'), diff --git a/RIGS/views/rigboard.py b/RIGS/views/rigboard.py index d60f0c2b..6a9072a0 100644 --- a/RIGS/views/rigboard.py +++ b/RIGS/views/rigboard.py @@ -3,6 +3,12 @@ import datetime import re import premailer import simplejson +import urllib +import hmac +import hashlib + +from envparse import env +from bs4 import BeautifulSoup from django.conf import settings from django.contrib import messages @@ -19,6 +25,7 @@ from django.urls import reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator from django.views import generic +from django.views.decorators.csrf import csrf_exempt from PyRIGS import decorators from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related @@ -377,3 +384,41 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView): context['to_name'] = self.request.GET.get('to_name', None) context['target'] = 'event_authorise_form_preview' return context + + +class CreateForumThread(generic.base.RedirectView): + permanent = False + + def get_redirect_url(self, *args, **kwargs): + event = get_object_or_404(models.Event, pk=kwargs['pk']) + + if event.forum_url: + return event.forum_url + + params = { + 'title': str(event), + 'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}', + 'category': 'rig-info' + } + return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}' + + +class RecieveForumWebhook(generic.View): + @method_decorator(csrf_exempt) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}" + if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed): + return HttpResponseForbidden('Invalid signature header') + # Check if this is the right kind of event. The webhook filters by category on the forum side + if request.headers.get('X-Discourse-Event') == "topic_created": + body = simplejson.loads(request.body.decode('utf-8')) + event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros + event = models.Event.objects.filter(pk=event_id).first() + if event: + event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}" + event.save() + return HttpResponse(status=202) + return HttpResponse(status=204)