Skip to content

Commit

Permalink
Update the UI to switch user aka become user
Browse files Browse the repository at this point in the history
- Rename to "switch user"
- Can switch from the user menu
- Switch to choices.js from Select2
- Update the hijacked view, so an alert at the top
  • Loading branch information
theskumar committed Dec 20, 2024
1 parent 01a98aa commit 5fa2c7c
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 37 deletions.
29 changes: 25 additions & 4 deletions hypha/apply/users/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.contrib.auth.forms import AuthenticationForm
from django.template.defaultfilters import mark_safe
from django.utils.translation import gettext_lazy as _
from django_select2.forms import Select2Widget
from rolepermissions import roles
from wagtail.users.forms import UserCreationForm, UserEditForm

Expand Down Expand Up @@ -216,11 +215,33 @@ def clean_full_name(self):
return strip_html_and_nerf_urls(self.cleaned_data["full_name"])


def get_become_user_choices():
"""Returns list of active non-superusers with their roles as choice tuples."""
active_users = User.objects.filter(is_active=True, is_superuser=False)
choices = []

for user in active_users:
role_names = user.get_role_names()
if role_names:
roles_html = ", ".join(
[
f'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">{str(g)}</span>'
for g in role_names
]
)
label = f"{user} ({roles_html})"
else:
label = str(user)

choices.append((user.pk, mark_safe(label)))

return choices


class BecomeUserForm(forms.Form):
user_pk = forms.ModelChoiceField(
widget=Select2Widget,
user_pk = forms.ChoiceField(
help_text=_("Only includes active, non-superusers"),
queryset=User.objects.filter(is_active=True, is_superuser=False),
choices=get_become_user_choices,
label="",
required=False,
)
Expand Down
38 changes: 12 additions & 26 deletions hypha/apply/users/templates/users/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,33 +83,19 @@ <h3 class="text-base mb-2">{% trans "Two-Factor Authentication (2FA)" %}</h3>
<a class="button button--primary" href="{% url 'two_factor:setup' %}">{% trans "Enable 2FA" %}</a>
{% endif %}
</div>
</div>


{% if swappable_form %}
<div class="flex-1 md:ps-8">
{% if swappable_form %}
<h2 class="text-2xl">{% trans "Become" %}:</h2>
<form action="{% url 'hijack-become' %}" method="post" class="form">
{{ swappable_form.media }}
{% csrf_token %}
{% for field in swappable_form %}
{% include "forms/includes/field.html" %}
{% endfor %}
<div class="form__group">
<button class="button button--primary" type="submit">{% trans "Become" %}</button>
</div>
</form>
{% endif %}

{# Remove the comment block tags below when such need arises. e.g. adding new providers #}
{% comment %}
{% can_use_oauth as show_oauth_link %}
{% if show_oauth_link %}
<a href="{% url 'users:oauth' %}">{% trans "Manage OAuth" %}</a>
{% endif %}
{% endcomment %}
{# Remove the comment block tags below when such need arises. e.g. adding new providers #}
{% comment %}
{% can_use_oauth as show_oauth_link %}
{% if show_oauth_link %}
<h3 class="text-base mb-2">Manage OAuth</h3>
<div>
<a href="{% url 'users:oauth' %}">
{% trans "Manage OAuth" %}
</a>
</div>
{% endif %}
{% endif %}
{% endcomment %}
</div>
</div>
{% endblock %}
48 changes: 48 additions & 0 deletions hypha/apply/users/templates/users/become.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{% load i18n static %}

{% modal_title %}{% trans "Switch to User..." %}{% endmodal_title %}

<form class="p-4 form" action="{{ request.path }}" method="post">
{% csrf_token %}
{% if next %}
<input type="hidden" name="next" value="{{ next }}" />
{% endif %}

<label for="id_user_pk" class="block text-sm font-semibold text-fg-muted mb-2">
{% trans "Select User:" %}
</label>

{{ form.user_pk }}

<p class="m-0 pt-2 text-fg-muted text-sm">
{{ form.user_pk.help_text }}
</p>

<div class="mt-5 sm:gap-4 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
class="button button--warning w-full sm:w-auto"
type="submit"
>
{% trans "Switch User" %}
</button>

<button
type="button"
class="inline-flex items-center justify-center w-full px-3 py-2 mt-3 text-sm font-semibold text-gray-900 bg-white rounded-sm shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
@click="show = false"
>{% trans "Cancel" %}</button>
</div>
</i>

<script type="module">
import Choices from "{% static 'js/esm/choices.js-10-2-0.js' %}";

const selectElements = document.querySelectorAll('select#id_user_pk');

// add choices to all select elements
selectElements.forEach((selectElement) => {
new Choices(selectElement, {
allowHTML: true
});
});
</script>
2 changes: 2 additions & 0 deletions hypha/apply/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
account_email_change,
create_password,
elevate_check_code_view,
hijack_view,
oauth,
send_confirm_access_email_view,
set_password_view,
Expand Down Expand Up @@ -112,6 +113,7 @@
ActivationView.as_view(),
name="activate",
),
path("hijack/", hijack_view, name="hijack"),
path("activate/", create_password, name="activate_password"),
path("oauth", oauth, name="oauth"),
# 2FA
Expand Down
25 changes: 19 additions & 6 deletions hypha/apply/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,23 +143,36 @@ def get_success_url(self):
return reverse_lazy("users:account")

def get_context_data(self, **kwargs):
if self.request.user.is_superuser and settings.HIJACK_ENABLE:
swappable_form = BecomeUserForm()
else:
swappable_form = None

show_change_password = (
password_management_enabled() and self.request.user.has_usable_password(),
)

return super().get_context_data(
swappable_form=swappable_form,
default_device=default_device(self.request.user),
show_change_password=show_change_password,
**kwargs,
)


@login_required
def hijack_view(request):
if not settings.HIJACK_ENABLE:
raise Http404(_("Hijack feature is not enabled."))

if not request.user.is_superuser:
raise PermissionDenied()

next = get_redirect_url(request, "next")
if request.method == "POST":
form = BecomeUserForm(request.POST)
if form.is_valid():
return AcquireUserView.as_view()(request)
else:
form = BecomeUserForm()

return render(request, "users/become.html", {"form": form, "next": next})


@login_required
def account_email_change(request):
if request.user.has_usable_password() and not request.is_elevated():
Expand Down
1 change: 1 addition & 0 deletions hypha/core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ def global_vars(request):
"SENTRY_DEBUG": settings.SENTRY_DEBUG,
"SENTRY_PUBLIC_KEY": settings.SENTRY_PUBLIC_KEY,
"SUBMISSIONS_TABLE_EXCLUDED_FIELDS": settings.SUBMISSIONS_TABLE_EXCLUDED_FIELDS,
"HIJACK_ENABLE": settings.HIJACK_ENABLE,
}
1 change: 1 addition & 0 deletions hypha/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
# Enable staff to "hijack" (become) other users.
# Good for testing, might not be a good idea in production.
HIJACK_ENABLE = env.bool("HIJACK_ENABLE", False)
HIJACK_INSERT_BEFORE = None

# Organisation name and e-mail address etc., used in e-mail templates etc.
ORG_EMAIL = env.str("ORG_EMAIL", "info@example.org")
Expand Down
13 changes: 13 additions & 0 deletions hypha/static_src/tailwind/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,16 @@
@import "./components/django-file-field.css";

@import "tailwindcss/utilities";

@layer utilities {
.border-stripe {
border-image: repeating-linear-gradient(
-55deg,
#000 0px,
#000 20px,
#ffb101 20px,
#ffb101 40px
)
10;
}
}
4 changes: 4 additions & 0 deletions hypha/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
class="antialiased min-h-full flex flex-col {% block body_class %}template-{{ page.get_verbose_name|slugify }}{% endblock %}"
>
{% if request.user.is_hijacked %}
{% include "includes/hijack-bar.html" %}
{% endif %}

{% include "includes/sprites.html" %}

{% if messages %}
Expand Down
29 changes: 29 additions & 0 deletions hypha/templates/includes/hijack-bar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% load i18n heroicons %}

<form action="{% url 'hijack:release' %}"
method="POST"
class="bg-yellow-500 text-black px-4 py-2 border-t-4 border-warning border-stripe text-center"
>
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<span>

{% heroicon_micro "exclamation-triangle" size=18 class="inline align-text-bottom" aria_hidden=true %}

{% blocktrans trimmed with user=request.user %}
You are currently working on behalf of <strong class="font-bold">{{ user }}</strong>
{% endblocktrans %}
&LT;{{ request.user.email }}&GT;
{% for role in request.user.get_role_names %}
<span class="inline-block bg-gray-200 text-gray-800 text-xs font-semibold px-2 rounded-full uppercase">
{{ role }}
</span>
{% endfor %}


</span>

<button class="button button--link underline ms-2 font-semibold uppercase" type="submit">
{% translate 'Release' %}
</button>
</form>
33 changes: 32 additions & 1 deletion hypha/templates/includes/user_menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,42 @@
</a>
{% endif %}
{% endif %}
{% if HIJACK_ENABLE and not user.is_hijacked and user.is_superuser %}
<a
class="px-3 py-2 text-black flex items-center gap-2
focus-visible:outline-dashed outline-1 transition-colors border-x
hover:bg-slate-100 hover:text-dark-blue hover:font-semibold group"
hx-get="{% url 'users:hijack' %}?next={{ request.path }}"
hx-target="#htmx-modal"
x-on:click="show = false"
>
{% heroicon_outline "arrows-right-left" size=18 class="stroke-gray-500 inline group-hover:scale-110 group-hover:stroke-2 group-hover:stroke-dark-blue transition-transform" aria_hidden=true %}
{% trans "Switch User" %}
</a>
{% endif %}
{% if user.is_hijacked %}
<form action="{% url 'hijack:release' %}"
method="POST"
>
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">

<button type="submit"
class="px-3 py-2 text-black flex items-center gap-2 w-full
focus-visible:outline-dashed outline-1 transition-colors border-x
hover:bg-slate-100 hover:text-dark-blue hover:font-semibold group"
>
{% heroicon_outline "user-minus" size=18 class="stroke-gray-500 inline group-hover:scale-110 group-hover:stroke-2 group-hover:stroke-dark-blue transition-transform" aria_hidden=true %}
{% translate 'Release User' %}
</button>
</form>
{% endif %}
<hr />
<a
href="{% url 'users:logout' %}"
title="Log out"
class="px-3 py-2 block focus-visible:outline-dashed
outline-1 transition-all border-x border-t text-center font-semibold text-red-600
outline-1 transition-all border-x text-center font-semibold text-red-600
hover:bg-red-100 hover:text-red-900 hover:font-semibold"
>
{% trans "Log out" %}
Expand Down

0 comments on commit 5fa2c7c

Please sign in to comment.