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 %}Crew meet: @@ -97,7 +97,7 @@ {{ object.description|linebreaksbr }}
{% endif %} - +