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 mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title mb-2">Users</h3>
|
<h3 class="card-title mb-2">Users</h3>
|
||||||
{% if users %}
|
<div class="d-flex align-items-center mb-2">
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="me-auto small text-muted">Sort by:</div>
|
||||||
<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">
|
||||||
<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="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="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>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary sort-btn" data-key="email">Email <span class="sort-caret"></span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul id="group-users-list" class="list-group list-group-flush">
|
<div class="mb-2">
|
||||||
{% for user in users %}
|
<form method="post" hx-post="{% url 'generic:cid_group_generate_preview' group.pk %}" hx-target="#cid-generate-preview" hx-swap="innerHTML">
|
||||||
<li class="list-group-item bg-transparent border-0 py-2"
|
{% csrf_token %}
|
||||||
data-username="{% if group_type == 'cid' %}{{ user.cid|default:''|escape }}{% else %}{{ user.username|default:''|escape }}{% endif %}"
|
<label class="small text-muted">Quick generate users (one per line: name,email)</label>
|
||||||
data-name="{% if group_type == 'cid' %}{{ user.name|default:''|escape }}{% else %}{{ user.get_full_name|default:user.username|escape }}{% endif %}"
|
<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>
|
||||||
data-email="{{ user.email|default:''|escape }}">
|
<div>
|
||||||
{% if group_type == "cid" %}
|
<button type="submit" class="btn btn-sm btn-outline-primary">Preview</button>
|
||||||
<div class="d-flex align-items-start">
|
</div>
|
||||||
<div>
|
</form>
|
||||||
<div class="fw-bold">{{ user.name }}</div>
|
<div id="cid-generate-preview"></div>
|
||||||
<div class="small text-muted">{{ user.email }}</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="ms-auto text-end small text-muted">
|
<div id="group-users-wrap">
|
||||||
<div>CID: {{ user.cid }}</div>
|
{% if users %}
|
||||||
<div>Passcode: {{ user.passcode }}</div>
|
{% include 'generic/partials/group_users_list.html' with users=users group_type=group_type %}
|
||||||
<div class="mt-1">
|
{% else %}
|
||||||
{% if user.login_email_sent %}
|
<p class="small text-muted">This group has no users.
|
||||||
<span class="badge bg-success ms-1 login-email-status">Email sent</span>
|
{% if group_type == "cid" %}
|
||||||
{% else %}
|
You can create or add users <a href="{% url 'generic:manage_cids' %}">here</a>.
|
||||||
<span class="badge bg-secondary ms-1 login-email-status">Not sent</span>
|
{% endif %}
|
||||||
{% endif %}
|
</p>
|
||||||
{% if user.email %}
|
{% endif %}
|
||||||
<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>
|
</div>
|
||||||
{% 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>
|
</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,
|
views.cid_group_view_detail,
|
||||||
name="cid_group_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>/email", views.group_email, name="group_email"),
|
||||||
path(
|
path(
|
||||||
"cids/group/<int:pk>/exams",
|
"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
|
@user_is_cid_user_manager
|
||||||
def cid_group_view_all(request):
|
def cid_group_view_all(request):
|
||||||
groups = CidUserGroup.objects.filter()
|
groups = CidUserGroup.objects.filter()
|
||||||
|
|||||||
Reference in New Issue
Block a user