mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 05:22:16 +00:00
"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
This commit is contained in:
@@ -121,7 +121,7 @@ class EventForm(forms.ModelForm):
|
|||||||
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
||||||
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
||||||
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
||||||
'purchase_order', 'collector']
|
'purchase_order', 'collector', 'forum_url']
|
||||||
|
|
||||||
|
|
||||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||||
|
|||||||
19
RIGS/migrations/0050_event_forum_url.py
Normal file
19
RIGS/migrations/0050_event_forum_url.py
Normal file
@@ -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]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -308,6 +308,14 @@ class EventManager(models.Manager):
|
|||||||
return qs
|
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'])
|
@reversion.register(follow=['items'])
|
||||||
class Event(models.Model, RevisionMixin):
|
class Event(models.Model, RevisionMixin):
|
||||||
# Done to make it much nicer on the database
|
# 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_at = models.DateTimeField(null=True, blank=True)
|
||||||
auth_request_to = models.EmailField(blank=True, default='')
|
auth_request_to = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
|
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_id(self):
|
def display_id(self):
|
||||||
if self.pk:
|
if self.pk:
|
||||||
@@ -505,7 +515,7 @@ class Event(models.Model, RevisionMixin):
|
|||||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.display_id}: {self.name}"
|
return f"{self.display_id} | {self.name}"
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
errdict = {}
|
errdict = {}
|
||||||
|
|||||||
@@ -231,7 +231,7 @@
|
|||||||
<label for="{{ form.start_date.id_for_label }}"
|
<label for="{{ form.start_date.id_for_label }}"
|
||||||
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-10">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
|
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
|
||||||
{% render_field form.start_date class+="form-control" %}
|
{% render_field form.start_date class+="form-control" %}
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
<label for="{{ form.end_date.id_for_label }}"
|
<label for="{{ form.end_date.id_for_label }}"
|
||||||
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-10">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
|
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
|
||||||
{% render_field form.end_date class+="form-control" %}
|
{% render_field form.end_date class+="form-control" %}
|
||||||
@@ -334,12 +334,26 @@
|
|||||||
|
|
||||||
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
|
||||||
<label for="{{ form.purchase_order.id_for_label }}"
|
<label for="{{ form.purchase_order.id_for_label }}"
|
||||||
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
|
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
{% render_field form.purchase_order class+="form-control" %}
|
{% render_field form.purchase_order class+="form-control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" data-toggle="tooltip" title="The thread for this event on the TEC Forum">
|
||||||
|
<label for="{{ form.forum_url.id_for_label }}"
|
||||||
|
class="col-sm-4 col-form-label">Forum Thread</label>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<p class="small mb-0">Paste URL</p>
|
||||||
|
{% render_field form.forum_url class+="form-control" %}
|
||||||
|
{% if object.pk %}
|
||||||
|
<p class="small mb-0">or</p>
|
||||||
|
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread">
|
||||||
|
<span class="fas fa-plus"></span> <span class="hidden-xs">Create Forum Thread</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,6 +77,15 @@
|
|||||||
<dt class="col-sm-6">PO</dt>
|
<dt class="col-sm-6">PO</dt>
|
||||||
<dd class="col-sm-6">{{ object.purchase_order }}</dd>
|
<dd class="col-sm-6">{{ object.purchase_order }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<dt class="col-6">Forum Thread</dt>
|
||||||
|
{% if object.forum_url %}
|
||||||
|
<dd class="col-6"><a href="{{object.forum_url}}">{{object.forum_url}}</a></dd>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread"><span
|
||||||
|
class="fas fa-plus"></span> <span
|
||||||
|
class="hidden-xs">Create Forum Thread</span></a>
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ urlpatterns = [
|
|||||||
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()),
|
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()),
|
||||||
name='event_checkin_override'),
|
name='event_checkin_override'),
|
||||||
|
|
||||||
|
path('event/<int:pk>/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
|
# Finance
|
||||||
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
||||||
name='invoice_list'),
|
name='invoice_list'),
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import datetime
|
|||||||
import re
|
import re
|
||||||
import premailer
|
import premailer
|
||||||
import simplejson
|
import simplejson
|
||||||
|
import urllib
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from envparse import env
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -19,6 +25,7 @@ from django.urls import reverse_lazy
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from PyRIGS import decorators
|
from PyRIGS import decorators
|
||||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
|
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['to_name'] = self.request.GET.get('to_name', None)
|
||||||
context['target'] = 'event_authorise_form_preview'
|
context['target'] = 'event_authorise_form_preview'
|
||||||
return context
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user