Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 38 additions & 18 deletions course/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django_select2.forms import ModelSelect2Widget
from django_tomselect.app_settings import TomSelectConfig
from django_tomselect.autocompletes import AutocompleteModelView
from django_tomselect.forms import TomSelectModelChoiceField
from djangosaml2.backends import Saml2Backend

from accounts.models import User
Expand Down Expand Up @@ -176,38 +178,56 @@
return self.get_response(request)


class UserSearchWidget(ModelSelect2Widget):
class UserAutocompleteView(AutocompleteModelView):
"""Autocomplete view for user search in the impersonation form."""

model = User
search_fields = [
search_lookups = [

Check warning on line 185 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation for attribute `search_lookups` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)
"username__icontains",
"email__icontains",
"first_name__icontains",
"last_name__icontains",
]

def label_from_instance(self, u):
if u.first_name and u.last_name:
return (
f"{u.get_full_name()} ({u.username} - {u.email})")
else:
# for users with "None" fullname
return (
f"{u.username} ({u.email})")
value_fields = ["id", "username", "email", "first_name", "last_name"]

Check warning on line 191 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation for attribute `value_fields` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)
virtual_fields = ["label"]

Check warning on line 192 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation for attribute `virtual_fields` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)

def get_queryset(self):

Check warning on line 194 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Method "get_queryset" is not marked as override but is overriding a method in class "AutocompleteModelView" (reportImplicitOverride)

Check warning on line 194 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Return type, "QuerySet[Unknown]", is partially unknown (reportUnknownParameterType)
qset = get_impersonable_user_qset(
cast("User", self.request.user)) # type: ignore[attr-defined]
queryset = (User.objects
.filter(pk__in=qset.values_list("pk", flat=True))
.order_by("last_name", "first_name", "username"))
return self.search(queryset, self.query)

Check warning on line 200 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Return type, "QuerySet[Unknown]", is partially unknown (reportUnknownVariableType)

Check warning on line 200 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type of "search" is partially unknown   Type of "search" is "(queryset: QuerySet[Unknown], query: str) -> QuerySet[Unknown]" (reportUnknownMemberType)

def hook_prepare_results(self, results):

Check warning on line 202 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation is missing for parameter "results" (reportMissingParameterType)

Check warning on line 202 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Method "hook_prepare_results" is not marked as override but is overriding a method in class "AutocompleteModelView" (reportImplicitOverride)

Check warning on line 202 in course/auth.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Return type, "list[Unknown]", is partially unknown (reportUnknownParameterType)
prepared = []
for item in results:
if item.get("first_name") and item.get("last_name"):
label = (
f"{item['first_name']} {item['last_name']}"
f" ({item['username']} - {item['email']})")
else:
label = f"{item['username']} ({item['email']})"
item["label"] = label
prepared.append(item)
return prepared


class ImpersonateForm(StyledForm):
def __init__(self, *args: Any, **kwargs: Any) -> None:

qset = kwargs.pop("impersonable_qset")
kwargs.pop("impersonable_qset")
super().__init__(*args, **kwargs)

self.fields["user"] = forms.ModelChoiceField(
queryset=qset,
self.fields["user"] = TomSelectModelChoiceField(
required=True,
help_text=_("Select user to impersonate."),
widget=UserSearchWidget(
queryset=qset,
attrs={"data-minimum-input-length": 0},
config=TomSelectConfig(
url="user-autocomplete",
value_field="id",
label_field="label",
minimum_query_length=0,
preload=True,
),
label=_("User"))

Expand Down
5 changes: 0 additions & 5 deletions course/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
from django.utils.translation import gettext_lazy as _, pgettext
from pytools.lex import RE, LexTable

from course.auth import UserSearchWidget
from course.constants import (
PARTICIPATION_PERMISSION_CHOICES,
ParticipationPermission as PPerm,
Expand Down Expand Up @@ -1001,10 +1000,6 @@ class Meta:
"course",
)

widgets = {
"user": UserSearchWidget,
}


@course_view
def edit_participation(
Expand Down
7 changes: 2 additions & 5 deletions course/exam.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,10 @@ def __init__(self, now_datetime, *args, **kwargs):

super().__init__(*args, **kwargs)

from course.auth import UserSearchWidget

self.fields["user"] = forms.ModelChoiceField(
queryset=(get_user_model().objects
.filter(is_active=True)
.order_by("last_name")),
widget=UserSearchWidget(),
required=True,
help_text=_("Select participant for whom ticket is to "
"be issued."),
Expand Down Expand Up @@ -774,7 +771,7 @@ def __call__(self, request: http.HttpRequest) -> http.HttpResponse:
sign_out,
set_pretend_facilities]
or request.path.startswith("/saml2")
or request.path.startswith("/select2")
or request.path.startswith("/user-autocomplete")
or ((request.user.is_staff
or request.user.has_perm("course.can_issue_exam_tickets"))
and resolver_match.func == issue_exam_ticket)):
Expand Down Expand Up @@ -857,7 +854,7 @@ def __call__(self, request: RelateHttpRequest):
user_profile,
sign_out]
or request.path.startswith("/saml2")
or request.path.startswith("/select2")
or request.path.startswith("/user-autocomplete")
or (
resolver_match.func in [
view_resume_flow,
Expand Down
4 changes: 1 addition & 3 deletions course/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext, gettext_lazy as _
from django_select2.forms import Select2Widget
from pytools import not_none

import course.utils as c_utils # These are mocked, so import as module
Expand Down Expand Up @@ -2825,8 +2824,7 @@ def __init__(self, flow_ids: list[str], *args: Any, **kwargs: Any) -> None:
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
required=True,
label=_("Flow ID"),
widget=Select2Widget())
label=_("Flow ID"))
self.fields["access_rules_tag"] = forms.CharField(
required=False,
help_text=_("If non-empty, limit the regrading to sessions "
Expand Down
2 changes: 0 additions & 2 deletions course/versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
pgettext_lazy,
)
from django.views.decorators.csrf import csrf_exempt
from django_select2.forms import Select2Widget
from dulwich.repo import Repo

from course.auth import with_course_api_auth
Expand Down Expand Up @@ -473,7 +472,6 @@ def format_sha(sha):
for entry in commit_iter
]),
required=True,
widget=Select2Widget(),
label=pgettext_lazy(
"new git SHA for revision of course contents",
"New git SHA"))
Expand Down
10 changes: 3 additions & 7 deletions course/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
pgettext_lazy,
)
from django.views.decorators.cache import cache_control
from django_select2.forms import Select2Widget

