display lists as images (#72176) #322

Merged
smihai merged 2 commits from wip/72176-option-to-display-list-as-images into main 2023-07-01 09:13:55 +02:00
14 changed files with 325 additions and 19 deletions

View File

@ -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()

View File

@ -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')

View File

@ -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']

View File

@ -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)

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.

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 que get_by_magictoken existe déjà.

> 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 que `get_by_magictoken` existe déjà.
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')

View File

@ -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 = {

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,

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

View File

@ -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

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
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 ````

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é.

> 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()

View File

@ -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 = []
Review

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 ?
Review

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.

> 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'):

View File

@ -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):

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):

View File

@ -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'''

View File

@ -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);

D'où arrive var(--primary-color) ? Il manque un ticket côté publik-base-theme ?

D'où arrive `var(--primary-color)` ? Il manque un ticket côté publik-base-theme ?

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.

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.

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 ?

> 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 ?

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.

> 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
Outdated
Review

margin-bottom devenu inutile parceque display grid

margin-bottom devenu inutile parceque display grid

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);
Outdated
Review

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;
}
}
Outdated
Review

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

View File

@ -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');

View File

@ -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 %}

View File

@ -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 }}
Outdated
Review

Pas de :root ici. Limiter le contexte de la variable à .CheckboxesWithImagesWidget

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 %}
Outdated
Review

Pourquoi utiliser le tag ? Il ne sert à rien puisqu'on utilise pas les images adaptatives et les balises .
L'image doit avoir une balise alt vide pour indiquer qu'elle n'est que "décorative"

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"

View File

@ -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 }}
Outdated
Review

Idem, ne pas utiliser :root

Idem, ne pas utiliser :root

Ç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)

Ç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)

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>
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> ```

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.

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.

J'y ai pensé aussi, mais j'hésitais à toucher au template des widgets.
Je le fais.

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 %}

É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 %}