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

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

@@ -1,15 +0,0 @@
from RIGS.models import Profile
from RIGS.forms import ProfileRegistrationFormUniqueEmail
from registration.signals import user_registered
def user_created(sender, user, request, **kwargs):
form = ProfileRegistrationFormUniqueEmail(request.POST)
user.first_name = form.data['first_name']
user.last_name = form.data['last_name']
user.initials = form.data['initials']
# user.phone = form.data['phone']
user.save()
user_registered.connect(user_created)

View File

@@ -1,12 +0,0 @@
{# pass in variable "profile" to this template #}
<button title="{{profile.name}}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover focus' data-toggle="popover" data-content='
<img src="{{profile.profile_picture}}" class="img-responsive img-rounded center-block" style="max-width:4em" />
<dl class="dl-vertical">
<dt>Email</dt>
<dd>{{profile.email}}</dd>
<dt>Phone</dt>
<dd>{{profile.phone}}</dd>
</dl>
'>{{profile.first_name}}</button>

View File

@@ -1,150 +0,0 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% block title %}RIGS Profile {{object.pk}}{% endblock %}
{% block js %}
<script>
$(document).ready(function() {
$('#urlParamForm').change(function(){
url = "?";
$('#urlParamForm *').filter(':input').each(function(index, value){
defaultVal = $(value).data('default');
param = $(value).val();
val = $(value).prop('checked');
if(val != defaultVal){
url = url+param+"="+val+"&";
}
});
ics_url = $('#cal-url').data('url') + url.substring(0, url.length - 1);
$('#cal-url').text(ics_url);
gcal_url = $('#gcal-link').data('url') + encodeURIComponent(url.substring(0, url.length - 1));
$('#gcal-link').attr('href',gcal_url);
});
$('#urlParamForm').change(); //Do the initial setting
});
</script>
{% endblock %}
{% block content %}
<h3>Profile: {{object.name}}</h3>
{% if not request.is_ajax %}
<div class="row py-3">
{% if object.pk == user.pk %}
<div>
<div class="btn-group">
<a href="{% url 'profile_update_self' %}" class="btn btn-light">
Edit Profile <i class="fas fa-edit"></i>
</a>
<a href="{% url 'password_change' %}" class="btn btn-light">
Change Password <span class="fas fa-lock"></span>
</a>
</div>
</div>
{% endif %}
</div>
{% endif %}
<div class="row">
<div class="col">
<div class="card">
<div class="row no-gutters">
<div class="col-md-3">
<img src="{{object.profile_picture}}" class="card-img img-fluid" />
</div>
<div class="col-md-9">
<div class="card-body">
<dl class="row">
<dt class="col-5">First Name</dt>
<dd class="col-7">{{object.first_name}}</dd>
<dt class="col-5">Last Name</dt>
<dd class="col-7">{{object.last_name}}</dd>
<dt class="col-5">Email</dt>
<dd class="col-7">{{object.email}}</dd>
<dt class="col-5">Last Login</dt>
<dd class="col-7">{{object.last_login|date:"d/m/Y H:i"}}</dd>
<dt class="col-5">Date Joined</dt>
<dd class="col-7">{{object.date_joined|date:"d/m/Y H:i"}}</dd>
<dt class="col-5">Initials</dt>
<dd class="col-7">{{object.initials}}</dd>
<dt class="col-5">Phone</dt>
<dd class="col-7">{% if object.phone %}}<a href="tel:{{ object.phone }}">{% endif %}{{object.phone}}{% if object.phone %}}</a>{% endif %}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{% if not request.is_ajax and object.pk == user.pk %}
<div class="col">
<div class="card">
<div class="card-body">
<h4>Personal iCal Details</h4>
<dl class="row">
<dt class="col-4">API Key</dt>
<dd class="col-5">
{% if user.api_key %}
{{user.api_key}}
{% else %}
No API Key Generated
{% endif %}
</dd>
<a href="{% url 'reset_api_key' %}" class="btn btn-secondary col-3">
{% if user.api_key %}Reset API Key{% else %}Generate API Key{% endif %}
<span class="fas fa-redo"></span>
</a>
<dt class="col-4">Calendar Options</dt>
<dd class="col-8">
<form class="form-inline" id="urlParamForm">
<div class="form-group">
<label class="checkbox-inline mr-3">
<input type="checkbox" value="rig" data-default="true" checked> Rigs
</label>
<label class="checkbox-inline mx-3">
<input type="checkbox" value="non-rig" data-default="true" checked> Non-Rigs
</label>
<label class="checkbox-inline mx-3">
<input type="checkbox" value="dry-hire" data-default="true" checked> Dry-Hires
</label>
<label class="checkbox-inline mx-3">
<input type="checkbox" value="cancelled" data-default="false" > Cancelled
</label>
<label class="checkbox-inline mx-3">
<input type="checkbox" value="provisional" data-default="true" checked> Provisional
</label>
<label class="checkbox-inline mx-3">
<input type="checkbox" value="confirmed" data-default="true" checked> Confirmed/Booked
</label>
</div>
</form>
</dd>
<dt class="col-4">Calendar URL</dt>
<dd class="col-8">
{% if user.api_key %}
<pre id="cal-url" data-url="http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}"></pre>
<small><a id="gcal-link" data-url="https://support.google.com/calendar/answer/37100" href="">Click here</a> for instructions on adding to google calendar.<br/>
To sync from google calendar to mobile device, visit <a href="https://www.google.com/calendar/syncselect" target="_blank">this page</a> on your device and tick "RIGS Calendar".</small>
{% else %}
<pre>No API Key Generated</pre>
{% endif %}
</dd>
</dl>
</div>
</div>
</div>
{% endif %}
</div>
<h4>Events</h4>
{% with object.latest_events as events %}
{% include 'event_table.html' %}
{% endwith %}
{% endblock %}

