Implement bulk user management features: add user generation preview and bulk creation functionality in CID group view

This commit is contained in:
Ross
2025-12-15 12:23:26 +00:00
parent 885ab1aad4
commit 00b736d8d6
5 changed files with 173 additions and 53 deletions
@@ -28,61 +28,38 @@
<div class="card mb-3">
<div class="card-body">
<h3 class="card-title mb-2">Users</h3>
{% if users %}
<div class="d-flex align-items-center mb-2">
<div class="me-auto small text-muted">Sort by:</div>
<div class="btn-group btn-group-sm user-sort-group" role="group" aria-label="Sort">
<button type="button" class="btn btn-sm btn-outline-secondary sort-btn" data-key="username">{% if group_type == 'cid' %}CID{% else %}Username{% endif %} <span class="sort-caret"></span></button>
<button type="button" class="btn btn-sm btn-outline-secondary sort-btn" data-key="name">Name <span class="sort-caret"></span></button>
<button type="button" class="btn btn-sm btn-outline-secondary sort-btn" data-key="email">Email <span class="sort-caret"></span></button>
</div>
<div class="d-flex align-items-center mb-2">
<div class="me-auto small text-muted">Sort by:</div>
<div class="btn-group btn-group-sm user-sort-group" role="group" aria-label="Sort">
<button type="button" class="btn btn-sm btn-outline-secondary sort-btn" data-key="username">{% if group_type == 'cid' %}CID{% else %}Username{% endif %} <span class="sort-caret"></span></button>
<button type="button" class="btn btn-sm btn-outline-secondary sort-btn" data-key="name">Name <span class="sort-caret"></span></button>
<button type="button" class="btn btn-sm btn-outline-secondary sort-btn" data-key="email">Email <span class="sort-caret"></span></button>
</div>
</div>
<ul id="group-users-list" class="list-group list-group-flush">
{% for user in users %}
<li class="list-group-item bg-transparent border-0 py-2"
data-username="{% if group_type == 'cid' %}{{ user.cid|default:''|escape }}{% else %}{{ user.username|default:''|escape }}{% endif %}"
data-name="{% if group_type == 'cid' %}{{ user.name|default:''|escape }}{% else %}{{ user.get_full_name|default:user.username|escape }}{% endif %}"
data-email="{{ user.email|default:''|escape }}">
{% if group_type == "cid" %}
<div class="d-flex align-items-start">
<div>
<div class="fw-bold">{{ user.name }}</div>
<div class="small text-muted">{{ user.email }}</div>
</div>
<div class="ms-auto text-end small text-muted">
<div>CID: {{ user.cid }}</div>
<div>Passcode: {{ user.passcode }}</div>
<div class="mt-1">
{% if user.login_email_sent %}
<span class="badge bg-success ms-1 login-email-status">Email sent</span>
{% else %}
<span class="badge bg-secondary ms-1 login-email-status">Not sent</span>
{% endif %}
{% if user.email %}
<button type="button" class="btn btn-sm btn-outline-primary ms-2 send-login-email-btn" data-cid="{{ user.cid }}" data-url="{% url 'generic:candidate_email_details' user.cid %}">Send</button>
{% else %}
<span class="small text-muted ms-2">No email</span>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="d-flex align-items-center">
<div class="fw-bold">{{ user.username }}</div>
<div class="ms-2 small text-muted">{{ user.email }}</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="small text-muted">This group has no users.
{% if group_type == "cid" %}
You can create or add users <a href="{% url 'generic:manage_cids' %}">here</a>.
{% endif %}
</p>
{% endif %}
<div class="mb-2">
<form method="post" hx-post="{% url 'generic:cid_group_generate_preview' group.pk %}" hx-target="#cid-generate-preview" hx-swap="innerHTML">
{% csrf_token %}
<label class="small text-muted">Quick generate users (one per line: name,email)</label>
<textarea name="rows" class="form-control form-control-sm mb-2" rows="5" placeholder="John Smith,j.smith@example.org\nJane Doe,jane@example.org"></textarea>
<div>
<button type="submit" class="btn btn-sm btn-outline-primary">Preview</button>
</div>
</form>
<div id="cid-generate-preview"></div>
</div>
<div id="group-users-wrap">
{% if users %}
{% include 'generic/partials/group_users_list.html' with users=users group_type=group_type %}
{% else %}
<p class="small text-muted">This group has no users.
{% if group_type == "cid" %}
You can create or add users <a href="{% url 'generic:manage_cids' %}">here</a>.
{% endif %}
</p>
{% endif %}
</div>
</div>
</div>
@@ -0,0 +1,22 @@
{% comment %} Preview partial for parsed rows; includes a form to create users {% endcomment %}
<div id="cid-generate-preview" class="mt-2">
<h6 class="small">Preview ({{ rows|length }} rows)</h6>
<div class="table-responsive">
<table class="table table-sm table-borderless">
<thead>
<tr><th>Name</th><th>Email</th></tr>
</thead>
<tbody>
{% for r in rows %}
<tr><td>{{ r.name }}</td><td>{{ r.email }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<form method="post" hx-post="{% url 'generic:cid_group_create_bulk' group.pk %}" hx-target="#group-users-wrap" hx-swap="outerHTML">
{% csrf_token %}
<textarea name="rows" class="d-none">{{ original|escape }}</textarea>
<button type="submit" class="btn btn-sm btn-primary">Create users and add to group</button>
</form>
</div>
@@ -0,0 +1,39 @@
{% comment %} Partial: renders the users <ul> for group detail pages {% endcomment %}
<ul id="group-users-list" class="list-group list-group-flush">
{% for user in users %}
<li class="list-group-item bg-transparent border-0 py-2"
data-username="{% if group_type == 'cid' %}{{ user.cid|default:''|escape }}{% else %}{{ user.username|default:''|escape }}{% endif %}"
data-name="{% if group_type == 'cid' %}{{ user.name|default:''|escape }}{% else %}{{ user.get_full_name|default:user.username|escape }}{% endif %}"
data-email="{{ user.email|default:''|escape }}">
{% if group_type == "cid" %}
<div class="d-flex align-items-start">
<div>
<div class="fw-bold">{{ user.name }}</div>
<div class="small text-muted">{{ user.email }}</div>
</div>
<div class="ms-auto text-end small text-muted">
<div>CID: {{ user.cid }}</div>
<div>Passcode: {{ user.passcode }}</div>
<div class="mt-1">
{% if user.login_email_sent %}
<span class="badge bg-success ms-1 login-email-status">Email sent</span>
{% else %}
<span class="badge bg-secondary ms-1 login-email-status">Not sent</span>
{% endif %}
{% if user.email %}
<button type="button" class="btn btn-sm btn-outline-primary ms-2 send-login-email-btn" data-cid="{{ user.cid }}" data-url="{% url 'generic:candidate_email_details' user.cid %}">Send</button>
{% else %}
<span class="small text-muted ms-2">No email</span>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="d-flex align-items-center">
<div class="fw-bold">{{ user.username }}</div>
<div class="ms-2 small text-muted">{{ user.email }}</div>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
+10
View File
@@ -88,6 +88,16 @@ urlpatterns = [
views.cid_group_view_detail,
name="cid_group_detail",
),
path(
"cids/group/<int:group_id>/generate_preview",
views.cid_group_generate_preview,
name="cid_group_generate_preview",
),
path(
"cids/group/<int:group_id>/create_bulk",
views.cid_group_create_bulk,
name="cid_group_create_bulk",
),
path("cids/group/<int:pk>/email", views.group_email, name="group_email"),
path(
"cids/group/<int:pk>/exams",
+72
View File
@@ -4561,6 +4561,78 @@ def cid_group_view_detail(request, group_id):
)
@user_is_cid_user_manager
def cid_group_generate_preview(request, group_id):
"""Parse submitted rows and return a preview fragment for HTMX."""
if not request.htmx:
raise Http404()
group = get_object_or_404(CidUserGroup, pk=group_id)
rows_text = request.POST.get('rows', '')
original = rows_text
parsed = []
for line in rows_text.splitlines():
line = line.strip()
if not line:
continue
# Accept CSV or tab-separated: name,email or just email
parts = [p.strip() for p in line.replace('\t', ',').split(',') if p.strip()]
if len(parts) == 1:
email = parts[0]
name = ''
else:
name = parts[0]
email = parts[1]
parsed.append({'name': name, 'email': email})
html = render_to_string('generic/partials/cid_group_generate_preview.html', {'rows': parsed, 'original': original, 'group': group}, request=request)
return HttpResponse(html)
@user_is_cid_user_manager
def cid_group_create_bulk(request, group_id):
"""Create CidUser rows from posted rows and add them to the group, returning updated users list HTML."""
if not request.htmx:
raise Http404()
group = get_object_or_404(CidUserGroup, pk=group_id)
rows_text = request.POST.get('rows', '')
created = []
import secrets
for line in rows_text.splitlines():
line = line.strip()
if not line:
continue
parts = [p.strip() for p in line.replace('\t', ',').split(',') if p.strip()]
if len(parts) == 1:
email = parts[0]
name = ''
else:
name = parts[0]
email = parts[1]
# generate cid and passcode
try:
cid_val = get_next_cid()
except Exception:
# fallback: max+1
last = CidUser.objects.order_by('-cid').first()
cid_val = (last.cid if last and last.cid else 0) + 1
passcode = secrets.token_hex(3)
cu = CidUser.objects.create(cid=cid_val, passcode=passcode, name=name, email=email, group=group)
created.append(cu)
# re-render the users list
users = group.ciduser_set.filter(active=True)
inner = render_to_string('generic/partials/group_users_list.html', {'users': users, 'group_type': 'cid'}, request=request)
# return the wrapper so HTMX can replace the container even if it didn't exist before
html = f"<div id=\"group-users-wrap\">{inner}</div>"
return HttpResponse(html)
@user_is_cid_user_manager
def cid_group_view_all(request):
groups = CidUserGroup.objects.filter()