mirror of
https://github.com/nikdoof/test-auth.git
synced 2025-12-13 22:32:15 +00:00
Reworking of SSO app for CBVs
This commit is contained in:
@@ -111,6 +111,15 @@ class APIPasswordForm(forms.Form):
|
||||
""" API Password reset form """
|
||||
|
||||
password = forms.CharField(widget=forms.PasswordInput, label="Password")
|
||||
password2 = forms.CharField(widget=forms.PasswordInput, label="Confirm Password")
|
||||
|
||||
def clean_password2(self):
|
||||
password1 = self.cleaned_data.get('password')
|
||||
password2 = self.cleaned_data.get('password2')
|
||||
if password1 and password2:
|
||||
if password1 != password2:
|
||||
raise forms.ValidationError("The two passwords do not match.")
|
||||
return password2
|
||||
|
||||
|
||||
class EmailChangeForm(forms.Form):
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
<p>This service will reset your password for all External Auth API Services. If you wish to continue please enter a new
|
||||
password below.</p>
|
||||
|
||||
<form action="{% url sso.views.set_apipasswd %}" method="post">
|
||||
<form action="{% url sso-apipassword %}" method="post">
|
||||
<fieldset>
|
||||
{% include "formtools/formerror.html" %}
|
||||
{% include "formtools/formfield.html" with field=form.password %}
|
||||
{% include "formtools/formfield.html" with field=form.password2 %}
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Reset Account" class="btn primary" />
|
||||
<input type="submit" value="Change Password" class="btn primary" />
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1>Change Email Address</h1>
|
||||
</div>
|
||||
|
||||
<form action="" method="post">
|
||||
<form action="{% url sso-emailupdate %}" method="post">
|
||||
<fieldset>
|
||||
{% include "formtools/formerror.html" %}
|
||||
{% include "formtools/formfield.html" with field=form.email1 %}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
{% block title %}Change your Primay Character{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Change Your Primary Character</h1>
|
||||
</div>
|
||||
|
||||
<p>Your primary character is used across auth services to show as your display name and corporation. Please set this to which character you consider your "main".</p>
|
||||
|
||||
|
||||
@@ -33,14 +33,14 @@
|
||||
<div class="active" id="overview">
|
||||
<p>
|
||||
<b>Username:</b> {{ user.username }}<br/>
|
||||
<b>Email:</b> {{ user.email }} (<a href="{% url sso.views.email_change %}">change</a>)<br/>
|
||||
<b>Primary Character:</b> {{ user.get_profile.primary_character }} (<a href="{% url sso.views.primarychar_change %}">change</a>)<br/>
|
||||
<b>Email:</b> {{ user.email }} (<a href="{% url sso-emailupdate %}">change</a>)<br/>
|
||||
<b>Primary Character:</b> {{ user.get_profile.primary_character }} (<a href="{% url sso-primarycharacterupdate %}">change</a>)<br/>
|
||||
<b>Groups:</b> {{ user.groups.all|join:", " }}<br/>
|
||||
</p>
|
||||
<h2>Passwords</h2>
|
||||
<p>
|
||||
<a href="{% url django.contrib.auth.views.password_change %}" class="btn">Change Auth Login Password</a>
|
||||
<a href="{% url sso.views.set_apipasswd %}" class="btn">Change Services Password</a><br/><br/>
|
||||
<a href="{% url sso-apipassword %}" class="btn">Change Services Password</a><br/><br/>
|
||||
<span class="label info">Note</span> Changing your services password will change it for all linked sites (forums, wiki, reimbursement tool)
|
||||
</p>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url sso.views.service_add %}" class="btn">Add Service</a>
|
||||
<a href="{% if available_services > 0 %}{% url sso.views.service_add %}{% else %}#{% endif %}" class="btn{% if available_services == 0 %} disabled{% endif %}">Add Service</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -144,9 +144,8 @@
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="{% url reddit-addaccount %}" class="btn">Add a Reddit account</a>
|
||||
<a href="{% url sso-reddittagging %}" class="btn">{% if user.get_profile.tag_reddit_accounts %}Disable{% else %}Enable{% endif %} Reddit Flare</a>
|
||||
</p>
|
||||
|
||||
<p>Reddit account tagging is {% if user.get_profile.tag_reddit_accounts %}Enabled{% else %}Disabled{% endif %}. <a href="{% url sso.views.toggle_reddit_tagging %}">{% if user.get_profile.tag_reddit_accounts %}Disable{% else %}Enable{% endif %}</a></p>
|
||||
</div>
|
||||
{% endifswitch %}
|
||||
{% endif %}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
you have recently joined a corporation, then please use the "Refresh"
|
||||
option on your API key</p>
|
||||
|
||||
<p><a href="{% url sso.views.profile %}">Return to your Profile</a></p>
|
||||
<p><a href="{% url sso-profile %}">Return to your Profile</a></p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,29 +2,30 @@ from django.conf.urls.defaults import *
|
||||
from django.core.urlresolvers import reverse, reverse_lazy
|
||||
from django.contrib.auth.views import password_change, password_change_done
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from sso import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
('^$', views.profile),
|
||||
url(r'^profile/$', views.profile, name='sso-profile'),
|
||||
url('^$', RedirectView.as_view(url=reverse_lazy('sso-profile'))),
|
||||
url(r'^profile/$', views.ProfileView.as_view(), name='sso-profile'),
|
||||
(r'^profile/add/service', views.service_add),
|
||||
(r'^profile/del/service/$', views.service_del),
|
||||
(r'^profile/del/service/(?P<serviceid>\d+)/$', views.service_del),
|
||||
(r'^profile/reset/service/(?P<serviceid>\d+)/$', views.service_reset),
|
||||
(r'^profile/reset/service/(?P<serviceid>\d+)/(?P<accept>\d+)$', views.service_reset),
|
||||
(r'^profile/apipassword/', views.set_apipasswd),
|
||||
url(r'^profile/apipassword/', views.APIPasswordUpdateView.as_view(), name='sso-apipassword'),
|
||||
(r'^profile/refresh/$', views.refresh_access),
|
||||
url(r'^profile/refresh/(?P<userid>\d+)/$', views.refresh_access, name='sso-refreshaccess'),
|
||||
(r'^profile/change/password/$', password_change),
|
||||
(r'^profile/change/email/$', views.email_change),
|
||||
(r'^profile/change/primary/$', views.primarychar_change),
|
||||
(r'^profile/change/reddittag/$', views.toggle_reddit_tagging),
|
||||
url(r'^profile/change/email/$', views.EmailUpdateView.as_view(), name='sso-emailupdate'),
|
||||
url(r'^profile/change/primary/$', views.PrimaryCharacterUpdateView.as_view(), name='sso-primarycharacterupdate'),
|
||||
url(r'^profile/change/reddittag/$', views.RedditTaggingUpdateView.as_view(), name='sso-reddittagging'),
|
||||
(r'^users/$', views.user_lookup),
|
||||
url(r'^users/(?P<username>.*)/addnote/$', login_required(views.AddUserNote.as_view()), name='sso-addusernote'),
|
||||
url(r'^users/(?P<username>.*)/$', views.user_view, name='sso-viewuser'),
|
||||
url(r'^users/(?P<username>.*)/addnote/$', views.AddUserNote.as_view(), name='sso-addusernote'),
|
||||
url(r'^users/(?P<username>.*)/$', views.UserDetailView.as_view(), name='sso-viewuser'),
|
||||
|
||||
url(r'^address/$', login_required(views.UserIPAddressView.as_view()), name='sso-ipaddress'),
|
||||
url(r'^address/$', views.UserIPAddressView.as_view(), name='sso-ipaddress'),
|
||||
)
|
||||
|
||||
urlpatterns += patterns('django.views.generic.simple',
|
||||
|
||||
230
app/sso/views.py
230
app/sso/views.py
@@ -2,47 +2,53 @@ import hashlib
|
||||
import random
|
||||
import re
|
||||
import unicodedata
|
||||
import celery
|
||||
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.shortcuts import render_to_response, get_object_or_404, redirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse, reverse_lazy
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.template import RequestContext
|
||||
from django.core import serializers
|
||||
from django.conf import settings
|
||||
from django.views.generic import FormView, ListView
|
||||
from django.views.generic import View, FormView, ListView, DetailView, TemplateView
|
||||
|
||||
import celery
|
||||
from gargoyle import gargoyle
|
||||
from gargoyle.decorators import switch_is_active
|
||||
from braces.views import LoginRequiredMixin
|
||||
|
||||
from utils import installed
|
||||
from eve_api.models import EVEAccount, EVEPlayerCharacter
|
||||
from eve_api.tasks import import_apikey, import_apikey_result, update_user_access
|
||||
from eve_proxy.models import ApiAccessLog
|
||||
from reddit.tasks import update_user_flair
|
||||
from sso.models import ServiceAccount, Service, SSOUser, ExistingUser, ServiceError, SSOUserIPAddress
|
||||
from sso.forms import UserServiceAccountForm, ServiceAccountResetForm, UserLookupForm, APIPasswordForm, EmailChangeForm, PrimaryCharacterForm, UserNoteForm
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
""" Displays the user's profile page """
|
||||
|
||||
try:
|
||||
profile = request.user.get_profile()
|
||||
except SSOUser.DoesNotExist:
|
||||
profile = SSOUser(user=request.user)
|
||||
profile.save()
|
||||
class ProfileView(LoginRequiredMixin, TemplateView):
|
||||
|
||||
if not profile.primary_character and EVEPlayerCharacter.objects.filter(eveaccount__user=request.user).count():
|
||||
return redirect('sso.views.primarychar_change')
|
||||
template_name = 'sso/profile.html'
|
||||
|
||||
context = {
|
||||
'profile': request.user.get_profile()
|
||||
}
|
||||
return render_to_response('sso/profile.html', context, context_instance=RequestContext(request))
|
||||
def get_profile(self, user):
|
||||
try:
|
||||
profile = user.get_profile()
|
||||
except SSOUser.DoesNotExist:
|
||||
profile = SSOUser.objects.create(user=user)
|
||||
return profile
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.profile = self.get_profile(request.user)
|
||||
if self.profile.primary_character is None and EVEPlayerCharacter.objects.filter(eveaccount__user=request.user).count():
|
||||
return HttpResponseRedirect(reverse('sso.views.primarychar_change'))
|
||||
return super(ProfileView, self).get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(ProfileView, self).get_context_data(**kwargs)
|
||||
ctx.update({
|
||||
'profile': self.profile,
|
||||
'available_services': Service.objects.filter(groups__in=self.request.user.groups.all()).exclude(id__in=ServiceAccount.objects.filter(user=self.request.user).values('service')).count()
|
||||
})
|
||||
return ctx
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -86,8 +92,8 @@ def service_add(request):
|
||||
return render_to_response('sso/serviceaccount/created.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
else:
|
||||
availserv = Service.objects.filter(groups__in=request.user.groups.all()).exclude(id__in=ServiceAccount.objects.filter(user=request.user).values('service'))
|
||||
if len(availserv) == 0:
|
||||
availserv = Service.objects.filter(groups__in=request.user.groups.all()).exclude(id__in=ServiceAccount.objects.filter(user=request.user).values('service')).count()
|
||||
if not availserv:
|
||||
return render_to_response('sso/serviceaccount/noneavailable.html', locals(), context_instance=RequestContext(request))
|
||||
else:
|
||||
form = clsform() # An unbound form
|
||||
@@ -153,29 +159,37 @@ def service_reset(request, serviceid, template='sso/serviceaccount/reset.html',
|
||||
return render_to_response(template, locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@login_required
|
||||
def user_view(request, username, template='sso/lookup/user.html'):
|
||||
""" View a user's profile as a admin """
|
||||
class UserDetailView(LoginRequiredMixin, DetailView):
|
||||
|
||||
if not request.user.has_perm('sso.can_view_users') and not request.user.has_perm('sso.can_view_users_restricted'):
|
||||
return redirect('sso.views.profile')
|
||||
model = User
|
||||
slug_url_kwarg = 'username'
|
||||
slug_field = 'username'
|
||||
template_name = 'sso/lookup/user.html'
|
||||
|
||||
user = get_object_or_404(User, username=username)
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.user.has_perm('sso.can_view_users') and not request.user.has_perm('sso.can_view_users_restricted'):
|
||||
return HttpResponseForbidden()
|
||||
return super(UserDetailView, self).get(request, *args, **kwargs)
|
||||
|
||||
context = {
|
||||
'user': user,
|
||||
'profile': user.get_profile(),
|
||||
'services': ServiceAccount.objects.select_related('service').filter(user=user).only('service__name', 'service_uid', 'active'),
|
||||
'characters': EVEPlayerCharacter.objects.select_related('corporation').filter(eveaccount__user=user).only('id', 'name', 'corporation__name'),
|
||||
}
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(UserDetailView, self).get_context_data(**kwargs)
|
||||
ctx.update({
|
||||
'profile': self.object.get_profile(),
|
||||
'services': ServiceAccount.objects.select_related('service').filter(user=self.object).only('service__name', 'service_uid', 'active'),
|
||||
'characters': EVEPlayerCharacter.objects.select_related('corporation', 'corporation__alliance').filter(eveaccount__user=self.object).only('id', 'name', 'corporation__name'),
|
||||
})
|
||||
|
||||
# If the HR app is installed, check the blacklist
|
||||
if installed('hr'):
|
||||
if request.user.has_perm('hr.add_blacklist'):
|
||||
from hr.utils import blacklist_values
|
||||
context['blacklisted'] = len(blacklist_values(user))
|
||||
# If the HR app is installed, check the blacklist
|
||||
if installed('hr'):
|
||||
if self.request.user.has_perm('hr.add_blacklist'):
|
||||
from hr.utils import blacklist_values
|
||||
output = blacklist_values(self.object)
|
||||
ctx.update({
|
||||
'blacklisted': bool(len(output)),
|
||||
'blacklist_items': output,
|
||||
})
|
||||
|
||||
return render_to_response(template, context, context_instance=RequestContext(request))
|
||||
return ctx
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -216,7 +230,7 @@ def user_lookup(request):
|
||||
return redirect('sso.views.user_lookup')
|
||||
|
||||
if users and len(users) == 1:
|
||||
return redirect(user_view, username=users[0].username)
|
||||
return redirect('sso-viewuser', username=users[0].username)
|
||||
elif users and len(users) > 1:
|
||||
return render_to_response('sso/lookup/lookuplist.html', locals(), context_instance=RequestContext(request))
|
||||
else:
|
||||
@@ -226,22 +240,18 @@ def user_lookup(request):
|
||||
return render_to_response('sso/lookup/userlookup.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@login_required
|
||||
def set_apipasswd(request):
|
||||
""" Sets the user's auth API password """
|
||||
class APIPasswordUpdateView(LoginRequiredMixin, FormView):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = APIPasswordForm(request.POST)
|
||||
if form.is_valid():
|
||||
profile = request.user.get_profile()
|
||||
profile.api_service_password = hashlib.sha1(form.cleaned_data['password']).hexdigest()
|
||||
profile.save()
|
||||
messages.add_message(request, messages.INFO, "Your API Services password has been set.")
|
||||
return redirect('sso.views.profile') # Redirect after POST
|
||||
else:
|
||||
form = APIPasswordForm() # An unbound form
|
||||
form_class = APIPasswordForm
|
||||
template_name = 'sso/apipassword.html'
|
||||
success_url = reverse_lazy('sso-profile')
|
||||
|
||||
return render_to_response('sso/apipassword.html', locals(), context_instance=RequestContext(request))
|
||||
def form_valid(self, form):
|
||||
profile = request.user.get_profile()
|
||||
profile.api_service_password = hashlib.sha1(form.cleaned_data['password']).hexdigest()
|
||||
profile.save()
|
||||
message.success(self.request, "Your API services password has been updated.")
|
||||
return super(APIPasswordUpdateView, self).form_valid(form)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -271,66 +281,82 @@ def refresh_access(request, userid=0, corpid=0, allianceid=0):
|
||||
return redirect('sso.views.profile')
|
||||
|
||||
|
||||
@login_required
|
||||
def email_change(request):
|
||||
""" Change the user's email address """
|
||||
class EmailUpdateView(LoginRequiredMixin, FormView):
|
||||
"""Updates a user's email address"""
|
||||
|
||||
if request.method == 'POST':
|
||||
form = EmailChangeForm(request.POST)
|
||||
if form.is_valid():
|
||||
request.user.email = form.cleaned_data['email2']
|
||||
request.user.save()
|
||||
messages.add_message(request, messages.INFO, "E-mail address changed to %s." % form.cleaned_data['email2'])
|
||||
return redirect('sso.views.profile') # Redirect after POST
|
||||
else:
|
||||
form = EmailChangeForm() # An unbound form
|
||||
form_class = EmailChangeForm
|
||||
template_name = 'sso/emailchange.html'
|
||||
success_url = reverse_lazy('sso-profile')
|
||||
|
||||
return render_to_response('sso/emailchange.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
@login_required
|
||||
def primarychar_change(request):
|
||||
""" Change the user's primary character """
|
||||
|
||||
if request.method == 'POST':
|
||||
form = PrimaryCharacterForm(request.POST, user=request.user)
|
||||
if form.is_valid():
|
||||
profile = request.user.get_profile()
|
||||
profile.primary_character = form.cleaned_data['character']
|
||||
profile.save()
|
||||
messages.add_message(request, messages.INFO, "Your primary character has changed to %s." % form.cleaned_data['character'])
|
||||
return redirect('sso.views.profile') # Redirect after POST
|
||||
else:
|
||||
form = PrimaryCharacterForm(initial={'character': request.user.get_profile().primary_character}, user=request.user) # An unbound form
|
||||
|
||||
return render_to_response('sso/primarycharchange.html', locals(), context_instance=RequestContext(request))
|
||||
def form_valid(self, form):
|
||||
request.user.email = form.cleaned_data['email2']
|
||||
request.user.save()
|
||||
messages.success(self.request, "E-mail address changed to %s." % form.cleaned_data['email2'])
|
||||
return super(EmailUpdateView).form_valid(form)
|
||||
|
||||
|
||||
@login_required
|
||||
@switch_is_active('reddit')
|
||||
def toggle_reddit_tagging(request):
|
||||
profile = request.user.get_profile()
|
||||
if profile.primary_character:
|
||||
class PrimaryCharacterUpdateView(LoginRequiredMixin, FormView):
|
||||
"""Updates a user's primary character selection"""
|
||||
|
||||
form_class = PrimaryCharacterForm
|
||||
template_name = 'sso/primarycharchange.html'
|
||||
success_url = reverse_lazy('sso-profile')
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(PrimaryCharacterUpdateView, self).get_form_kwargs()
|
||||
kwargs.update({
|
||||
'user': self.request.user
|
||||
})
|
||||
return kwargs
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(PrimaryCharacterUpdateView, self).get_initial()
|
||||
initial.update({
|
||||
'character': self.request.user.get_profile().primary_character
|
||||
})
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
profile = self.request.user.get_profile()
|
||||
profile.primary_character = form.cleaned_data['character']
|
||||
profile.save()
|
||||
messages.success(self.request, "Your primary character has changed to %s." % form.cleaned_data['character'])
|
||||
return super(PrimaryCharacterUpdateView, self).form_valid(form)
|
||||
|
||||
|
||||
class RedditTaggingUpdateView(LoginRequiredMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not gargoyle.is_active('reddit', request):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
profile = request.user.get_profile()
|
||||
|
||||
if profile.primary_character is None:
|
||||
messages.error("Reddit account tagging requires a primary character before using. Please set one.")
|
||||
if EVEPlayerCharacter.objects.filter(eveaccount__user=request.user).count():
|
||||
return HttpResponseRedirect(reverse('sso-primarycharacterupdate'))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('sso-profile'))
|
||||
profile.tag_reddit_accounts = not profile.tag_reddit_accounts
|
||||
profile.save()
|
||||
if profile.tag_reddit_accounts:
|
||||
tag = 'Enabled'
|
||||
else:
|
||||
tag = 'Disabled'
|
||||
messages.add_message(request, messages.INFO, "Reddit account tagging is now %s" % tag)
|
||||
messages.info(request, "Reddit account tagging is now %s" % tag)
|
||||
|
||||
if profile.tag_reddit_accounts:
|
||||
name = profile.primary_character.name
|
||||
else:
|
||||
name = ''
|
||||
for acc in request.user.redditaccount_set.all():
|
||||
from reddit.tasks import update_user_flair
|
||||
update_user_flair.delay(acc.username, name)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, "You need to set a primary character before using this feature!")
|
||||
|
||||
return redirect('sso.views.profile')
|
||||
return HttpResponseRedirect(reverse('sso-profile'))
|
||||
|
||||
|
||||
class AddUserNote(FormView):
|
||||
class AddUserNote(LoginRequiredMixin, FormView):
|
||||
|
||||
template_name = 'sso/add_usernote.html'
|
||||
form_class = UserNoteForm
|
||||
@@ -348,27 +374,29 @@ class AddUserNote(FormView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(AddUserNote, self).get_context_data(**kwargs)
|
||||
ctx['user'] = self.get_user()
|
||||
ctx.update({
|
||||
'user': self.get_user()
|
||||
})
|
||||
return ctx
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(AddUserNote, self).get_initial()
|
||||
initial['user'] = self.get_user()
|
||||
initial.update({
|
||||
'user': self.get_user()
|
||||
})
|
||||
return initial
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('sso-viewuser', args=[self.get_user()])
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
obj = form.save(commit=False)
|
||||
obj.created_by = self.request.user
|
||||
obj.save()
|
||||
|
||||
return super(AddUserNote, self).form_valid(form)
|
||||
|
||||
|
||||
class UserIPAddressView(ListView):
|
||||
class UserIPAddressView(LoginRequiredMixin, ListView):
|
||||
|
||||
model = SSOUserIPAddress
|
||||
|
||||
|
||||
Reference in New Issue
Block a user