Enhance trainee management UI and functionality
- Improved bulk edit options for user groups with success alerts. - Updated trainee selection count display. - Refined HTMX responses for user actions, providing better feedback. - Enhanced checkbox toggle functionality for trainee selection.
This commit is contained in:
@@ -27,15 +27,15 @@
|
|||||||
<th class="sortable" data-col="3">Supervisor <span class="sort-indicator"></span></th>
|
<th class="sortable" data-col="3">Supervisor <span class="sort-indicator"></span></th>
|
||||||
<th>Edit</th>
|
<th>Edit</th>
|
||||||
<th class="trainee-bulk-edit">Bulk Edit<br/>
|
<th class="trainee-bulk-edit">Bulk Edit<br/>
|
||||||
<span _="on click log 'test' then log 'hello' then repeat for i in <input/>
|
<span _="on click repeat for i in .trainee-checkbox
|
||||||
-- set i.checked to not i.checked
|
toggle [@checked] on i
|
||||||
toggle [@checked] on i
|
end then call updateSelectedCount()
|
||||||
">Toggle All</span>
|
">Toggle All</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% for trainee in trainees %}
|
{% for trainee in trainees %}
|
||||||
<tr class="trainee" _="on click log 'test' then log the next <input/> then toggle [@checked] on <input/> in me">
|
<tr class="trainee" _="on click if target.tagName is not 'INPUT' and target.tagName is not 'A' toggle [@checked] on <input/> in me end then call updateSelectedCount()">
|
||||||
<td>{{trainee.user.first_name}} {{trainee.user.last_name}}
|
<td>{{trainee.user.first_name}} {{trainee.user.last_name}}
|
||||||
<a href="{% url 'account_profile' trainee.username %}" class="opacity-25"><i class="bi bi-link"></i></a>
|
<a href="{% url 'account_profile' trainee.username %}" class="opacity-25"><i class="bi bi-link"></i></a>
|
||||||
</td>
|
</td>
|
||||||
@@ -61,7 +61,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td><a href="{% url 'account_update' trainee.user.username %}?redirect={{request.path}}">User</a>/<a href="{% url 'account_profile_update' trainee.user.username %}?redirect={{request.path}}">Profile</a>
|
<td><a href="{% url 'account_update' trainee.user.username %}?redirect={{request.path}}">User</a>/<a href="{% url 'account_profile_update' trainee.user.username %}?redirect={{request.path}}">Profile</a>
|
||||||
<span class="hover-highlight remove-trainee"><i class="bi bi-x-circle" title="Click to remove trainee status"
|
<span class="hover-highlight remove-trainee"><i class="bi bi-x-circle" title="Click to remove trainee status"
|
||||||
hx-get = "{% url 'generic:user_not_trainee' trainee.user.pk %}"
|
hx-get="{% url 'generic:user_not_trainee' trainee.user.pk %}"
|
||||||
|
hx-target="closest tr"
|
||||||
|
hx-swap="outerHTML swap:0.5s"
|
||||||
|
hx-confirm="Are you sure you want to remove trainee status for this user?"
|
||||||
></i></span>
|
></i></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="trainee-bulk-edit">
|
<td class="trainee-bulk-edit">
|
||||||
@@ -85,6 +88,7 @@
|
|||||||
|
|
||||||
<div id="bulk-edit" class="trainee-bulk-edit">
|
<div id="bulk-edit" class="trainee-bulk-edit">
|
||||||
<h3>Bulk edit options</h3>
|
<h3>Bulk edit options</h3>
|
||||||
|
<p class="text-muted small mb-2">Selected trainees: <span id="selected-count" class="fw-bold">0</span></p>
|
||||||
These actions will act on all the selected trainees. Refresh the page to cancel / clear.
|
These actions will act on all the selected trainees. Refresh the page to cancel / clear.
|
||||||
<div id="htmx-info"></div>
|
<div id="htmx-info"></div>
|
||||||
<div id="htmx-options"></div>
|
<div id="htmx-options"></div>
|
||||||
@@ -101,15 +105,25 @@
|
|||||||
hx-include="[name='selection']"
|
hx-include="[name='selection']"
|
||||||
hx-target="#htmx-options"
|
hx-target="#htmx-options"
|
||||||
title="Changes the selected users grade."
|
title="Changes the selected users grade."
|
||||||
|
|
||||||
>Edit grade</button>
|
>Edit grade</button>
|
||||||
|
<button id="bulk-add-group-button"
|
||||||
|
hx-post="{% url 'generic:users_bulk_edit' %}"
|
||||||
|
hx-include="[name='selection']"
|
||||||
|
hx-target="#htmx-options"
|
||||||
|
title="Adds the selected user to a group."
|
||||||
|
>Add group</button>
|
||||||
|
<button id="bulk-remove-group-button"
|
||||||
|
hx-post="{% url 'generic:users_bulk_edit' %}"
|
||||||
|
hx-include="[name='selection']"
|
||||||
|
hx-target="#htmx-options"
|
||||||
|
title="Removes the selected user from a group."
|
||||||
|
>Remove group</button>
|
||||||
<button id="bulk-deactivate-user-button"
|
<button id="bulk-deactivate-user-button"
|
||||||
hx-post="{% url 'generic:users_bulk_edit' %}"
|
hx-post="{% url 'generic:users_bulk_edit' %}"
|
||||||
hx-include="[name='selection']"
|
hx-include="[name='selection']"
|
||||||
hx-target="#htmx-options"
|
hx-target="#htmx-options"
|
||||||
hx-confirm="This will deactivate all selected users, are you sure you wish to continue?"
|
hx-confirm="This will deactivate all selected users, are you sure you wish to continue?"
|
||||||
title="Deactivates the selected users"
|
title="Deactivates the selected users"
|
||||||
|
|
||||||
>Deactivate</button>
|
>Deactivate</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,6 +217,27 @@
|
|||||||
lastSorted = {idx, asc};
|
lastSorted = {idx, asc};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update selection count
|
||||||
|
const updateSelectedCount = () => {
|
||||||
|
const checkedCount = table.querySelectorAll('.trainee-checkbox:checked').length;
|
||||||
|
const countEl = document.getElementById('selected-count');
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = checkedCount;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for checkbox changes directly
|
||||||
|
table.addEventListener('change', function(e) {
|
||||||
|
if (e.target.classList.contains('trainee-checkbox')) {
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose to window so hyperscript can call it
|
||||||
|
window.updateSelectedCount = updateSelectedCount;
|
||||||
|
|
||||||
|
updateSelectedCount();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -68,3 +68,71 @@ def test_trainee_stuff(client, create_cid_manager, create_users):
|
|||||||
assert (
|
assert (
|
||||||
len(trainee_rows) == UserProfile.objects.filter(peninsula_trainee=True).count()
|
len(trainee_rows) == UserProfile.objects.filter(peninsula_trainee=True).count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bulk_edit_groups(client, create_cid_manager, create_users):
|
||||||
|
from generic.models import UserUserGroup
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
client.login(username="cidmanager", password="password")
|
||||||
|
|
||||||
|
group = UserUserGroup.objects.create(name="Test Group")
|
||||||
|
|
||||||
|
user1 = User.objects.get(username="user1")
|
||||||
|
user2 = User.objects.get(username="user2")
|
||||||
|
|
||||||
|
# Bulk add to group
|
||||||
|
response = client.post(
|
||||||
|
reverse("generic:users_bulk_edit"),
|
||||||
|
data={"selection": [user1.pk, user2.pk]},
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
HTTP_HX_TRIGGER=f"add-group--{group.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Group added" in response.content.decode()
|
||||||
|
assert "alert alert-success" in response.content.decode()
|
||||||
|
|
||||||
|
user1.refresh_from_db()
|
||||||
|
user2.refresh_from_db()
|
||||||
|
assert group in user1.user_groups.all()
|
||||||
|
assert group in user2.user_groups.all()
|
||||||
|
|
||||||
|
# Bulk remove from group
|
||||||
|
response = client.post(
|
||||||
|
reverse("generic:users_bulk_edit"),
|
||||||
|
data={"selection": [user1.pk, user2.pk]},
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
HTTP_HX_TRIGGER=f"remove-group--{group.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Group removed" in response.content.decode()
|
||||||
|
assert "alert alert-success" in response.content.decode()
|
||||||
|
|
||||||
|
user1.refresh_from_db()
|
||||||
|
user2.refresh_from_db()
|
||||||
|
assert group not in user1.user_groups.all()
|
||||||
|
assert group not in user2.user_groups.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_not_trainee_htmx(client, create_cid_manager, create_users):
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
client.login(username="cidmanager", password="password")
|
||||||
|
user2 = User.objects.get(username="user2")
|
||||||
|
assert user2.userprofile.peninsula_trainee is True
|
||||||
|
|
||||||
|
# Call view via HTMX
|
||||||
|
response = client.get(
|
||||||
|
reverse("generic:user_not_trainee", kwargs={"user_id": user2.pk}),
|
||||||
|
HTTP_HX_REQUEST="true"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == b""
|
||||||
|
|
||||||
|
user2.refresh_from_db()
|
||||||
|
assert user2.userprofile.peninsula_trainee is False
|
||||||
|
|||||||
+52
-9
@@ -4916,15 +4916,28 @@ def users_bulk_delete_supervisors(request):
|
|||||||
u.userprofile.supervisor = None
|
u.userprofile.supervisor = None
|
||||||
u.save()
|
u.save()
|
||||||
|
|
||||||
modified_users = ",".join([user.username for user in user_models])
|
modified_users = ", ".join([f"{user.first_name} {user.last_name}" for user in user_models])
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
f"Supervisors removed from {modified_users}. Refresh page to see update",
|
f"""
|
||||||
content_type="text/plain",
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<strong>Success!</strong> Supervisors removed from: {modified_users}. Refresh page to see update.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
content_type="text/html",
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return HttpResponse("No users selected", content_type="text/plain")
|
return HttpResponse(
|
||||||
|
"""
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<strong>Warning!</strong> No users selected.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
content_type="text/html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NoUsersSelected(Exception):
|
class NoUsersSelected(Exception):
|
||||||
@@ -4947,6 +4960,8 @@ def user_not_trainee(request, user_id):
|
|||||||
user.userprofile.peninsula_trainee = False
|
user.userprofile.peninsula_trainee = False
|
||||||
user.userprofile.save()
|
user.userprofile.save()
|
||||||
|
|
||||||
|
if request.htmx:
|
||||||
|
return HttpResponse("")
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
f"{user.username} is no longer a trainee", content_type="text/plain"
|
f"{user.username} is no longer a trainee", content_type="text/plain"
|
||||||
)
|
)
|
||||||
@@ -4968,7 +4983,12 @@ def users_bulk_edit(request):
|
|||||||
u.is_active = False
|
u.is_active = False
|
||||||
u.save()
|
u.save()
|
||||||
|
|
||||||
html = "Users deactivated"
|
html = """
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<strong>Success!</strong> Selected users have been deactivated.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
case "bulk-edit-grade-button":
|
case "bulk-edit-grade-button":
|
||||||
user_grades = UserGrades.objects.all()
|
user_grades = UserGrades.objects.all()
|
||||||
@@ -5013,7 +5033,12 @@ def users_bulk_edit(request):
|
|||||||
for u in user_models:
|
for u in user_models:
|
||||||
u.userprofile.grade_id = grade
|
u.userprofile.grade_id = grade
|
||||||
u.save()
|
u.save()
|
||||||
html = "Grades updated"
|
html = """
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<strong>Success!</strong> Grades updated successfully.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
case "bulk-add-group-button":
|
case "bulk-add-group-button":
|
||||||
user_groups = UserUserGroup.objects.all()
|
user_groups = UserUserGroup.objects.all()
|
||||||
|
|
||||||
@@ -5064,7 +5089,12 @@ def users_bulk_edit(request):
|
|||||||
pass
|
pass
|
||||||
u.user_groups.add(r.split("--")[-1])
|
u.user_groups.add(r.split("--")[-1])
|
||||||
u.save()
|
u.save()
|
||||||
html = "Group added"
|
html = """
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<strong>Success!</strong> Group added to all selected users.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
case r if r.startswith("remove-group"):
|
case r if r.startswith("remove-group"):
|
||||||
user_models = get_user_selection_from_request(request)
|
user_models = get_user_selection_from_request(request)
|
||||||
|
|
||||||
@@ -5072,12 +5102,25 @@ def users_bulk_edit(request):
|
|||||||
for u in user_models:
|
for u in user_models:
|
||||||
group_id = r.split("--")[-1]
|
group_id = r.split("--")[-1]
|
||||||
u.user_groups.remove(group_id)
|
u.user_groups.remove(group_id)
|
||||||
html = "Group removed"
|
html = """
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<strong>Success!</strong> Group removed from all selected users.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
case _:
|
case _:
|
||||||
html = "None"
|
html = "None"
|
||||||
return HttpResponse(html, content_type="text/html")
|
return HttpResponse(html, content_type="text/html")
|
||||||
except NoUsersSelected:
|
except NoUsersSelected:
|
||||||
return HttpResponse("No users selected", content_type="text/html")
|
return HttpResponse(
|
||||||
|
"""
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<strong>Warning!</strong> No users selected.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
content_type="text/html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
|
|||||||
Reference in New Issue
Block a user