Implement bulk user management features: add user generation preview and bulk creation functionality in CID group view
This commit is contained in:
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user