display lists as images (#72176) #322
|
@ -28,6 +28,7 @@ from wcs.qommon.ident.password_accounts import PasswordAccount
|
|||
from wcs.qommon.misc import ConnectionError
|
||||
from wcs.qommon.substitution import CompatibilityNamesDict
|
||||
from wcs.qommon.template import Template
|
||||
from wcs.qommon.upload_storage import PicklableUpload
|
||||
from wcs.roles import logged_users_role
|
||||
from wcs.sql import TransientData
|
||||
from wcs.tracking_code import TrackingCode
|
||||
|
@ -9630,6 +9631,80 @@ def test_form_item_timetable_data_source_with_date_alignment(pub, http_requests)
|
|||
assert formdef.data_class().count() == 1
|
||||
|
||||
|
||||
def test_form_item_with_card_image_data_source(pub, http_requests):
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
TransientData.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'Images'
|
||||
carddef.fields = [
|
||||
fields.StringField(id='0', label='Label', varname='label'),
|
||||
fields.FileField(id='1', label='Image', varname='image'),
|
||||
]
|
||||
carddef.digest_template = '{{ label }}'
|
||||
carddef.store()
|
||||
carddef.data_class().wipe()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), '..', 'image-with-gps-data.jpeg'), 'rb') as fd:
|
||||
image_content = fd.read()
|
||||
|
||||
for i, value in enumerate(['Label 1', 'Label 2', 'Label 3']):
|
||||
carddata = carddef.data_class()()
|
||||
upload = PicklableUpload('test-%s.jpg' % i, content_type='image/jpeg')
|
||||
upload.receive([image_content])
|
||||
carddata.data = {'0': value, '1': upload}
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
|
||||
NamedDataSource.wipe()
|
||||
data_source = NamedDataSource(name='images')
|
||||
data_source.data_source = {'type': 'carddef:images'}
|
||||
data_source.store()
|
||||
|
||||
data_source.id_property = 'id'
|
||||
data_source.label_template_property = '{{ label }}'
|
||||
data_source.cache_duration = '5'
|
||||
data_source.store()
|
||||
|
||||
formdef = create_formdef()
|
||||
formdef.fields = [
|
||||
fields.ItemField(
|
||||
id='1', label='Choice', display_mode='images', data_source={'type': 'carddef:images'}
|
||||
),
|
||||
]
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
app = get_app(pub)
|
||||
resp = app.get('/test/')
|
||||
|
||||
assert len(resp.pyquery('div.RadiobuttonsWithImagesWidget')) == 1
|
||||
assert len(resp.pyquery('img.item-with-image--picture')) == 3
|
||||
|
||||
assert TransientData.count() == 3
|
||||
|
||||
# new session, check a new token is generated
|
||||
token = TransientData.select()[0]
|
||||
resp = get_app(pub).get('/test/')
|
||||
assert '/api/card-file-by-token/%s' % token.id not in resp.text
|
||||
|
||||
FormDef.wipe()
|
||||
|
||||
formdef2 = create_formdef()
|
||||
formdef2.fields = [
|
||||
fields.ItemsField(
|
||||
id='1', label='Choices', display_mode='images', data_source={'type': 'carddef:images'}
|
||||
),
|
||||
]
|
||||
formdef2.store()
|
||||
formdef2.data_class().wipe()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get('/test/')
|
||||
assert len(resp.pyquery('div.CheckboxesWithImagesWidget')) == 1
|
||||
assert len(resp.pyquery('img.item-with-image--picture')) == 3
|
||||
|
||||
|
||||
def test_file_prefill_on_edit(pub, http_requests):
|
||||
user = create_user(pub)
|
||||
formdef = create_formdef()
|
||||
|
|
|
@ -1847,7 +1847,7 @@ def test_block_with_static_condition(pub):
|
|||
assert 'f1$element0$f234' in resp.form.fields
|
||||
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style == 'display: none'
|
||||
assert 'f1$element0$f345' in resp.form.fields
|
||||
assert resp.pyquery('[data-widget-name="f1$element0$f345"]').attr.style is None
|
||||
assert resp.pyquery('[data-widget-name="f1$element0$f345"]').attr.style == ''
|
||||
assert resp.pyquery('[data-field-id="456"]').attr.style == 'display: none'
|
||||
assert resp.pyquery('[data-field-id="567"]').attr.style is None
|
||||
|
||||
|
@ -2002,7 +2002,7 @@ def test_block_with_block_counter_condition(pub):
|
|||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style is None
|
||||
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style == ''
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert resp.pyquery('[data-widget-name="f1$element1$f234"]').attr.style == 'display: none'
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
|
|
|
@ -89,7 +89,7 @@ def test_field_live_condition(pub):
|
|||
resp.form['f1'] = 'bye'
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'There were errors' in resp.text
|
||||
assert resp.html.find('div', {'data-field-id': '2'}).attrs.get('style') is None
|
||||
assert resp.html.find('div', {'data-field-id': '2'}).attrs.get('style') == ''
|
||||
resp.form['f2'] = 'bye'
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
@ -257,7 +257,7 @@ def test_field_live_condition_multipages(pub):
|
|||
resp.form['f2'] = 'bye'
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.form.submit('previous')
|
||||
assert resp.html.find('div', {'data-field-id': '2'}).attrs.get('style') is None
|
||||
assert resp.html.find('div', {'data-field-id': '2'}).attrs.get('style') == ''
|
||||
live_resp = app.post('/foo/live', params=resp.form.submit_fields())
|
||||
assert live_resp.json['result']['1']['visible']
|
||||
assert live_resp.json['result']['2']['visible']
|
||||
|
|
20
wcs/api.py
|
@ -38,7 +38,7 @@ from wcs.data_sources import NamedDataSource
|
|||
from wcs.data_sources import get_object as get_data_source_object
|
||||
from wcs.data_sources import request_json_items
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.forms.common import FormStatusPage
|
||||
from wcs.forms.common import FileDirectory, FormStatusPage
|
||||
from wcs.qommon import get_cfg
|
||||
from wcs.qommon.afterjobs import AfterJob
|
||||
from wcs.roles import logged_users_role
|
||||
|
@ -428,6 +428,22 @@ class ApiCardPage(ApiFormPageMixin, BackofficeCardPage):
|
|||
)
|
||||
|
||||
|
||||
class CardFileByTokenDirectory(Directory):
|
||||
def _q_lookup(self, component):
|
||||
get_request().ignore_session = True
|
||||
context = get_session().get_by_magictoken(component)
|
||||
|
||||
try:
|
||||
carddef = CardDef.get_by_urlname(context['carddef_slug'])
|
||||
data = carddef.data_class().get(context['data_id'])
|
||||
for field_data in data.get_all_file_data():
|
||||
if not hasattr(field_data, 'file_digest'):
|
||||
continue
|
||||
if field_data.file_digest() == context['file_digest']:
|
||||
return FileDirectory.serve_file(field_data)
|
||||
except (KeyError, ValueError):
|
||||
raise TraversalError()
|
||||
|
||||
|
||||
class ApiFormsDirectory(Directory):
|
||||
_q_exports = ['', 'geojson']
|
||||
|
||||
|
@ -1312,6 +1328,7 @@ class ApiDirectory(Directory):
|
|||
'cards',
|
||||
'geojson',
|
||||
'jobs',
|
||||
('card-file-by-token', 'card_file_by_token'),
|
||||
]
|
||||
|
||||
cards = ApiCardsDirectory()
|
||||
|
@ -1324,6 +1341,7 @@ class ApiDirectory(Directory):
|
|||
autocomplete = AutocompleteDirectory()
|
||||
geojson = GeoJsonDirectory()
|
||||
jobs = AfterJobsDirectory()
|
||||
card_file_by_token = CardFileByTokenDirectory()
|
||||
|
||||
def roles(self):
|
||||
get_response().set_content_type('application/json')
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from quixote import get_publisher, get_request
|
||||
from quixote import get_publisher, get_request, get_session
|
||||
|
||||
from wcs.formdata import FormData
|
||||
|
||||
|
@ -39,7 +39,9 @@ class CardData(FormData):
|
|||
|
||||
formdef = property(get_formdef)
|
||||
|
||||
def get_data_source_structured_item(self, digest_key='default', with_related_urls=False):
|
||||
def get_data_source_structured_item(
|
||||
self, digest_key='default', with_related_urls=False, with_files_urls=False
|
||||
):
|
||||
if self.digests is None:
|
||||
if digest_key == 'default':
|
||||
summary = _('Digest (default) not defined')
|
||||
|
@ -62,6 +64,10 @@ class CardData(FormData):
|
|||
if not field.varname or field.varname in ('id', 'text'):
|
||||
continue
|
||||
value = self.data and self.data.get(field.id)
|
||||
|
||||
if with_files_urls and hasattr(value, 'file_digest'):
|
||||
item['%s_url' % field.varname] = self.get_file_by_token_url(value.file_digest())
|
||||
|
||||
if isinstance(value, str):
|
||||
item[field.varname] = value
|
||||
return item
|
||||
|
@ -111,3 +117,14 @@ class CardData(FormData):
|
|||
return cls.select([Equal('uuid', value)], limit=1)[0]
|
||||
except IndexError:
|
||||
raise KeyError(value)
|
||||
|
||||
def get_file_by_token_url(self, file_digest):
|
||||
session = get_session()
|
||||
context = {
|
||||
fpeters
commented
Il faudrait un nom approprié pour cette méthode, même si c'est la même infra de token qui est utilisée derrière. Il faudrait un nom approprié pour cette méthode, même si c'est la même infra de token qui est utilisée derrière.
|
||||
'carddef_slug': self.formdef.url_name,
|
||||
fpeters
commented
Et quelque chose de moins générique comme chemin, type /api/card-file-token/, peut-être. Et quelque chose de moins générique comme chemin, type /api/card-file-token/, peut-être.
|
||||
'data_id': self.id,
|
||||
'file_digest': file_digest,
|
||||
'session_id': session.id,
|
||||
}
|
||||
token = session.create_autocomplete_token(context)
|
||||
return "/api/card-file-by-token/%s" % token.id
|
||||
|
|
|
@ -143,7 +143,7 @@ class CardDef(FormDef):
|
|||
@classmethod
|
||||
def get_carddefs_as_data_source(cls):
|
||||
carddefs_by_id = {}
|
||||
for carddef in cls.select(lightweight=True, ignore_errors=True, order_by='name'):
|
||||
for carddef in cls.select(ignore_errors=True, order_by='name'):
|
||||
if not carddef.default_digest_template:
|
||||
continue
|
||||
data_source_id = 'carddef:%s' % carddef.url_name
|
||||
|
@ -191,6 +191,7 @@ class CardDef(FormDef):
|
|||
get_by_id=None,
|
||||
get_by_text=None,
|
||||
with_related_urls=False,
|
||||
with_files_urls=False,
|
||||
):
|
||||
assert data_source_id.startswith('carddef:')
|
||||
parts = data_source_id.split(':')
|
||||
|
@ -252,7 +253,9 @@ class CardDef(FormDef):
|
|||
criterias.append(ElementEqual('digests', digest_key, get_by_text))
|
||||
|
||||
items = [
|
||||
x.get_data_source_structured_item(digest_key=digest_key, with_related_urls=with_related_urls)
|
||||
x.get_data_source_structured_item(
|
||||
digest_key=digest_key, with_related_urls=with_related_urls, with_files_urls=with_files_urls
|
||||
)
|
||||
for x in carddef.data_class().select(clause=criterias, order_by=order_by, limit=limit)
|
||||
]
|
||||
if order_by is None:
|
||||
|
@ -304,6 +307,12 @@ class CardDef(FormDef):
|
|||
varnames.extend(Field.get_referenced_varnames(formdef, criteria.value))
|
||||
return varnames
|
||||
|
||||
def has_image_field(self):
|
||||
for f in self.fields:
|
||||
if f.key == 'file' or f.varname != 'image':
|
||||
return True
|
||||
fpeters
commented
Je trouve cette combinaison avec le not == et le != est peu lisible; pour moi ça serait plus lisible d'exprimer ce qui est souhaité,
Je trouve cette combinaison avec le not == et le != est peu lisible; pour moi ça serait plus lisible d'exprimer ce qui est souhaité,
```
if f.key == 'file' and f.varname == 'image':
return True
````
smihai
commented
T'as raison. C'est corrigé. > Je trouve cette combinaison avec le not == et le != est peu lisible; pour moi ça serait plus lisible d'exprimer ce qui est souhaité,
>
> ```
> if f.key == 'file' and f.varname == 'image':
> return True
> ````
T'as raison. C'est corrigé.
|
||||
return False
|
||||
|
||||
|
||||
def get_cards_graph(category=None, show_orphans=False):
|
||||
out = io.StringIO()
|
||||
|
|
|
@ -86,6 +86,7 @@ class DataSourceSelectionWidget(CompositeWidget):
|
|||
option[3].update({'data-goto-url': '%s%s' % (ds[0].get_url(), ds[3].get_url_slug())})
|
||||
elif get_publisher().get_backoffice_root().is_accessible('cards'):
|
||||
option[3].update({'data-goto-url': ds[0].get_admin_url()})
|
||||
option[3].update({'data-has-image': str(ds[0].has_image_field()).lower()})
|
||||
cards_options.append(option)
|
||||
cards_options.sort(key=lambda x: misc.simplify(x[1]))
|
||||
if cards_options:
|
||||
|
@ -247,14 +248,23 @@ def get_cache_key(url, data_source):
|
|||
return force_str(hashlib.md5(force_bytes(cache_key)).hexdigest())
|
||||
|
||||
|
||||
def get_items(data_source, include_disabled=False, mode=None):
|
||||
structured_items = get_structured_items(data_source, mode=mode, include_disabled=include_disabled)
|
||||
def get_tupled_items(structured_items):
|
||||
tupled_items = []
|
||||
fpeters
commented
Mauvais nom de méthode (cf dessous il y a un seul p), mais surtout tu peux dire deux mots sur pourquoi cette découpe supplémentaire ? Mauvais nom de méthode (cf dessous il y a un seul p), mais surtout tu peux dire deux mots sur pourquoi cette découpe supplémentaire ?
smihai
commented
En effet, typo dans le nom de la méthode. L'idée de la découpe est d'avoir une méthode ( Et > Mauvais nom de méthode (cf dessous il y a un seul p), mais surtout tu peux dire deux mots sur pourquoi cette découpe supplémentaire ?
En effet, typo dans le nom de la méthode.
L'idée de la découpe est d'avoir une méthode (`get_structured_carddef_items`) pour passer un paramètre qui permet d'avoir l'url de l'image à la source de données des fiches.
Et `get_tupled_items` remplace `get_items` pour le formattage: `id`, `text`, etc.
|
||||
for item in structured_items:
|
||||
tupled_items.append((str(item['id']), str(item['text']), str(item.get('key', item['id'])), item))
|
||||
return tupled_items
|
||||
|
||||
|
||||
def get_items(data_source, include_disabled=False, mode=None):
|
||||
items = get_structured_items(data_source, mode=mode, include_disabled=include_disabled)
|
||||
return get_tupled_items(items)
|
||||
|
||||
|
||||
def get_carddef_items(data_source):
|
||||
structured_items = get_structured_carddef_items(data_source, with_files_urls=True)
|
||||
return get_tupled_items(structured_items)
|
||||
|
||||
|
||||
def get_id_by_option_text(data_source, text_value):
|
||||
data_source = get_object(data_source)
|
||||
if data_source:
|
||||
|
@ -400,12 +410,16 @@ def get_structured_items(data_source, mode=None, include_disabled=True, raise_on
|
|||
return items
|
||||
|
||||
|
||||
def get_structured_carddef_items(data_source, with_files_urls=False):
|
||||
from wcs.carddef import CardDef
|
||||
|
||||
return CardDef.get_data_source_items(data_source['type'], with_files_urls=with_files_urls)
|
||||
|
||||
|
||||
def _get_structured_items(data_source, mode=None, raise_on_error=False):
|
||||
if data_source.get('type') and data_source.get('type').startswith('carddef:'):
|
||||
# cards
|
||||
from wcs.carddef import CardDef
|
||||
|
||||
return CardDef.get_data_source_items(data_source['type'])
|
||||
return get_structured_carddef_items(data_source)
|
||||
|
||||
cache_duration = 0
|
||||
if data_source.get('type') not in ('json', 'jsonp', 'geojson', 'formula', 'jsonvalue', 'wcs:users'):
|
||||
|
|
|
@ -41,6 +41,7 @@ from .qommon import N_, _, evalutils, force_str, get_cfg, misc
|
|||
from .qommon.form import (
|
||||
AutocompleteStringWidget,
|
||||
CheckboxesWidget,
|
||||
CheckboxesWithImagesWidget,
|
||||
CheckboxWidget,
|
||||
CommentWidget,
|
||||
CompositeWidget,
|
||||
|
@ -60,6 +61,7 @@ from .qommon.form import (
|
|||
MultiSelectWidget,
|
||||
PasswordEntryWidget,
|
||||
RadiobuttonsWidget,
|
||||
RadiobuttonsWithImagesWidget,
|
||||
RankedItemsWidget,
|
||||
RichTextWidget,
|
||||
SingleSelectHintWidget,
|
||||
|
@ -2121,6 +2123,31 @@ class MapOptionsMixin:
|
|||
)
|
||||
|
||||
|
||||
class ItemWithImageFieldMixin:
|
||||
# images options
|
||||
image_desktop_size = 150
|
||||
image_mobile_size = 75
|
||||
|
||||
def fill_image_options_admin_form(self, form, **kwargs):
|
||||
form.add(
|
||||
IntWidget,
|
||||
'image_desktop_size',
|
||||
title=_('Image size on desktop'),
|
||||
value=self.image_desktop_size,
|
||||
hint=_('In pixels.'),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
form.add(
|
||||
IntWidget,
|
||||
'image_mobile_size',
|
||||
title=_('Image size on mobile'),
|
||||
value=self.image_mobile_size,
|
||||
hint=_('In pixels.'),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ItemFieldMixin:
|
||||
def get_real_data_source(self):
|
||||
return data_sources.get_real(self.data_source)
|
||||
|
@ -2266,7 +2293,7 @@ class ItemFieldMixin:
|
|||
return display_value
|
||||
|
||||
|
||||
class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
||||
class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin, ItemWithImageFieldMixin):
|
||||
fpeters
commented
Ici un détail mais à avoir correct dès le début pour s'éviter des migrations : columns_count, ou number_of_columns, pas columns_number. Ici un détail mais à avoir correct dès le début pour s'éviter des migrations : columns_count, ou number_of_columns, pas columns_number.
|
||||
key = 'item'
|
||||
description = _('List')
|
||||
allow_complex = True
|
||||
|
@ -2370,7 +2397,15 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
|||
|
||||
if self.display_mode != 'map':
|
||||
if self.data_source:
|
||||
items = data_sources.get_items(self.data_source, include_disabled=self.display_disabled_items)
|
||||
if display_mode == 'images':
|
||||
self.widget_class = RadiobuttonsWithImagesWidget
|
||||
items = data_sources.get_carddef_items(
|
||||
self.data_source,
|
||||
)
|
||||
else:
|
||||
items = data_sources.get_items(
|
||||
self.data_source, include_disabled=self.display_disabled_items
|
||||
)
|
||||
kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
|
||||
kwargs['options_with_attributes'] = items[:]
|
||||
else:
|
||||
|
@ -2540,6 +2575,7 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
|||
('autocomplete', _('Autocomplete'), 'autocomplete'),
|
||||
('map', _('Map (requires geographical data)'), 'map'),
|
||||
('timetable', _('Timetable'), 'timetable'),
|
||||
('images', _('Images'), 'images'),
|
||||
]
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
|
@ -2569,6 +2605,12 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
|||
'data-dynamic-display-value': 'timetable',
|
||||
},
|
||||
)
|
||||
|
||||
self.fill_image_options_admin_form(
|
||||
form,
|
||||
attrs={'data-dynamic-display-child-of': 'display_mode', 'data-dynamic-display-value': 'images'},
|
||||
)
|
||||
|
||||
self.fill_zoom_admin_form(
|
||||
form, attrs={'data-dynamic-display-child-of': 'display_mode', 'data-dynamic-display-value': 'map'}
|
||||
)
|
||||
|
@ -2627,6 +2669,8 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
|||
'position_template',
|
||||
'initial_date_alignment',
|
||||
'use_hint_as_first_option',
|
||||
'image_desktop_size',
|
||||
'image_mobile_size',
|
||||
]
|
||||
|
||||
def check_admin_form(self, form):
|
||||
|
@ -2685,7 +2729,7 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
|||
register_field_class(ItemField)
|
||||
|
||||
|
||||
class ItemsField(WidgetField, ItemFieldMixin):
|
||||
class ItemsField(WidgetField, ItemFieldMixin, ItemWithImageFieldMixin):
|
||||
key = 'items'
|
||||
description = _('Multiple choice list')
|
||||
allow_complex = True
|
||||
|
@ -2723,7 +2767,11 @@ class ItemsField(WidgetField, ItemFieldMixin):
|
|||
kwargs['min_choices'] = self.min_choices
|
||||
kwargs['max_choices'] = self.max_choices
|
||||
if self.data_source:
|
||||
items = data_sources.get_items(self.data_source, include_disabled=self.display_disabled_items)
|
||||
if self.display_mode == 'images':
|
||||
items = data_sources.get_carddef_items(self.data_source)
|
||||
self.widget_class = CheckboxesWithImagesWidget
|
||||
else:
|
||||
items = data_sources.get_items(self.data_source, include_disabled=self.display_disabled_items)
|
||||
kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
|
||||
kwargs['options_with_attributes'] = items[:]
|
||||
|
||||
|
@ -2745,6 +2793,7 @@ class ItemsField(WidgetField, ItemFieldMixin):
|
|||
options = [
|
||||
('checkboxes', _('Checkboxes'), 'checkboxes'),
|
||||
('autocomplete', _('Autocomplete'), 'autocomplete'),
|
||||
('images', _('Images'), 'images'),
|
||||
]
|
||||
form.add(
|
||||
RadiobuttonsWidget,
|
||||
|
@ -2752,9 +2801,17 @@ class ItemsField(WidgetField, ItemFieldMixin):
|
|||
title=_('Display Mode'),
|
||||
options=options,
|
||||
value=self.display_mode,
|
||||
attrs={'data-dynamic-display-parent': 'true'},
|
||||
extra_css_class='widget-inline-radio',
|
||||
)
|
||||
self.add_items_fields_admin_form(form)
|
||||
self.fill_image_options_admin_form(
|
||||
form,
|
||||
attrs={
|
||||
'data-dynamic-display-child-of': 'display_mode',
|
||||
'data-dynamic-display-value': 'images',
|
||||
},
|
||||
)
|
||||
form.add(
|
||||
IntWidget,
|
||||
'min_choices',
|
||||
|
@ -2789,6 +2846,8 @@ class ItemsField(WidgetField, ItemFieldMixin):
|
|||
'in_filters',
|
||||
'anonymise',
|
||||
'display_disabled_items',
|
||||
'image_desktop_size',
|
||||
'image_mobile_size',
|
||||
]
|
||||
|
||||
def check_admin_form(self, form):
|
||||
|
|
|
@ -296,6 +296,24 @@ class RadiobuttonsWidget(quixote.form.RadiobuttonsWidget):
|
|||
}
|
||||
|
||||
|
||||
class RadiobuttonsWithImagesWidget(RadiobuttonsWidget):
|
||||
template_name = 'qommon/forms/widgets/radiobuttons-with-images.html'
|
||||
|
||||
def add_media(self):
|
||||
get_response().add_css_include('item-with-image.css')
|
||||
|
||||
def get_options(self):
|
||||
for option in self.options_with_attributes:
|
||||
obj, description, key, attributes = option[:4]
|
||||
yield {
|
||||
'value': key,
|
||||
'label': description,
|
||||
'disabled': bool(self.options_with_attributes and option[-1].get('disabled')),
|
||||
'selected': self.is_selected(obj),
|
||||
'image_url': attributes.get('image_url'),
|
||||
}
|
||||
|
||||
|
||||
def get_selection_error_text(*args):
|
||||
return _('invalid value selected')
|
||||
|
||||
|
@ -1767,6 +1785,13 @@ class CheckboxesWidget(Widget):
|
|||
request.form[option['name']] = True
|
||||
|
||||
|
||||
class CheckboxesWithImagesWidget(CheckboxesWidget):
|
||||
template_name = 'qommon/forms/widgets/checkboxes-with-images.html'
|
||||
|
||||
def add_media(self):
|
||||
get_response().add_css_include('item-with-image.css')
|
||||
|
||||
|
||||
class ValidatedStringWidget(StringWidget):
|
||||
'''StringWidget which checks the value entered is correct according to a regex'''
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
.CheckboxesWithImagesWidget, .RadiobuttonsWithImagesWidget {
|
||||
.content {
|
||||
--image-size: var(--image-mobile-size);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
@media (min-width: 761px) {
|
||||
--image-size: var(--image-desktop-size);
|
||||
}
|
||||
&:focus-within {
|
||||
outline: 1px dashed var(--primary-color, #bbb);
|
||||
fpeters
commented
D'où arrive D'où arrive `var(--primary-color)` ? Il manque un ticket côté publik-base-theme ?
smihai
commented
On s'est basé sur le rendu en backoffice où la variable Et quand ça sera disponible côté front le widget gagnera automatiquement le focus. On s'est basé sur le rendu en backoffice où la variable `--primary-color` est déjà disponible.
Et quand ça sera disponible côté front le widget gagnera automatiquement le focus.
fpeters
commented
Ça demande a minima qu'un ticket soit créé, ça ne va pas arriver tout seul. Et quel est donc le comportement actuel en front, pas de focus du tout (= problème d'accessibilité) ou un focus d'une mauvaise couleur ? > Et quand ça sera disponible côté front le widget gagnera automatiquement le focus.
Ça demande a minima qu'un ticket soit créé, ça ne va pas arriver tout seul.
Et quel est donc le comportement actuel en front, pas de focus du tout (= problème d'accessibilité) ou un focus d'une mauvaise couleur ?
smihai
commented
Pas de focus du tout. J'ai rajouté un fallback vers un gri clair mais il faudrait le gérer côté publik-base-theme pour utiliser plutôt > Et quel est donc le comportement actuel en front, pas de focus du tout (= problème d'accessibilité) ou un focus d'une mauvaise couleur ?
Pas de focus du tout. J'ai rajouté un fallback vers un gri clair mais il faudrait le gérer côté publik-base-theme pour utiliser plutôt `$widget-focus-outline`. Je fais le ticket.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-with-image {
|
||||
padding: 10px;
|
||||
smihai marked this conversation as resolved
Outdated
tjund
commented
margin-bottom devenu inutile parceque display grid margin-bottom devenu inutile parceque display grid
smihai
commented
Merci, c'est retiré. Merci, c'est retiré.
|
||||
flex: 0 0 var(--image-size);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: var(--image-size) auto;
|
||||
grid-row-gap: 5px;
|
||||
grid-template-areas:
|
||||
"picture picture"
|
||||
"input label";
|
||||
align-items: baseline;
|
||||
justify-items: center;
|
||||
&--picture {
|
||||
margin-bottom: 5px;
|
||||
grid-area: picture;
|
||||
width: var(--image-size);
|
||||
height: var(--image-size);
|
||||
tjund
commented
align-items ne marche que sur container grid, donc inutile ici. align-items ne marche que sur container grid, donc inutile ici.
|
||||
object-fit: contain;
|
||||
object-position: bottom;
|
||||
}
|
||||
&--input {
|
||||
grid-area: input;
|
||||
}
|
||||
&--label {
|
||||
grid-area: label;
|
||||
}
|
||||
}
|
||||
tjund
commented
align-items ne marche que sur container grid, donc inutile ici. .item-with-image--input n'existe pas sur .RadiobuttonsWithImagesWidget align-items ne marche que sur container grid, donc inutile ici.
.item-with-image--input n'existe pas sur .RadiobuttonsWithImagesWidget
|
|
@ -334,6 +334,14 @@ $(function() {
|
|||
$('select[name="data_source$type"] option:visible').first().prop('selected', true).trigger('change');
|
||||
}
|
||||
}
|
||||
if ($(this).val() == 'images') {
|
||||
$('input[name="data_mode"][value="simple-list"]').prop('disabled', true);
|
||||
$('input[name="data_mode"][value="data-source"]').click()
|
||||
$('select[name="data_source$type"] option:not([data-has-image="true"])').hide();
|
||||
if ($('select[name="data_source$type"] option:selected:visible').length == 0) {
|
||||
$('select[name="data_source$type"] option:visible').first().prop('selected', true).trigger('change');
|
||||
}
|
||||
}
|
||||
});
|
||||
$('[type=radio][name=display_mode]:checked').trigger('change');
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% if widget.is_required %}widget-required{% else %}widget-optional{% endif %}
|
||||
{% if widget.is_prefilled %}widget-prefilled{% endif %}{% endblock %}"
|
||||
{% block widget-attrs %}
|
||||
{% if widget.is_hidden %}style="display: none"{% endif %}
|
||||
style="{% block widget-style-attributes %}{% if widget.is_hidden %}display: none{% endif %}{% endblock %}"
|
||||
{% if widget.field %}data-field-id="{{ widget.field.id }}"{% endif %}
|
||||
{% if not widget.readonly %}
|
||||
{% if widget.field.validation.type %}data-validation-type="{{ widget.field.validation.type }}"{% endif %}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "qommon/forms/widgets/checkboxes.html" %}
|
||||
|
||||
{% block widget-style-attributes %}
|
||||
--image-desktop-size: {{ widget.field.image_desktop_size }}px;--image-mobile-size: {{ widget.field.image_mobile_size }}px;
|
||||
{{ block.super }}
|
||||
tjund
commented
Pas de :root ici. Limiter le contexte de la variable à Pas de :root ici. Limiter le contexte de la variable à `.CheckboxesWithImagesWidget`
|
||||
{% endblock %}
|
||||
|
||||
{% block widget-control %}
|
||||
{% for option in widget.get_options %}
|
||||
<label for="{{ widget.get_name_for_id }}_op_{{ forloop.counter0 }}" class="item-with-image">
|
||||
<input id="{{ widget.get_name_for_id }}_op_{{ forloop.counter0 }}"
|
||||
{% if option.selected %}checked="checked"{% endif %}
|
||||
{% for attr in widget.attrs.items %}{{attr.0}}="{{attr.1}}" {% endfor %}
|
||||
type="checkbox" name="{{ option.name }}" value="yes" class="item-with-image--input" />
|
||||
{% if option.options.image_url %}<img src="{{ option.options.image_url }}" title="{{ option.label }}" alt="" class="item-with-image--picture" />{% endif %}
|
||||
<span class="item-with-image--label">{{ option.label }}</span></label>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
tjund
commented
Pourquoi utiliser le tag ? Il ne sert à rien puisqu'on utilise pas les images adaptatives et les balises . Pourquoi utiliser le tag <picture >? Il ne sert à rien puisqu'on utilise pas les images adaptatives et les balises <source>.
L'image doit avoir une balise alt vide pour indiquer qu'elle n'est que "décorative"
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "qommon/forms/widgets/radiobuttons.html" %}
|
||||
|
||||
{% block widget-style-attributes %}
|
||||
--image-desktop-size: {{ widget.field.image_desktop_size }}px;--image-mobile-size: {{ widget.field.image_mobile_size }}px;
|
||||
{{ block.super }}
|
||||
tjund
commented
Idem, ne pas utiliser :root Idem, ne pas utiliser :root
fpeters
commented
Ça va clasher avec le cas où le champ est conditionné/caché, qui ajoute déjà un
(pareil pour le cas checkboxes) Ça va clasher avec le cas où le champ est conditionné/caché, qui ajoute déjà un
```
{% if widget.is_hidden %}style="display: none"{% endif %}
```
(pareil pour le cas checkboxes)
smihai
commented
Ici je ne vois pas d'autres options que de revenir à la declaration CSS dans le template du widget:
Ici je ne vois pas d'autres options que de revenir à la declaration CSS dans le template du widget:
```
<style>
.{{ widget.class_name }}[data-widget-name="{{ widget.name }}"] {
--image-desktop-size: ...;
--image-mobile-size: ...;
}
</style>
```
fpeters
commented
J'aurais dû l'écrire, je pensais plutôt qu'on pourrait introduire un J'aurais dû l'écrire, je pensais plutôt qu'on pourrait introduire un `{% block widget-style-attribute %}` et dedans alors il y aurait la gestion du cas champ caché, et dans le gabarit des images ça serait récupérer via block.super.
smihai
commented
J'y ai pensé aussi, mais j'hésitais à toucher au template des widgets. J'y ai pensé aussi, mais j'hésitais à toucher au template des widgets.
Je le fais.
|
||||
{% endblock %}
|
||||
|
||||
{% block widget-control %}
|
||||
{% for option in widget.get_options %}
|
||||
<label for="{{ widget.get_name_for_id }}_op_{{ forloop.counter0 }}" class="item-with-image">
|
||||
<input id="{{ widget.get_name_for_id }}_op_{{ forloop.counter0 }}"
|
||||
{% if option.selected %}checked="checked" {% endif %}
|
||||
{% for attr in widget.attrs.items %}{{attr.0}}="{{attr.1}}" {% endfor %}
|
||||
type="radio" name="{{ widget.name }}" value="{{ option.value }}" class="item-with-image--input" />
|
||||
{% if option.image_url %}<img src="{{ option.image_url }}" title="{{ option.label }}" alt="" class="item-with-image--picture" />{% endif %}
|
||||
<span class="item-with-image--label">{{ option.label }}</span>
|
||||
</label>
|
||||
{% if not forloop.last %}{{ widget.delim|safe }}{% endif %}
|
||||
{% endfor %}
|
||||
fpeters
commented
Évitons vraiment tant de styles inline (il faut ces styles dans les styles backoffice + dans publik-base-theme, où ils pourront être adaptés si nécessaire. Évitons vraiment tant de styles inline (il faut ces styles dans les styles backoffice + dans publik-base-theme, où ils pourront être adaptés si nécessaire.
|
||||
{% endblock %}
|
Il faudrait un nom approprié pour cette méthode, même si c'est la même infra de token qui est utilisée derrière.
Je me suis embourbé à appeler
get_autocomplete_token
alors queget_by_magictoken
existe déjà.