Breakout (most) user stuff to separate module

The model remains in RIGS for now, as it's pretty painful to move...
This commit is contained in:
2020-04-12 19:13:06 +01:00
parent 2bf643cd7a
commit f308a095f3
32 changed files with 314 additions and 250 deletions

0
PyRIGS/forms.py Normal file
View File

View File

@@ -59,6 +59,7 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
'django.contrib.humanize',
'versioning',
'users',
'RIGS',
'assets',

View File

@@ -8,28 +8,19 @@ from django.views.decorators.clickjacking import xframe_options_exempt
from django.contrib.auth.views import LoginView
from registration.backends.default.views import RegistrationView
from PyRIGS.decorators import permission_required_with_403
import RIGS
from RIGS import regbackend, forms, views
import RIGS, users
from PyRIGS import views
urlpatterns = [
path('', include('users.urls')),
path('', include('RIGS.urls')),
path('assets/', include('assets.urls')),
path('user/', include('django.contrib.auth.urls')),
path('user/', include('registration.backends.default.urls')),
path('user/register/', RegistrationView.as_view(form_class=forms.ProfileRegistrationFormUniqueEmail),
name="registration_register"),
path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'),
path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'),
# User editing
path('user/edit/', login_required(views.ProfileUpdateSelf.as_view()),
name='profile_update_self'),
path('user/reset_api_key', login_required(views.ResetApiKey.as_view(permanent=False)),
name='reset_api_key'),
path('user/', login_required(views.ProfileDetail.as_view()), name='profile_detail'),
path('user/<pk>/',
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
name='profile_detail'),
# API
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"),
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"),
path('admin/', admin.site.urls),
]

136
PyRIGS/views.py Normal file
View File

@@ -0,0 +1,136 @@
from django.core.exceptions import PermissionDenied
from django.http.response import HttpResponseRedirect
from django.http import HttpResponse
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
from django.contrib.auth.views import LoginView
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.core import serializers
from django.conf import settings
import simplejson
from django.contrib import messages
import datetime
import pytz
import operator
from registration.views import RegistrationView
from django.views.decorators.csrf import csrf_exempt
from RIGS import models, forms
from assets import models as asset_models
from functools import reduce
class SecureAPIRequest(generic.View):
models = {
'venue': models.Venue,
'person': models.Person,
'organisation': models.Organisation,
'profile': models.Profile,
'event': models.Event,
'supplier': asset_models.Supplier
}
perms = {
'venue': 'RIGS.view_venue',
'person': 'RIGS.view_person',
'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile',
'event': None,
'supplier': None
}
'''
Validate the request is allowed based on user permissions.
Raises 403 if denied.
Potential to add API key validation at a later date.
'''
def __validate__(self, request, key, perm):
if request.user.is_active:
if request.user.is_superuser or perm is None:
return True
elif request.user.has_perm(perm):
return True
raise PermissionDenied()
def get(self, request, model, pk=None, param=None):
# Request permission validation things
key = request.GET.get('apikey', None)
perm = self.perms[model]
self.__validate__(request, key, perm)
# Response format where applicable
format = request.GET.get('format', 'json')
fields = request.GET.get('fields', None)
if fields:
fields = fields.split(",")
# Supply data for one record
if pk:
object = get_object_or_404(self.models[model], pk=pk)
data = serializers.serialize(format, [object], fields=fields)
return HttpResponse(data, content_type="application/" + format)
# Supply data for autocomplete ajax request in json form
term = request.GET.get('q', None)
if term:
if fields is None: # Default to just name
fields = ['name']
# Build a list of Q objects for use later
queries = []
for part in term.split(" "):
qs = []
for field in fields:
q = Q(**{field + "__icontains": part})
qs.append(q)
queries.append(reduce(operator.or_, qs))
# Build the data response list
results = []
query = reduce(operator.and_, queries)
objects = self.models[model].objects.filter(query)
for o in objects:
data = {
'pk': o.pk,
'value': o.pk,
'text': o.name,
}
try: # See if there is a valid update URL
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
except NoReverseMatch:
pass
results.append(data)
# return a data response
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
start = request.GET.get('start', None)
end = request.GET.get('end', None)
if model == "event" and start and end:
# Probably a calendar request
start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
results = []
for item in objects:
data = {
'pk': item.pk,
'title': item.name,
'is_rig': item.is_rig,
'status': str(item.get_status_display()),
'earliest': item.earliest_time.isoformat(),
'latest': item.latest_time.isoformat(),
'url': str(item.get_absolute_url())
}
results.append(data)
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
return HttpResponse(model)

View File

