19. Turning Talks into Events – Mastering Plone 6 development

19. Turning Talks into Events#

Save and show date and time of a talk.

Frontend chapter

Check out the code at the relevant tags!

Code for the beginning of this chapter:

# frontend
git checkout talkview
# backend
git checkout frontpage

Code for the end of this chapter:

# frontend
git checkout events
# backend
git checkout events

More info in The code for the training

We need a schedule and for this we need to store the information when a talk will happen.

Luckily the default type Event is based on reusable behaviors from the package plone.app.event that we can reuse.

In this chapter you will

  • Enable the event behavior for talks

  • Display the date of event in the talkview

19.1. Add date fields#

Instead of adding Datetime-fields to the talk schema we will use the behavior plone.eventbasic.

Enable the behavior plone.eventbasic for talks in profiles/default/types/talk.xml.

1<property name="behaviors">
2  <element value="plone.dublincore"/>
3  <element value="plone.namefromtitle"/>
4  <element value="plone.versioning"/>
5  <element value="ploneconf.featured"/>
6  <element value="plone.eventbasic"/>
7</property>

After you activate the behavior by hand or you reinstalled the add-on you will now have some additional fields for start, end, open_end and whole_day.

Note

While we're editing behaviors we can also add our own featured-behavior to News Items.

Add profiles/default/types/News_Item.xml:

<?xml version="1.0"?>
<object name="News Item" meta_type="Dexterity FTI" i18n:domain="plone"
    xmlns:i18n="http://xml.zope.org/namespaces/i18n">
  <property name="behaviors" purge="false">
    <element value="plone.constraintypes"/>
    <element value="ploneconf.featured"/>
  </property>
</object>

19.2. Display the dates#

Now we need to update the event view to show this information.

Unfortuanely displaying dates and times is not as simple as it might sound since we'd have to account for different use cases that all look different:

Here are some examples how dates might be displayed if they are full-day events, open-ended events or events with a defined end-time.

  • Apr 22, 2020 from 3:00 PM to 5:00 PM

  • Apr 22, 2020

  • Apr 22, 2020 7:00 PM

  • Apr 22, 2020 to Apr 24, 2020

  • Apr 22, 2020 7:00 PM to Apr 29, 2020 8:00 PM

Now consider that dates are displayed different in other languages and it really gets complicated.

So it would be a good idea to reuse a component that already deals with these use cases. Since we use the same behavior as the default content type Event in Plone, the default event view might have what we need.

Add an event und use the React Developer Tools to inspect the component displaying the date. The component is called When and is defined in frontend/node_modules/@plone/volto/src/components/theme/View/EventDatesInfo.jsx.

<When
  start={content.start}
  end={content.end}
  whole_day={content.whole_day}
  open_end={content.open_end}
/>

We'll reuse it in frontend/src/components/Views/Talk.jsx. We'll let us inspire by the event-view and add a <Segment floated="right"> that will contain the date and the audience. In this box we will also use <Header dividing sub> from seamantic-ui to separate the data.

frontend/src/components/Views/Talk.jsx:

import {
  Container as SemanticContainer,
  Header,
  Image,
  Label,
  Segment,
} from 'semantic-ui-react';
import { flattenToAppURL } from '@plone/volto/helpers';
import { When } from '@plone/volto/components/theme/View/EventDatesInfo';
import config from '@plone/volto/registry';

