data: add select2 widget for page selection in link list cell (#76059) #79

Merged
vdeniaud merged 1 commits from wip/76059-manager-cellule-lien-lien-intern into main 2023-04-20 09:48:47 +02:00
7 changed files with 152 additions and 2 deletions

View File

@ -23,12 +23,13 @@ from django.utils.translation import gettext_lazy as _
from combo.utils import cache_during_request
from .models import ConfigJsonCell, LinkCell, LinkListCell, MenuCell, Page
from .widgets import PageSelect2Widget
@cache_during_request
def get_page_choices():
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all())
return [(x.id, '%s %s' % ('\u00a0' * x.level * 2, x.title)) for x in pages]
return [(x.id, '%s%s' % ('\u00a0' * x.level * 2, x.title)) for x in pages]
class MenuCellForm(forms.ModelForm):
@ -48,7 +49,9 @@ class LinkCellForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['link_page'].widget = forms.Select(choices=[(None, '-----')] + get_page_choices())
self.fields['link_page'].widget = PageSelect2Widget(
self.instance.link_page, choices=[(None, '-----')] + get_page_choices()
)
class LinkCellForLinkListCellForm(LinkCellForm):

View File

@ -14,8 +14,28 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.forms.widgets import Select
from django.urls import reverse
class FlexSize(Select):
template_name = 'combo/widgets/flexsize.html'
class PageSelect2Widget(Select):
min_choices = 20
class Media:
js = 'xstatic/select2.min.js'
css = {'all': ('xstatic/select2.min.css',)}
def __init__(self, selected_page, *args, **kwargs):
super().__init__(*args, **kwargs)
if len(self.choices) > self.min_choices:
current_choice = selected_page.id if selected_page else None
self.choices = [x for x in self.choices if x[0] == current_choice]
self.attrs['data-autocomplete'] = 'true'
self.attrs['lang'] = settings.LANGUAGE_CODE
self.attrs['data-select2-url'] = reverse('combo-manager-select2-page-choices')

View File

@ -467,6 +467,19 @@ $(function() {
$('.cell').on('combo:cellform-reloaded', function() {
multiSelectWidget.init(this);
});
function init_select2() {
$('select[data-autocomplete]').each(function(idx, elem) {
$(elem).select2({
ajax: {
url: $(elem).data('select2-url'),
dataType: 'json'
}
});
});
}
init_select2();
$('.cell').on('combo:cellform-reloaded', init_select2);
});

View File

@ -6,6 +6,7 @@
<link rel="stylesheet" type="text/css" media="all" href="{% static "xstatic/leaflet.css" %}"/>
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/combo.map.css" %}"/>
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.multiselectwidget.css" %}"/>
<link rel="stylesheet" type="text/css" media="all" href="{% static "xstatic/select2.min.css" %}"/>
{% endblock %}
{% block page-title %}{% firstof site_title "Combo" %}{% endblock %}
{% block site-title %}{% firstof site_title "Combo" %}{% endblock %}
@ -36,5 +37,7 @@
<script src="{% static "xstatic/leaflet.js" %}"></script>
<script src="{% static "js/combo.map.js" %}"></script>
<script src="{% static "js/gadjo.multiselectwidget.js" %}"></script>
<script src="{% static "xstatic/select2.min.js" %}"></script>
<script src="{% static "xstatic/i18n/fr.js" %}"></script>

Ici on exploite les traductions packagées avec select2, c'est peu explicite mais nettement plus simple que de les prendre à notre charge comme dans wcs et a2.

Ici on exploite les traductions packagées avec select2, c'est peu explicite mais nettement plus simple que de les prendre à notre charge comme dans wcs et a2.
<script src="{% url "javascript-catalog" %}"></script>
{% endblock %}

View File

