Skip to content

Commit

Permalink
Merge pull request #1121 from TOMToolkit/1102-create-a-user-profile-page
Browse files Browse the repository at this point in the history
1102 create a user profile page
  • Loading branch information
jchate6 authored Nov 19, 2024
2 parents af15325 + 3d22539 commit 2ec4d26
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 8 deletions.
24 changes: 24 additions & 0 deletions tom_common/templates/tom_common/partials/confirm_user_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% load tom_common_extras %}

<div class="modal fade" id="userDeleteModal" tabindex="-1" role="dialog" aria-labelledby="userDeleteModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header alert-danger">
<h5 class="modal-title" id="userDeleteModalLabel">Warning: This will permanently delete user.</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Are you sure that you want to remove <b>{{ user.username }}</b> from {% tom_name %}?
</div>
<div class="modal-footer">
<form action="{% url 'user-delete' user.id %}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-danger" role="button">Delete User</button>
</form>
</div>
</div>
</div>
</div>
4 changes: 2 additions & 2 deletions tom_common/templates/tom_common/partials/navbar_login.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{% if user.is_authenticated %}
<li class="nav-item">
{% if user.first_name or user.last_name %}
<a class="nav-link" href="{% url 'user-update' user.id %}">{{ user.first_name }} {{ user.last_name }} ({{ user.username }})</a>
<a class="nav-link" href="{% url 'user-profile' user.id %}">{{ user.first_name }} {{ user.last_name }} ({{ user.username }})</a>
{% else %}
<a class="nav-link" href="{% url 'user-update' user.id %}">{{ user.username }}</a>
<a class="nav-link" href="{% url 'user-profile' user.id %}">{{ user.username }}</a>
{% endif %}
</li>
<li>
Expand Down
26 changes: 26 additions & 0 deletions tom_common/templates/tom_common/partials/user_data.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% load tom_common_extras %}

<div class="card">
<div class="card-header">
<div style="display: flex; justify-content: space-between">
<h4 class="card-title">User Info</h4>
<div>
<a title="Edit" href="{% url 'user-update' pk=user.id %}" ><i class="fa fa-pencil" aria-hidden="true"></i></a>
<a title="Remove User" data-toggle="modal" data-target="#userDeleteModal" href="#" ><i class="fa fa-trash" aria-hidden="true"></i></a>
</div>
</div>
</div>
<div class="card-body">
<dl class="row">
{% for key, value in profile_data.items %}
{% if value %}
<dt class="col-sm-6" >{% verbose_name user key %}</dt>
<dd class="col-sm-6">{{ value }}</dd>
{% endif %}
{% endfor %}
</dl>
</div>
</div>

<!-- Modal -->
{% include 'tom_common/partials/confirm_user_delete.html' %}
24 changes: 24 additions & 0 deletions tom_common/templates/tom_common/user_profile.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends 'tom_common/base.html' %}
{% load tom_common_extras user_extras %}
{% load bootstrap4 %}
{% block title %}User Profile{% endblock %}
{% block content %}
<h3>
{% if user.first_name or user.last_name %}
{{ user.first_name }} {{ user.last_name }} ({{ user.username }})
{% else %}
{{ user.username }}
{% endif %}
</h3>

<div class="container">
<div class="row">
<div class="col-lg">
{% user_data user %}
</div>
<div class="col-lg">
</div>
</div>
</div>

{% endblock %}
14 changes: 14 additions & 0 deletions tom_common/templatetags/user_extras.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django import template
from django.contrib.auth.models import Group, User
from django.forms.models import model_to_dict

register = template.Library()

Expand All @@ -24,3 +25,16 @@ def user_list(context):
'request': context['request'],
'users': User.objects.all()
}


@register.inclusion_tag('tom_common/partials/user_data.html')
def user_data(user):
"""
Returns the user information as a dictionary.
"""
exlcude_fields = ['password', 'last_login', 'id', 'is_active']
user_fields = [field.name for field in user._meta.fields if field.name not in exlcude_fields]
return {
'user': user,
'profile_data': model_to_dict(user, fields=user_fields),
}
10 changes: 7 additions & 3 deletions tom_common/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,19 @@ def test_user_delete(self):
self.assertEqual(response.status_code, 302)
self.assertFalse(User.objects.filter(pk=user.id).exists())

