From a8a7a4ee6782776fcf7b0ade1ee4cacf39c3c637 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Thu, 7 Jul 2022 07:53:56 +0100 Subject: [PATCH 01/15] fix: fix typo --- br/templates/backend/message_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/br/templates/backend/message_list.html b/br/templates/backend/message_list.html index 4c00ee6..83fdebb 100644 --- a/br/templates/backend/message_list.html +++ b/br/templates/backend/message_list.html @@ -57,7 +57,7 @@

{{ page_title }}

{% empty %} - No records fo + No records found {% endfor %} From 0296fdc10daa748d5999542db0cf7c1074c4d7b3 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Thu, 7 Jul 2022 07:56:12 +0100 Subject: [PATCH 02/15] feat: add basic users list --- br/templates/backend/user_edit.html | 49 +++++++++++++++ br/templates/backend/user_list.html | 95 +++++++++++++++++++++++++++++ profiles/forms.py | 46 +++++++++++++- profiles/urls.py | 10 +++ profiles/views.py | 59 +++++++++++++++++- unicefng/urls.py | 1 + 6 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 br/templates/backend/user_edit.html create mode 100644 br/templates/backend/user_list.html create mode 100644 profiles/urls.py diff --git a/br/templates/backend/user_edit.html b/br/templates/backend/user_edit.html new file mode 100644 index 0000000..ee480e9 --- /dev/null +++ b/br/templates/backend/user_edit.html @@ -0,0 +1,49 @@ +{% extends 'backend/_layout.html' %} +{% block content %} +
+
+
+

{{ page_title }}

+
+
+
+
+
+
+ {% csrf_token %} +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/br/templates/backend/user_list.html b/br/templates/backend/user_list.html new file mode 100644 index 0000000..cac14cd --- /dev/null +++ b/br/templates/backend/user_list.html @@ -0,0 +1,95 @@ +{% extends 'backend/_layout.html' %} +{% load staticfiles %} +{% block content %} +
+
+
+

{{ page_title }}

+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
Full nameUsernameEmailActive?
+ + + Edit this user + + + + {{ user.get_full_name }}{{ user.username }}{{ user.email|default:'N/A' }} + {% if user.is_active %} + + + + {% else %} + + + + {% endif %} +
No records found
+
+
+
+
+
+{% include 'backend/pagination.html' %} +{% endblock content %} +{% block scripts %} + + +{% endblock scripts %} \ No newline at end of file diff --git a/profiles/forms.py b/profiles/forms.py index e0b1918..3bb6654 100644 --- a/profiles/forms.py +++ b/profiles/forms.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from django import forms +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist from locations.models import Location from profiles.models import Profile @@ -7,8 +9,50 @@ class ProfileAdminForm(forms.ModelForm): locations = forms.ModelMultipleChoiceField( - queryset=Location.objects.filter(type__name=u'State')) + queryset=Location.objects.filter(type__name=u'State')) class Meta: model = Profile fields = (u'user', u'locations') + + +class UserForm(forms.ModelForm): + locations = forms.ModelMultipleChoiceField( + queryset=Location.objects.filter(type__name=u'State')) + can_add_locations = forms.BooleanField(required=False) + can_change_br_reports = forms.BooleanField(required=False) + can_change_dr_reports = forms.BooleanField(required=False) + can_change_mnchw_reports = forms.BooleanField(required=False) + can_change_reporters = forms.BooleanField(required=False) + + class Meta: + model = User + fields = ( + 'username', 'email', 'first_name', 'last_name', 'is_active', + 'is_superuser' + ) + + def __init__(self, *args, **kwargs): + user = kwargs.get('instance') + if user is not None: + initial_data = kwargs.get('initial', {}).copy() + + # set up locations for subscription + try: + profile = user.profile + except ObjectDoesNotExist: + profile = None + + if profile is not None: + initial_data['locations'] = profile.locations + + # set up permissions + initial_data['can_add_locations'] = user.has_perm('locations.add_location') + initial_data['can_change_br_reports'] = user.has_perm('br.change_birthregistration') + initial_data['can_change_dr_reports'] = user.has_perm('dr.change_deathreport') + initial_data['can_change_mnchw_reports'] = user.has_perm('ipd.change_report') + initial_data['can_change_reporters'] = user.has_perm('reporters.change_reporter') + + kwargs['initial'] = initial_data + + super(UserForm, self).__init__(*args, **kwargs) diff --git a/profiles/urls.py b/profiles/urls.py new file mode 100644 index 0000000..9999f44 --- /dev/null +++ b/profiles/urls.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from django.conf.urls import url + +from profiles.views import UserListView + +urlpatterns = [ + url(r'^$', UserListView.as_view(), name='users_list'), + # url(r'^new/?$', UserCreateView.as_view(), name='user_create'), + # url(r'^(?P\d+)/?$', UserUpdateView.as_view(), name='user_edit'), +] \ No newline at end of file diff --git a/profiles/views.py b/profiles/views.py index 91ea44a..5bc4d73 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -1,3 +1,58 @@ -from django.shortcuts import render +# -*- coding: utf-8 -*- +from braces.views import LoginRequiredMixin, PermissionRequiredMixin +from django.conf import settings +from django.contrib.auth.models import User +from django.views.generic import CreateView, ListView, UpdateView -# Create your views here. +from profiles.forms import UserForm +from profiles.models import Profile + +PROTECTED_VIEW_PERMISSION = 'auth.change_user' + + +class UserListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): + context_object_name = 'users' + model = User + ordering = ('-pk',) + page_title = 'Users' + paginate_by = settings.PAGE_SIZE + permission_required = PROTECTED_VIEW_PERMISSION + template_name = 'backend/user_list.html' + + def get_context_data(self, **kwargs): + context = super(UserListView, self).get_context_data(**kwargs) + context['page_title'] = self.page_title + return context + + +class UserCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): + form_class = UserForm + model = User + page_title = 'Create User' + permission_required = PROTECTED_VIEW_PERMISSION + template_name = 'backend/user_create.html' + + def form_valid(self, form): + pass + + def get_context_data(self, **kwargs): + context = super(UserCreateView, self).get_context_data(**kwargs) + context['page_title'] = self.page_title + return context + + +class UserUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): + form_class = UserForm + model = User + page_title = 'Edit User' + permission_required = PROTECTED_VIEW_PERMISSION + template_name = 'backend/user_edit.html' + + def form_valid(self, form): + pass + + def get_context_data(self, **kwargs): + context = super(UserUpdateView, self).get_context_data(**kwargs) + context['page_title'] = self.page_title + return context + diff --git a/unicefng/urls.py b/unicefng/urls.py index 0789435..b264e78 100644 --- a/unicefng/urls.py +++ b/unicefng/urls.py @@ -27,6 +27,7 @@ url(r'incoming/', HttpBackendView.as_view(backend_name='polling')), url(r'^messages/', include('messagebox.urls', namespace='messaging')), url(r'^reporters/', include('reporters.urls', namespace=u'reporters')), + url(r'^users/', include('profiles.urls', namespace='users')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # authentication urls From c262622b631967197707172772b9b532f1575906 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Thu, 7 Jul 2022 08:09:23 +0100 Subject: [PATCH 03/15] feat: add user list to menu --- br/templates/backend/_layout.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/br/templates/backend/_layout.html b/br/templates/backend/_layout.html index 2f6cbd0..52e9b96 100644 --- a/br/templates/backend/_layout.html +++ b/br/templates/backend/_layout.html @@ -35,6 +35,8 @@

RapidSMS