@ -30,6 +30,7 @@ urlpatterns = [
re_path(r'^site-export$', pages_admin_required(views.site_export), name='combo-manager-site-export'),
re_path(r'^site-import$', pages_admin_required(views.site_import), name='combo-manager-site-import'),
re_path(r'^site-settings$', views.site_settings, name='combo-manager-site-settings'),
re_path(r'^ajax-page-choices$', views.select2_page_choices, name='combo-manager-select2-page-choices'),
re_path(
r'^cells/invalid-report/$',
pages_admin_required(views.invalid_cell_report),

View File

@ -18,6 +18,7 @@ import datetime
import hashlib
import json
import tarfile
import unicodedata
from io import BytesIO
from operator import attrgetter, itemgetter
@ -1195,6 +1196,34 @@ class LinkListOrder(ManagedPageMixin, View):
link_list_order = LinkListOrder.as_view()
class Select2PageChoicesView(View):
def get(self, request, *args, **kwargs):
search_term = request.GET.get('term', '')
search_term = unicodedata.normalize('NFKC', search_term).casefold()
try:
page_number = int(request.GET.get('page', 1))
except ValueError:
page_number = 1
pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all())
pages = [x for x in pages if search_term in unicodedata.normalize('NFKC', x.title).casefold()]

J'ai fait au plus simple pour la partie recherche.

Un autre truc simple et plus performant aurait été le filtrage en SQL via genre __icontains, mais la gestion des accents aurait été moins bonne.

Sinon la meilleure approche ça serait probablement trigram + unaccent mais c'est relou à mettre en place.

D'une manière ou d'une autre cette vue à vocation à devenir générique au détour d'un autre ticket qui nécessite select2 pour un champ d'un cellule.

J'ai fait au plus simple pour la partie recherche. Un autre truc simple et plus performant aurait été le filtrage en SQL via genre `__icontains`, mais la gestion des accents aurait été moins bonne. Sinon la meilleure approche ça serait probablement trigram + unaccent mais c'est relou à mettre en place. D'une manière ou d'une autre cette vue à vocation à devenir générique au détour d'un autre ticket qui nécessite select2 pour un champ d'un cellule.
options = [
{'id': x.id, 'text': '%s%s' % ('\u00a0' * x.level * 2, x.title)}
for x in pages[(page_number - 1) * 10 : page_number * 10]
]
if page_number == 1:
options = [{'id': '', 'text': '-----'}] + options
return JsonResponse(
{'results': options, 'pagination': {'more': bool(len(pages) >= page_number * 10)}}
)
select2_page_choices = Select2PageChoicesView.as_view()
class SiteSettingsView(UpdateView):
form_class = SiteSettingsForm
template_name = 'combo/site_settings.html'

View File

@ -2779,6 +2779,87 @@ def test_link_cell_validation():
assert form.errors['url'] == ["syntax error: Could not parse the remainder: '{test_url' from '{test_url'"]
def test_link_cell_page_choices(app, admin_user):
Page.objects.all().delete()
page = Page.objects.create(title='One', slug='one')
page2 = Page.objects.create(title='Two', slug='two')
cell = LinkCell.objects.create(order=1, page=page, placeholder='content')
app = login(app)
resp = app.get('/manage/pages/%s/' % page.pk)
assert 'data-autocomplete' not in resp.text
field_prefix = 'cdata_linkcell-%s-' % cell.id
assert resp.form[field_prefix + 'link_page'].options == [
('', True, '-----'),
(str(page.pk), False, 'One'),
(str(page2.pk), False, 'Two'),
]
resp.form[field_prefix + 'link_page'] = page2.pk
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'link_page'].value == str(page2.pk)
# add many pages to enable select2 field
for i in range(1, 21):
Page.objects.create(title='Page %s' % i, slug=str(i))
manager_submit_cell(resp.form)
assert 'data-autocomplete' in resp.text
# only selected option exists, other will be added by JS
assert resp.form[field_prefix + 'link_page'].options == [(str(page2.pk), True, 'Two')]
new_page = Page.objects.get(slug='1')
resp.form[field_prefix + 'link_page'].force_value(new_page.pk)
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'link_page'].options == [(str(new_page.pk), True, 'Page 1')]
resp.form[field_prefix + 'link_page'].force_value('')
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'link_page'].options == [('', True, '-----')]
resp = app.get('/manage/ajax-page-choices')
assert len(resp.json['results']) == 11
assert [x['text'] for x in resp.json['results'][:4]] == ['-----', 'One', 'Two', 'Page 1']
assert resp.json['results'][-1]['text'] == 'Page 8'
assert resp.json['pagination']['more'] is True
resp = app.get('/manage/ajax-page-choices?page=2')
assert len(resp.json['results']) == 10
assert resp.json['results'][0]['text'] == 'Page 9'
assert resp.json['results'][-1]['text'] == 'Page 18'
assert resp.json['pagination']['more'] is True
resp = app.get('/manage/ajax-page-choices?page=3')
assert len(resp.json['results']) == 2
assert resp.json['results'][0]['text'] == 'Page 19'
assert resp.json['results'][-1]['text'] == 'Page 20'
assert resp.json['pagination']['more'] is False
resp = app.get('/manage/ajax-page-choices?term=page')
assert len(resp.json['results']) == 11
assert resp.json['results'][0]['text'] == '-----'
assert resp.json['results'][1]['text'] == 'Page 1'
assert resp.json['results'][-1]['text'] == 'Page 10'
assert resp.json['pagination']['more'] is True
resp = app.get('/manage/ajax-page-choices?term=O')
assert len(resp.json['results']) == 3
assert resp.json['results'][0]['text'] == '-----'
assert resp.json['results'][1]['text'] == 'One'
assert resp.json['results'][2]['text'] == 'Two'
assert resp.json['pagination']['more'] is False
Page.objects.create(title='Child one', slug='child', parent=page)
resp = app.get('/manage/ajax-page-choices?term=One')
assert len(resp.json['results']) == 3
assert resp.json['results'][0]['text'] == '-----'
assert resp.json['results'][1]['text'] == 'One'
assert resp.json['results'][2]['text'] == '\u00a0' * 2 + 'Child one'
assert resp.json['pagination']['more'] is False
def test_add_edit_delete_list_link_item(app, admin_user):
Page.objects.all().delete()
page = Page.objects.create(title='One', slug='one', template_name='standard')