@@ -1,5 +1,6 @@
from django.contrib import admin
from RIGS import models, forms
from users import forms as user_forms
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from reversion.admin import VersionAdmin
@@ -21,7 +22,6 @@ admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice)
admin.site.register(models.Payment)
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
@@ -48,8 +48,8 @@ class ProfileAdmin(UserAdmin):
'fields': ('username', 'password1', 'password2'),
}),
)
form = forms.ProfileChangeForm
add_form = forms.ProfileCreationForm
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = [approve_user]

View File

@@ -16,54 +16,6 @@ forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
forms.TimeField.widget = forms.TextInput(attrs={'type': 'time'})
forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'})
# Registration
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
captcha = ReCaptchaField()
class Meta:
model = models.Profile
fields = ('username', 'email', 'first_name', 'last_name', 'initials')
def clean_initials(self):
"""
Validate that the supplied initials are unique.
"""
if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']):
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
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(CheckApprovedForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop('autofocus', None)
class PasswordReset(PasswordResetForm):
captcha = ReCaptchaField(label='Captcha')
class ProfileCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = models.Profile
class ProfileChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = models.Profile
# Events Shit
class EventForm(forms.ModelForm):
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + list(settings.DATETIME_INPUT_FORMATS)

View File

@@ -20,7 +20,6 @@ from django.core.exceptions import ValidationError
from django.urls import reverse_lazy
# Create your models here.
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)

View File

@@ -125,12 +125,6 @@ urlpatterns = [
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
name="ics_calendar"),
# API
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"),
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"),
# Risk assessment API
path('log_risk_assessment/', rigboard.LogRiskAssessment.as_view(), name='log_risk_assessment'),

View File

