Add user content management views and templates for content review
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
{% load static %}
|
||||||
|
{% if user_obj %}
|
||||||
|
<div class="card p-2">
|
||||||
|
<h5>Content by {{ user_obj.get_full_name }} ({{ user_obj.username }})</h5>
|
||||||
|
|
||||||
|
{% for app_key, info in content.items %}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>{{ info.label }}</strong>
|
||||||
|
<div class="small">
|
||||||
|
Questions: {{ info.questions|length }} Exams: {{ info.exams|length }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
{% if info.questions %}
|
||||||
|
<ul class="list-unstyled ms-3">
|
||||||
|
{% for q in info.questions %}
|
||||||
|
<li>
|
||||||
|
{% comment %} Try to link to app question detail if available {% endcomment %}
|
||||||
|
<a href="{% url app_key|add:':question_detail' q.pk %}" target="_blank">{{ q }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if info.exams %}
|
||||||
|
<ul class="list-unstyled ms-3">
|
||||||
|
{% for ex in info.exams %}
|
||||||
|
<li>{{ ex }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Atlas Cases</strong>
|
||||||
|
<div class="small">Cases: {{ atlas_cases|length }}</div>
|
||||||
|
{% if atlas_cases %}
|
||||||
|
<ul class="list-unstyled ms-3">
|
||||||
|
{% for case in atlas_cases %}
|
||||||
|
<li><a href="{% url 'atlas:case_detail' case.pk %}" target="_blank">{{ case }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Atlas Collections</strong>
|
||||||
|
<div class="small">Collections: {{ atlas_collections|length }}</div>
|
||||||
|
{% if atlas_collections %}
|
||||||
|
<ul class="list-unstyled ms-3">
|
||||||
|
{% for coll in atlas_collections %}
|
||||||
|
<li><a href="{% url 'atlas:collection_detail' coll.pk %}" target="_blank">{{ coll }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div>No user found</div>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends 'generic/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>User content</h2>
|
||||||
|
|
||||||
|
<p>Click "Show content" to load items created by a user.</p>
|
||||||
|
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Content count</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.first_name }} {{ user.last_name }}</td>
|
||||||
|
<td><a href="{% url 'account_profile' user.username %}">{{ user.username }}</a></td>
|
||||||
|
<td>{{ user.content_count }}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
|
hx-get="{% url 'generic:user_content_review' user.pk %}"
|
||||||
|
hx-target="closest tr"
|
||||||
|
hx-swap="afterend">Show content</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -8,6 +8,9 @@ from generic.views import ExamViews as GenericExamViews, GenericViewBase
|
|||||||
app_name = "generic"
|
app_name = "generic"
|
||||||
|
|
||||||
urlpatterns = [
|
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("examination/", views.ExaminationView.as_view(), name="examination_view"),
|
path("examination/", views.ExaminationView.as_view(), name="examination_view"),
|
||||||
path("examination/<int:pk>", views.examination_detail, name="examination_detail"),
|
path("examination/<int:pk>", views.examination_detail, name="examination_detail"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -6877,3 +6877,87 @@ def supervisor_search(request):
|
|||||||
|
|
||||||
return render(request, "generic/partials/supervisor_search_results.html", {"results": results, "row": row})
|
return render(request, "generic/partials/supervisor_search_results.html", {"results": results, "row": row})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@user_passes_test(lambda u: u.is_superuser or u.groups.filter(name__in=["content_moderator", "atlas_editor"]).exists())
|
||||||
|
def user_content_list(request):
|
||||||
|
"""
|
||||||
|
Shows a list of all users with the ability to load the content they have created via htmx requests
|
||||||
|
"""
|
||||||
|
users = User.objects.annotate(
|
||||||
|
content_count=Count("user_anatomy_exams", filter=Q(user_anatomy_exams__archive=False))
|
||||||
|
+ Count("user_longs_exams", filter=Q(user_longs_exams__archive=False))
|
||||||
|
+ Count("user_physics_exams", filter=Q(user_physics_exams__archive=False))
|
||||||
|
+ Count("user_rapid_exams", filter=Q(user_rapid_exams__archive=False))
|
||||||
|
+ Count("user_sba_exams", filter=Q(user_sba_exams__archive=False))
|
||||||
|
+ Count("user_shorts_exams", filter=Q(user_shorts_exams__archive=False))
|
||||||
|
).filter(content_count__gt=0)
|
||||||
|
|
||||||
|
return render(request, "generic/user_content_list.html", {"users": users})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@user_passes_test(lambda u: u.is_superuser or u.groups.filter(name__in=["content_moderator", "atlas_editor"]).exists())
|
||||||
|
def user_content_review(request, user_id):
|
||||||
|
"""
|
||||||
|
HTMX partial to show content authored by a given user.
|
||||||
|
Includes authored questions and authored exams for each app,
|
||||||
|
plus atlas cases and case collections.
|
||||||
|
"""
|
||||||
|
user_obj = get_object_or_404(User, pk=user_id)
|
||||||
|
|
||||||
|
# Mapping of app -> (question_related_name, exam_author_related_name, display_label)
|
||||||
|
APP_MAP = {
|
||||||
|
"anatomy": ("anatomy_authored_questions", "anatomy_exam_author", "Anatomy"),
|
||||||
|
"physics": ("physics_authored_questions", "physics_exam_author", "Physics"),
|
||||||
|
"rapids": ("rapid_authored_questions", "rapid_exam_author", "Rapids"),
|
||||||
|
"shorts": ("shorts_authored_questions", "shorts_exam_author", "Shorts"),
|
||||||
|
"longs": ("long_authored_questions", "long_exam_author", "Longs"),
|
||||||
|
"sbas": ("sbas_authored_questions", "sba_exam_author", "SBAs"),
|
||||||
|
}
|
||||||
|
|
||||||
|
content = {}
|
||||||
|
|
||||||
|
for app_key, (q_rel, e_rel, label) in APP_MAP.items():
|
||||||
|
questions = []
|
||||||
|
exams = []
|
||||||
|
try:
|
||||||
|
questions = list(getattr(user_obj, q_rel).all().order_by("-pk")[:50])
|
||||||
|
except Exception:
|
||||||
|
questions = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
exams = list(getattr(user_obj, e_rel).all().filter(archive=False).order_by("-pk")[:50])
|
||||||
|
except Exception:
|
||||||
|
exams = []
|
||||||
|
|
||||||
|
content[app_key] = {
|
||||||
|
"label": label,
|
||||||
|
"questions": questions,
|
||||||
|
"exams": exams,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Atlas authored content
|
||||||
|
atlas_cases = []
|
||||||
|
atlas_collections = []
|
||||||
|
try:
|
||||||
|
atlas_cases = list(user_obj.atlas_authored_cases.all().order_by("-pk")[:50])
|
||||||
|
except Exception:
|
||||||
|
atlas_cases = []
|
||||||
|
try:
|
||||||
|
atlas_collections = list(user_obj.casecollection_authored_cases.all().order_by("-pk")[:50])
|
||||||
|
except Exception:
|
||||||
|
atlas_collections = []
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"user_obj": user_obj,
|
||||||
|
"content": content,
|
||||||
|
"atlas_cases": atlas_cases,
|
||||||
|
"atlas_collections": atlas_collections,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Render HTMX partial (or full page if not HTMX)
|
||||||
|
template = "generic/partials/user_content_review.html"
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user