Skip to content

Commit cb86849

Browse files
committed
Added admin themed views
1 parent 407b179 commit cb86849

17 files changed

+504
-13
lines changed

tests/test_admin.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
from django.conf import settings
4-
from django.shortcuts import resolve_url
3+
from django.core.urlresolvers import reverse
54
from django.test import TestCase
65
from django.test.utils import override_settings
76

@@ -21,13 +20,13 @@ def tearDown(self):
2120

2221
def test(self):
2322
response = self.client.get('/admin/', follow=True)
24-
redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL)
23+
redirect_to = '%s?next=/admin/' % reverse('admin:login')
2524
self.assertRedirects(response, redirect_to)
2625

2726
@override_settings(LOGIN_URL='two_factor:login')
2827
def test_named_url(self):
2928
response = self.client.get('/admin/', follow=True)
30-
redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL)
29+
redirect_to = '%s?next=/admin/' % reverse('admin:login')
3130
self.assertRedirects(response, redirect_to)
3231

3332

@@ -54,13 +53,13 @@ def setUp(self):
5453

5554
def test_otp_admin_without_otp(self):
5655
response = self.client.get('/otp_admin/', follow=True)
57-
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL)
56+
redirect_to = '%s?next=/otp_admin/' % reverse('admin:login')
5857
self.assertRedirects(response, redirect_to)
5958

6059
@override_settings(LOGIN_URL='two_factor:login')
6160
def test_otp_admin_without_otp_named_url(self):
6261
response = self.client.get('/otp_admin/', follow=True)
63-
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL)
62+
redirect_to = '%s?next=/otp_admin/' % reverse('admin:login')
6463
self.assertRedirects(response, redirect_to)
6564

6665
def test_otp_admin_with_otp(self):

two_factor/admin.py

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,115 @@
1+
from functools import update_wrapper
2+
13
from django.conf import settings
24
from django.contrib import admin
35
from django.contrib.admin import AdminSite
46
from django.contrib.auth import REDIRECT_FIELD_NAME
57
from django.contrib.auth.views import redirect_to_login
8+
from django.core.urlresolvers import reverse
69
from django.shortcuts import resolve_url
710
from django.utils.http import is_safe_url
11+
from django.utils.translation import ugettext
812

