mirror of
https://github.com/jlengrand/bugsink.git
synced 2026-03-09 23:51:20 +00:00
Users: some interface to edit/view them
This commit is contained in:
@@ -34,6 +34,9 @@ urlpatterns = [
|
||||
|
||||
path("accounts/preferences/", preferences, name="preferences"),
|
||||
|
||||
# many user-related views are directly exposed above (/accounts/), the rest is here:
|
||||
path("users/", include("users.urls")),
|
||||
|
||||
path('api/', include('ingest.urls')),
|
||||
|
||||
path('projects/', include('projects.urls')),
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
<a href="/admin/"><div class="px-4 py-2 my-2 hover:bg-slate-300 rounded-xl">Admin</div></a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_superuser %}
|
||||
<a href="/users/"><div class="px-4 py-2 my-2 hover:bg-slate-300 rounded-xl">Users</div></a>
|
||||
{% endif %}
|
||||
|
||||
{% if logged_in_user.is_anonymous %}
|
||||
<a href="/accounts/login/"><div class="px-4 py-2 my-2 hover:bg-slate-300 rounded-xl">Login</div></a> {# I don't think this is actually ever shown in practice, because you must always be logged in #}
|
||||
{% else %}
|
||||
|
||||
@@ -26,7 +26,6 @@ UserModel = get_user_model()
|
||||
|
||||
|
||||
class UserCreationForm(BaseUserCreationForm):
|
||||
|
||||
# Our UserCreationForm is the place where the "use email for usernames" logic is implemented.
|
||||
# We could instead push such logic in the model, and do it more thoroughly (i.e. remove either field, and point the
|
||||
# USERNAME_FIELD to the other). But I'm not sure that this is the most future-proof way forward. In particular,
|
||||
@@ -82,6 +81,34 @@ class UserCreationForm(BaseUserCreationForm):
|
||||
return user
|
||||
|
||||
|
||||
class UserEditForm(ModelForm):
|
||||
# See notes in UserCreationForm about the "use email for usernames" logic; it's the same here.
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['username'].validators = [EmailValidator()]
|
||||
self.fields['username'].label = "Email"
|
||||
|
||||
self.fields['username'].help_text = None # "Email" is descriptive enough
|
||||
|
||||
class Meta:
|
||||
model = UserModel
|
||||
fields = ("username",)
|
||||
|
||||
def clean_username(self):
|
||||
if UserModel.objects.exclude(pk=self.instance.pk).filter(username=self.cleaned_data['username']).exists():
|
||||
raise ValidationError(mark_safe("This email is already registered by another user."))
|
||||
return self.cleaned_data['username']
|
||||
|
||||
def save(self, **kwargs):
|
||||
commit = kwargs.pop("commit", True)
|
||||
user = super().save(commit=False)
|
||||
user.email = user.username
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class ResendConfirmationForm(forms.Form):
|
||||
email = forms.EmailField()
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Sign up</button>
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Reset password</button>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
33
users/templates/users/user_edit.html
Normal file
33
users/templates/users/user_edit.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
|
||||
{% block title %}Edit {{ form.instance.username }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
|
||||
<div class="m-4 max-w-4xl flex-auto">
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">{{ form.instance.username }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
Settings for "{{ form.instance.username }}".
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.username %}
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 border-slate-500 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 hover:bg-cyan-400 active:ring rounded-md">Save</button>
|
||||
<a href="{% url "user_list" %}" class="text-cyan-500 font-bold ml-2">Cancel</a>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
86
users/templates/users/user_list.html
Normal file
86
users/templates/users/user_list.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Users · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
|
||||
<div class="m-4 max-w-4xl flex-auto">
|
||||
|
||||
{% if messages %}
|
||||
<ul class="mb-4">
|
||||
{% for message in messages %}
|
||||
{# if we introduce different levels we can use{% message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %} #}
|
||||
<li class="bg-cyan-50 border-2 border-cyan-800 p-4 rounded-lg">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex">
|
||||
<h1 class="text-4xl mt-4 font-bold">Users</h1>
|
||||
|
||||
{% comment %}
|
||||
Our current invite-system is tied to either a team or a project; no "global" invites (yet).
|
||||
<div class="ml-auto mt-6">
|
||||
<a class="font-bold text-slate-800 border-slate-500 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 hover:bg-cyan-400 active:ring rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">Invite Member</a>
|
||||
</div>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<table class="w-full mt-8">
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr class="bg-slate-200">
|
||||
<th class="w-full p-4 text-left text-xl" colspan="2">Users</th>
|
||||
</tr>
|
||||
|
||||
{% for user in users %}
|
||||
<tr class="bg-white border-slate-200 border-b-2">
|
||||
<td class="w-full p-4">
|
||||
<div>
|
||||
<a href="{% url "user_edit" user_pk=user.pk %}" class="text-xl text-cyan-500 font-bold">{{ user.username }}</a> {# "best name" perhaps later? #}
|
||||
{# <span class="bg-slate-100 rounded-2xl px-4 py-2 ml-2 text-sm">Invitation pending</span> #} {# perhaps useful for "not active"? #}
|
||||
{% if member.is_superuser %}
|
||||
<span class="bg-cyan-100 rounded-2xl px-4 py-2 ml-2 text-sm">Superuser</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="p-4">
|
||||
<div class="flex justify-end">
|
||||
{% if not request.user == user %}
|
||||
{% if user.is_active %}
|
||||
<button name="action" value="deactivate:{{ user.id }}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 active:ring rounded-md">Deactivate</button>
|
||||
{% else %}
|
||||
<button name="action" value="activate:{{ user.id }}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 active:ring rounded-md">Activate</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{#% empty %} not needed, a site without users cannot be visited by a user #}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
<div class="flex flex-direction-row">
|
||||
<div class="ml-auto py-8 pr-4">
|
||||
<a href="{% url "..." %}" class="text-cyan-500 font-bold">Back to Xxxx</a> {# perhaps once this is part of some other flow #}
|
||||
</div>
|
||||
</div>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
8
users/urls.py
Normal file
8
users/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import user_list, user_edit
|
||||
|
||||
urlpatterns = [
|
||||
path('', user_list, name="user_list"),
|
||||
path('<str:user_pk>/edit/', user_edit, name="user_edit"),
|
||||
]
|
||||
@@ -6,12 +6,13 @@ from django.contrib.auth import get_user_model
|
||||
from django.http import Http404
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
|
||||
from bugsink.app_settings import get_settings, CB_ANYBODY
|
||||
from bugsink.decorators import atomic_for_request_method
|
||||
|
||||
from .forms import UserCreationForm, ResendConfirmationForm, RequestPasswordResetForm, SetPasswordForm, PreferencesForm
|
||||
from .forms import (
|
||||
UserCreationForm, ResendConfirmationForm, RequestPasswordResetForm, SetPasswordForm, PreferencesForm, UserEditForm)
|
||||
from .models import EmailVerification
|
||||
from .tasks import send_confirm_email, send_reset_email
|
||||
|
||||
@@ -19,6 +20,52 @@ from .tasks import send_confirm_email, send_reset_email
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
@atomic_for_request_method
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def user_list(request):
|
||||
users = UserModel.objects.all().order_by('username')
|
||||
|
||||
if request.method == 'POST':
|
||||
full_action_str = request.POST.get('action')
|
||||
action, user_pk = full_action_str.split(":", 1)
|
||||
if action == "deactivate":
|
||||
user = UserModel.objects.get(pk=user_pk)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
|
||||
messages.success(request, 'User %s deactivated' % user.username)
|
||||
return redirect('user_list')
|
||||
|
||||
if action == "activate":
|
||||
user = UserModel.objects.get(pk=user_pk)
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
||||
messages.success(request, 'User %s activated' % user.username)
|
||||
return redirect('user_list')
|
||||
|
||||
return render(request, 'users/user_list.html', {
|
||||
'users': users,
|
||||
})
|
||||
|
||||
|
||||
@atomic_for_request_method
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def user_edit(request, user_pk):
|
||||
user = UserModel.objects.get(pk=user_pk)
|
||||
if request.method == 'POST':
|
||||
form = UserEditForm(request.POST, instance=user)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("user_list")
|
||||
|
||||
else:
|
||||
form = UserEditForm(instance=user)
|
||||
|
||||
return render(request, "users/user_edit.html", {"form": form})
|
||||
|
||||
|
||||
@atomic_for_request_method
|
||||
def signup(request):
|
||||
if get_settings().USER_REGISTRATION != CB_ANYBODY:
|
||||
@@ -62,7 +109,7 @@ def confirm_email(request, token=None):
|
||||
if request.method == 'POST':
|
||||
# We insist on POST requests to do the actual confirmation (at the cost of an extra click). See:
|
||||
# https://softwareengineering.stackexchange.com/a/422579/168778
|
||||
# there's no form, the'res just a button to generate the post request
|
||||
# there's no Django form (fields), there's just a button to generate the post request
|
||||
|
||||
verification.user.is_active = True
|
||||
verification.user.save()
|
||||
@@ -159,8 +206,8 @@ def reset_password(request, token=None):
|
||||
|
||||
|
||||
@atomic_for_request_method
|
||||
# in the general case this is done by Middleware but we're under /accounts/. not security-critical because we simply
|
||||
# get a failure on request.user if this wasn't there, but still the "right thing"
|
||||
# in the general case this is done by Middleware but we're under /accounts/, so we need it back.
|
||||
# not security-critical because we simply get a failure on request.user if this wasn't there, but still the right thing.
|
||||
@login_required
|
||||
def preferences(request):
|
||||
user = request.user
|
||||
|
||||
Reference in New Issue
Block a user