diff --git a/PyRIGS/forms.py b/PyRIGS/forms.py new file mode 100644 index 00000000..e69de29b diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index d8f00cec..da984d07 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -59,6 +59,7 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', 'django.contrib.humanize', 'versioning', + 'users', 'RIGS', 'assets', diff --git a/PyRIGS/urls.py b/PyRIGS/urls.py index f2d23ffa..b42cfa75 100644 --- a/PyRIGS/urls.py +++ b/PyRIGS/urls.py @@ -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//', - permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()), - name='profile_detail'), + # API + path('api//', login_required(views.SecureAPIRequest.as_view()), + name="api_secure"), + path('api///', login_required(views.SecureAPIRequest.as_view()), + name="api_secure"), path('admin/', admin.site.urls), ] diff --git a/PyRIGS/views.py b/PyRIGS/views.py new file mode 100644 index 00000000..4b435dc7 --- /dev/null +++ b/PyRIGS/views.py @@ -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) diff --git a/RIGS/admin.py b/RIGS/admin.py index 65442968..f487eeb6 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -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] diff --git a/RIGS/forms.py b/RIGS/forms.py index a006bc87..5f449c5f 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -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) diff --git a/RIGS/models.py b/RIGS/models.py index dd6f2cfc..54cffd88 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -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) diff --git a/RIGS/urls.py b/RIGS/urls.py index d63b706c..10ade55b 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -125,12 +125,6 @@ urlpatterns = [ url(r'^ical/(?P\d+)/(?P\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"), - # API - path('api//', login_required(views.SecureAPIRequest.as_view()), - name="api_secure"), - path('api///', login_required(views.SecureAPIRequest.as_view()), - name="api_secure"), - # Risk assessment API path('log_risk_assessment/', rigboard.LogRiskAssessment.as_view(), name='log_risk_assessment'), diff --git a/RIGS/views.py b/RIGS/views.py index c08f4345..18f0c25b 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -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') diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 00000000..79b22be8 --- /dev/null +++ b/users/forms.py @@ -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 diff --git a/RIGS/regbackend.py b/users/regbackend.py similarity index 87% rename from RIGS/regbackend.py rename to users/regbackend.py index c8007993..bf0ca3a5 100644 --- a/RIGS/regbackend.py +++ b/users/regbackend.py @@ -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 diff --git a/RIGS/templates/profile_button.html b/users/templates/profile_button.html similarity index 100% rename from RIGS/templates/profile_button.html rename to users/templates/profile_button.html diff --git a/RIGS/templates/profile_detail.html b/users/templates/profile_detail.html similarity index 100% rename from RIGS/templates/profile_detail.html rename to users/templates/profile_detail.html diff --git a/RIGS/templates/profile_form.html b/users/templates/profile_form.html similarity index 100% rename from RIGS/templates/profile_form.html rename to users/templates/profile_form.html diff --git a/templates/registration/activation_complete.html b/users/templates/registration/activation_complete.html similarity index 100% rename from templates/registration/activation_complete.html rename to users/templates/registration/activation_complete.html diff --git a/templates/registration/activation_email.txt b/users/templates/registration/activation_email.txt similarity index 100% rename from templates/registration/activation_email.txt rename to users/templates/registration/activation_email.txt diff --git a/templates/registration/activation_email_subject.txt b/users/templates/registration/activation_email_subject.txt similarity index 100% rename from templates/registration/activation_email_subject.txt rename to users/templates/registration/activation_email_subject.txt diff --git a/templates/registration/logged_out.html b/users/templates/registration/logged_out.html similarity index 100% rename from templates/registration/logged_out.html rename to users/templates/registration/logged_out.html diff --git a/templates/registration/login.html b/users/templates/registration/login.html similarity index 100% rename from templates/registration/login.html rename to users/templates/registration/login.html diff --git a/templates/registration/login_embed.html b/users/templates/registration/login_embed.html similarity index 100% rename from templates/registration/login_embed.html rename to users/templates/registration/login_embed.html diff --git a/templates/registration/loginform.html b/users/templates/registration/loginform.html similarity index 100% rename from templates/registration/loginform.html rename to users/templates/registration/loginform.html diff --git a/templates/registration/password_change_done.html b/users/templates/registration/password_change_done.html similarity index 100% rename from templates/registration/password_change_done.html rename to users/templates/registration/password_change_done.html diff --git a/templates/registration/password_change_form.html b/users/templates/registration/password_change_form.html similarity index 100% rename from templates/registration/password_change_form.html rename to users/templates/registration/password_change_form.html diff --git a/templates/registration/password_reset_complete.html b/users/templates/registration/password_reset_complete.html similarity index 100% rename from templates/registration/password_reset_complete.html rename to users/templates/registration/password_reset_complete.html diff --git a/templates/registration/password_reset_confirm.html b/users/templates/registration/password_reset_confirm.html similarity index 100% rename from templates/registration/password_reset_confirm.html rename to users/templates/registration/password_reset_confirm.html diff --git a/templates/registration/password_reset_done.html b/users/templates/registration/password_reset_done.html similarity index 100% rename from templates/registration/password_reset_done.html rename to users/templates/registration/password_reset_done.html diff --git a/templates/registration/password_reset_email.html b/users/templates/registration/password_reset_email.html similarity index 100% rename from templates/registration/password_reset_email.html rename to users/templates/registration/password_reset_email.html diff --git a/templates/registration/password_reset_form.html b/users/templates/registration/password_reset_form.html similarity index 100% rename from templates/registration/password_reset_form.html rename to users/templates/registration/password_reset_form.html diff --git a/templates/registration/registration_complete.html b/users/templates/registration/registration_complete.html similarity index 100% rename from templates/registration/registration_complete.html rename to users/templates/registration/registration_complete.html diff --git a/templates/registration/registration_form.html b/users/templates/registration/registration_form.html similarity index 100% rename from templates/registration/registration_form.html rename to users/templates/registration/registration_form.html diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 00000000..7fc9303d --- /dev/null +++ b/users/urls.py @@ -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//', + permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()), + name='profile_detail'), +] diff --git a/users/views.py b/users/views.py new file mode 100644 index 00000000..2be14a61 --- /dev/null +++ b/users/views.py @@ -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')