913
from .models import PhoneDevice
1014
from .utils import monkeypatch_method
15+
from .views import BackupTokensView, LoginView, ProfileView, SetupView
16+
17+
18+
class AdminLoginView(LoginView):
19+
form_templates = {
20+
'auth': 'two_factor/admin/_wizard_form_auth.html',
21+
'token': 'two_factor/admin/_wizard_form_token.html',
22+
'backup': 'two_factor/admin/_wizard_form_backup.html',
23+
}
24+
redirect_url = 'admin:two_factor:setup'
25+
template_name = 'two_factor/admin/login.html'
26+
27+
def get_context_data(self, form, **kwargs):
28+
context = super(AdminLoginView, self).get_context_data(form, **kwargs)
29+
if self.kwargs['extra_context']:
30+
context.update(self.kwargs['extra_context'])
31+
user_is_validated = getattr(self.request.user, 'is_verified', None)
32+
context.update({
33+
'cancel_url': reverse('admin:index' if user_is_validated else 'admin:login'),
34+
'wizard_form_template': self.form_templates.get(self.steps.current),
35+
})
36+
return context
37+
38+
def get_redirect_url(self):
39+
redirect_to = self.request.GET.get(self.redirect_field_name, '')
40+
url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host())
41+
if url_is_safe:
42+
self.request.session[REDIRECT_FIELD_NAME] = redirect_to
43+
user_is_validated = getattr(self.request.user, 'is_verified', None)
44+
if not url_is_safe or not user_is_validated:
45+
redirect_to = resolve_url(self.redirect_url)
46+
return redirect_to
47+
48+
49+
admin_login_view = AdminLoginView.as_view()
50+
51+
52+
class AdminSetupView(SetupView):
53+
form_templates = {
54+
'method': 'two_factor/admin/_wizard_form_method.html',
55+
'generator': 'two_factor/admin/_wizard_form_generator.html',
56+
'sms': 'two_factor/admin/_wizard_form_phone_number.html',
57+
'call': 'two_factor/admin/_wizard_form_phone_number.html',
58+
'validation': 'two_factor/admin/_wizard_form_validation.html',
59+
'yubikey': 'two_factor/admin/_wizard_form_yubikey.html',
60+
}
61+
redirect_url = 'admin:two_factor:profile'
62+
template_name = 'two_factor/admin/setup.html'
63+
64+
def get_context_data(self, form, **kwargs):
65+
context = super(AdminSetupView, self).get_context_data(form, **kwargs)
66+
user_is_validated = getattr(self.request.user, 'is_verified', None)
67+
context.update({
68+
'cancel_url': reverse('admin:two_factor:profile' if user_is_validated else 'admin:login'),
69+
'site_header': ugettext("Enable Two-Factor Authentication"),
70+
'title': ugettext("Enable Two-Factor Authentication"),
71+
'wizard_form_template': self.form_templates.get(self.steps.current),
72+
})
73+
return context
74+
75+
def get_redirect_url(self):
76+
redirect_to = self.request.session.pop(REDIRECT_FIELD_NAME, '')
77+
url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host())
78+
user_is_validated = self.request.user.is_verified()
79+
if url_is_safe and user_is_validated:
80+
return redirect_to
81+
return super(AdminSetupView, self).get_redirect_url()
82+
83+
admin_setup_view = AdminSetupView.as_view()
84+
85+
86+
class AdminBackupTokensView(BackupTokensView):
87+
redirect_url = 'admin:two_factor:backup_tokens'
88+
template_name = 'two_factor/admin/backup_tokens.html'
89+
90+
def get_context_data(self, **kwargs):
91+
context = super(AdminBackupTokensView, self).get_context_data(**kwargs)
92+
context.update({
93+
'site_header': ugettext("Backup Tokens"),
94+
'title': ugettext("Backup Tokens"),
95+
})
96+
return context
97+
98+
admin_backup_tokens_view = AdminBackupTokensView.as_view()
99+
100+
101+
class AdminProfileView(ProfileView):
102+
template_name = 'two_factor/admin/profile.html'
103+
104+
def get_context_data(self, **kwargs):
105+
context = super(AdminProfileView, self).get_context_data(**kwargs)
106+
context.update({
107+
'site_header': ugettext("Account Security"),
108+
'title': ugettext("Account Security"),
109+
})
110+
return context
111+
112+
admin_profile_view = AdminProfileView.as_view()
11113

12114

13115
class AdminSiteOTPRequiredMixin(object):
@@ -27,16 +129,38 @@ def has_permission(self, request):
27129
return False
28130
return request.user.is_verified()
29131

132+
def get_urls(self):
133+
from django.conf.urls import include, url
134+
135+
def wrap(view, cacheable=False):
136+
def wrapper(*args, **kwargs):
137+
return self.admin_view(view, cacheable)(*args, **kwargs)
138+
wrapper.admin_site = self
139+
return update_wrapper(wrapper, view)
140+
141+
urlpatterns_2fa = [
142+
url(r'^profile/$', wrap(self.two_factor_profile), name='profile'),
143+
url(r'^setup/$', self.two_factor_setup, name='setup'),
144+
url(r'^backup/tokens/$', wrap(self.two_factor_backup_tokens), name='backup_tokens'),
145+
]
146+
147+
urlpatterns = [
148+
url(r'^two_factor/', include(urlpatterns_2fa, namespace='two_factor'))
149+
]
150+
urlpatterns += super(AdminSiteOTPRequiredMixin, self).get_urls()
151+
return urlpatterns
152+
30153
def login(self, request, extra_context=None):
31-
"""
32-
Redirects to the site login page for the given HttpRequest.
33-
"""
34-
redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME))
154+
return admin_login_view(request, extra_context=extra_context)
35155

36-
if not redirect_to or not is_safe_url(url=redirect_to, host=request.get_host()):
37-
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
156+
def two_factor_profile(self, request):
157+
return admin_profile_view(request)
38158

39-
return redirect_to_login(redirect_to)
159+
def two_factor_setup(self, request):
160+
return admin_setup_view(request)
161+
162+
def two_factor_backup_tokens(self, request):
163+
return admin_backup_tokens_view(request)
40164

41165