View File

@@ -1,71 +0,0 @@
{% extends 'base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}Update Profile {{object.name}}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
{% include 'form_errors.html' %}
<h3>Update Profile</h3>
<div class="col-sm-6">
<form action="{{form.action|default:request.path}}" method="post" class="form-horizontal">{% csrf_token %}
<div class="form-group">
<label for="{{form.first_name.id_for_label}}" class="col-sm-4 control-label">{{form.first_name.label}}</label>
<div class="col-sm-8">
{% render_field form.first_name class+="form-control" placeholder=form.first_name.label %}
</div>
</div>
<div class="form-group">
<label for="{{form.last_name.id_for_label}}" class="col-sm-4 control-label">{{form.last_name.label}}</label>
<div class="col-sm-8">
{% render_field form.last_name class+="form-control" placeholder=form.last_name.label %}
</div>
</div>
<div class="form-group">
<label for="{{form.email.id_for_label}}" class="col-sm-4 control-label">{{form.email.label}}</label>
<div class="col-sm-8">
{% render_field form.email type="email" class+="form-control" placeholder=form.email.label %}
</div>
</div>
<div class="form-group">
<label for="{{form.initials.id_for_label}}" class="col-sm-4 control-label">{{form.initials.label}}</label>
<div class="col-sm-8">
{% render_field form.initials class+="form-control" placeholder=form.initials.label %}
</div>
</div>
<div class="form-group">
<label for="{{form.phone.id_for_label}}" class="col-sm-4 control-label">{{form.phone.label}}</label>
<div class="col-sm-8">
{% render_field form.phone type="tel" class+="form-control" placeholder=form.phone.label %}
</div>
</div>
<div class="form-group">
<input class="btn btn-primary pull-right" type="submit"/>
</div>
</form>
</div>
<div class="col-sm-3 col-sm-offset-2">
<div class="center-block">
<a href="https://gravatar.com/">
<img src="{{object.profile_picture}}" class="img-responsive img-rounded" />
<div class="text-center">
Images hosted by Gravatar
</div>
</a>
</div>
</div>
</div>
</div>
{% endblock %}

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')