from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.safestring import mark_safe
class UserSearchSelectMultipleWidget:
"""Reusable lightweight widget renderer for a searchable multi-user selector.
Usage: instantiate with (name, value) where value is an iterable of user ids
and call .render() to get the HTML fragment.
"""
def __init__(self, name, value):
self.name = name
self.value = value or []
def render(self):
field_id = f"id_{self.name}"
search_id = f"user_search_{self.name}"
results_id = f"user_search_results_{self.name}"
selected_list_id = f"selected_users_{self.name}"
users = User.objects.filter(pk__in=self.value) if self.value else []
options_html = ""
selected_html = ""
for u in users:
options_html += f''
grade_text = ""
try:
up = getattr(u, "userprofile", None)
if up and getattr(up, "grade", None):
g = up.grade
grade_text = getattr(g, "name", str(g)) if g is not None else ""
except Exception:
grade_text = ""
selected_html += (
f'
'
f'{u.get_full_name() or u.username} {u.email}'
+ (f' {grade_text}' if grade_text else '')
+ ''
f''
f'
'
)
url = reverse("generic:user_search_widget")
grades_options = ''
try:
from generic.models import UserGrades
grades_qs = UserGrades.objects.all()
for g in grades_qs:
grades_options += f''
except Exception:
grades_options = ''
html = f"""
Advanced search tips
Basic: type any part of a user's first name, last name, username or email (case-insensitive substring match). Example: smith or joe@example.com.
Wildcards: use * or % as the query to return many users (use carefully). Wildcard queries return up to 50 results; normal queries return up to 10.
Field-specific searches: prefix with field:term to restrict the search. Supported fields:
name: or full_name: — matches first OR last name (e.g. name:smith).
first: or first_name: — matches first name.
last: or last_name: — matches last name.
email: — matches email address.
username: — matches username.
id: or pk: — match by numeric user id (e.g. id:123).
grade: — match by grade name or grade id (e.g. grade:ST3 or grade:2).
Grade filter: use the grade dropdown next to the search box to narrow results by trainee grade in addition to your query.
To add users, click the Add button beside a result. If an Add all results button appears, it will add all currently visible results.
If you expect many matches, narrow your query or use a field-specific search to improve relevance and performance.
{selected_html}
"""
return mark_safe(html)
class UserSearchWidget:
"""Django-widget-compatible lightweight wrapper around
`UserSearchSelectMultipleWidget` so it can be reused as a field widget.
Implements the small subset of the Widget API that Django's forms
and templates expect: `render`, `value_from_datadict`, `id_for_label`,
`use_required_attribute`, and a couple of simple attributes.
"""
is_hidden = False
needs_multipart_form = False
use_fieldset = False
def __init__(self, field=None):
# keep a reference to the original field if available (used for
# delegation such as `use_required_attribute`).
self.field = field
self.attrs = {}
def render(self, name, value, attrs=None, renderer=None):
self.attrs = attrs or {}
value = value or []
widget = UserSearchSelectMultipleWidget(name, value)
return widget.render()
def value_from_datadict(self, data, files, name):
# Most form backends provide getlist
if hasattr(data, "getlist"):
return data.getlist(name)
val = data.get(name)
if val is None:
return []
return [val]
def id_for_label(self, id_):
try:
if hasattr(self, "attrs") and self.attrs and self.attrs.get("id"):
return self.attrs.get("id")
except Exception:
pass
return id_
def use_required_attribute(self, initial):
try:
if hasattr(self, "field") and getattr(self, "field") is not None:
orig_widget = getattr(self.field, "widget", None)
if orig_widget and hasattr(orig_widget, "use_required_attribute"):
return orig_widget.use_required_attribute(initial)
except Exception:
pass
return False