mirror of https://github.com/snachodog/mybuddy.git
Indicate user locked state and allow unlocking from users admin (#600)
* Use custom template for account lock * Rename Baby Buddy base template tags * Add user unlock view * Add user unlock test
This commit is contained in:
parent
bf2884f544
commit
996d81966c
|
@ -337,6 +337,10 @@ AXES_COOLOFF_TIME = 1
|
||||||
|
|
||||||
AXES_FAILURE_LIMIT = 5
|
AXES_FAILURE_LIMIT = 5
|
||||||
|
|
||||||
|
AXES_LOCKOUT_TEMPLATE = "error/lockout.html"
|
||||||
|
|
||||||
|
AXES_LOCKOUT_URL = "/login/lock"
|
||||||
|
|
||||||
# Session configuration
|
# Session configuration
|
||||||
# Used by RollingSessionMiddleware to determine how often to reset the session.
|
# Used by RollingSessionMiddleware to determine how often to reset the session.
|
||||||
# See https://docs.djangoproject.com/en/4.0/topics/http/sessions/
|
# See https://docs.djangoproject.com/en/4.0/topics/http/sessions/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load babybuddy_tags i18n static %}
|
{% load babybuddy i18n static %}
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
{% get_current_locale as LOCALE %}
|
{% get_current_locale as LOCALE %}
|
||||||
{% get_current_timezone as TIMEZONE %}
|
{% get_current_timezone as TIMEZONE %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load i18n widget_tweaks babybuddy_tags qr_code %}
|
{% load i18n widget_tweaks babybuddy qr_code %}
|
||||||
{% url 'babybuddy:root-router' as relative_root_url %}
|
{% url 'babybuddy:root-router' as relative_root_url %}
|
||||||
{% make_absolute_url relative_root_url as absolute_root_url %}
|
{% make_absolute_url relative_root_url as absolute_root_url %}
|
||||||
BABYBUDDY-LOGIN:{"url":"{{ absolute_root_url }}","api_key":"{{ user.settings.api_key }}"}
|
BABYBUDDY-LOGIN:{"url":"{{ absolute_root_url }}","api_key":"{{ user.settings.api_key }}"}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'babybuddy/base.html' %}
|
{% extends 'babybuddy/base.html' %}
|
||||||
{% load babybuddy_tags i18n static timers %}
|
{% load babybuddy i18n static timers %}
|
||||||
|
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark sticky-top">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark sticky-top">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load i18n babybuddy_tags %}
|
{% load i18n babybuddy %}
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'babybuddy/page.html' %}
|
{% extends 'babybuddy/page.html' %}
|
||||||
{% load i18n widget_tweaks babybuddy_tags qr_code %}
|
{% load i18n widget_tweaks babybuddy qr_code %}
|
||||||
|
|
||||||
{% block title %}{% trans "Add a device" %}{% endblock %}
|
{% block title %}{% trans "Add a device" %}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends 'babybuddy/page.html' %}
|
||||||
|
{% load i18n widget_tweaks %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Unlock User" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'babybuddy:user-list' %}">{% trans "Users" %}</a></li>
|
||||||
|
<li class="breadcrumb-item">{{ object }}</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{% trans "Unlock" %}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form role="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
<h1>Are you sure you want to unlock <span class="text-info">{{ object }}</span>?</h1>
|
||||||
|
{% endblocktrans %}
|
||||||
|
<input type="submit" value="{% trans "Unlock" %}" class="btn btn-danger" />
|
||||||
|
<a href="{% url 'babybuddy:user-update' object.pk %}" class="btn btn-default">{% trans "Cancel" %}</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'babybuddy/page.html' %}
|
{% extends 'babybuddy/page.html' %}
|
||||||
{% load i18n %}
|
{% load babybuddy i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if object %}
|
{% if object %}
|
||||||
|
@ -21,6 +21,17 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if object %}
|
{% if object %}
|
||||||
|
{% user_is_locked object as is_locked %}
|
||||||
|
{% if is_locked %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<div class="alert-heading h4">
|
||||||
|
{% blocktrans %}User locked.{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'babybuddy:user-unlock' object.id %}" class="btn btn-danger">
|
||||||
|
{% blocktrans %}Unlock{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
<h1>Update <span class="text-info">{{ object }}</span></h1>
|
<h1>Update <span class="text-info">{{ object }}</span></h1>
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'babybuddy/page.html' %}
|
{% extends 'babybuddy/page.html' %}
|
||||||
{% load babybuddy_tags bootstrap i18n widget_tweaks %}
|
{% load babybuddy bootstrap i18n widget_tweaks %}
|
||||||
|
|
||||||
{% block title %}{% trans "Users" %}{% endblock %}
|
{% block title %}{% trans "Users" %}{% endblock %}
|
||||||
|
|
||||||
|
@ -18,9 +18,10 @@
|
||||||
<th>{% trans "First Name" %}</th>
|
<th>{% trans "First Name" %}</th>
|
||||||
<th>{% trans "Last Name" %}</th>
|
<th>{% trans "Last Name" %}</th>
|
||||||
<th>{% trans "Email" %}</th>
|
<th>{% trans "Email" %}</th>
|
||||||
<th>{% trans "Read only" %}</th>
|
<th class="text-center">{% trans "Read only" %}</th>
|
||||||
<th>{% trans "Staff" %}</th>
|
<th class="text-center">{% trans "Staff" %}</th>
|
||||||
<th>{% trans "Active" %}</th>
|
<th class="text-center">{% trans "Active" %}</th>
|
||||||
|
<th class="text-center">{% trans "Locked" %}</th>
|
||||||
<th class="text-center">{% trans "Actions" %}</th>
|
<th class="text-center">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -32,9 +33,11 @@
|
||||||
<td>{{ object.last_name }}</td>
|
<td>{{ object.last_name }}</td>
|
||||||
<td>{{ object.email }}</td>
|
<td>{{ object.email }}</td>
|
||||||
{% user_is_read_only object as is_read_only %}
|
{% user_is_read_only object as is_read_only %}
|
||||||
<td>{{ is_read_only|bool_icon }}</td>
|
<td class="text-center">{{ is_read_only|bool_icon }}</td>
|
||||||
<td>{{ object.is_staff|bool_icon }}</td>
|
<td class="text-center">{{ object.is_staff|bool_icon }}</td>
|
||||||
<td>{{ object.is_active|bool_icon }}</td>
|
<td class="text-center">{{ object.is_active|bool_icon }}</td>
|
||||||
|
{% user_is_locked object as is_locked %}
|
||||||
|
<td class="text-center">{{ is_locked|bool_icon }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="btn-group btn-group-sm" role="group" aria-label="Actions">
|
<div class="btn-group btn-group-sm" role="group" aria-label="Actions">
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'babybuddy/page.html' %}
|
{% extends 'babybuddy/page.html' %}
|
||||||
{% load i18n widget_tweaks babybuddy_tags %}
|
{% load i18n widget_tweaks babybuddy %}
|
||||||
|
|
||||||
{% block title %}{% trans "User Settings" %}{% endblock %}
|
{% block title %}{% trans "User Settings" %}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'error/base.html' %}
|
||||||
|
{% load babybuddy i18n %}
|
||||||
|
|
||||||
|
{% block title %}403 {% trans "Too Many Login Attempts" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% axes_lockout_message %}</h1>
|
||||||
|
{% endblock %}
|
|
@ -5,11 +5,19 @@ from django.apps import apps
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import to_locale, get_language
|
from django.utils.translation import to_locale, get_language
|
||||||
|
|
||||||
|
from axes.helpers import get_lockout_message
|
||||||
|
from axes.models import AccessAttempt
|
||||||
|
|
||||||
from core.models import Child
|
from core.models import Child
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def axes_lockout_message():
|
||||||
|
return get_lockout_message()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def relative_url(context, field_name, value):
|
def relative_url(context, field_name, value):
|
||||||
"""
|
"""
|
||||||
|
@ -65,6 +73,11 @@ def make_absolute_url(context, url):
|
||||||
return abs_url
|
return abs_url
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def user_is_locked(user):
|
||||||
|
return AccessAttempt.objects.filter(username=user.username).exists()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def user_is_read_only(user):
|
def user_is_read_only(user):
|
||||||
return user.groups.filter(name="read_only").exists()
|
return user.groups.filter(name="read_only").exists()
|
|
@ -5,27 +5,27 @@ from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.models import Child
|
from core.models import Child
|
||||||
from babybuddy.templatetags import babybuddy_tags
|
from babybuddy.templatetags import babybuddy
|
||||||
|
|
||||||
|
|
||||||
class TemplateTagsTestCase(TestCase):
|
class TemplateTagsTestCase(TestCase):
|
||||||
def test_child_count(self):
|
def test_child_count(self):
|
||||||
self.assertEqual(babybuddy_tags.get_child_count(), 0)
|
self.assertEqual(babybuddy.get_child_count(), 0)
|
||||||
Child.objects.create(
|
Child.objects.create(
|
||||||
first_name="Test", last_name="Child", birth_date=timezone.localdate()
|
first_name="Test", last_name="Child", birth_date=timezone.localdate()
|
||||||
)
|
)
|
||||||
self.assertEqual(babybuddy_tags.get_child_count(), 1)
|
self.assertEqual(babybuddy.get_child_count(), 1)
|
||||||
Child.objects.create(
|
Child.objects.create(
|
||||||
first_name="Test", last_name="Child 2", birth_date=timezone.localdate()
|
first_name="Test", last_name="Child 2", birth_date=timezone.localdate()
|
||||||
)
|
)
|
||||||
self.assertEqual(babybuddy_tags.get_child_count(), 2)
|
self.assertEqual(babybuddy.get_child_count(), 2)
|
||||||
|
|
||||||
def user_is_read_only(self):
|
def user_is_read_only(self):
|
||||||
user = get_user_model().objects.create_user(
|
user = get_user_model().objects.create_user(
|
||||||
username="readonly", password="readonly", is_superuser=False, is_staf=False
|
username="readonly", password="readonly", is_superuser=False, is_staf=False
|
||||||
)
|
)
|
||||||
self.assertFalse(babybuddy_tags.user_is_read_only(user))
|
self.assertFalse(babybuddy.user_is_read_only(user))
|
||||||
|
|
||||||
group = Group.objects.get(name="read_only")
|
group = Group.objects.get(name="read_only")
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
self.assertTrue(babybuddy_tags.user_is_read_only(user))
|
self.assertTrue(babybuddy.user_is_read_only(user))
|
||||||
|
|
|
@ -10,6 +10,8 @@ from django.core.management import call_command
|
||||||
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
|
from babybuddy.views import UserUnlock
|
||||||
|
|
||||||
|
|
||||||
class ViewsTestCase(TestCase):
|
class ViewsTestCase(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -77,6 +79,20 @@ class ViewsTestCase(TestCase):
|
||||||
page = self.c.get("/users/{}/delete/".format(entry.id))
|
page = self.c.get("/users/{}/delete/".format(entry.id))
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
|
|
||||||
|
def test_user_unlock(self):
|
||||||
|
# Staff setting is required to unlock users.
|
||||||
|
self.user.is_staff = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
entry = get_user_model().objects.first()
|
||||||
|
url = "/users/{}/unlock/".format(entry.id)
|
||||||
|
|
||||||
|
page = self.c.get(url)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
page = self.c.post(url, follow=True)
|
||||||
|
self.assertEqual(page.status_code, 200)
|
||||||
|
self.assertContains(page, UserUnlock.success_message)
|
||||||
|
|
||||||
def test_welcome(self):
|
def test_welcome(self):
|
||||||
page = self.c.get("/welcome/")
|
page = self.c.get("/welcome/")
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
|
|
|
@ -39,6 +39,7 @@ app_patterns = [
|
||||||
path("users/", views.UserList.as_view(), name="user-list"),
|
path("users/", views.UserList.as_view(), name="user-list"),
|
||||||
path("users/add/", views.UserAdd.as_view(), name="user-add"),
|
path("users/add/", views.UserAdd.as_view(), name="user-add"),
|
||||||
path("users/<int:pk>/edit/", views.UserUpdate.as_view(), name="user-update"),
|
path("users/<int:pk>/edit/", views.UserUpdate.as_view(), name="user-update"),
|
||||||
|
path("users/<int:pk>/unlock/", views.UserUnlock.as_view(), name="user-unlock"),
|
||||||
path("users/<int:pk>/delete/", views.UserDelete.as_view(), name="user-delete"),
|
path("users/<int:pk>/delete/", views.UserDelete.as_view(), name="user-delete"),
|
||||||
path("user/password/", views.UserPassword.as_view(), name="user-password"),
|
path("user/password/", views.UserPassword.as_view(), name="user-password"),
|
||||||
path("user/settings/", views.UserSettings.as_view(), name="user-settings"),
|
path("user/settings/", views.UserSettings.as_view(), name="user-settings"),
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.forms import PasswordChangeForm
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.views import LogoutView as LogoutViewBase
|
from django.contrib.auth.views import LogoutView as LogoutViewBase
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.core.exceptions import BadRequest
|
from django.core.exceptions import BadRequest
|
||||||
|
from django.forms import Form
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.middleware.csrf import REASON_BAD_ORIGIN
|
from django.middleware.csrf import REASON_BAD_ORIGIN
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
@ -21,9 +22,17 @@ from django.views.decorators.csrf import csrf_protect
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.views.generic.base import TemplateView, RedirectView
|
from django.views.generic.base import TemplateView, RedirectView
|
||||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
from django.views.generic.detail import BaseDetailView
|
||||||
|
from django.views.generic.edit import (
|
||||||
|
CreateView,
|
||||||
|
DeleteView,
|
||||||
|
FormMixin,
|
||||||
|
SingleObjectTemplateResponseMixin,
|
||||||
|
UpdateView,
|
||||||
|
)
|
||||||
from django.views.i18n import set_language
|
from django.views.i18n import set_language
|
||||||
|
|
||||||
|
from axes.utils import reset
|
||||||
from django_filters.views import FilterView
|
from django_filters.views import FilterView
|
||||||
|
|
||||||
from babybuddy import forms
|
from babybuddy import forms
|
||||||
|
@ -114,6 +123,33 @@ class UserUpdate(
|
||||||
success_message = gettext_lazy("User %(username)s updated.")
|
success_message = gettext_lazy("User %(username)s updated.")
|
||||||
|
|
||||||
|
|
||||||
|
class UserUnlock(
|
||||||
|
StaffOnlyMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
SuccessMessageMixin,
|
||||||
|
FormMixin,
|
||||||
|
SingleObjectTemplateResponseMixin,
|
||||||
|
BaseDetailView,
|
||||||
|
):
|
||||||
|
model = get_user_model()
|
||||||
|
template_name = "babybuddy/user_confirm_unlock.html"
|
||||||
|
permission_required = ("admin.change_user",)
|
||||||
|
form_class = Form
|
||||||
|
success_message = gettext_lazy("User unlocked.")
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
user = self.get_object()
|
||||||
|
form = self.get_form()
|
||||||
|
if form.is_valid():
|
||||||
|
reset(username=user.username)
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse("babybuddy:user-update", kwargs={"pk": self.kwargs["pk"]})
|
||||||
|
|
||||||
|
|
||||||
class UserDelete(
|
class UserDelete(
|
||||||
StaffOnlyMixin, PermissionRequiredMixin, DeleteView, SuccessMessageMixin
|
StaffOnlyMixin, PermissionRequiredMixin, DeleteView, SuccessMessageMixin
|
||||||
):
|
):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'babybuddy/page.html' %}
|
{% extends 'babybuddy/page.html' %}
|
||||||
{% load babybuddy_tags duration i18n timers %}
|
{% load babybuddy duration i18n timers %}
|
||||||
{% get_child_count as CHILD_COUNT %}
|
{% get_child_count as CHILD_COUNT %}
|
||||||
|
|
||||||
{% block title %}{{ object }}{% endblock %}
|
{% block title %}{{ object }}{% endblock %}
|
||||||
|
|
Loading…
Reference in New Issue