From 4a4d4a5cf308be3faf81c8562cf39f8605863a94 Mon Sep 17 00:00:00 2001 From: Arona Jones Date: Sat, 29 Feb 2020 11:34:50 +0000 Subject: [PATCH 1/4] Add authorisation process for sign ups and allow access to EventDetail for basic users (#399) * CHANGE: First pass at opening up RIGS #233 Whilst it makes it something of a misnomer, the intent is to make the 'view_event' perm a permission to view event details like client/price. I don't see the point in giving everyone 'view_event' and adding a new 'view_event_detail'...Open to arguments the other way. * CHANGE: New user signups now require admin approval Given that I intend to reveal much more data to new users this seems necessary... * CHORE: Fix CI * FIX: Legacy Profiles are now auto-approved correctly * Add testing of approval mechanism This fixes the other functional tests failing because the user cannot login without being approved. * Superusers bypass approval check This should fix the remainder of the tests * Prevent unapproved users logging in through embeds Test suite doing its job...! * FIX: Require login on events and event embeds again Little too far to the open side there Arona... Whooooooops! * FIX: Use has_oembed decorator for events * FIX: Re-prevent basic seeing reversion This is to prevent financials/client data leaking when changed. Hopefully can show them a filtered version in future. * FIX: Remove mitigation for #264 Someone quietly fixed it, it appears * FEAT: Add admin email notif when an account is activated and awaiting approval No async or time-since shenanigans yet! * FIX: Whoops, undo accidental whitespace change * FEAT: Add a fifteen min cooldown between emails to admins Probably not the right way to go about it...but it does work! TODO: How to handle cooldown-emailing shared mailbox addresses? * FIX: Remove event modal history deadlink for basic users Also removes some links on the RIGS homepage that will deadlink for them * FIX: Wrong perms syntax for history pages * CHORE: Squash migrations * FIX: Use a setting for cooldown * FIX: Minor code improvements --- PyRIGS/settings.py | 9 +++-- RIGS/admin.py | 12 ++++++- RIGS/forms.py | 12 ++++++- RIGS/migrations/0036_profile_is_approved.py | 23 ++++++++++++ RIGS/migrations/0037_approve_legacy.py | 19 ++++++++++ RIGS/models.py | 10 ++++++ RIGS/signals.py | 36 +++++++++++++++++++ .../RIGS/admin_awaiting_approval.html | 9 +++++ .../RIGS/admin_awaiting_approval.txt | 5 +++ RIGS/templates/RIGS/event_detail.html | 22 ++++++++---- RIGS/templates/RIGS/event_embed.html | 10 +++--- RIGS/templates/RIGS/event_table.html | 2 +- RIGS/templates/RIGS/index.html | 7 ++-- RIGS/templates/RIGS/item_row.html | 8 +++-- RIGS/templates/RIGS/item_table.html | 6 ++++ RIGS/test_functional.py | 25 ++++++++++++- RIGS/urls.py | 5 ++- RIGS/views.py | 2 +- .../registration/activation_complete.html | 4 +-- 19 files changed, 196 insertions(+), 30 deletions(-) create mode 100644 RIGS/migrations/0036_profile_is_approved.py create mode 100644 RIGS/migrations/0037_approve_legacy.py create mode 100644 RIGS/templates/RIGS/admin_awaiting_approval.html create mode 100644 RIGS/templates/RIGS/admin_awaiting_approval.txt diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index 5877ad74..b787bdd1 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ import os import raven import secrets +import datetime BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -44,9 +45,9 @@ if not DEBUG: INTERNAL_IPS = ['127.0.0.1'] -ADMINS = ( - ('Tom Price', 'tomtom5152@gmail.com') -) +ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'), ('Arona Jones', 'arona.jones@nottinghamtec.co.uk')] +if DEBUG: + ADMINS.append(('Testing Superuser', 'superuser@example.com')) # Application definition @@ -182,6 +183,8 @@ if not DEBUG or EMAILER_TEST: else: EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_COOLDOWN = datetime.timedelta(minutes=15) + # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ diff --git a/RIGS/admin.py b/RIGS/admin.py index 49b8aa1e..846f014a 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -22,13 +22,22 @@ admin.site.register(models.Invoice) admin.site.register(models.Payment) +def approve_user(modeladmin, request, queryset): + queryset.update(is_approved=True) + + +approve_user.short_description = "Approve selected users" + + @admin.register(models.Profile) class ProfileAdmin(UserAdmin): + # Don't know how to add 'is_approved' whilst preserving the default list... + list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups') fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), { 'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}), - (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', + (_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), (_('Important dates'), { 'fields': ('last_login', 'date_joined')}), @@ -41,6 +50,7 @@ class ProfileAdmin(UserAdmin): ) form = forms.ProfileChangeForm add_form = forms.ProfileCreationForm + actions = [approve_user] class AssociateAdmin(VersionAdmin): diff --git a/RIGS/forms.py b/RIGS/forms.py index 47b0f062..a006bc87 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -2,8 +2,10 @@ from django import forms from django.utils import formats from django.conf import settings from django.core import serializers +from django.core.mail import EmailMessage, EmailMultiAlternatives from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm from registration.forms import RegistrationFormUniqueEmail +from django.contrib.auth.forms import AuthenticationForm from captcha.fields import ReCaptchaField import simplejson @@ -33,8 +35,16 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail): return self.cleaned_data['initials'] +class CheckApprovedForm(AuthenticationForm): + def confirm_login_allowed(self, user): + if user.is_approved or user.is_superuser: + return AuthenticationForm.confirm_login_allowed(self, user) + else: + raise forms.ValidationError("Your account hasn't been approved by an administrator yet. Please check back in a few minutes!") + + # Embedded Login form - remove the autofocus -class EmbeddedAuthenticationForm(AuthenticationForm): +class EmbeddedAuthenticationForm(CheckApprovedForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['username'].widget.attrs.pop('autofocus', None) diff --git a/RIGS/migrations/0036_profile_is_approved.py b/RIGS/migrations/0036_profile_is_approved.py new file mode 100644 index 00000000..3fdb5af7 --- /dev/null +++ b/RIGS/migrations/0036_profile_is_approved.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.13 on 2020-01-10 14:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0035_auto_20191124_1319'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='is_approved', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profile', + name='last_emailed', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/RIGS/migrations/0037_approve_legacy.py b/RIGS/migrations/0037_approve_legacy.py new file mode 100644 index 00000000..72348b3f --- /dev/null +++ b/RIGS/migrations/0037_approve_legacy.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.13 on 2020-01-11 18:29 +# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved +from django.db import migrations + +def approve_legacy(apps, schema_editor): + Profile = apps.get_model('RIGS', 'Profile') + for person in Profile.objects.all(): + person.is_approved = True + person.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0036_profile_is_approved'), + ] + + operations = [ + migrations.RunPython(approve_legacy) + ] diff --git a/RIGS/models.py b/RIGS/models.py index 937d5354..0b97ca61 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -27,6 +27,8 @@ class Profile(AbstractUser): initials = models.CharField(max_length=5, unique=True, null=True, blank=False) phone = models.CharField(max_length=13, null=True, blank=True) api_key = models.CharField(max_length=40, blank=True, editable=False, null=True) + is_approved = models.BooleanField(default=False) + last_emailed = models.DateTimeField(blank=True, null=True) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that... @classmethod def make_api_key(cls): @@ -53,6 +55,14 @@ class Profile(AbstractUser): def latest_events(self): return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') + @classmethod + def admins(cls): + return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x]) + + @classmethod + def users_awaiting_approval_count(cls): + return Profile.objects.filter(models.Q(is_approved=False)).count() + def __str__(self): return self.name diff --git a/RIGS/signals.py b/RIGS/signals.py index ea0395d7..5c5e6c66 100644 --- a/RIGS/signals.py +++ b/RIGS/signals.py @@ -1,3 +1,4 @@ +import datetime import re import urllib.request import urllib.error @@ -10,6 +11,9 @@ from django.conf import settings from django.contrib.staticfiles.storage import staticfiles_storage from django.core.mail import EmailMessage, EmailMultiAlternatives from django.template.loader import get_template +from django.urls import reverse +from django.utils import timezone +from registration.signals import user_activated from premailer import Premailer from z3c.rml import rml2pdf @@ -102,3 +106,35 @@ def on_revision_commit(sender, instance, created, **kwargs): post_save.connect(on_revision_commit, sender=models.EventAuthorisation) + + +def send_admin_awaiting_approval_email(user, request, **kwargs): + # Bit more controlled than just emailing all superusers + for admin in models.Profile.admins(): + # Check we've ever emailed them before and if so, if cooldown has passed. + if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now(): + context = { + 'request': request, + 'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0', + 'number_of_users': models.Profile.users_awaiting_approval_count(), + 'to_name': admin.first_name + } + + email = EmailMultiAlternatives( + "%s new users awaiting approval on RIGS" % (context['number_of_users']), + get_template("RIGS/admin_awaiting_approval.txt").render(context), + to=[admin.email], + reply_to=[user.email], + ) + css = staticfiles_storage.path('css/email.css') + html = Premailer(get_template("RIGS/admin_awaiting_approval.html").render(context), + external_styles=css).transform() + email.attach_alternative(html, 'text/html') + email.send() + + # Update last sent + admin.last_emailed = timezone.now() + admin.save() + + +user_activated.connect(send_admin_awaiting_approval_email) diff --git a/RIGS/templates/RIGS/admin_awaiting_approval.html b/RIGS/templates/RIGS/admin_awaiting_approval.html new file mode 100644 index 00000000..8db5433c --- /dev/null +++ b/RIGS/templates/RIGS/admin_awaiting_approval.html @@ -0,0 +1,9 @@ +{% extends 'base_client_email.html' %} + +{% block content %} +

Hi {{ to_name|default_if_none:"Administrator" }},

+ +

{{ number_of_users|default_if_none:"Some" }} new users are awaiting administrator approval on RIGS. Click here to approve them.

+ +

TEC PA & Lighting

+{% endblock %} diff --git a/RIGS/templates/RIGS/admin_awaiting_approval.txt b/RIGS/templates/RIGS/admin_awaiting_approval.txt new file mode 100644 index 00000000..e89785ee --- /dev/null +++ b/RIGS/templates/RIGS/admin_awaiting_approval.txt @@ -0,0 +1,5 @@ +Hi {{ to_name|default_if_none:"Administrator" }}, + +{{ number_of_users|default_if_none:"Some" }} new users are awaiting administrator approval on RIGS. Use this link to approve them: {{ request.scheme }}://{{ request.get_host }}/{{ link_suffix }} + +TEC PA & Lighting diff --git a/RIGS/templates/RIGS/event_detail.html b/RIGS/templates/RIGS/event_detail.html index e22dafe9..10a947fe 100644 --- a/RIGS/templates/RIGS/event_detail.html +++ b/RIGS/templates/RIGS/event_detail.html @@ -10,12 +10,14 @@ | {{ object.name }} {% if event.dry_hire %}Dry Hire{% endif %} + {% if perms.RIGS.view_event %}
{% include 'RIGS/event_detail_buttons.html' %}
+ {% endif %} {% endif %} - {% if object.is_rig %} + {% if object.is_rig and perms.RIGS.view_event %} {# only need contact details for a rig #}
@@ -72,7 +74,7 @@ {% endif %}
{% endif %} -
+
Event Info
@@ -147,7 +149,7 @@
{{ object.collector }}
{% endif %} - {% if event.is_rig and not event.internal %} + {% if event.is_rig and not event.internal and perms.RIGS.view_event %}
 
PO
{{ object.purchase_order }}
@@ -156,7 +158,7 @@
- {% if event.is_rig and event.internal %} + {% if event.is_rig and event.internal and perms.RIGS.view_event %}
{% include 'RIGS/event_detail_buttons.html' %}
@@ -222,21 +224,23 @@
Event Details
+ {% if perms.RIGS.view_event %}

Notes

{{ event.notes|linebreaksbr }}
+ {% endif %} {% include 'RIGS/item_table.html' %}
- {% if not request.is_ajax %} + {% if not request.is_ajax and perms.RIGS.view_event %}
{% include 'RIGS/event_detail_buttons.html' %}
{% endif %} {% endif %} - {% if not request.is_ajax %} + {% if not request.is_ajax and perms.RIGS.view_event %}
@@ -251,12 +255,16 @@ {% if request.is_ajax %} {% block footer %}
+ {% if perms.RIGS.view_event %}
+ {% else %} +
+ {% endif %}
Open Event Page diff --git a/RIGS/templates/RIGS/event_embed.html b/RIGS/templates/RIGS/event_embed.html index a6e3e586..78816cae 100644 --- a/RIGS/templates/RIGS/event_embed.html +++ b/RIGS/templates/RIGS/event_embed.html @@ -6,7 +6,7 @@
- + {% if object.meet_at %}

Crew meet: @@ -97,7 +97,7 @@ {{ object.description|linebreaksbr }}

{% endif %} - +
diff --git a/RIGS/templates/RIGS/event_table.html b/RIGS/templates/RIGS/event_table.html index c84b6624..f1bdb5f6 100644 --- a/RIGS/templates/RIGS/event_table.html +++ b/RIGS/templates/RIGS/event_table.html @@ -33,7 +33,7 @@

- + {{ event.name }} {% if event.venue %} diff --git a/RIGS/templates/RIGS/index.html b/RIGS/templates/RIGS/index.html index acd1a707..29b7c320 100644 --- a/RIGS/templates/RIGS/index.html +++ b/RIGS/templates/RIGS/index.html @@ -11,7 +11,7 @@

- +

Quick Links

@@ -26,10 +26,11 @@ TEC Forum TEC Wiki + {% if perms.RIGS.view_event %} Pre-Event Risk Assessment Price List Subhire Insurance Form - + {% endif %}
@@ -73,7 +74,7 @@
{% if perms.RIGS.view_event %}
- {% include 'RIGS/activity_feed.html' %} + {% include 'RIGS/activity_feed.html' %}
{% endif %}
diff --git a/RIGS/templates/RIGS/item_row.html b/RIGS/templates/RIGS/item_row.html index 656d9812..b2ff0c44 100644 --- a/RIGS/templates/RIGS/item_row.html +++ b/RIGS/templates/RIGS/item_row.html @@ -6,17 +6,21 @@ {{item.description|linebreaksbr}}
+ {% if perms.RIGS.view_event %} £ {{item.cost|floatformat:2}} + {% endif %} {{item.quantity}} + {% if perms.RIGS.view_event %} £ {{item.total_cost|floatformat:2}} + {% endif %} {% if edit %} - - diff --git a/RIGS/templates/RIGS/item_table.html b/RIGS/templates/RIGS/item_table.html index 0c652daf..9f055aa9 100644 --- a/RIGS/templates/RIGS/item_table.html +++ b/RIGS/templates/RIGS/item_table.html @@ -3,9 +3,13 @@ Item + {% if perms.RIGS.view_event %} Price + {% endif %} Quantity + {% if perms.RIGS.view_event %} Sub-total + {% endif %} {% if edit %}
diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py index 691f5294..dc7692a6 100644 --- a/RIGS/test_functional.py +++ b/RIGS/test_functional.py @@ -141,18 +141,41 @@ class UserRegistrationTest(LiveServerTestCase): self.assertEqual(password.get_attribute('placeholder'), 'Password') self.assertEqual(password.get_attribute('type'), 'password') + # Expected to fail as not approved username.send_keys('TestUsername') password.send_keys('correcthorsebatterystaple') self.browser.execute_script( "return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()") password.send_keys(Keys.ENTER) + # Test approval + profileObject = models.Profile.objects.all()[0] + self.assertFalse(profileObject.is_approved) + + # Read what the error is + alert = self.browser.find_element_by_css_selector( + 'div.alert-danger').text + self.assertIn("approved", alert) + + # Approve the user so we can proceed + profileObject.is_approved = True + profileObject.save() + + # Retry login + self.browser.get(self.live_server_url + '/user/login') + username = self.browser.find_element_by_id('id_username') + username.send_keys('TestUsername') + password = self.browser.find_element_by_id('id_password') + password.send_keys('correcthorsebatterystaple') + self.browser.execute_script( + "return function() {jQuery('#g-recaptcha-response').val('PASSED'); return 0}()") + password.send_keys(Keys.ENTER) + # Check we are logged in udd = self.browser.find_element_by_class_name('navbar').text self.assertIn('Hi John', udd) # Check all the data actually got saved - profileObject = models.Profile.objects.all()[0] self.assertEqual(profileObject.username, 'TestUsername') self.assertEqual(profileObject.first_name, 'John') self.assertEqual(profileObject.last_name, 'Smith') diff --git a/RIGS/urls.py b/RIGS/urls.py index 46e70f10..ab897d85 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -6,7 +6,7 @@ from RIGS import models, views, rigboard, finance, ical, versioning, forms from django.views.generic import RedirectView from django.views.decorators.clickjacking import xframe_options_exempt -from PyRIGS.decorators import permission_required_with_403 +from PyRIGS.decorators import permission_required_with_403, has_oembed from PyRIGS.decorators import api_key_required urlpatterns = [ @@ -87,8 +87,7 @@ urlpatterns = [ permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()), name='activity_feed'), - url(r'^event/(?P\d+)/$', - permission_required_with_403('RIGS.view_event', oembed_view="event_oembed")( + url(r'^event/(?P\d+)/$', has_oembed(oembed_view="event_oembed")( rigboard.EventDetail.as_view()), name='event_detail'), url(r'^event/(?P\d+)/embed/$', diff --git a/RIGS/views.py b/RIGS/views.py index f8494e25..1faa244d 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -41,7 +41,7 @@ def login(request, **kwargs): else: from django.contrib.auth.views import login - return login(request) + return login(request, authentication_form=forms.CheckApprovedForm) # This view should be exempt from requiring CSRF token. diff --git a/templates/registration/activation_complete.html b/templates/registration/activation_complete.html index 5aed33e9..470ee14d 100644 --- a/templates/registration/activation_complete.html +++ b/templates/registration/activation_complete.html @@ -5,6 +5,6 @@ {% block content %}

Activation Complete

-

You user account is now fully registered. Enjoy RIGS

+

Your user account is now awaiting administrator approval. Won't be long!

-{% endblock %} \ No newline at end of file +{% endblock %} From 797ad778a9683d37cc19946c2f2aeb0a34fa0d02 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Sat, 29 Feb 2020 11:57:33 +0000 Subject: [PATCH 2/4] Improve search logic and allow search of event archive (#248) * Added search to person, venue, organisation and event archive * Added search to invoice archive * Added event search to homepage * Tidy up event search logic and optimise * Fixed merge issues * Stopped 404 on failed search * Set default ordering of people, organisations & venues to alphabetical (rather than order of addition to database) * Added invoice search to home page (if you have permissions) * Made invoice archive sort by reverse invoice date (rather than order added to database) * Added search help page (very pretty) * Made single search box for all search types * FIX: Missing date field breaking archive view * FEAT: Add omnisearch to header Tis a bit broken on mobile at the moment... * CHORE: Conform old code to pep8 * FIX: Select the event form, not the search one in tests! * Revert "FEAT: Add omnisearch to header" This reverts commit 6bcb242d6be25caef7f9c2d8619e12faa4f22b3b because it caused MANY more problems than anticipated... * FIX: Stop 404 on failed search, again * FEAT: Basic testing of search * Use a tooltip to help explain the UX Obviously since it needs a tooltip it isn't brilliant UX but the best I can think of for now... Co-authored-by: Tom Price Co-authored-by: David Taylor Co-authored-by: Arona Jones --- RIGS/finance.py | 29 +++++ RIGS/rigboard.py | 47 ++++++-- RIGS/templates/RIGS/event_archive.html | 53 +++++---- RIGS/templates/RIGS/index.html | 64 ++++++----- RIGS/templates/RIGS/invoice_list.html | 1 + RIGS/templates/RIGS/invoice_list_archive.html | 11 ++ RIGS/templates/RIGS/search_help.html | 70 ++++++++++++ RIGS/test_functional.py | 60 ++++++++-- RIGS/test_unit.py | 104 ++++++++++++++++++ RIGS/urls.py | 2 + RIGS/views.py | 61 +++++++--- 11 files changed, 423 insertions(+), 79 deletions(-) create mode 100644 RIGS/templates/RIGS/search_help.html diff --git a/RIGS/finance.py b/RIGS/finance.py index b64b2e71..d536969b 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -11,6 +11,7 @@ from django.template.loader import get_template from django.views import generic from django.db.models import Q from z3c.rml import rml2pdf +from django.db.models import Q from RIGS import models @@ -122,6 +123,34 @@ class InvoiceArchive(generic.ListView): template_name = 'RIGS/invoice_list_archive.html' paginate_by = 25 + def get_queryset(self): + q = self.request.GET.get('q', "") + + filter = Q(event__name__icontains=q) + + # try and parse an int + try: + val = int(q) + filter = filter | Q(pk=val) + filter = filter | Q(event__pk=val) + except: # noqa + # not an integer + pass + + try: + if q[0] == "N": + val = int(q[1:]) + filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number + elif q[0] == "#": + val = int(q[1:]) + filter = Q(pk=val) # If string is #xxxxx then filter by invoice number + except: # noqa + pass + + object_list = self.model.objects.filter(filter).order_by('-invoice_date') + + return object_list + class InvoiceWaiting(generic.ListView): model = models.Event diff --git a/RIGS/rigboard.py b/RIGS/rigboard.py index abb0ed9e..ae960f4f 100644 --- a/RIGS/rigboard.py +++ b/RIGS/rigboard.py @@ -226,10 +226,18 @@ class EventPrint(generic.View): return response -class EventArchive(generic.ArchiveIndexView): +class EventArchive(generic.ListView): model = models.Event - date_field = "start_date" paginate_by = 25 + template_name = "RIGS/event_archive.html" + + def get_context_data(self, **kwargs): + # get super context + context = super(EventArchive, self).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')) + return context def get_queryset(self): start = self.request.GET.get('start', None) @@ -241,19 +249,34 @@ class EventArchive(generic.ArchiveIndexView): "Muppet! Check the dates, it has been fixed for you.") start, end = end, start # Stop the impending fail - filter = False + filter = Q() if end != "": - filter = Q(start_date__lte=end) + filter &= Q(start_date__lte=end) if start: - if filter: - filter = filter & Q(start_date__gte=start) - else: - filter = Q(start_date__gte=start) + filter &= Q(start_date__gte=start) - if filter: - qs = self.model.objects.filter(filter).order_by('-start_date') - else: - qs = self.model.objects.all().order_by('-start_date') + q = self.request.GET.get('q', "") + + if q is not "": + qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q) + + # try and parse an int + try: + val = int(q) + qfilter = qfilter | Q(pk=val) + except: # noqa not an integer + pass + + try: + if q[0] == "N": + val = int(q[1:]) + qfilter = Q(pk=val) # If string is N###### then do a simple PK filter + except: # noqa + pass + + filter &= qfilter + + qs = self.model.objects.filter(filter).order_by('-start_date') # Preselect related for efficiency qs.select_related('person', 'organisation', 'venue', 'mic') diff --git a/RIGS/templates/RIGS/event_archive.html b/RIGS/templates/RIGS/event_archive.html index 4fc5642e..18459073 100644 --- a/RIGS/templates/RIGS/event_archive.html +++ b/RIGS/templates/RIGS/event_archive.html @@ -5,34 +5,49 @@ {% block content %}
-

Event Archive

- -
diff --git a/RIGS/templates/RIGS/invoice_list_archive.html b/RIGS/templates/RIGS/invoice_list_archive.html index 77bab204..f0335f2f 100644 --- a/RIGS/templates/RIGS/invoice_list_archive.html +++ b/RIGS/templates/RIGS/invoice_list_archive.html @@ -10,4 +10,15 @@ All Invoices {% block description %}

This page displays all invoices: outstanding, paid, and void

+{% endblock %} + +{% block search %} +
+
+
+ +
+ +
{% endblock %} \ No newline at end of file diff --git a/RIGS/templates/RIGS/search_help.html b/RIGS/templates/RIGS/search_help.html new file mode 100644 index 00000000..9b460043 --- /dev/null +++ b/RIGS/templates/RIGS/search_help.html @@ -0,0 +1,70 @@ +{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %} + +{% block title %}Search Help{% endblock %} + +{% block content %} +
+ {% if not request.is_ajax %} +
+

Search Help

+
+ {% endif %} +
+
+
+

Searching Events

+
+
+

+ Searches for entire query in: + + and + +

+

You can search for an event by by entering an integer, or using the format N01234

+

On the search results page you can also specify the date range for the of the event

+

Events are sorted in reverse order (most recent events at the top)

+
+
+ +
+
+

Searching People/Organisations/Venues

+
+
+

+ Searches for entire search phrase in: + + + + and + +

+

You can search for an entry by by entering an integer

+

Entries are sorted in alphabetical order by

+
+
+ + {% if perms.RIGS.view_invoice %} +
+
+

Searching Invoices

+
+
+

+ Searches for entire search phrase in: + +

+

You can search for an event's invoice by entering the using the format N01234

+

You can search for an invoice by using the format #01234

+

Entering a raw integer will search by both and

+

Entries are sorted in reverse order

+
+
+ {% endif %} + + + +
+
+{% endblock %} \ No newline at end of file diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py index dc7692a6..df4863c1 100644 --- a/RIGS/test_functional.py +++ b/RIGS/test_functional.py @@ -254,7 +254,7 @@ class EventTest(LiveServerTestCase): # Slider expands and save button visible self.assertTrue(save.is_displayed()) - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form') # For now, just check that HTML5 Client validation is in place TODO Test needs rewriting to properly test all levels of validation. self.assertTrue(self.browser.find_element_by_id('id_name').get_attribute('required') is not None) @@ -492,7 +492,7 @@ class EventTest(LiveServerTestCase): save = self.browser.find_element_by_xpath( '(//button[@type="submit"])[3]') - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form') # Check the items are visible table = self.browser.find_element_by_id('item-table') # ID number is known, see above @@ -574,7 +574,7 @@ class EventTest(LiveServerTestCase): # Click Rig button self.browser.find_element_by_xpath('//button[.="Rig"]').click() - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('//*[@id="content"]/form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') # Set title @@ -604,7 +604,7 @@ class EventTest(LiveServerTestCase): self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text) # Same date, end time before start time - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") @@ -623,7 +623,7 @@ class EventTest(LiveServerTestCase): self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text) # Same date, end time before start time - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") @@ -636,7 +636,7 @@ class EventTest(LiveServerTestCase): form.find_element_by_id('id_end_time').send_keys('06:00') # No end date, end time before start time - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") @@ -655,7 +655,7 @@ class EventTest(LiveServerTestCase): self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text) # 2 dates, end after start - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'") self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-26'") @@ -688,7 +688,7 @@ class EventTest(LiveServerTestCase): # Click Rig button self.browser.find_element_by_xpath('//button[.="Rig"]').click() - form = self.browser.find_element_by_tag_name('form') + form = self.browser.find_element_by_xpath('/html/body/div[2]/div[1]/form') save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]') # Set title @@ -1222,3 +1222,47 @@ class TECEventAuthorisationTest(TestCase): self.assertEqual(self.event.auth_request_by, self.profile) self.assertEqual(self.event.auth_request_to, 'client@functional.test') self.assertIsNotNone(self.event.auth_request_at) + + +class SearchTest(LiveServerTestCase): + def setUp(self): + self.profile = models.Profile( + username="SearchTest", first_name="Search", last_name="Test", initials="STU", is_superuser=True) + self.profile.set_password("SearchTestPassword") + self.profile.save() + + self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + + self.browser = create_browser() + self.browser.implicitly_wait(10) # Set implicit wait session wide + + os.environ['RECAPTCHA_TESTING'] = 'True' + + models.Event.objects.create(name="Right Event", status=models.Event.PROVISIONAL, + start_date=date.today(), description="This event is searched for endlessly over and over") + models.Event.objects.create(name="Wrong Event", status=models.Event.PROVISIONAL, start_date=date.today(), + description="This one should never be found.") + + def tearDown(self): + self.browser.quit() + os.environ['RECAPTCHA_TESTING'] = 'False' + + def test_search(self): + self.browser.get(self.live_server_url) + username = self.browser.find_element_by_id('id_username') + password = self.browser.find_element_by_id('id_password') + submit = self.browser.find_element_by_css_selector( + 'input[type=submit]') + + username.send_keys("SearchTest") + password.send_keys("SearchTestPassword") + submit.click() + + form = self.browser.find_element_by_id('searchForm') + search_box = form.find_element_by_id('id_search_input') + search_box.send_keys('Right') + search_box.send_keys(Keys.ENTER) + + event_name = self.browser.find_element_by_xpath('//*[@id="content"]/div[1]/div[4]/div/div/table/tbody/tr[1]/td[3]/h4').text + self.assertIn('Right', event_name) + self.assertNotIn('Wrong', event_name) diff --git a/RIGS/test_unit.py b/RIGS/test_unit.py index 0a360a10..fb89cfb7 100644 --- a/RIGS/test_unit.py +++ b/RIGS/test_unit.py @@ -423,3 +423,107 @@ class TestSampleDataGenerator(TestCase): from django.core.management.base import CommandError self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData') + + +class TestSearchLogic(TestCase): + @classmethod + def setUpTestData(cls): + cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, + is_active=True, is_staff=True) + + cls.persons = { + 1: models.Person.objects.create(name="Right Person", phone="1234"), + 2: models.Person.objects.create(name="Wrong Person", phone="5678"), + } + + cls.organisations = { + 1: models.Organisation.objects.create(name="Right Organisation", email="test@example.com"), + 2: models.Organisation.objects.create(name="Wrong Organisation", email="check@fake.co.uk"), + } + + cls.venues = { + 1: models.Venue.objects.create(name="Right Venue", address="1 Test Street, EX1"), + 2: models.Venue.objects.create(name="Wrong Venue", address="2 Check Way, TS2"), + } + + cls.events = { + 1: models.Event.objects.create(name="Right Event", start_date=date.today(), person=cls.persons[1], + organisation=cls.organisations[1], venue=cls.venues[1]), + 2: models.Event.objects.create(name="Wrong Event", start_date=date.today(), person=cls.persons[2], + organisation=cls.organisations[2], venue=cls.venues[2]), + } + + def setUp(self): + self.profile.set_password('testuser') + self.profile.save() + self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + + def test_event_search(self): + # Test search by name + request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].name) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.events[1].name) + self.assertNotContains(response, self.events[2].name) + + # Test search by ID + request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].pk) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.events[1].name) + self.assertNotContains(response, self.events[2].name) + + def test_people_search(self): + # Test search by name + request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].name) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.persons[1].name) + self.assertNotContains(response, self.persons[2].name) + + # Test search by ID + request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].pk) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.persons[1].name) + self.assertNotContains(response, self.persons[2].name) + + # Test search by phone + request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].phone) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.persons[1].name) + self.assertNotContains(response, self.persons[2].name) + + def test_organisation_search(self): + # Test search by name + request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].name) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.organisations[1].name) + self.assertNotContains(response, self.organisations[2].name) + + # Test search by ID + request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].pk) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.organisations[1].name) + self.assertNotContains(response, self.organisations[2].name) + + # Test search by email + request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].email) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.organisations[1].email) + self.assertNotContains(response, self.organisations[2].email) + + def test_venue_search(self): + # Test search by name + request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].name) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.venues[1].name) + self.assertNotContains(response, self.venues[2].name) + + # Test search by ID + request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].pk) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.venues[1].name) + self.assertNotContains(response, self.venues[2].name) + + # Test search by address + request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].address) + response = self.client.get(request_url, follow=True) + self.assertContains(response, self.venues[1].address) + self.assertNotContains(response, self.venues[2].address) diff --git a/RIGS/urls.py b/RIGS/urls.py index ab897d85..006270c9 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -21,6 +21,8 @@ urlpatterns = [ url(r'^user/password_reset/$', views.PasswordResetDisabled.as_view()), + url(r'^search_help/$', views.SearchHelp.as_view(), name='search_help'), + # People url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()), name='person_list'), diff --git a/RIGS/views.py b/RIGS/views.py index 1faa244d..db2c6a2d 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -44,6 +44,10 @@ def login(request, **kwargs): return login(request, authentication_form=forms.CheckApprovedForm) +class SearchHelp(generic.TemplateView): + template_name = 'RIGS/search_help.html' + + # This view should be exempt from requiring CSRF token. # Then we can check for it and show a nice error # Don't worry, django.contrib.auth.views.login will @@ -86,11 +90,20 @@ class PersonList(generic.ListView): def get_queryset(self): q = self.request.GET.get('q', "") - if len(q) >= 3: - object_list = self.model.objects.filter(Q(name__icontains=q) | Q(email__icontains=q)) - else: - object_list = self.model.objects.all() - orderBy = self.request.GET.get('orderBy', None) + + filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q) + + # try and parse an int + try: + val = int(q) + filter = filter | Q(pk=val) + except: # noqa + # not an integer + pass + + object_list = self.model.objects.filter(filter) + + orderBy = self.request.GET.get('orderBy', 'name') if orderBy is not None: object_list = object_list.order_by(orderBy) return object_list @@ -140,11 +153,20 @@ class OrganisationList(generic.ListView): def get_queryset(self): q = self.request.GET.get('q', "") - if len(q) >= 3: - object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q)) - else: - object_list = self.model.objects.all() - orderBy = self.request.GET.get('orderBy', "") + + filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q) + + # try and parse an int + try: + val = int(q) + filter = filter | Q(pk=val) + except: # noqa + # not an integer + pass + + object_list = self.model.objects.filter(filter) + + orderBy = self.request.GET.get('orderBy', "name") if orderBy is not "": object_list = object_list.order_by(orderBy) return object_list @@ -194,11 +216,20 @@ class VenueList(generic.ListView): def get_queryset(self): q = self.request.GET.get('q', "") - if len(q) >= 3: - object_list = self.model.objects.filter(Q(name__icontains=q) | Q(address__icontains=q)) - else: - object_list = self.model.objects.all() - orderBy = self.request.GET.get('orderBy', "") + + filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(phone__startswith=q) | Q(phone__endswith=q) + + # try and parse an int + try: + val = int(q) + filter = filter | Q(pk=val) + except: # noqa + # not an integer + pass + + object_list = self.model.objects.filter(filter) + + orderBy = self.request.GET.get('orderBy', "name") if orderBy is not "": object_list = object_list.order_by(orderBy) return object_list From 8568c591a9954a612e8f35965c3497c0fe5a53e5 Mon Sep 17 00:00:00 2001 From: Arona Jones Date: Sat, 7 Mar 2020 16:21:48 +0000 Subject: [PATCH 3/4] Update Python Dependencies (#404) * [requires.io] dependency update * Server starts... Various things are broken, but it runs! * [requires.io] dependency update * [requires.io] dependency update * [requires.io] dependency update * FIX: Broken migrations * FIX: Update auth framework * FIX: Correct static use in templates * FIX: Fix supplier sort * FIX: Remaining tests * Revert "Disable password reset as temporary fix to vulnerability (#396)" This reverts commit e0c6a56263d4e6b1034d9bfe42b14f04624cbdfe. # Conflicts: # RIGS/urls.py * FIX: Fix broken newlining in PDFs Introduced by a change in Django 2.1 'HTML rendered by form widgets no longer includes a closing slash on void elements, e.g.
. This is incompatible within XHTML, although some widgets already used aspects of HTML5 such as boolean attributes.' * FIX: Fix some Django4 deprecation warnings Why not... * Refactor dependency file Should now only include dependencies we actually use, not dependencies of dependencies and unused things * Add newlines to the paperwork print test event This will catch the error encountered in 79ec9214f972099bbf495a8db5c3a61a996831ad * Swap to pycodestyle rather than pep8 in Travis And eliminate W605 errors * Bit too heavy handed with the dep purge there... * Whoops, helps if one installs pycodestyle... * FIX: Re-add overridden login view * Better fix for previous commit * FIX: Bloody smartquotes Co-authored-by: requires.io --- .travis.yml | 4 +- PyRIGS/settings.py | 3 +- PyRIGS/tests/base.py | 2 +- PyRIGS/urls.py | 5 +- RIGS/admin.py | 2 +- RIGS/finance.py | 2 +- RIGS/migrations/0038_auto_20200306_2000.py | 37 ++++++++++++ RIGS/models.py | 39 +----------- RIGS/rigboard.py | 5 +- RIGS/signals.py | 2 +- RIGS/templates/RIGS/event_embed.html | 7 +-- RIGS/templates/RIGS/event_print_page.xml | 15 +++-- RIGS/templates/RIGS/invoice_detail.html | 2 +- RIGS/templates/RIGS/item_modal.html | 5 +- RIGS/templates/RIGS/item_table.html | 2 +- .../RIGS/password_reset_disable.html | 9 --- RIGS/templatetags/filters.py | 14 +++++ RIGS/test_functional.py | 9 ++- RIGS/test_models.py | 20 +++---- RIGS/test_unit.py | 2 +- RIGS/urls.py | 10 ++-- RIGS/versioning.py | 3 +- RIGS/views.py | 28 ++------- ...8_1451_squashed_0021_auto_20190105_1156.py | 4 +- assets/migrations/0010_auto_20200207_1737.py | 17 ------ assets/migrations/0010_auto_20200219_1444.py | 21 +++++++ assets/models.py | 14 ++--- assets/templates/asset_embed.html | 5 +- assets/tests/test_assets.py | 2 +- requirements.txt | 59 +++++++------------ templates/400.html | 2 +- templates/401.html | 2 +- templates/403.html | 2 +- templates/404.html | 2 +- templates/500.html | 2 +- templates/base.html | 3 +- templates/base_client.html | 2 +- templates/base_client_email.html | 12 +--- templates/base_embed.html | 2 +- templates/login_redirect.html | 2 +- templates/registration/loginform.html | 2 - 41 files changed, 169 insertions(+), 213 deletions(-) create mode 100644 RIGS/migrations/0038_auto_20200306_2000.py delete mode 100644 RIGS/templates/RIGS/password_reset_disable.html delete mode 100644 assets/migrations/0010_auto_20200207_1737.py create mode 100644 assets/migrations/0010_auto_20200219_1444.py diff --git a/.travis.yml b/.travis.yml index b2527c43..4eca0630 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,14 +12,14 @@ install: - export PATH=$PATH:$(pwd) - chmod +x chromedriver - pip install -r requirements.txt - - pip install coveralls codeclimate-test-reporter pep8 + - pip install coveralls codeclimate-test-reporter pycodestyle before_script: - export PATH=$PATH:/usr/lib/chromium-browser/ - python manage.py collectstatic --noinput script: - - pep8 . --exclude=migrations,importer* + - pycodestyle . --exclude=migrations,importer* - python manage.py check - python manage.py makemigrations --check --dry-run - coverage run manage.py test --verbosity=2 diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index b787bdd1..6d2fbbd6 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -50,7 +50,6 @@ if DEBUG: ADMINS.append(('Testing Superuser', 'superuser@example.com')) # Application definition - INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', @@ -169,6 +168,8 @@ RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', "6LeIxAcTAAAAAJcZV RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key NOCAPTCHA = True +SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error'] + # Email EMAILER_TEST = False if not DEBUG or EMAILER_TEST: diff --git a/PyRIGS/tests/base.py b/PyRIGS/tests/base.py index ecdacd21..4beb1eeb 100644 --- a/PyRIGS/tests/base.py +++ b/PyRIGS/tests/base.py @@ -11,7 +11,7 @@ def create_browser(): if os.environ.get('CI', False): options.add_argument("--headless") options.add_argument("--no-sandbox") - driver = webdriver.Chrome(chrome_options=options) + driver = webdriver.Chrome(options=options) return driver diff --git a/PyRIGS/urls.py b/PyRIGS/urls.py index cb78130c..9ef4fa53 100644 --- a/PyRIGS/urls.py +++ b/PyRIGS/urls.py @@ -1,3 +1,4 @@ +from django.urls import path from django.conf.urls import include, url from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns @@ -15,8 +16,8 @@ urlpatterns = [ url('^assets/', include('assets.urls')), url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail), name="registration_register"), - url('^user/', include('django.contrib.auth.urls')), - url('^user/', include('registration.backends.default.urls')), + path('user/', include('django.contrib.auth.urls')), + path('user/', include('registration.backends.default.urls')), url(r'^admin/', admin.site.urls), ] diff --git a/RIGS/admin.py b/RIGS/admin.py index 846f014a..65442968 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from RIGS import models, forms from django.contrib.auth.admin import UserAdmin -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from reversion.admin import VersionAdmin from django.contrib.admin import helpers diff --git a/RIGS/finance.py b/RIGS/finance.py index d536969b..061c81a8 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -77,7 +77,7 @@ class InvoicePrint(generic.View): pdfData = buffer.read() - escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name) + escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name) response = HttpResponse(content_type='application/pdf') response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName) diff --git a/RIGS/migrations/0038_auto_20200306_2000.py b/RIGS/migrations/0038_auto_20200306_2000.py new file mode 100644 index 00000000..f1f893e0 --- /dev/null +++ b/RIGS/migrations/0038_auto_20200306_2000.py @@ -0,0 +1,37 @@ +# Generated by Django 2.0.13 on 2020-03-06 20:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0037_approve_legacy'), + ] + + operations = [ + migrations.AlterModelOptions( + name='event', + options={}, + ), + migrations.AlterModelOptions( + name='invoice', + options={'ordering': ['-invoice_date']}, + ), + migrations.AlterModelOptions( + name='organisation', + options={}, + ), + migrations.AlterModelOptions( + name='person', + options={}, + ), + migrations.AlterModelOptions( + name='profile', + options={'verbose_name': 'user', 'verbose_name_plural': 'users'}, + ), + migrations.AlterModelOptions( + name='venue', + options={}, + ), + ] diff --git a/RIGS/models.py b/RIGS/models.py index 0b97ca61..2b22da05 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -8,7 +8,6 @@ from django.contrib.auth.models import AbstractUser from django.conf import settings from django.utils import timezone from django.utils.functional import cached_property -from django.utils.encoding import python_2_unicode_compatible from reversion import revisions as reversion from reversion.models import Version import string @@ -22,7 +21,6 @@ from django.urls import reverse_lazy # Create your models here. -@python_2_unicode_compatible class Profile(AbstractUser): initials = models.CharField(max_length=5, unique=True, null=True, blank=False) phone = models.CharField(max_length=13, null=True, blank=True) @@ -66,11 +64,6 @@ class Profile(AbstractUser): def __str__(self): return self.name - class Meta: - permissions = ( - ('view_profile', 'Can view Profile'), - ) - class RevisionMixin(object): @property @@ -101,7 +94,6 @@ class RevisionMixin(object): @reversion.register -@python_2_unicode_compatible class Person(models.Model, RevisionMixin): name = models.CharField(max_length=50) phone = models.CharField(max_length=15, blank=True, null=True) @@ -137,14 +129,8 @@ class Person(models.Model, RevisionMixin): def get_absolute_url(self): return reverse_lazy('person_detail', kwargs={'pk': self.pk}) - class Meta: - permissions = ( - ('view_person', 'Can view Persons'), - ) - @reversion.register -@python_2_unicode_compatible class Organisation(models.Model, RevisionMixin): name = models.CharField(max_length=50) phone = models.CharField(max_length=15, blank=True, null=True) @@ -181,11 +167,6 @@ class Organisation(models.Model, RevisionMixin): def get_absolute_url(self): return reverse_lazy('organisation_detail', kwargs={'pk': self.pk}) - class Meta: - permissions = ( - ('view_organisation', 'Can view Organisations'), - ) - class VatManager(models.Manager): def current_rate(self): @@ -202,7 +183,6 @@ class VatManager(models.Manager): @reversion.register -@python_2_unicode_compatible class VatRate(models.Model, RevisionMixin): start_at = models.DateField() rate = models.DecimalField(max_digits=6, decimal_places=6) @@ -223,7 +203,6 @@ class VatRate(models.Model, RevisionMixin): @reversion.register -@python_2_unicode_compatible class Venue(models.Model, RevisionMixin): name = models.CharField(max_length=255) phone = models.CharField(max_length=15, blank=True, null=True) @@ -246,11 +225,6 @@ class Venue(models.Model, RevisionMixin): def get_absolute_url(self): return reverse_lazy('venue_detail', kwargs={'pk': self.pk}) - class Meta: - permissions = ( - ('view_venue', 'Can view Venues'), - ) - class EventManager(models.Manager): def current_events(self): @@ -297,7 +271,6 @@ class EventManager(models.Manager): @reversion.register(follow=['items']) -@python_2_unicode_compatible class Event(models.Model, RevisionMixin): # Done to make it much nicer on the database PROVISIONAL = 0 @@ -491,11 +464,6 @@ class Event(models.Model, RevisionMixin): self.full_clean() super(Event, self).save(*args, **kwargs) - class Meta: - permissions = ( - ('view_event', 'Can view Events'), - ) - class EventItem(models.Model): event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE) @@ -533,7 +501,7 @@ class EventAuthorisation(models.Model, RevisionMixin): uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID") account_code = models.CharField(max_length=50, blank=True, null=True) amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount") - sent_by = models.ForeignKey('RIGS.Profile', on_delete=models.CASCADE) + sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE) def get_absolute_url(self): return reverse_lazy('event_detail', kwargs={'pk': self.event.pk}) @@ -543,7 +511,6 @@ class EventAuthorisation(models.Model, RevisionMixin): return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')') -@python_2_unicode_compatible class Invoice(models.Model): event = models.OneToOneField('Event', on_delete=models.CASCADE) invoice_date = models.DateField(auto_now_add=True) @@ -576,13 +543,9 @@ class Invoice(models.Model): return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) class Meta: - permissions = ( - ('view_invoice', 'Can view Invoices'), - ) ordering = ['-invoice_date'] -@python_2_unicode_compatible class Payment(models.Model): CASH = 'C' INTERNAL = 'I' diff --git a/RIGS/rigboard.py b/RIGS/rigboard.py index ae960f4f..6aeff551 100644 --- a/RIGS/rigboard.py +++ b/RIGS/rigboard.py @@ -110,7 +110,7 @@ class EventCreate(generic.CreateView): context['currentVAT'] = models.VatRate.objects.current_rate() form = context['form'] - if re.search('"-\d+"', form['items_json'].value()): + if 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 some other objects to include in the form. Used when there are errors but also nice and quick. @@ -206,7 +206,6 @@ class EventPrint(generic.View): } rml = template.render(context) - buffer = rml2pdf.parseString(rml) merger.append(PdfFileReader(buffer)) buffer.close() @@ -219,7 +218,7 @@ class EventPrint(generic.View): response = HttpResponse(content_type='application/pdf') - escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name) + escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name) response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName) response.write(merged.getvalue()) diff --git a/RIGS/signals.py b/RIGS/signals.py index 5c5e6c66..e1772e07 100644 --- a/RIGS/signals.py +++ b/RIGS/signals.py @@ -73,7 +73,7 @@ def send_eventauthorisation_success_email(instance): external_styles=css).transform() client_email.attach_alternative(html, 'text/html') - escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', instance.event.name) + escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name) client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName), merged.getvalue(), diff --git a/RIGS/templates/RIGS/event_embed.html b/RIGS/templates/RIGS/event_embed.html index 78816cae..d331ae7e 100644 --- a/RIGS/templates/RIGS/event_embed.html +++ b/RIGS/templates/RIGS/event_embed.html @@ -1,8 +1,7 @@ {% extends 'base_embed.html' %} -{% load static from staticfiles %} +{% load static %} {% block content %} -
- - {% endblock %} diff --git a/RIGS/templates/RIGS/event_print_page.xml b/RIGS/templates/RIGS/event_print_page.xml index 4b1ddfdf..09507167 100644 --- a/RIGS/templates/RIGS/event_print_page.xml +++ b/RIGS/templates/RIGS/event_print_page.xml @@ -1,7 +1,6 @@ +{% load filters %} - - @@ -13,7 +12,7 @@ - {{ object.description|default_if_none:""|linebreaksbr }} + {{ object.description|default_if_none:""|linebreaksxml }} @@ -75,9 +74,9 @@ {% if invoice %} {% if object.organisation.address %} - {{ object.organisation.address|default_if_none:""|linebreaksbr }} + {{ object.organisation.address|default_if_none:""|linebreaksxml }} {% elif object.person.address %} - {{ object.person.address|default_if_none:""|linebreaksbr }} + {{ object.person.address|default_if_none:""|linebreaksxml }} {% endif %} {% endif %} @@ -109,12 +108,12 @@

{{ object.venue.name }}

{% if not invoice %} - {{ object.venue.address|default_if_none:""|linebreaksbr }} + {{ object.venue.address|default_if_none:""|linebreaksxml }} {% endif %} - +

Timings

@@ -185,7 +184,7 @@ {% if item.description %} - {{ item.description|linebreaksbr }} + {{ item.description|linebreaksxml }} {% endif %} diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html index 2e0211da..72c87a49 100644 --- a/RIGS/templates/RIGS/invoice_detail.html +++ b/RIGS/templates/RIGS/invoice_detail.html @@ -111,7 +111,7 @@ {% endif %} -
Authorsation request sent by
+
Authorisation request sent by
{{ object.authorisation.sent_by }}
diff --git a/RIGS/templates/RIGS/item_modal.html b/RIGS/templates/RIGS/item_modal.html index 602f24ef..e9802230 100644 --- a/RIGS/templates/RIGS/item_modal.html +++ b/RIGS/templates/RIGS/item_modal.html @@ -1,4 +1,4 @@ -