Implement hiding and showing functionality for user exams and collections

This commit is contained in:
Ross
2026-06-15 11:23:00 +01:00
parent 35ab7443d2
commit c50ace771e
11 changed files with 400 additions and 50 deletions
+31 -5
View File
@@ -98,13 +98,39 @@ Instead
## Docker (Dev)
### Start Dev Environment
Start the development server with local configurations:
```bash
# Start dev environment
COMPOSE_ENV=dev docker compose -f docker/docker-compose.dev.yml up -d --build
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml up -d --build
```
# Run management commands inside the container
docker compose exec web python manage.py migrate
docker compose exec web python manage.py createsuperuser
### Run Django Management Commands inside the Container
Since PostgreSQL and Redis run in docker, run all database/management commands within the web container:
```bash
# Run database migrations
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py migrate
# Generate new migrations
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py makemigrations
# Check if migrations are missing (useful for CI/pre-commit)
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py makemigrations --check
# Run system checks
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py check
# Create a Django superuser
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml exec web python manage.py createsuperuser
```
### Runserver and service commands
To restart the web server/runserver or tail the container output:
```bash
# Restart the runserver/web service container
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml restart web
# View container logs (interactive)
COMPOSE_ENV=dev docker compose -f docker/docker-compose.local.yml -f docker/docker-compose.dev.yml logs -f web
```
Dev server runs on `http://localhost:8080` (nginx → gunicorn/runserver).
@@ -5,40 +5,73 @@
<h5 class="card-title">Your collections</h5>
<h6 class="mt-3">Available to start</h6>
<ul class="list-unstyled">
<ul class="list-group list-group-sm mb-3">
{% for collection in available_collections %}
<li>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
{% if collection.description %}<small class="text-muted">{{ collection.description }}</small>{% endif %}
<li class="list-group-item d-flex justify-content-between align-items-center {% if collection.is_hidden %}opacity-50 bg-secondary bg-opacity-10{% endif %}">
<div>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
{% if collection.description %}<small class="text-muted"> — {{ collection.description }}</small>{% endif %}
{% if collection.is_hidden %}<span class="badge bg-warning text-dark ms-2">Hidden</span>{% endif %}
</div>
<form method="post" action="{% url 'generic:toggle_hide_item' %}" hx-post="{% url 'generic:toggle_hide_item' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="content_type_id" value="{{ collection.content_type_id }}">
<input type="hidden" name="object_id" value="{{ collection.pk }}">
<button type="submit" class="btn btn-sm btn-link text-muted p-0" title="{% if collection.is_hidden %}Unhide collection{% else %}Hide collection{% endif %}">
<i class="bi {% if collection.is_hidden %}bi-eye-slash-fill text-warning{% else %}bi-eye{% endif %}" style="font-size: 1.15rem;"></i>
</button>
</form>
</li>
{% empty %}
<li class="text-muted">No collections available to start.</li>
<li class="list-group-item text-muted">No collections available to start.</li>
{% endfor %}
</ul>
<h6 class="mt-3">In progress</h6>
<ul class="list-unstyled">
<ul class="list-group list-group-sm mb-3">
{% for collection in in_progress_collections %}
<li>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
<span class="badge bg-warning text-dark ms-2">In progress</span>
{% if collection.description %}<small class="text-muted"> — {{ collection.description }}</small>{% endif %}
<li class="list-group-item d-flex justify-content-between align-items-center {% if collection.is_hidden %}opacity-50 bg-secondary bg-opacity-10{% endif %}">
<div>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
<span class="badge bg-warning text-dark ms-2">In progress</span>
{% if collection.description %}<small class="text-muted"> — {{ collection.description }}</small>{% endif %}
{% if collection.is_hidden %}<span class="badge bg-warning text-dark ms-2">Hidden</span>{% endif %}
</div>
<form method="post" action="{% url 'generic:toggle_hide_item' %}" hx-post="{% url 'generic:toggle_hide_item' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="content_type_id" value="{{ collection.content_type_id }}">
<input type="hidden" name="object_id" value="{{ collection.pk }}">
<button type="submit" class="btn btn-sm btn-link text-muted p-0" title="{% if collection.is_hidden %}Unhide collection{% else %}Hide collection{% endif %}">
<i class="bi {% if collection.is_hidden %}bi-eye-slash-fill text-warning{% else %}bi-eye{% endif %}" style="font-size: 1.15rem;"></i>
</button>
</form>
</li>
{% empty %}
<li class="text-muted">No in-progress collections.</li>
<li class="list-group-item text-muted">No in-progress collections.</li>
{% endfor %}
</ul>
<h6 class="mt-3">Finished</h6>
<ul class="list-unstyled">
<ul class="list-group list-group-sm">
{% for collection in finished_collections %}
<li>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
<span class="badge bg-success ms-2">Completed</span>
{% if collection.description %}<small class="text-muted"> — {{ collection.description }}</small>{% endif %}
<li class="list-group-item d-flex justify-content-between align-items-center {% if collection.is_hidden %}opacity-50 bg-secondary bg-opacity-10{% endif %}">
<div>
<a href="{% url 'atlas:collection_take_start' collection.pk %}">{{ collection.name }}</a>
<span class="badge bg-success ms-2">Completed</span>
{% if collection.description %}<small class="text-muted"> — {{ collection.description }}</small>{% endif %}
{% if collection.is_hidden %}<span class="badge bg-warning text-dark ms-2">Hidden</span>{% endif %}
</div>
<form method="post" action="{% url 'generic:toggle_hide_item' %}" hx-post="{% url 'generic:toggle_hide_item' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="content_type_id" value="{{ collection.content_type_id }}">
<input type="hidden" name="object_id" value="{{ collection.pk }}">
<button type="submit" class="btn btn-sm btn-link text-muted p-0" title="{% if collection.is_hidden %}Unhide collection{% else %}Hide collection{% endif %}">
<i class="bi {% if collection.is_hidden %}bi-eye-slash-fill text-warning{% else %}bi-eye{% endif %}" style="font-size: 1.15rem;"></i>
</button>
</form>
</li>
{% empty %}
<li class="text-muted">No finished collections.</li>
<li class="list-group-item text-muted">No finished collections.</li>
{% endfor %}
</ul>
+25 -3
View File
@@ -112,6 +112,7 @@ from .forms import (
UserQuestionAnswerForm,
UserReportAnswerForm,
)
from generic.models import UserHiddenItem
from .models import (
Case,
CaseCollection,
@@ -2786,11 +2787,32 @@ def user_collections(request):
# Finished (completed)
finished_qs = CaseCollection.objects.filter(pk__in=list(finished_ids)).order_by("name")
show_hidden = request.GET.get("show_hidden") in ("true", "1")
# Get content type for CaseCollection
ct_cc = ContentType.objects.get_for_model(CaseCollection)
hidden_ids = set(
UserHiddenItem.objects.filter(user=target_user, content_type=ct_cc).values_list("object_id", flat=True)
)
if not show_hidden:
available_qs = available_qs.exclude(pk__in=hidden_ids)
in_progress_qs = in_progress_qs.exclude(pk__in=hidden_ids)
finished_qs = finished_qs.exclude(pk__in=hidden_ids)
available_list = list(available_qs)
in_progress_list = list(in_progress_qs)
finished_list = list(finished_qs)
for coll in available_list + in_progress_list + finished_list:
coll.is_hidden = coll.pk in hidden_ids
context = {
"target_user": target_user,
"available_collections": available_qs,
"in_progress_collections": in_progress_qs,
"finished_collections": finished_qs,
"available_collections": available_list,
"in_progress_collections": in_progress_list,
"finished_collections": finished_list,
"show_hidden": show_hidden,
}
# If called via HTMX return a small fragment suitable for swapping
+30
View File
@@ -0,0 +1,30 @@
# Generated by Django 6.0.1 on 2026-06-15 09:44
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('generic', '0032_ciduserexam_shared_with_users'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserHiddenItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hidden_items', to=settings.AUTH_USER_MODEL)),
],
options={
'constraints': [models.UniqueConstraint(fields=('user', 'content_type', 'object_id'), name='unique_user_hidden_item')],
},
),
]
+31
View File
@@ -698,6 +698,12 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
default=True,
)
@property
def content_type_id(self) -> int:
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get_for_model(self).id
authors_only = models.BooleanField(
help_text="If true only exam/collection authors will be able to view.",
default=False,
@@ -2390,3 +2396,28 @@ class FindingBase(models.Model):
return get_pretty_json(json.loads(self.viewport_json))
except Exception:
return ""
class UserHiddenItem(models.Model):
"""Model to track which exams or collections have been hidden by a user.
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="hidden_items",
)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_type", "object_id")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["user", "content_type", "object_id"],
name="unique_user_hidden_item",
)
]
def __str__(self):
return f"{self.user} hid {self.content_object}"
+1
View File
@@ -11,6 +11,7 @@ urlpatterns = [
# User content review views
path("user-content/", views.user_content_list, name="user_content_list"),
path("user-content/<int:user_id>/", views.user_content_review, name="user_content_review"),
path("toggle-hide-item/", views.toggle_hide_item, name="toggle_hide_item"),
path("examination/", views.ExaminationView.as_view(), name="examination_view"),
path("examination/<int:pk>", views.examination_detail, name="examination_detail"),
path(
+39
View File
@@ -7114,4 +7114,43 @@ def user_content_review(request, user_id):
# Render HTMX partial (or full page if not HTMX)
template = "generic/partials/user_content_review.html"
return render(request, template, context)
@login_required
@require_POST
def toggle_hide_item(request):
"""Toggle the hidden status of an exam or collection for the current user.
Scope: Authenticated end users.
Functionality: Adds or removes a UserHiddenItem record for the given
content_type_id and object_id, then triggers a page reload via HX-Refresh.
"""
from django.contrib.contenttypes.models import ContentType
from generic.models import UserHiddenItem
content_type_id = request.POST.get("content_type_id")
object_id = request.POST.get("object_id")
if not content_type_id or not object_id:
return HttpResponse("Missing parameters", status=400)
try:
content_type = ContentType.objects.get(pk=int(content_type_id))
object_id = int(object_id)
except (ContentType.DoesNotExist, ValueError):
return HttpResponse("Invalid parameters", status=400)
hidden_item, created = UserHiddenItem.objects.get_or_create(
user=request.user,
content_type=content_type,
object_id=object_id,
)
if not created:
hidden_item.delete()
response = HttpResponse("")
response["HX-Refresh"] = "true"
return response
+99
View File
@@ -182,4 +182,103 @@ def test_people_page_and_user_search(client, create_users, create_cid_manager):
assert b"No users found matching" in response_no_match.content
@pytest.mark.django_db
def test_hide_exams_and_collections(client, create_users):
from django.contrib.contenttypes.models import ContentType
from physics.models import Exam as PhysicsExam
from atlas.models import CaseCollection
from generic.models import UserHiddenItem
user1, user2 = create_users
# 1. Create a PhysicsExam and associate with user1
exam = PhysicsExam.objects.create(
name="Test Physics Exam", exam_mode=True, active=True
)
user1.user_physics_exams.add(exam)
# 2. Create a CaseCollection and associate with user1
collection = CaseCollection.objects.create(
name="Test Case Collection", exam_mode=True, active=True
)
user1.user_casecollection_exams.add(collection)
# Log in as user1
client.force_login(user1)
# Verify they are visible on user scores page initially
response = client.get(reverse("user_scores"))
assert response.status_code == 200
assert b"Test Physics Exam" in response.content
# Load atlas collections partial
response_cc = client.get(reverse("atlas:user_collections"))
assert response_cc.status_code == 200
assert b"Test Case Collection" in response_cc.content
# 3. Hide the PhysicsExam
exam_ct = ContentType.objects.get_for_model(PhysicsExam)
response_hide_exam = client.post(
reverse("generic:toggle_hide_item"),
{"content_type_id": exam_ct.id, "object_id": exam.id},
)
assert response_hide_exam.status_code == 200
assert response_hide_exam.headers.get("HX-Refresh") == "true"
# Verify UserHiddenItem exists
assert UserHiddenItem.objects.filter(
user=user1, content_type=exam_ct, object_id=exam.id
).exists()
# Verify exam is hidden now from standard user_scores
response_hidden = client.get(reverse("user_scores"))
assert response_hidden.status_code == 200
assert b"Test Physics Exam" not in response_hidden.content
# But present when show_hidden=true is requested
response_show_hidden = client.get(reverse("user_scores") + "?show_hidden=true")
assert response_show_hidden.status_code == 200
assert b"Test Physics Exam" in response_show_hidden.content
assert b"Hidden" in response_show_hidden.content
# 4. Hide the CaseCollection
cc_ct = ContentType.objects.get_for_model(CaseCollection)
response_hide_cc = client.post(
reverse("generic:toggle_hide_item"),
{"content_type_id": cc_ct.id, "object_id": collection.pk},
)
assert response_hide_cc.status_code == 200
assert response_hide_cc.headers.get("HX-Refresh") == "true"
assert UserHiddenItem.objects.filter(
user=user1, content_type=cc_ct, object_id=collection.pk
).exists()
# Verify collection is hidden from normal view
response_cc_hidden = client.get(reverse("atlas:user_collections"))
assert response_cc_hidden.status_code == 200
assert b"Test Case Collection" not in response_cc_hidden.content
# Verify collection is visible in show_hidden view
response_cc_show_hidden = client.get(reverse("atlas:user_collections") + "?show_hidden=true")
assert response_cc_show_hidden.status_code == 200
assert b"Test Case Collection" in response_cc_show_hidden.content
assert b"Hidden" in response_cc_show_hidden.content
# 5. Unhide the PhysicsExam by toggling again
response_unhide_exam = client.post(
reverse("generic:toggle_hide_item"),
{"content_type_id": exam_ct.id, "object_id": exam.id},
)
assert response_unhide_exam.status_code == 200
assert not UserHiddenItem.objects.filter(
user=user1, content_type=exam_ct, object_id=exam.id
).exists()
response_unhidden = client.get(reverse("user_scores"))
assert response_unhidden.status_code == 200
assert b"Test Physics Exam" in response_unhidden.content
+42 -12
View File
@@ -706,39 +706,69 @@ def user_scores(request, user=None):
user = request.user
admin = False
from django.contrib.contenttypes.models import ContentType
from generic.models import UserHiddenItem
show_hidden = request.GET.get("show_hidden") in ("true", "1")
# Fetch user's hidden items map: {content_type_id: {object_ids}}
hidden_map = {}
for ct_id, obj_id in UserHiddenItem.objects.filter(user=user).values_list("content_type_id", "object_id"):
hidden_map.setdefault(ct_id, set()).add(obj_id)
# Filter Exam Results list (all_exams)
exams = get_user_exams(user)
filtered_exams = []
for exam_type, exams_qs in exams:
exam_model = exams_qs.model
ct_id = ContentType.objects.get_for_model(exam_model).id
hidden_ids = hidden_map.get(ct_id, set())
# case_collections = request.user.casecollection_exams.all()
if not show_hidden:
exams_qs = exams_qs.exclude(id__in=hidden_ids)
exams_list = list(exams_qs)
for exam in exams_list:
exam.is_hidden = exam.id in hidden_ids
if exams_list:
filtered_exams.append((exam_type, exams_list))
# Filter Assigned Exams list (available_exams)
available_exams = []
for n, t in USER_EXAM_TYPES:
exam_rel = getattr(user, t)
if exam_rel.exists():
temp_exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
available_exams.append((n, temp_exams))
exam_model = temp_exams.model
ct_id = ContentType.objects.get_for_model(exam_model).id
hidden_ids = hidden_map.get(ct_id, set())
# available_exams = request.user.get_cid_exams()
if not show_hidden:
temp_exams = temp_exams.exclude(id__in=hidden_ids)
temp_exams_list = list(temp_exams)
for exam in temp_exams_list:
exam.is_hidden = exam.id in hidden_ids
if temp_exams_list:
available_exams.append((n, temp_exams_list))
return render(
request,
"user_scores.html",
{
# "physics_exams": physics_exams,
# "anatomy_exams": anatomy_exams,
# "rapid_exams": rapid_exams,
# "longs_exams": longs_exams,
# "sba_exams": sba_exams,
"all_exams": exams,
# "passcode": passcode,
"all_exams": filtered_exams,
"cid_user": user,
"available_exams": available_exams,
"case_collections": [], # case_collections,
"case_collections": [],
"admin": admin,
"show_hidden": show_hidden,
},
)
@login_required
def user_marking(request):
"""Main page for user marking overview. Uses HTMX to fetch per-app partials."""
+2 -2
View File
@@ -11,8 +11,8 @@
<div class="rounded-circle bg-secondary text-white d-inline-flex align-items-center justify-content-center" style="width:96px;height:96px;font-size:32px;">{{ user.first_name|default:user.username|slice:":1"|upper }}</div>
</div>
<h4 class="card-title mb-1">{{ user.get_full_name|default:user.username }}</h4>
<p class="text-muted mb-1">@{{ user.username }}</p>
<p class="mb-0"><a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
<p class="text-muted mb-1">@<span id="username">{{ user.username }}</span></p>
<p class="mb-0"><a href="mailto:{{ user.email }}" id="email">{{ user.email }}</a></p>
<div class="mt-2">
<small class="text-muted">Permission groups:</small>
<div id="auth-groups-{{ user.pk }}" class="mt-1 d-flex flex-wrap gap-2 align-items-center">
+50 -11
View File
@@ -5,7 +5,20 @@
<div class="container py-4">
<div class="row mb-3">
<div class="col-md-8">
<h2 class="mb-1">User: {{ cid_user.username }}</h2>
<div class="d-flex justify-content-between align-items-center mb-1">
<h2 class="mb-0">User: {{ cid_user.username }}</h2>
<div>
{% if show_hidden %}
<a class="btn btn-sm btn-outline-secondary" href="?">
<i class="bi bi-eye-slash me-1"></i>Hide Hidden
</a>
{% else %}
<a class="btn btn-sm btn-outline-info" href="?show_hidden=true">
<i class="bi bi-eye me-1"></i>Show Hidden
</a>
{% endif %}
</div>
</div>
{% help "Help" %}
<div class="card mt-2">
<div class="card-body small">
@@ -32,18 +45,31 @@
<h6 class="mt-3">{{ exam_type|title }}</h6>
<ul class="list-group">
{% for exam in exams %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<li class="list-group-item d-flex justify-content-between align-items-center {% if exam.is_hidden %}opacity-50 bg-secondary bg-opacity-10{% endif %}">
<div>
{{ exam }}
{% if exam.active %}<span class="badge bg-success ms-2">Active</span>{% endif %}
{% if exam.publish_results %}<span class="badge bg-info text-dark ms-2">Results Published</span>{% endif %}
{% if exam.is_hidden %}<span class="badge bg-warning text-dark ms-2">Hidden</span>{% endif %}
</div>
<div class="btn-group btn-group-sm" role="group">
{% if exam.active %}
<a class="btn btn-primary" href="{{ exam.get_take_url }}" target="_blank" title="Click to take exam">Start</a>
{% endif %}
{% if exam.publish_results %}
<a class="btn btn-outline-secondary" href="{% url exam_type|lower|add:':exam_scores_user' pk=exam.pk %}" title="Click to view results">Results</a>
<div class="d-flex align-items-center gap-2">
<div class="btn-group btn-group-sm" role="group">
{% if exam.active %}
<a class="btn btn-primary" href="{{ exam.get_take_url }}" target="_blank" title="Click to take exam">Start</a>
{% endif %}
{% if exam.publish_results %}
<a class="btn btn-outline-secondary" href="{% url exam_type|lower|add:':exam_scores_user' pk=exam.pk %}" title="Click to view results">Results</a>
{% endif %}
</div>
{% if not admin %}
<form method="post" action="{% url 'generic:toggle_hide_item' %}" hx-post="{% url 'generic:toggle_hide_item' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="content_type_id" value="{{ exam.content_type_id }}">
<input type="hidden" name="object_id" value="{{ exam.id }}">
<button type="submit" class="btn btn-sm btn-link text-muted p-0 ms-1" title="{% if exam.is_hidden %}Unhide exam{% else %}Hide exam{% endif %}">
<i class="bi {% if exam.is_hidden %}bi-eye-slash-fill text-warning{% else %}bi-eye{% endif %}" style="font-size: 1.15rem;"></i>
</button>
</form>
{% endif %}
</div>
</li>
@@ -66,7 +92,7 @@
<h6 class="mt-3">{{ exam_type|title }}</h6>
<ul class="list-group">
{% for exam in exams %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<li class="list-group-item d-flex justify-content-between align-items-center {% if exam.is_hidden %}opacity-50 bg-secondary bg-opacity-10{% endif %}">
<div>
{% if admin %}
<a href="{% url exam_type|add:':exam_scores_user_admin' pk=exam.pk user_id=user.pk %}">{{ exam }}</a>
@@ -75,8 +101,21 @@
{% endif %}
{% if exam.active %}<span class="badge bg-success ms-2">Active</span>{% endif %}
{% if exam.publish_results %}<span class="badge bg-info text-dark ms-2">Results Published</span>{% endif %}
{% if exam.is_hidden %}<span class="badge bg-warning text-dark ms-2">Hidden</span>{% endif %}
</div>
<div class="d-flex align-items-center gap-2">
{% if not admin %}
<form method="post" action="{% url 'generic:toggle_hide_item' %}" hx-post="{% url 'generic:toggle_hide_item' %}" class="d-inline">
{% csrf_token %}
<input type="hidden" name="content_type_id" value="{{ exam.content_type_id }}">
<input type="hidden" name="object_id" value="{{ exam.id }}">
<button type="submit" class="btn btn-sm btn-link text-muted p-0 ms-1" title="{% if exam.is_hidden %}Unhide exam{% else %}Hide exam{% endif %}">
<i class="bi {% if exam.is_hidden %}bi-eye-slash-fill text-warning{% else %}bi-eye{% endif %}" style="font-size: 1.15rem;"></i>
</button>
</form>
{% endif %}
<div class="small text-muted">&nbsp;</div>
</div>
<div class="small text-muted">&nbsp;</div>
</li>
{% endfor %}
</ul>
@@ -94,7 +133,7 @@
<p class="text-muted mb-2">Available, in-progress, and completed Atlas collections for this user.</p>
<div
id="atlas-collections-scores"
hx-get="{% url 'atlas:user_collections' %}?user_id={{ cid_user.id }}"
hx-get="{% url 'atlas:user_collections' %}?user_id={{ cid_user.id }}{% if show_hidden %}&show_hidden=1{% endif %}"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">