from course.auth import get_pre_impersonation_user
from course.constants import (
Expand Down Expand Up @@ -618,8 +617,7 @@ def __init__(self, flow_ids, *args, **kwargs):
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
required=True,
label=_("Flow ID"),
widget=Select2Widget())
label=_("Flow ID"))
self.fields["duration_in_minutes"] = forms.IntegerField(
required=True, initial=20,
label=pgettext_lazy("Duration for instant flow",
Expand Down Expand Up @@ -699,8 +697,7 @@ def __init__(self, flow_ids, *args, **kwargs):
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
required=True,
label=_("Flow ID"),
widget=Select2Widget())
label=_("Flow ID"))

self.helper.add_input(
Submit(
Expand Down Expand Up @@ -765,8 +762,7 @@ def __init__(self, course, flow_ids, *args, **kwargs):
required=True,
help_text=_("Select participant for whom exception is to "
"be granted."),
label=_("Participant"),
widget=Select2Widget())
label=_("Participant"))
self.fields["flow_id"] = forms.ChoiceField(
choices=[(fid, fid) for fid in flow_ids],
required=True,
Expand Down
2 changes: 1 addition & 1 deletion frontend/css/base.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
$color-mode-type: media-query;

@import "node_modules/bootstrap/scss/bootstrap.scss";
@import "node_modules/select2/dist/css/select2";
@import "node_modules/tom-select/dist/scss/tom-select.bootstrap5";

// work around spurious extra quoting in woff include
$bootstrap-icons-font-file: "./fonts/bootstrap-icons";
Expand Down
14 changes: 11 additions & 3 deletions frontend/js/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ import './jquery-importer';
import jQuery from 'jquery';
import * as bootstrap from 'bootstrap';

import select2 from 'select2';
import TomSelect from 'tom-select';
import * as rlUtils from './rlUtils';
import * as bsUtils from './bsUtils';

import 'htmx.org';

import '../css/base.scss';

select2(jQuery);

document.addEventListener('DOMContentLoaded', () => {
// Initialize TomSelect on all plain <select> elements (i.e. those not already
// handled by django-tomselect's own widget templates).
document.querySelectorAll('select:not([data-tomselect-initialized])').forEach((el) => {
// eslint-disable-next-line no-new
new TomSelect(el, { allowEmptyOption: true });
});

// document.body is not available until the DOM is loaded.
document.body.addEventListener('htmx:responseError', (evt) => {
bsUtils.showToast(
Expand All @@ -23,6 +28,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
});

// Make TomSelect available globally so django-tomselect widget templates can use it.
globalThis.TomSelect = TomSelect;
globalThis.jQuery = jQuery;
globalThis.rlUtils = rlUtils;
globalThis.bsUtils = bsUtils;
globalThis.bootstrap = bootstrap;
77 changes: 55 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.4",
"prosemirror-view": "^1.41.6",
"select2": "^4.0.5",
"select2-bootstrap-theme": "^0.1.0-beta.10",
"tom-select": "^2.5.2",
"video.js": "^8.23.7"
},
"//dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ dependencies = [
"celery>=5.2.2,<6",
"kombu>=5.4.2,<6",
"django-celery-results>=2.4.0,<3",
"django_select2>=8.2.1,<9",
"django-tomselect>=2026.1.3",
"bleach~=6.2",
"html5lib~=1.1",
"pytools>=2024.1.8",
Expand Down
Loading
Loading