def test_non_superuser_cannot_delete_other_user(self):
user = User.objects.create(username='deleteme', email='[email protected]', password='deleted')
other_user = User.objects.create_user(username='other', email='[email protected]', password='other')
self.client.force_login(user)
response = self.client.post(reverse('user-delete', kwargs={'pk': other_user.id}))
self.assertRedirects(response, reverse('user-delete', kwargs={'pk': user.id}))

def test_must_be_superuser(self):
user = User.objects.create_user(username='notallowed', email='[email protected]', password='notallowed')
self.client.force_login(user)
response = self.client.get(reverse('admin-user-change-password', kwargs={'pk': user.id}))
self.assertEqual(response.status_code, 302)

response = self.client.get(reverse('user-delete', kwargs={'pk': user.id}))
self.assertEqual(response.status_code, 302)

def test_user_can_update_self(self):
user = User.objects.create(username='luke', password='forc3')
self.client.force_login(user)
Expand Down
3 changes: 2 additions & 1 deletion tom_common/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from tom_base import __version__
from tom_common.api_views import GroupViewSet
from tom_common.views import UserListView, UserPasswordChangeView, UserCreateView, UserDeleteView, UserUpdateView
from tom_common.views import CommentDeleteView, GroupCreateView, GroupUpdateView, GroupDeleteView
from tom_common.views import CommentDeleteView, GroupCreateView, GroupUpdateView, GroupDeleteView, UserDetailView
from tom_common.views import robots_txt

from .api_router import collect_api_urls, SharedAPIRootRouter # DRF routers are setup in each INSTALL_APPS url.py
Expand All @@ -49,6 +49,7 @@
path('users/create/', UserCreateView.as_view(), name='user-create'),
path('users/<int:pk>/delete/', UserDeleteView.as_view(), name='user-delete'),
path('users/<int:pk>/update/', UserUpdateView.as_view(), name='user-update'),
path('users/<int:pk>/profile/', UserDetailView.as_view(), name='user-profile'),
path('groups/create/', GroupCreateView.as_view(), name='group-create'),
path('groups/<int:pk>/update/', GroupUpdateView.as_view(), name='group-update'),
path('groups/<int:pk>/delete/', GroupDeleteView.as_view(), name='group-delete'),
Expand Down
33 changes: 31 additions & 2 deletions tom_common/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.views.generic import TemplateView
from django.views.generic.edit import FormView, DeleteView
from django.views.generic.edit import UpdateView, CreateView
from django.views.generic.detail import DetailView
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.contrib.auth.mixins import LoginRequiredMixin
Expand Down Expand Up @@ -63,13 +64,41 @@ class UserListView(LoginRequiredMixin, TemplateView):
template_name = 'auth/user_list.html'


class UserDeleteView(SuperuserRequiredMixin, DeleteView):
class UserDeleteView(LoginRequiredMixin, DeleteView):
"""
View that handles deletion of a ``User``. Requires authorization.
View that handles deletion of a ``User``. Requires login.
"""
success_url = reverse_lazy('user-list')
model = User

def dispatch(self, *args, **kwargs):
"""
Directs the class-based view to the correct method for the HTTP request method. Ensures that non-superusers
are not incorrectly updating the profiles of other users.
"""
if not self.request.user.is_superuser and self.request.user.id != self.kwargs['pk']:
return redirect('user-delete', self.request.user.id)
else:
return super().dispatch(*args, **kwargs)


class UserDetailView(LoginRequiredMixin, DetailView):
"""
View to handle creating a user profile page. Requires a login.
"""
template_name = 'tom_common/user_profile.html'
model = User

def dispatch(self, *args, **kwargs):
"""
Directs the class-based view to the correct method for the HTTP request method. Ensures that non-superusers
are not incorrectly updating the profiles of other users.
"""
if not self.request.user.is_superuser and self.request.user.id != self.kwargs['pk']:
return redirect('user-profile', self.request.user.id)
else:
return super().dispatch(*args, **kwargs)


class UserPasswordChangeView(SuperuserRequiredMixin, FormView):
"""
Expand Down

0 comments on commit 2ec4d26

Please sign in to comment.