{% if perms.reporters.change_reporter %}
  • Reporters
  • + {% if perms.auth.change_user %} +
  • Users
  • {% endif %}
  • Messages
  • From e80ca029e071bfc1d5b33813de3c3f6d97750e85 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Thu, 7 Jul 2022 14:05:48 +0100 Subject: [PATCH 04/15] feat: add user creation and editing --- br/templates/backend/_layout.html | 2 +- br/templates/backend/user_create.html | 124 ++++++++++++++++++++++++ br/templates/backend/user_edit.html | 79 ++++++++++++++- br/templates/backend/user_list.html | 4 +- profiles/forms.py | 15 ++- profiles/urls.py | 6 +- profiles/views.py | 82 +++++++++++++++- unicefng/templates/common/usermenu.html | 2 + 8 files changed, 301 insertions(+), 13 deletions(-) create mode 100644 br/templates/backend/user_create.html diff --git a/br/templates/backend/_layout.html b/br/templates/backend/_layout.html index 52e9b96..1a0f9b9 100644 --- a/br/templates/backend/_layout.html +++ b/br/templates/backend/_layout.html @@ -36,7 +36,7 @@

    RapidSMS

    {% if perms.reporters.change_reporter %}
  • Reporters
  • {% if perms.auth.change_user %} -
  • Users
  • +
  • Users
  • {% endif %}
  • Messages
  • diff --git a/br/templates/backend/user_create.html b/br/templates/backend/user_create.html new file mode 100644 index 0000000..c072bcb --- /dev/null +++ b/br/templates/backend/user_create.html @@ -0,0 +1,124 @@ +{% extends 'backend/_layout.html' %} +{% block content %} +
    +
    +
    +

    {{ page_title }}

    +
    +
    +
    +
    +
    +
    + {% csrf_token %} +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +

    Enter a new password to change the current password

    +
    +
    +
    + +
    + +

    Repeat the password entered above

    +
    +
    +
    +
    +
    + + +

    An inactive user will not be able to use the system.

    +
    +
    +
    +
    + Permissions +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +

    A superuser has access to everything without needing specific access to anything. Use with caution.

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/br/templates/backend/user_edit.html b/br/templates/backend/user_edit.html index ee480e9..9e95771 100644 --- a/br/templates/backend/user_edit.html +++ b/br/templates/backend/user_edit.html @@ -32,11 +32,86 @@

    {{ page_title }}

    - +
    - +
    +
    + +
    + +

    Enter a new password to change the current password

    +
    +
    +
    + +
    + +

    Repeat the password entered above

    +
    +
    +
    +
    +
    + + +

    An inactive user will not be able to use the system.

    +
    +
    +
    +
    + Permissions +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +

    A superuser has access to everything without needing specific access to anything. Use with caution.

    +
    +
    +
    +
    {% include 'backend/pagination.html' %} + {% endblock content %} {% block scripts %} @@ -88,8 +116,33 @@

    {{ page_title }}

    flatpickr('.date-picker', { dateFormat: 'm/d/Y' }); + + // set up toggling of delete button + const deleteButton = document.getElementById('delete_btn'); + const toggleDeleteButton = () => { + const numChecked = document.querySelectorAll('input[data-delete-checkbox]:checked').length; + if (numChecked === 0) { + // hide delete button + deleteButton.classList.add('invisible'); + } else { + // show delete button + deleteButton.classList.remove('invisible'); + } + }; + + document.addEventListener('change', function (event) { + if (event.target.matches('[data-delete-checkbox]')) + toggleDeleteButton(); + }); }; + // set up submission of delete form + const deleteForm = document.getElementById('delete_form'); + const deleteConfirmButton = document.getElementById('confirm_delete'); + deleteConfirmButton.addEventListener('click', () => { + deleteForm.submit(); + }); + document.addEventListener('DOMContentLoaded', loader); {% endblock scripts %} \ No newline at end of file diff --git a/profiles/forms.py b/profiles/forms.py index dc06df4..b683b8d 100644 --- a/profiles/forms.py +++ b/profiles/forms.py @@ -73,4 +73,24 @@ def clean(self): class UserCreateForm(UserForm): password = forms.CharField(widget=forms.PasswordInput()) - password_confirm = forms.CharField(widget=forms.PasswordInput()) \ No newline at end of file + password_confirm = forms.CharField(widget=forms.PasswordInput()) + + +class UserDeleteForm(forms.Form): + users = forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + self.initiator = kwargs.pop('user', None) + super(UserDeleteForm, self).__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super(UserDeleteForm, self).clean() + users = cleaned_data.get('users') + if self.initiator is not None: + if users.filter(pk=self.initiator.pk).exists(): + self.add_error('users', 'Cannot delete the current user') + + return cleaned_data diff --git a/profiles/urls.py b/profiles/urls.py index f9de473..84e9aa5 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- from django.conf.urls import url -from profiles.views import UserCreateView, UserListView, UserUpdateView +from profiles.views import ( + UserCreateView, UserListView, UserUpdateView, user_delete) urlpatterns = [ url(r'^$', UserListView.as_view(), name='users_list'), url(r'^new/?$', UserCreateView.as_view(), name='user_create'), + url(r'^delete/?$', user_delete, name='user_delete'), url(r'^(?P\d+)/?$', UserUpdateView.as_view(), name='user_edit'), ] \ No newline at end of file diff --git a/profiles/views.py b/profiles/views.py index 76befe8..0967769 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- from braces.views import LoginRequiredMixin, PermissionRequiredMixin from django.conf import settings +from django.contrib import messages from django.contrib.auth.models import Permission, User from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect +from django.http import ( + HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect) +from django.utils.http import is_safe_url from django.views.generic import CreateView, ListView, UpdateView -from profiles.forms import UserCreateForm, UserForm +from profiles.forms import UserCreateForm, UserDeleteForm, UserForm from profiles.models import Profile PROTECTED_VIEW_PERMISSION = 'auth.change_user' @@ -85,6 +88,7 @@ class UserListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super(UserListView, self).get_context_data(**kwargs) context['page_title'] = self.page_title + context['delete_form'] = UserDeleteForm() return context @@ -134,3 +138,30 @@ def get_context_data(self, **kwargs): context = super(UserUpdateView, self).get_context_data(**kwargs) context['page_title'] = self.page_title return context + + +def user_delete(request): + if request.method != 'POST': + return HttpResponseNotAllowed(['POST']) + + if not request.user.is_authenticated: + return HttpResponseForbidden() + + form = UserDeleteForm(request.POST, user=request.user) + redirect_path = request.META.get( + 'HTTP_REFERER', reverse('users:users_list')) + + if form.is_valid(): + users = form.cleaned_data.get('users') + users.delete() + + messages.add_message( + request, + messages.SUCCESS, + 'Success! The selected users were deleted.' + ) + + if not is_safe_url(url=redirect_path, host=request.get_host()): + redirect_path = reverse('users:users_list') + + return HttpResponseRedirect(redirect_path) From fa488a28109d29800f54043d20e0b1b68645017c Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Tue, 19 Jul 2022 09:01:16 +0100 Subject: [PATCH 13/15] chore: correct styling, add message display --- br/templates/backend/reports_list.html | 5 +++-- br/templates/backend/user_list.html | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/br/templates/backend/reports_list.html b/br/templates/backend/reports_list.html index ff6cdf2..85bb1c7 100644 --- a/br/templates/backend/reports_list.html +++ b/br/templates/backend/reports_list.html @@ -16,7 +16,7 @@

    {{ page_title }}

    {% for message in messages %}
    -
    + @@ -84,12 +84,13 @@

    {{ page_title }}

    + {% csrf_token %} {% for report in reports %}
    - +
    diff --git a/br/templates/backend/user_list.html b/br/templates/backend/user_list.html index 7dad2a3..644a404 100644 --- a/br/templates/backend/user_list.html +++ b/br/templates/backend/user_list.html @@ -8,19 +8,31 @@

    {{ page_title }}

    +{% if messages %} +{% for message in messages %} +
    +
    + +
    +
    +{% endfor %} +{% endif %}
    - + Add new user - +
    From db766889cb1d2a39554472ddb1cad8ad72bc8248 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Tue, 19 Jul 2022 09:02:21 +0100 Subject: [PATCH 14/15] chore: add delete form to view --- br/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/br/views.py b/br/views.py index 8337cc9..cf63f4e 100644 --- a/br/views.py +++ b/br/views.py @@ -354,6 +354,7 @@ def get_context_data(self, **kwargs): context = super(ReportListView, self).get_context_data(**kwargs) context['filter_form'] = self.filter_set.form context['page_title'] = self.page_title + context['delete_form'] = ReportDeleteForm() return context def get_queryset(self): From 96978bfb345d509049c6a4fb788bf29f629a8685 Mon Sep 17 00:00:00 2001 From: Odumosu Dipo Date: Tue, 19 Jul 2022 09:07:51 +0100 Subject: [PATCH 15/15] chore: add newline to end of file --- profiles/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiles/urls.py b/profiles/urls.py index 84e9aa5..a3d48b5 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -9,4 +9,4 @@ url(r'^new/?$', UserCreateView.as_view(), name='user_create'), url(r'^delete/?$', user_delete, name='user_delete'), url(r'^(?P\d+)/?$', UserUpdateView.as_view(), name='user_edit'), -] \ No newline at end of file +]