@@ -38,26 +38,6 @@ class Index(generic.TemplateView):
class SearchHelp(generic.TemplateView):
template_name = '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
# check for it before logging the user in
class LoginEmbed(LoginView):
template_name = 'registration/login_embed.html'
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
if request.method == "POST":
csrf_cookie = request.COOKIES.get('csrftoken', None)
if csrf_cookie is None:
messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.')
request.method = 'GET' # Render the page without trying to login
return super().dispatch(request, *args, **kwargs)
"""
Called from a modal window (e.g. when an item is submitted to an event/invoice).
May optionally also include some javascript in a success message to cause a load of
@@ -270,157 +250,3 @@ class VenueUpdate(generic.UpdateView):
'pk': self.object.pk,
})
return url
class SecureAPIRequest(generic.View):
models = {
'venue': models.Venue,
'person': models.Person,
'organisation': models.Organisation,
'profile': models.Profile,
'event': models.Event,
'supplier': asset_models.Supplier
}
perms = {
'venue': 'RIGS.view_venue',
'person': 'RIGS.view_person',
'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile',
'event': None,
'supplier': None
}
'''
Validate the request is allowed based on user permissions.
Raises 403 if denied.
Potential to add API key validation at a later date.
'''
def __validate__(self, request, key, perm):
if request.user.is_active:
if request.user.is_superuser or perm is None:
return True
elif request.user.has_perm(perm):
return True
raise PermissionDenied()
def get(self, request, model, pk=None, param=None):
# Request permission validation things
key = request.GET.get('apikey', None)
perm = self.perms[model]
self.__validate__(request, key, perm)
# Response format where applicable
format = request.GET.get('format', 'json')
fields = request.GET.get('fields', None)
if fields:
fields = fields.split(",")
# Supply data for one record
if pk:
object = get_object_or_404(self.models[model], pk=pk)
data = serializers.serialize(format, [object], fields=fields)
return HttpResponse(data, content_type="application/" + format)
# Supply data for autocomplete ajax request in json form
term = request.GET.get('q', None)
if term:
if fields is None: # Default to just name
fields = ['name']
# Build a list of Q objects for use later
queries = []
for part in term.split(" "):
qs = []
for field in fields:
q = Q(**{field + "__icontains": part})
qs.append(q)
queries.append(reduce(operator.or_, qs))
# Build the data response list
results = []
query = reduce(operator.and_, queries)
objects = self.models[model].objects.filter(query)
for o in objects:
data = {
'pk': o.pk,
'value': o.pk,
'text': o.name,
}
try: # See if there is a valid update URL
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
except NoReverseMatch:
pass
results.append(data)
# return a data response
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
start = request.GET.get('start', None)
end = request.GET.get('end', None)
if model == "event" and start and end:
# Probably a calendar request
start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
results = []
for item in objects:
data = {
'pk': item.pk,
'title': item.name,
'is_rig': item.is_rig,
'status': str(item.get_status_display()),
'earliest': item.earliest_time.isoformat(),
'latest': item.latest_time.isoformat(),
'url': str(item.get_absolute_url())
}
results.append(data)
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
return HttpResponse(model)
class ProfileDetail(generic.DetailView):
template_name = "profile_detail.html"
model = models.Profile
def get_queryset(self):
try:
pk = self.kwargs['pk']
except KeyError:
pk = self.request.user.id
self.kwargs['pk'] = pk
return self.model.objects.filter(pk=pk)
class ProfileUpdateSelf(generic.UpdateView):
template_name = "profile_form.html"
model = models.Profile
fields = ['first_name', 'last_name', 'email', 'initials', 'phone']
def get_queryset(self):
pk = self.request.user.id
self.kwargs['pk'] = pk
return self.model.objects.filter(pk=pk)
def get_success_url(self):
url = reverse_lazy('profile_detail')
return url
class ResetApiKey(generic.RedirectView):
def get_redirect_url(self, *args, **kwargs):
self.request.user.api_key = self.request.user.make_api_key()
self.request.user.save()
return reverse_lazy('profile_detail')

57
users/forms.py Normal file
View File

@@ -0,0 +1,57 @@
import simplejson
from captcha.fields import ReCaptchaField
from django import forms
from django.conf import settings
from django.contrib.auth.forms import (AuthenticationForm, PasswordResetForm,
UserChangeForm, UserCreationForm)
from django.core import serializers
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.utils import formats
from registration.forms import RegistrationFormUniqueEmail
from RIGS import models
# Registration
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
captcha = ReCaptchaField()
class Meta:
model = models.Profile
fields = ('username', 'email', 'first_name', 'last_name', 'initials')
def clean_initials(self):
"""
Validate that the supplied initials are unique.
"""
if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']):
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
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(CheckApprovedForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop('autofocus', None)
class PasswordReset(PasswordResetForm):
captcha = ReCaptchaField(label='Captcha')
class ProfileCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = models.Profile
class ProfileChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = models.Profile

View File

@@ -1,5 +1,5 @@
from RIGS.models import Profile
from RIGS.forms import ProfileRegistrationFormUniqueEmail
from users.forms import ProfileRegistrationFormUniqueEmail
from registration.signals import user_registered

29
users/urls.py Normal file
View File

@@ -0,0 +1,29 @@
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
from django.contrib.auth.decorators import login_required
from django.conf import settings
from django.views.decorators.clickjacking import xframe_options_exempt
from django.contrib.auth.views import LoginView
from registration.backends.default.views import RegistrationView
from PyRIGS.decorators import permission_required_with_403
from users import regbackend, forms, views
urlpatterns = [
path('user/', include('django.contrib.auth.urls')),
path('user/', include('registration.backends.default.urls')),
path('user/register/', RegistrationView.as_view(form_class=forms.ProfileRegistrationFormUniqueEmail),
name="registration_register"),
path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'),
path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'),
# User editing
path('user/edit/', login_required(views.ProfileUpdateSelf.as_view()),
name='profile_update_self'),
path('user/reset_api_key', login_required(views.ResetApiKey.as_view(permanent=False)),
name='reset_api_key'),
path('user/', login_required(views.ProfileDetail.as_view()), name='profile_detail'),
path('user/<pk>/',
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
name='profile_detail'),
]

79
users/views.py Normal file
View File

@@ -0,0 +1,79 @@
from django.core.exceptions import PermissionDenied
from django.http.response import HttpResponseRedirect
from django.http import HttpResponse
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
from django.contrib.auth.views import LoginView
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.core import serializers
from django.conf import settings
import simplejson
from django.contrib import messages
import datetime
import pytz
import operator
from registration.views import RegistrationView
from django.views.decorators.csrf import csrf_exempt
from RIGS import models, forms
from assets import models as asset_models
from functools import reduce
# 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
# check for it before logging the user in
class LoginEmbed(LoginView):
template_name = 'registration/login_embed.html'
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
if request.method == "POST":
csrf_cookie = request.COOKIES.get('csrftoken', None)
if csrf_cookie is None:
messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.')
request.method = 'GET' # Render the page without trying to login
return super().dispatch(request, *args, **kwargs)
class ProfileDetail(generic.DetailView):
template_name = "profile_detail.html"
model = models.Profile
def get_queryset(self):
try:
pk = self.kwargs['pk']
except KeyError:
pk = self.request.user.id
self.kwargs['pk'] = pk
return self.model.objects.filter(pk=pk)
class ProfileUpdateSelf(generic.UpdateView):
template_name = "profile_form.html"
model = models.Profile
fields = ['first_name', 'last_name', 'email', 'initials', 'phone']
def get_queryset(self):
pk = self.request.user.id
self.kwargs['pk'] = pk
return self.model.objects.filter(pk=pk)
def get_success_url(self):
url = reverse_lazy('profile_detail')
return url
class ResetApiKey(generic.RedirectView):
def get_redirect_url(self, *args, **kwargs):
self.request.user.api_key = self.request.user.make_api_key()
self.request.user.save()
return reverse_lazy('profile_detail')