Enable member editing functionality: implement MemberEditView, update routes, modify templates for dynamic filtering and superuser badges, and standardize contact info handling.
This commit is contained in:
@@ -6,7 +6,7 @@ app_name = "members"
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", MemberListView.as_view(), name="list"),
|
path("", MemberListView.as_view(), name="list"),
|
||||||
path("add/", MemberAddView.as_view(), name="add"),
|
path("add/", MemberAddView.as_view(), name="add"),
|
||||||
# path("<int:pk>/edit/", MemberEditView.as_view(), name="edit"),
|
path("<int:pk>/edit/", MemberEditView.as_view(), name="edit"),
|
||||||
path("<int:pk>/delete/", MemberDeleteView.as_view(), name="delete"),
|
path("<int:pk>/delete/", MemberDeleteView.as_view(), name="delete"),
|
||||||
# path("load/", MemberLoadView.as_view(), name="load"),
|
# path("load/", MemberLoadView.as_view(), name="load"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DeleteView, UpdateView, CreateView
|
from django.views.generic import CreateView, DeleteView, UpdateView
|
||||||
from django_filters.views import FilterView
|
from django_filters.views import FilterView
|
||||||
from rules.contrib.views import PermissionRequiredMixin
|
from rules.contrib.views import PermissionRequiredMixin
|
||||||
|
|
||||||
from members.filters import MemberFilter
|
from members.filters import MemberFilter
|
||||||
from members.models import Member
|
|
||||||
from members.forms import MemberForm
|
from members.forms import MemberForm
|
||||||
|
from members.models import Member
|
||||||
|
|
||||||
from ..mixins import HTMXViewMixin
|
from ..mixins import HTMXViewMixin
|
||||||
|
|
||||||
|
|
||||||
class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
||||||
filterset_class = MemberFilter
|
filterset_class = MemberFilter
|
||||||
paginate_by = 50
|
paginate_by = 50
|
||||||
@@ -25,15 +27,15 @@ class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
|||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_filterset_kwargs(self, filterset_class) -> dict[str, Any]:
|
def get_filterset_kwargs(self, filterset_class) -> dict[str, Any]:
|
||||||
kwargs = super().get_filterset_kwargs(filterset_class)
|
kwargs = super().get_filterset_kwargs(filterset_class)
|
||||||
|
|
||||||
filter_values = {} if kwargs["data"] is None else kwargs["data"].dict()
|
filter_values = {} if kwargs["data"] is None else kwargs["data"].dict()
|
||||||
|
|
||||||
if not filter_values:
|
if not filter_values:
|
||||||
filter_values.update({"user__is_active": "true"})
|
filter_values.update({"user__is_active": "true"})
|
||||||
|
|
||||||
kwargs["data"] = filter_values
|
kwargs["data"] = filter_values
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@@ -47,11 +49,11 @@ class MemberAddView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin,
|
|||||||
success_url = reverse_lazy("backend:members:list")
|
success_url = reverse_lazy("backend:members:list")
|
||||||
partial_name = "members/member_form.html#content"
|
partial_name = "members/member_form.html#content"
|
||||||
menu_highlight = "members"
|
menu_highlight = "members"
|
||||||
|
|
||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_success_message(self, cleaned_data):
|
def get_success_message(self, cleaned_data):
|
||||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
||||||
|
|
||||||
@@ -65,14 +67,22 @@ class MemberEditView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin
|
|||||||
success_url = reverse_lazy("backend:members:list")
|
success_url = reverse_lazy("backend:members:list")
|
||||||
partial_name = "members/member_form.html#content"
|
partial_name = "members/member_form.html#content"
|
||||||
menu_highlight = "members"
|
menu_highlight = "members"
|
||||||
|
|
||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_success_message(self, cleaned_data):
|
def get_success_message(self, cleaned_data):
|
||||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
user = self.get_object().user
|
||||||
|
|
||||||
|
initial.update({"first_name": user.first_name, "last_name": user.last_name, "email": user.email, "admin": user.is_superuser})
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
|
|
||||||
class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView):
|
class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||||
model = Member
|
model = Member
|
||||||
@@ -82,21 +92,21 @@ class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMix
|
|||||||
success_url = reverse_lazy("backend:members:list")
|
success_url = reverse_lazy("backend:members:list")
|
||||||
partial_name = "members/member_confirm_delete.html#content"
|
partial_name = "members/member_confirm_delete.html#content"
|
||||||
menu_highlight = "members"
|
menu_highlight = "members"
|
||||||
|
|
||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_success_message(self, cleaned_data):
|
def get_success_message(self, cleaned_data):
|
||||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
||||||
# Soft delete user
|
# Soft delete user
|
||||||
self.object.user.is_active = False
|
self.object.user.is_active = False
|
||||||
self.object.user.save()
|
self.object.user.save()
|
||||||
|
|
||||||
# Do not delete the member object
|
# Do not delete the member object
|
||||||
messages.success(self.request, self.get_success_message({"name": self.object.user.get_full_name()}))
|
messages.success(self.request, self.get_success_message({"name": self.object.user.get_full_name()}))
|
||||||
return HttpResponseRedirect(self.get_success_url())
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<h1 class="page-title">{% translate "Members" %}</h1>
|
<h1 class="page-title">{% translate "Members" %}</h1>
|
||||||
|
|
||||||
<div class="lg:hidden collapse collapse-plus bg-base-100 border-neutral border">
|
<div class="lg:hidden collapse collapse-plus bg-base-100 border-neutral border">
|
||||||
<input type="checkbox" />
|
<input type="checkbox"/>
|
||||||
<div class="collapse-title text-sm font-semibold"><i class="fa-solid fa-filter mr-2"></i>{% translate "Filter" %}{% if filter.is_bound %}<span class="ml-2 badge badge-sm badge-neutral">active</span>{% endif %}</div>
|
<div class="collapse-title text-sm font-semibold"><i class="fa-solid fa-filter mr-2"></i>{% translate "Filter" %}{% if filter.is_bound %}<span class="ml-2 badge badge-sm badge-neutral">active</span>{% endif %}</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<form class="flex flex-col gap-2" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
<form class="flex flex-col gap-2" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
{% for member in object_list %}
|
{% for member in object_list %}
|
||||||
<tr class="hover:bg-base-300">
|
<tr class="hover:bg-base-300">
|
||||||
<td>
|
<td>
|
||||||
<a href="">
|
<a href="{% url "backend:members:edit" member.pk %}" hx-get="{% url "backend:members:edit" member.pk %}" hx-target="#content">
|
||||||
<div class="flex flex-row items-center gap-3">
|
<div class="flex flex-row items-center gap-3">
|
||||||
<div>
|
<div>
|
||||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not member.user.is_active %}
|
{% if not member.user.is_active %}
|
||||||
<div class="badge badge-neutral badge-sm">{% translate "Inactive"%}</div>
|
<div class="badge badge-neutral badge-sm">{% translate "Inactive" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<a class="btn btn-outline btn-sm" href="">
|
<a class="btn btn-outline btn-sm" href="{% url "backend:members:edit" member.pk %}" hx-get="{% url "backend:members:edit" member.pk %}" hx-target="#content">
|
||||||
<i class="fa-solid fa-eye"></i>{% translate "Details" %}
|
<i class="fa-solid fa-eye"></i>{% translate "Details" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -151,13 +151,23 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{% for member in object_list %}
|
{% for member in object_list %}
|
||||||
<a class="border border-base-300 rounded-lg p-2 flex flex-row gap-2 items-center" href="">
|
<a class="border border-base-300 rounded-lg p-2 flex flex-row gap-2 items-center" href="{% url "backend:members:edit" member.pk %}" hx-get="{% url "backend:members:edit" member.pk %}" hx-target="#content">
|
||||||
<div>
|
<div>
|
||||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name width="sm" %}
|
{% avatar first_name=member.user.first_name last_name=member.user.last_name width="sm" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<div class="font-semibold text-sm">{{ member.user.get_full_name }} {% if member.license %}#{{ member.license }}{% endif %}</div>
|
<div class="font-semibold text-sm">
|
||||||
|
{{ member.user.get_full_name }}
|
||||||
|
{% if member.license %}
|
||||||
|
#{{ member.license }}
|
||||||
|
{% endif %}
|
||||||
|
{% if member.user.is_superuser %}
|
||||||
|
<div class="badge badge-xs badge-accent">
|
||||||
|
<i class="fa-solid fa-user-shield"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="opacity-50 text-xs">{{ member.birthday|date:"d M Y"|default:"" }}</div>
|
<div class="opacity-50 text-xs">{{ member.birthday|date:"d M Y"|default:"" }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,6 @@
|
|||||||
<div class="flex flex-row gap-2 grow justify-center items-center">
|
<div class="flex flex-row gap-2 grow justify-center items-center">
|
||||||
{% if member %}
|
{% if member %}
|
||||||
<div class="font-bold text-xl">{{ member.user.get_full_name }}</div>
|
<div class="font-bold text-xl">{{ member.user.get_full_name }}</div>
|
||||||
|
|
||||||
{% if member.user.is_superuser %}
|
|
||||||
<div class="tooltip" data-tip="{% translate "This user is a site admin" %}">
|
|
||||||
<div class="badge badge-sm badge-accent">
|
|
||||||
<i class="fa-solid fa-user-shield"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="font-bold text-xl">{% translate "Create new member" %}</div>
|
<div class="font-bold text-xl">{% translate "Create new member" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -44,29 +36,28 @@
|
|||||||
|
|
||||||
{% if member %}
|
{% if member %}
|
||||||
<div class="mt-4 lg:hidden flex flex-row gap-2">
|
<div class="mt-4 lg:hidden flex flex-row gap-2">
|
||||||
<div class="mt-4 lg:hidden flex flex-row gap-2">
|
{% if member.phone_number %}
|
||||||
{% if member.phone_number %}
|
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-info btn-outline btn-sm grow">
|
||||||
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-info btn-outline btn-sm grow">
|
<i class="fa-solid fa-phone"></i>
|
||||||
<i class="fa-solid fa-phone"></i>
|
{{ member.phone_number }}
|
||||||
{{ member.phone }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if member.emergency_phone_number %}
|
|
||||||
<a href="{{ member.emergency_phone_number.as_rfc3966 }}" class="btn btn-error btn-outline btn-sm grow">
|
|
||||||
<i class="fa-solid fa-file-medical"></i>
|
|
||||||
{{ member.emergency_phone_number }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if config.TF_ENABLE_TEAMS %}
|
|
||||||
<a href="?member__user__first_name={{ member.user.first_name }}&member__user__last_name={{ member.user.last_name }}" class="btn btn-sm w-full mt-2 btn-outline btn-neutral lg:hidden">
|
|
||||||
<i class="fa-solid fa-ticket"></i>{% translate "View team memberships" %}
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="flex flex-row items-center mt-8 gap-x-3 hidden lg:flex">
|
{% if member.emergency_phone_number %}
|
||||||
|
<a href="{{ member.emergency_phone_number.as_rfc3966 }}" class="btn btn-error btn-outline btn-sm grow">
|
||||||
|
<i class="fa-solid fa-file-medical"></i>
|
||||||
|
{{ member.emergency_phone_number }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if config.TF_ENABLE_TEAMS %}
|
||||||
|
<a href="?member__user__first_name={{ member.user.first_name }}&member__user__last_name={{ member.user.last_name }}" class="btn btn-sm w-full mt-2 btn-outline btn-neutral lg:hidden">
|
||||||
|
<i class="fa-solid fa-ticket"></i>{% translate "View team memberships" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="hidden flex-row items-center mt-8 gap-x-3 hidden lg:flex">
|
||||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
||||||
|
|
||||||
<h2 class="page-subtitle border-b-0! mt-0!">{{ member.user.get_full_name }}</h2>
|
<h2 class="page-subtitle border-b-0! mt-0!">{{ member.user.get_full_name }}</h2>
|
||||||
@@ -76,12 +67,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="justify-end hidden gap-2 lg:flex lg:flex-row grow">
|
<div class="justify-end hidden gap-2 lg:flex lg:flex-row grow">
|
||||||
{% if member.phone %}
|
{% if member.phone_number %}
|
||||||
<a href="{{ member.phone.as_rfc3966 }}" class="btn btn-outline btn-info"><i class="fa-solid fa-phone"></i>{{ member.phone }}</a>
|
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-outline btn-info"><i class="fa-solid fa-phone"></i>{{ member.phone_number }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if member.emergency_phone %}
|
{% if member.emergency_phone_number %}
|
||||||
<a href="{{ member.emergency_phone.as_rfc3966 }}" class="btn btn-outline btn-error"><i class="fa-solid fa-file-medical"></i>{{ member.emergency_phone }}</a>
|
<a href="{{ member.emergency_phone_number.as_rfc3966 }}" class="btn btn-outline btn-error"><i class="fa-solid fa-file-medical"></i>{{ member.emergency_phone_number }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if config.TF_ENABLE_TEAMS %}
|
{% if config.TF_ENABLE_TEAMS %}
|
||||||
@@ -90,7 +81,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="{% url "backend:members:members_delete" member.id %}" class="btn btn-error btn-outline">
|
<a href="{% url "backend:members:delete" member.pk %}" class="btn btn-error btn-outline">
|
||||||
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
|
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,33 +105,34 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Personal information" %}</h2>
|
<h2 class="page-subtitle">{% translate "Personal information" %}</h2>
|
||||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{% form_field form.first_name %}
|
{% form_field form.first_name %}
|
||||||
{% form_field form.last_name %}
|
{% form_field form.last_name %}
|
||||||
{% form_field form.birthday %}
|
{% form_field form.birthday %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Contact information" %}</h2>
|
<h2 class="page-subtitle">{% translate "Contact information" %}</h2>
|
||||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{% form_field form.email %}
|
{% form_field form.email %}
|
||||||
{% form_field form.phone_number %}
|
{% form_field form.phone_number %}
|
||||||
{% form_field form.emergency_phone_number %}
|
{% form_field form.emergency_phone_number %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Family information" %}</h2>
|
<h2 class="page-subtitle">{% translate "Family information" %}</h2>
|
||||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{% form_field form.family_members %}
|
{% form_field form.family_members %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Club information" %}</h2>
|
<h2 class="page-subtitle">{% translate "Club information" %}</h2>
|
||||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{% form_field form.license %}
|
{% form_field form.license %}
|
||||||
{% form_field form.admin show_as_toggle=True %}
|
{% form_field form.admin show_as_toggle=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Password" %}</h2>
|
<h2 class="page-subtitle">{% translate "Password" %}</h2>
|
||||||
<div class="mt-2 text-sm text-justify">{% blocktranslate %}Setting the password here will overwrite the current password for this member, after changing the member will be prompted to set a new password at the next login.<br /><br />If both fields are empty the current password will not be changed.{% endblocktranslate %}</div>
|
<div class="mt-2 text-sm text-justify">{% blocktranslate %}Setting the password here will overwrite the current password for this member, after changing the member will be prompted to set a new password at the next login.<br/><br/>If both
|
||||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
fields are empty the current password will not be changed.{% endblocktranslate %}</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{% form_field form.password %}
|
{% form_field form.password %}
|
||||||
{% form_field form.password_confirmation %}
|
{% form_field form.password_confirmation %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user