mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 03:42:37 +02:00
wip: use autocomplete to add users in groups
This commit is contained in:
parent
1058e6074f
commit
6b6be017bb
8 changed files with 144 additions and 14 deletions
|
@ -113,11 +113,25 @@ class UserProfileForm(forms.ModelForm):
|
||||||
fields = ("username", "first_name", "last_name")
|
fields = ("username", "first_name", "last_name")
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMembersField(forms.ModelMultipleChoiceField):
|
||||||
|
def set_choices(self, choices):
|
||||||
|
iterator = self.iterator(self)
|
||||||
|
# Override queryset so to expose only selected choices:
|
||||||
|
# - we don't want a select with 100000 options
|
||||||
|
# - the select values will be used by the autocomplete widget to display
|
||||||
|
# already existing members of the group
|
||||||
|
iterator.queryset = choices
|
||||||
|
self.choices = iterator
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(forms.ModelForm):
|
class GroupForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Group
|
model = Group
|
||||||
fields = ["name", "members"]
|
fields = ["name", "members"]
|
||||||
|
|
||||||
members = forms.ModelMultipleChoiceField(
|
def __init__(self, *args, **kwargs):
|
||||||
queryset=User.objects.all(), widget=forms.CheckboxSelectMultiple
|
super().__init__(*args, **kwargs)
|
||||||
)
|
self.fields["members"].set_choices(self.initial["members"])
|
||||||
|
self.fields["members"].widget.attrs["hidden"] = "hidden"
|
||||||
|
|
||||||
|
members = GroupMembersField(queryset=User.objects.all())
|
||||||
|
|
|
@ -273,9 +273,7 @@ ul.umap-autocomplete {
|
||||||
border: 1px solid #202425;
|
border: 1px solid #202425;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
color: #eeeeec;
|
color: #eeeeec;
|
||||||
}
|
margin-bottom: 7px;
|
||||||
.umap-multiresult li + li {
|
|
||||||
margin-top: 7px;
|
|
||||||
}
|
}
|
||||||
.umap-singleresult div .close,
|
.umap-singleresult div .close,
|
||||||
.umap-multiresult li .close {
|
.umap-multiresult li .close {
|
||||||
|
|
|
@ -9,8 +9,8 @@ import { Request, ServerRequest } from './request.js'
|
||||||
import { escapeHTML, generateId } from './utils.js'
|
import { escapeHTML, generateId } from './utils.js'
|
||||||
|
|
||||||
export class BaseAutocomplete {
|
export class BaseAutocomplete {
|
||||||
constructor(el, options) {
|
constructor(parent, options) {
|
||||||
this.el = el
|
this.parent = parent
|
||||||
this.options = {
|
this.options = {
|
||||||
placeholder: translate('Start typing...'),
|
placeholder: translate('Start typing...'),
|
||||||
emptyMessage: translate('No result'),
|
emptyMessage: translate('No result'),
|
||||||
|
@ -43,7 +43,7 @@ export class BaseAutocomplete {
|
||||||
this.input = DomUtil.element({
|
this.input = DomUtil.element({
|
||||||
tagName: 'input',
|
tagName: 'input',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
parent: this.el,
|
parent: this.parent,
|
||||||
placeholder: this.options.placeholder,
|
placeholder: this.options.placeholder,
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
className: this.options.className,
|
className: this.options.className,
|
||||||
|
|
|
@ -25,4 +25,34 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module" defer>
|
||||||
|
const form = document.querySelector("#group_form")
|
||||||
|
const select = form.querySelector('#id_members')
|
||||||
|
function onSelect({item: {value, label}}) {
|
||||||
|
const option = document.createElement('option')
|
||||||
|
option.value = value
|
||||||
|
option.textContent = label
|
||||||
|
option.selected = "selected"
|
||||||
|
select.appendChild(option)
|
||||||
|
}
|
||||||
|
function onUnselect({item: {value, label}}) {
|
||||||
|
const option = select.querySelector(`[value="${value}"]`)
|
||||||
|
select.removeChild(option)
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
className: 'edit-group-members',
|
||||||
|
on_select: onSelect,
|
||||||
|
on_unselect: onUnselect,
|
||||||
|
placeholder: "{% trans "Add user" %}"
|
||||||
|
}
|
||||||
|
const autocomplete = new U.AjaxAutocompleteMultiple(form, options)
|
||||||
|
for (const option of select.options) {
|
||||||
|
autocomplete.displaySelected({
|
||||||
|
item: { value: option.value, label: option.textContent },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const submit = form.querySelector('input[type="submit"]')
|
||||||
|
// Move it after the autocomplete widget.
|
||||||
|
form.appendChild(submit)
|
||||||
|
</script>
|
||||||
{% endblock maincontent %}
|
{% endblock maincontent %}
|
||||||
|
|
46
umap/tests/integration/test_group.py
Normal file
46
umap/tests/integration/test_group.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_add_user_to_group(live_server, map, user, group, login):
|
||||||
|
map.owner.groups.add(group)
|
||||||
|
map.owner.save()
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
page = login(map.owner)
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="My teams").click()
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="Edit").click()
|
||||||
|
page.get_by_placeholder("Add user").click()
|
||||||
|
with page.expect_response(re.compile(r".*/agnocomplete/.*")):
|
||||||
|
page.get_by_placeholder("Add user").press_sequentially("joe")
|
||||||
|
page.get_by_text("Joe").click()
|
||||||
|
page.get_by_role("button", name="Save").click()
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
modified = Group.objects.first()
|
||||||
|
assert user in modified.user_set.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_remove_user_from_group(live_server, map, user, user2, group, login):
|
||||||
|
map.owner.groups.add(group)
|
||||||
|
map.owner.save()
|
||||||
|
user.groups.add(group)
|
||||||
|
user.save()
|
||||||
|
user2.groups.add(group)
|
||||||
|
user2.save()
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
page = login(map.owner)
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="My teams").click()
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.get_by_role("link", name="Edit").click()
|
||||||
|
page.locator("li").filter(has_text="Averell").locator(".close").click()
|
||||||
|
page.get_by_role("button", name="Save").click()
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
modified = Group.objects.first()
|
||||||
|
assert user in modified.user_set.all()
|
||||||
|
assert user2 not in modified.user_set.all()
|
|
@ -37,7 +37,7 @@ def test_can_create_a_group(client, user):
|
||||||
assert group in user.groups.all()
|
assert group in user.groups.all()
|
||||||
|
|
||||||
|
|
||||||
def test_can_edit_a_group(client, user, group):
|
def test_can_edit_a_group_name(client, user, group):
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
user.save()
|
user.save()
|
||||||
assert Group.objects.count() == 1
|
assert Group.objects.count() == 1
|
||||||
|
@ -52,6 +52,38 @@ def test_can_edit_a_group(client, user, group):
|
||||||
assert modified in user.groups.all()
|
assert modified in user.groups.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_add_user_to_group(client, user, user2, group):
|
||||||
|
user.groups.add(group)
|
||||||
|
user.save()
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
url = reverse("group_update", args=(group.pk,))
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url, {"name": group.name, "members": [user.pk, user2.pk]})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response["Location"] == "/en/me/groups"
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
modified = Group.objects.first()
|
||||||
|
assert user in modified.user_set.all()
|
||||||
|
assert user2 in modified.user_set.all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_remove_user_from_group(client, user, user2, group):
|
||||||
|
user.groups.add(group)
|
||||||
|
user.save()
|
||||||
|
user2.groups.add(group)
|
||||||
|
user2.save()
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
url = reverse("group_update", args=(group.pk,))
|
||||||
|
client.login(username=user.username, password="123123")
|
||||||
|
response = client.post(url, {"name": group.name, "members": [user.pk]})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response["Location"] == "/en/me/groups"
|
||||||
|
assert Group.objects.count() == 1
|
||||||
|
modified = Group.objects.first()
|
||||||
|
assert user in modified.user_set.all()
|
||||||
|
assert user2 not in modified.user_set.all()
|
||||||
|
|
||||||
|
|
||||||
def test_cannot_edit_a_group_if_not_member(client, user, user2, group):
|
def test_cannot_edit_a_group_if_not_member(client, user, user2, group):
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
|
@ -25,6 +25,10 @@ admin.autodiscover()
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r"^admin/", admin.site.urls),
|
re_path(r"^admin/", admin.site.urls),
|
||||||
re_path("", include("social_django.urls", namespace="social")),
|
re_path("", include("social_django.urls", namespace="social")),
|
||||||
|
re_path(
|
||||||
|
r"^agnocomplete/",
|
||||||
|
include(("agnocomplete.urls", "agnocomplete"), namespace="agnocomplete"),
|
||||||
|
),
|
||||||
re_path(r"^m/(?P<pk>\d+)/$", views.MapShortUrl.as_view(), name="map_short_url"),
|
re_path(r"^m/(?P<pk>\d+)/$", views.MapShortUrl.as_view(), name="map_short_url"),
|
||||||
re_path(r"^ajax-proxy/$", cache_page(180)(views.ajax_proxy), name="ajax-proxy"),
|
re_path(r"^ajax-proxy/$", cache_page(180)(views.ajax_proxy), name="ajax-proxy"),
|
||||||
re_path(
|
re_path(
|
||||||
|
@ -40,7 +44,6 @@ urlpatterns = [
|
||||||
name="password_change_done",
|
name="password_change_done",
|
||||||
),
|
),
|
||||||
re_path(r"^i18n/", include("django.conf.urls.i18n")),
|
re_path(r"^i18n/", include("django.conf.urls.i18n")),
|
||||||
re_path(r"^agnocomplete/", include("agnocomplete.urls")),
|
|
||||||
re_path(r"^map/oembed/", views.MapOEmbed.as_view(), name="map_oembed"),
|
re_path(r"^map/oembed/", views.MapOEmbed.as_view(), name="map_oembed"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^map/(?P<map_id>\d+)/download/",
|
r"^map/(?P<map_id>\d+)/download/",
|
||||||
|
|
|
@ -213,9 +213,16 @@ class GroupUpdate(UpdateView):
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
for user in form.cleaned_data["members"]:
|
actual = self.object.user_set.all()
|
||||||
user.groups.add(self.object)
|
wanted = form.cleaned_data["members"]
|
||||||
user.save()
|
for user in wanted:
|
||||||
|
if user not in actual:
|
||||||
|
user.groups.add(self.object)
|
||||||
|
user.save()
|
||||||
|
for user in actual:
|
||||||
|
if user not in wanted:
|
||||||
|
user.groups.remove(self.object)
|
||||||
|
user.save()
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue