Custom css in DateWidget

I seem to be unable to add a custom css class to a DateWidget in a DataGrid.

The following works for other types of fields, as described in plone.autoform

    invoice_amount = schema.TextLine(
        title = _(
            'invoice_amount_title',
            default = u'Invoice Amount',
        ),
        required=True,
    )
    directives.widget('invoice_amount', klass='float-end')

Because of the awesome work that was done to integrate Bootstrap 5, the element then will nicely align to the right - e.g. in a DatagridField.

<td class="">
   <span id="form-widgets-remadv_items-0-widgets-invoice_amount"
         class="float-end textline-field">514.97</span>
 </td>

Not so for a DateWidget however :frowning:

invoic_in_date = schema.Date(
    title = _(
        'invoic_in_date_title',
        default = u'Date',
    ),
    required=False,
    default=date.today(),
)
directives.widget('invoic_in_date', klass='float-end')
<td class="">
    9/17/21
  </td>

An alternate way to insert additional parameters to my widget, is by using the method described here:

@provider(IFormFieldProvider)
class IRemadvItem(model.Schema):
    """ Schema of a line item contained in a remadv message
    """

    invoic_in_date = schema.Date(
        title = _(
            'invoic_in_date_title',
            default = u'Date',
        ),
        required=False,
        default=date.today(),
    )
    directives.widget('invoic_in_date', DateWidget, wrapper_css_class='text-end')

No juice (this time a date represented in German, just to spice things up a bit). Other field types are ok...

<tbody id="datagridwidget-tbody" data-name_prefix="form.widgets.remadv_items" data-id_prefix="form-widgets-remadv_items">
    <tr class="datagridwidget-row">
        <td class="">
            17.09.21
        </td>
        <td class="">
            <span id="form-widgets-remadv_items-0-widgets-document_reference_type" 
                  class="select-widget choice-field">
                <span class="selected-option">Rechnung</span>
            </span>
        </td>
...

Interestingly: if the date is in the main form, the first method does not work but the wrapper_css_class text-end is added just fine.

<div id="formfield-form-widgets-document_date" 
     class="mb-3 field fieldname-form.widgets.document_date widget-mode-display text-end" data-fieldname="form.widgets.document_date">
    <b class="widget-label form-label d-block">
        Datum
    </b>
    01.09.22
</div>

Where do I start digging to fix this?

Off topic, but as a workaround you could add a CSS class like this (I think),

    invoic_in_date = schema.Date(
        klass="someclass"

Actually that was my initial assumption, until I looked it up...

Doing what you suggest results in
TypeError: __init__() got an unexpected keyword argument 'klass'

Working example here:

from plone.app.z3cform.widget import DateFieldWidget
from plone.autoform import directives
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from zope import schema
from zope.interface import provider


@provider(IFormFieldProvider)
class ITestSchema(model.Schema):

    test_date = schema.Date(
        title="TEST",
        required=False,
    )

    directives.widget(
        "test_date",
        DateFieldWidget,
        klass="text-end",
    )

NOTE: this also worked for me in a datagrid.DictRow schema:

...
class ICourseOccurrences(model.Schema):
    start_date = schema.Date(
        title=_("Startdate"),
        required=False,
    )
    start_time = schema.Time(
        title=_("Starttime"),
        required=False,
    )
    end_time = schema.Time(
        title=_("Endtime"),
        required=False,
    )

    directives.widget(
        "start_date",
        DateFieldWidget,
        klass="text-end",
    )

class ICourseSchema(model.Schema):

    title = schema.TextLine(
        title=_("Course Title"),
        required=True,
    )

    occurrences = schema.List(
        title=_("Occurrences"),
        value_type=DictRow(
            title=_("Occurrences"),
            schema=ICourseOccurrences,
        ),
        required=False,
    )
    directives.widget(
        "occurrences",
        DataGridFieldWidgetFactory,
        auto_append=False,
        input_table_css_class="table table-sm",
        display_table_css_class="table table-sm",
    )
...

NOTE 2: this also worked for the "default" widget definition (without FieldWidget class as second parameter):

class ICourseOccurrences(model.Schema):
    start_date = schema.Date(
        title=_("Startdate"),
        required=False,
    )
    start_time = schema.Time(
        title=_("Starttime"),
        required=False,
    )
    end_time = schema.Time(
        title=_("Endtime"),
        required=False,
    )

    directives.widget(
        "start_date",
        klass="text-end",
    )

Though I'm working with datagridfield master branch ... I think I should make a release with my recent changes :wink:

My example was 'taken from memory', but I also (think I) remember that the class is not on the 'parent', but somewhere 'down in the widget'. I had some problems styling it, maybe you can use pseudo class .someclass:has()

Heh heh :slight_smile:

Were these very recent changes? I am using the master form a few days ago...

This also doesn't work for me (DateFieldWidget instead of DateWidget):

@provider(IFormFieldProvider)
class IRemadvItem(model.Schema):
    """ Schema of a line item contained in a remadv message
    """

    invoic_in_date = schema.Date(
        title = _(
            'invoic_in_date_title',
            default = u'Date',
        ),
        required=False,
        default=date.today(),
    )
    # for Date fields, neither of these approaches works in a datagrid field.
    # the second option works in a main form only
    #directives.widget('invoic_in_date', klass='float-end')
    #directives.widget('invoic_in_date', DateWidget, wrapper_css_class='text-end')
    directives.widget('invoic_in_date', DateFieldWidget, klass='text-end')

@petschki

Maybe this is why we see different results?

    remadv_items = schema.List(
        title=_(
            'remadv_items_title',
            default = u'Remadv Items',
        ),
        value_type=schema.Object(
            title=_(
                'value_type_title',
                default = u'Table',
            ),
            schema=IRemadvItem,
        ),
        default=[],
        required=False,
    )
    directives.widget('remadv_items', DataGridFieldWidgetFactory)

Related to this

schema.Object() als value_type doesn't work with DataGridFieldWidgetFactory. You have to use collective.z3cform.datagridfield.row.DictRow (which in fact is a subclass of zope.schema.Object but with all the converters wired) as value_type to make it work in datagridfield's environment.

Latest changes are from Aug. 8th 2022 with the plone.autoform.directives feature for the DictRow schema.

Wow, I can use DictRow again! :man_dancing: I had ended up with schema.Object after all else failed.

Your insights so far have helped me a lot, thank you!

I have a minimal representation of my issue, a mvp content type that gets its data via SQLAlchemy.

When using schema.Object, I have make my content subscriptable like this:

    def __getitem__(self, name):
        """ We shall be subscriptable
        """
        if safe_hasattr(self, name):
            value = getattr(self, name) 
            return value
        else:
            raise AttributeError(name) 

But using DictRow, this fails with TypeError: getattr(): attribute name must be string

That was an easy fix:

    def __getitem__(self, name):
        """ We shall be subscriptable
        """
        field_name, field = schema.getFieldsInOrder(IMinimalRelated)[name]
        if safe_hasattr(self, field_name):
            value = getattr(self, field_name)
            return value
        else:
            raise AttributeError(field_name)  

And now the content is displayed as expected :partying_face:

The Date field is still displayed left-aligned, identical as when using schema.Object - so something still seems wrong in my approach. (image at bottom)

Here are my content object and schema:

from pnz.erpediem.core import Base

from sqlalchemy import Column
from sqlalchemy import ForeignKey

from sqlalchemy import types
from sqlalchemy import null

from sqlalchemy.orm import backref
from sqlalchemy.orm import relationship

from zope.component import adapter
from zope.interface import implementer
from zope.interface import provider
from zope.interface import alsoProvides

from ..interfaces.mvp import IMinimal
from ..interfaces.mvp import IMinimalRelated

from plone.base.utils import safe_hasattr

import logging
log = logging.getLogger(__name__)


@implementer(IMinimalRelated)
class MinimalRelated(Base):
    """ Related data to test the SQLA Minimal Object
    """

    __tablename__ = 'mvp_testdata'

    id = Column(
        types.Integer(),
        default=999999,
        nullable=False,
        primary_key=True,
        unique=True,
    )

    ace_code = Column(
        types.Unicode(3),
        ForeignKey('ftx_ace_codes.ace_code'),
        default=u'',
        nullable=False,
    )

    test_date = Column(
        types.Date(),
        default=u'',
        nullable=False,
    )
    
    vocab_term = Column(
        types.Unicode(45),
        default=u'',
        nullable=False,
    )

    def __init__(self, context):
        self.context = context

    def __getitem__(self, name):
        """ We shall be subscriptable
        """
        field_name, field = schema.getFieldsInOrder(IMinimalRelated)[name]
        if safe_hasattr(self, field_name):
            value = getattr(self, field_name)
            return value
        else:
            raise AttributeError(field_name)   
        
    def __repr__(self):
        return "<%s %s - %s>" % (self.__class__.__name__, self.id, self.ace_code,)
   

@implementer(IMinimal)
class Minimal(Base):
    """ SQLA Minimal Object
    """

    __tablename__ = 'ftx_ace_codes'

    ace_code = Column(
        types.Unicode(3),
        default=u'',
        nullable=False,
        primary_key=True,
        unique=True,
    )
    
    title = Column(
        types.Unicode(255),
        default=u'',
        nullable=False,
        index=True,
    )

    description = Column(
        types.Unicode(1024),
        default=null(),
    )

    related_test = relationship(
        'MinimalRelated',
        order_by=MinimalRelated.id,
        foreign_keys=[MinimalRelated.ace_code,],
    )

    def __init__(self, context):
        self.context = context

    def __getitem__(self, name):
        """ We shall be subscriptable
        """
        field_name, field = schema.getFieldsInOrder(IMinimalRelated)[name]
        if safe_hasattr(self, field_name):
            value = getattr(self, field_name)
            return value
        else:
            raise AttributeError(field_name)      
        
    def __repr__(self):
        return "<%s %s - %s>" % (self.__class__.__name__, self.ace_code, self.title)
        
""" Minimal Viable Product (mvp) Interfaces
"""
from pnz.erpediem.core import _

from zope.interface import Interface
from zope import schema
from plone.supermodel import model
from plone.autoform import directives
from plone.autoform.interfaces import IFormFieldProvider

from zope.component import adapter
from zope.interface import implementer
from zope.interface import provider

from plone.app.z3cform.widget import DateFieldWidget
from collective.z3cform.datagridfield.datagridfield import DataGridFieldWidgetFactory
from collective.z3cform.datagridfield.interfaces import IDataGridFieldWidget
from collective.z3cform.datagridfield.row import DictRow

from datetime import date

class IMinimalContainer(Interface):
    """ """
    
    
@provider(IFormFieldProvider)
class IMinimalRelated(model.Schema):
    """Form schema to test MinimalRelated objects
    """

    id = schema.TextLine(
        title = _(u'ID'),
        required=True,
    )
    directives.widget('id', klass='float-end')

    ace_code = schema.TextLine(
        title = _(u'ACE Code'),
        required=True,
    )
    directives.widget('ace_code', klass='float-end')

    test_date = schema.Date(
        title = _(u'Date'),
        required=False,
        default=date.today(),
    )
    # neither works
    #directives.widget('test_date', DateFieldWidget, klass='text-end')
    directives.widget('test_date', klass='text-end')

    vocab_term = schema.Choice(
        title = _(u'Vocabulary Choice'),
        vocabulary="pnz.erpediem.core.MinimalTestVocabulary",
        required=False,
    )


@provider(IFormFieldProvider)
class IMinimal(model.Schema):
    """  Form schema for Minimal objects
    """  

    ace_code = schema.TextLine(
        title = _(u'ACE Code'),
        required=True,
    )
        
    title = schema.TextLine(
        title = _(u'Title'),
        required=False,
    )
        
    description = schema.TextLine(
        title = _(u'Description'),
            required=False,
    )

    related_test = schema.List(
        title=_(u'Related Items'),
        value_type=DictRow(
            title=u'Table',
            schema=IMinimalRelated,
        ),
        default=[],
        required=False,
    )
    directives.widget('related_test', DataGridFieldWidgetFactory)

    """
    model.fieldset(
        'related_items',
        label=_(u'Related'),
        fields=[
            'related_test',
        ]
    )
    """

Screenshot from 2022-09-28 13-31-57

Just for the record: I've released now a 3.0.0 final version.