const TalkView = (props) => {
  const { content } = props;
  const Container =
    config.getComponent({ name: 'Container' }).component || SemanticContainer;
  const color_mapping = {
    beginner: 'green',
    advanced: 'yellow',
    professional: 'purple',
  };
  return (
    <Container id="view-wrapper talk-view">
      <h1 className="documentFirstHeading">
        <span className="type_of_talk">{content.type_of_talk.token}: </span>
        {content.title}
      </h1>
      {content.description && (
        <p className="documentDescription">{content.description}</p>
      )}
      <Segment floated="right">
        {content.start && !content.hide_date && (
          <>
            <Header dividing sub>
              When
            </Header>
            <When
              start={content.start}
              end={content.end}
              whole_day={content.whole_day}
              open_end={content.open_end}
            />
          </>
        )}
        {content.audience && (
          <Header dividing sub>
            Audience
          </Header>
        )}
        {content.audience?.map((item) => {
          let audience = item.title || item.token;
          let color = color_mapping[audience] || 'green';
          return (
            <Label key={audience} color={color}>
              {audience}
            </Label>
          );
        })}
      </Segment>
      <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
      <Segment clearing>
        {content.speaker && <Header dividing>{content.speaker}</Header>}
        {content.website ? (
          <p>
            <a href={content.website}>{content.company || content.website}</a>
          </p>
        ) : (
          <p>{content.company}</p>
        )}
        {content.email && (
          <p>
            Email: <a href={`mailto:${content.email}`}>{content.email}</a>
          </p>
        )}
        {content.twitter && (
          <p>
            X:{' '}
            <a href={`https://x.com/${content.twitter}`}>
              {content.twitter.startsWith('@')
                ? content.twitter
                : '@' + content.twitter}
            </a>
          </p>
        )}
        {content.github && (
          <p>
            Github:{' '}
            <a href={`https://github.com/${content.github}`}>
              {content.github}
            </a>
          </p>
        )}
        <Image
          src={flattenToAppURL(content.image?.scales?.preview?.download)}
          size="small"
          floated="right"
          alt={content.speaker}
          avatar
        />
        {content.speaker_biography && (
          <div
            dangerouslySetInnerHTML={{
              __html: content.speaker_biography.data,
            }}
          />
        )}
      </Segment>
    </Container>
  );
};
export default TalkView;

The result should look like this:

../_images/event_view_volto.png

19.3. Hiding fields from certain users#

Note

This chapter is about displaying, not editing. So setting values is not the topic here.

The problem now appears that speakers submitting their talks should not be able to set a time and day for their talks.

Sadly it is not easy to modify permissions of fields provided by behaviors unless you write the behavior yourself. At least in this case we can take the easy way out since the field does not contain secret information: We can simply hide the fields from contributors using CSS and show them for reviewers.

Warning

This trick does not yet work in Volto because some css-classes are still missing from the body-tag (see plone/volto#1189). Skip ahead!

Modify frontend/theme/extras/custom.overrides and add:

/* Hide date fields from contributors */
body.userrole-contributor {
  #default-start.field,
  #default-end.field,
  #default-whole_day.field,
  #default-open_end.field {
    display: none;
  }
}

body.userrole-reviewer {
  #default-start.field,
  #default-end.field,
  #default-whole_day.field,
  #default-open_end.field {
    display: block;
  }
}

Exercise#

Find out where the event behavior is defined and which fields it offers.

Solution

The name you used to enable the behavior Talk.xml is registered in zcml. So name="plone.eventbasic" should be easy to find. You will find it in backend/packages/plone/app/event/dx/configure.zcml and it points to IEventBasic in packages/plone.app.event/plone/app/event/dx/behaviors.py

class IEventBasic(model.Schema, IDXEvent):

    """ Basic event schema.
    """
    start = schema.Datetime(
        title=_(
            u'label_event_start',
            default=u'Event Starts'
        ),
        description=_(
            u'help_event_start',
            default=u'Date and Time, when the event begins.'
        ),
        required=True,
        defaultFactory=default_start
    )
    directives.widget(
        'start',
        DatetimeFieldWidget,
        default_timezone=default_timezone,
        klass=u'event_start'
    )

    end = schema.Datetime(
        title=_(
            u'label_event_end',
            default=u'Event Ends'
        ),
        description=_(
            u'help_event_end',
            default=u'Date and Time, when the event ends.'
        ),
        required=True,
        defaultFactory=default_end
    )
    directives.widget(
        'end',
        DatetimeFieldWidget,
        default_timezone=default_timezone,
        klass=u'event_end'
    )

    whole_day = schema.Bool(
        title=_(
            u'label_event_whole_day',
            default=u'Whole Day'
        ),
        description=_(
            u'help_event_whole_day',
            default=u'Event lasts whole day.'
        ),
        required=False,
        default=False
    )
    directives.widget(
        'whole_day',
        SingleCheckBoxFieldWidget,
        klass=u'event_whole_day'
    )

    open_end = schema.Bool(
        title=_(
            u'label_event_open_end',
            default=u'Open End'
        ),
        description=_(
            u'help_event_open_end',
            default=u"This event is open ended."
        ),
        required=False,
        default=False
    )
    directives.widget(
        'open_end',
        SingleCheckBoxFieldWidget,
        klass=u'event_open_end'
    )

Note how it uses defaultFactory to set an initial value.

19.4. Summary#

  • You applied an existing behavior to a content type to add new fields

  • You benefited of an existing Volto component to display the date

  • You did not have to write your own datetime fields and indexers o/