42166
class AdminSiteOTPRequired(AdminSiteOTPRequiredMixin, AdminSite):
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "admin/base.html" %}
2+
3+
{% load i18n %}
4+
5+
{% block userlinks %}
6+
<a href="{% url 'admin:two_factor:profile' %}">{% trans 'Two Factor' %}</a> /
7+
{{ block.super }}
8+
{% endblock %}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<style type="text/css">
2+
.form-row button {
3+
background: #79aec8;
4+
padding: 10px 15px;
5+
border: none;
6+
border-radius: 4px;
7+
color: #fff;
8+
cursor: pointer;
9+
}
10+
.login .form-row #id_auth-username,
11+
.login .form-row #id_auth-password,
12+
.login .form-row #id_token-otp_token,
13+
.login .form-row #id_backup-otp_token,
14+
.login .form-row #id_method-method,
15+
.login .form-row #id_generator-token,
16+
.login .form-row #id_call-number,
17+
.login .form-row #id_sms-number {
18+
clear: both;
19+
padding: 8px;
20+
width: 100%;
21+
-webkit-box-sizing: border-box;
22+
-moz-box-sizing: border-box;
23+
box-sizing: border-box;
24+
}
25+
.login .form-row #id_method-method li {
26+
list-style: none;
27+
}
28+
29+
.form-row button:active,
30+
.form-row button:focus,
31+
.form-row button:hover {
32+
background: #609ab6;
33+
}
34+
35+
.form-row button[disabled] {
36+
opacity: 0.4;
37+
}
38+
39+
.form-row button.default {
40+
float: right;
41+
border: none;
42+
font-weight: 400;
43+
background: #417690;
44+
}
45+
46+
.form-row button.default:active,
47+
.form-row button.default:focus,
48+
.form-row button.default:hover {
49+
background: #205067;
50+
}
51+
52+
.form-row button.default,
53+
.form-row button.default {
54+
opacity: 0.4;
55+
}
56+
</style>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% load i18n %}
2+
3+
{# hidden submit button to enable [enter] key #}
4+
<div style="margin-left: -9999px; position: absolute;"><input type="submit" value=""/></div>
5+
6+
<div class="form-row">
7+
<a href="{{ cancel_url }}" class="pull-right btn btn-link">{% trans "Cancel" %}</a>
8+
{% if wizard.steps.prev %}
9+
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="">{% trans "Back" %}</button>
10+
{% else %}
11+
<button disabled name="" type="button">{% trans "Back" %}</button>
12+
{% endif %}
13+
<button type="submit" class="">{% trans "Next" %}</button>
14+
</div>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% load i18n %}
2+
<div class="form-row">
3+
{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}
4+
<label for="id_auth-username" class="required">{{ form.username.label }}:</label> {{ form.username }}
5+
</div>
6+
<div class="form-row">
7+
{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}
8+
<label for="id_auth-password" class="required">{% trans 'Password:' %}</label> {{ form.password }}
9+
<input type="hidden" name="this_is_the_login_form" value="1" />
10+
<input type="hidden" name="next" value="{{ next }}" />
11+
</div>
12+
{% url 'admin_password_reset' as password_reset_url %}
13+
{% if password_reset_url %}
14+
<div class="password-reset-link">
15+
<a href="{{ password_reset_url }}">{% trans 'Forgotten your password or username?' %}</a>
16+
</div>
17+
{% endif %}
18+
19+
<script type="text/javascript">
20+
document.getElementById('id_auth-username').focus()
21+
</script>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="form-row">
2+
{% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %}
3+
<label for="id_backup-otp_token" class="required">{{ form.otp_token.label }}:</label> {{ form.otp_token }}
4+
</div>
5+
6+
<script type="text/javascript">
7+
document.getElementById('id_backup-otp_token').focus()
8+
</script>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="form-row">
2+
{% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %}
3+
<label for="id_generator-token" class="required">{{ form.token.label }}:</label> {{ form.token }}
4+
</div>
5+
6+
<script type="text/javascript">
7+
document.getElementById('id_generator-token').focus()
8+
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% load i18n %}
2+
<div class="form-row">
3+
{% if not form.this_is_the_login_form.errors %}{{ form.method.errors }}{% endif %}
4+
{{ form.method }}
5+
</div>
6+
7+
<script type="text/javascript">
8+
document.getElementById('id_auth-username').focus()
9+
</script>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="form-row">
2+
{% if not form.this_is_the_login_form.errors %}{{ form.number.errors }}{% endif %}
3+
<label for="id_call-number" class="required">{{ form.number.label }}:</label> {{ form.number }}
4+
</div>
5+
6+
<script type="text/javascript">
7+
document.getElementById('id_call-number').focus()
8+
</script>

0 commit comments

Comments
 (0)