Use case for a listing component

Hello all!

The institution I work for is studying the possibility of migrating to Plone + Volto and I was given the task of testing and implementing a few components.

Between them, there's this sort of "Featured" list of News Items. The default listing block is not what I need, though. I've been trying to implement it using the Training course as basis, but I've hit a wall.

So basically, the component's behavior is as follows:

  1. The user should be able of selecting 4 News Items that will be exhibited as a list of links on the front-page;
  2. One of the items should be highlighted with a featured image and bigger text;
  3. The user should be able to choose in which position each list item will be placed;
  4. The publication of this component can be scheduled;
  5. The publication of the News Item isn't directly related to it being shown or not (it's possible for an item from the list to appear again in the featured list after a few days).

This is roughly how it should look like on the front-page:

I know this could be done using the "featured" option that's described in the Training, but its behavior isn't ideal.
Since there's a highlighted item, ordering and scheduling, that would generate a lot of confusion for the user, that would be forced to keep track of which items were already chosen and would need to check them individually constantly to see what was chosen. It would be much better if this could all be assembled in one place. Besides, it's what they're used to in their current CMS.

Speaking about the user interface, I think it'd make more sense if it was possible to do the selection in a sort of control panel instead of creating a new content type, because it would generate a lot of useless pages, and this feature will be used daily, sometimes more than once. I'd like to know if that's possible, since I haven't found anything about how to fetch data from a control panel in Volto yet.

What I managed to do so far:

  1. I created a new vocabulary that lets the user select only the News Items. I tested the relationship field, but since I found no way to filter it to show only News Items, I gave up on it;
  2. I used the searchContent() function from actions/search/search as taught by the Training to fetch my "Featured" content type. I limited the search to the last published "Featured" item.

I was able to get the News Items from the vocabulary field like I intended, but I could only fetch the "token" and "title" fields. I could not get the URL because I have no idea how to get it from a token.

I tried using the searchContent() function again to fetch each News Item by their token and the full object, but it fires an exception.

Except for the URL issue, it works somewhat like I wanted, but after I tested the schedule I found out there was an error occurring for anonymous users. I've no idea why this is happening yet, but I think it's trying to get the last "Featured Item" even though the user is not logged in and it's not the correct time to show it yet, so it's returning a null value instead of the last published item that the guest is allowed to see.

So this is basically my use case. I don't know much about Plone or Volto, so I'd appreciate any suggestions and thoughts about the best way of implementing this.

Hi Ana!

  1. The user should be able of selecting 4 News Items that will be exhibited as a list of links on the front-page;

There's nothing quite like your mockup already built. There is something pretty close, though: https://github.com/kitconcept/volto-blocks-grid In particular, the "Teaser" aspect of that addon, which can be used to link to already existing content.

I would suggest, though, to mold your requirements around the Listing block, which allows you to create and register new "templates" for how it would look like. For your frontpage, you could instantiate a listing block, set the filters to News items, set sorting to "Effective date" reversed and whenever you publish the new news item, it would become the first in the list. Writing a custom listing block variation is pretty straight forward. By default Volto ships already with several listing block variations, so you can take one as example: https://github.com/plone/volto/blob/master/src/config/Blocks.jsx#L244 and https://github.com/plone/volto/blob/master/src/components/manage/Blocks/Listing/SummaryTemplate.jsx

Hi Tiberiuichim!
Thanks for the answer :slight_smile:

The listing block is pretty nice, but I don't think it quite fits my use case, which requires the manual selection of a few News Items. Unless I'm missing something, the listing block only works with filters that will retrieve things automatically. I need something like a relationship field, which lets the user choose the items individually, but restricted to the News Items.

Even if the listing block allowed manual selection, since my users aren't super tech-savvy, I wouldn't want them to handle the listing block directly because they could end up messing up the configuration and displaying random stuff on the front-page instead of the News Items.

It would be nice to be able to take a look at your code.

I have an example on how to create a relations field that uses a catalog query for its data source. The vocabulary is defined like this:

and registered like this:

and the field that uses it looks like this:

Caveat, I don't know how well this would work on Volto, though, I've never tried it.

Ideally you would have something like this (and I hope I properly understood your requirements):

  • A Volto block that allows you to pick news items via a special widget. The Relations widget would work fine for this. You could implement something more fancy, but at its basics it would work like the relations widget.
  • The "database value" of the block would be a list of UIDs for the news items.
  • You would use the @search endpoint with the UIDs. Just an example that this works, https://biodiversity.europa.eu/search?UID=1fbc40827f2345a0b9a89892ff7e9686. I don't know if you can pass a list of UIDs to the search endpoint, I've tried https://biodiversity.europa.eu/search?UID:list:string=1fbc40827f2345a0b9a89892ff7e9686&UID:list:string=ac59022def9e47e4b1235b8a2c712880 and it didn't work. If you want to avoid multiple API calls, (in a custom Plone backend addon) you would write a new restapi endpoint service (something similar to the @search endpoint that, given a block uid, it would return the metadata for the related news items).

Except for the URL issue, it works somewhat like I wanted, but after I tested the schedule I found out there was an error occurring for anonymous users. I've no idea why this is happening yet, but I think it's trying to get the last "Featured Item" even though the user is not logged in and it's not the correct time to show it yet, so it's returning a null value instead of the last published item that the guest is allowed to see.

If I understand correctly, you have the Newsitem linked in the block, but the user is not able to see the details because that news item shouldn't actually be published yet. You'd have to "fix" what the anonymous user sees as the value for that block using a block serialization transformer. Basically, on the backend, in that transfomer, you'd get the actual news items, inspect them for publishing status and remove them from the response if they're not ready.

1 Like

Hola Carolina,
Hello Caroline,

Podrías añadir el campo de selección "Portada" al tipo de contenido "News Item" con las opciones 1, 2, 3, 4 y No.
You could add the "Cover" selection field to the "News Item" content type with options 1, 2, 3, 4 and No.

Y en la portada añadir 4 búsquedas: la primera para que muestre con imagen la noticia más recientemente publicada o modificada (a tu gusto) que tenga seleccionado 1, la segunda para que muestre sin imagen la noticia más recientemente publicada o modificada que tenga seleccionado 2, etc.
And on the cover add 4 searches: the first to show with an image the most recently published or modified news (to your liking) that you have selected 1, the second to show without an image the most recently published or modified news that you have selected 2 , etc.

La opción por defecto del campo de selección sería "No".
The default option for the selection field would be "No".

Y si deseas que dicho campo sólo se muestre y pueda ser editado por usuarios/as con determinado nivel de permisos, podrías configurarlo en el esquema del tipo de contenido.
And if you want this field to only be displayed and can be edited by users with a certain level of permissions, you could configure it in the content type scheme.

Así lo tengo yo funcionando en un sitio desde hace un año y pico sin ningún problema.
So I have it running on a site for a year or so without any problem.

\ :wink:

Besos
Kisses

1 Like

Gracias, amigo!
Yo soy de Brasil hahaha.
Entendí todo lo que dijiste, pero mi español es muy malo (google me ayudó) :rofl:

Thanks for the suggestion. However, my boss really wants me to try to implement it the most complicated way because unfortunately that's what the people that are gonna use this component are used to.

Ok, so this is the code I used for the vocabulary:

def make_terms(items):
    """ Create zope.schema terms for vocab from tuples """
    terms = [ SimpleTerm(value=pair[0], token=pair[0], title=pair[1]) for pair in items ]
    return terms

@provider(IContextSourceBinder)
def FeaturedNewsVocabularyFactory(context):
    portal_catalog = api.portal.get_tool('portal_catalog')
    brains = portal_catalog.searchResults(portal_type=('News Item'))

    result = [ (brain["UID"], brain["Title"] for brain in brains ]

    terms = make_terms(result)

    return SimpleVocabulary(terms)

This filters the News Items like intended, but I can't get the URL.
Is there no way to pass the URL as a field alongside the token and title when the vocabulary is created?
Because the title and the URL are all I really need for this to work.

I thought about passing it as a "value" in the make_terms function, but I figured that's probably not the best way to do this. Besides, I'm not sure if that could have negative side-effects later on.

So, I tried following the example you provided, but I'm not really sure how it filters things, especially because it's filtering a vocabulary instead of a content type (if I understood correctly):

@implementer(IVocabularyFactory)
class CaseStudiesVocabulary(CatalogVocabularyFactory):

          
    def __call__(self, context, query=None):
        query = query or {}

          
        if 'criteria' not in query:
            query['criteria'] = []

          
        query['criteria'].append(
            {u'i': u'portal_type',
             u'o': u'plone.app.querystring.operation.selection.is',
             u'v': [u'eea.climateadapt.casestudy']}
        )

So I tried this:

@implementer(IVocabularyFactory)
def FeaturedNewsVocabularyFactory(context):
    query = {}

    if 'criteria' not in query:
        query['criteria'] = []

    query['criteria'].append(
        {u'i': u'portal_type',
         u'o': u'plone.app.querystring.operation.selection.is',
         u'v': [u'News Item']}
    )

    parsed = {}

    if query:
        parsed = queryparser.parseFormquery(context, query['criteria'])

        if 'sort_on' in query:
            parsed['sort_on'] = query['sort_on']

        if 'sort_order' in query:
            parsed['sort_order'] = str(query['sort_order'])
    try:
        catalog = getToolByName(context, 'portal_catalog')
    except AttributeError:
        catalog = getToolByName(getSite(), 'portal_catalog')

    brains = catalog(**parsed)

    return CatalogVocabulary.fromItems(brains, context)

But all it did was create a regular relation field (it didn't filter the News Items).

Hm... for some reason the Volto reference widget doesn't read the vocabulary source. I thought it should? I need to look at that at some point.

One option to progress now is to use the @search endpoint to retrieve the metadata info on each UID. Also check how to write block transformers and plone.restapi endpoints, they'll open new doors for you if you want to optimize the way the block behaves.

One way to go is a custom block with a field for selecting news items.

So no backend stuff needed.

  • The block comes with a Sidebar InlineForm with schema.
  • The schema has a list field with a custom widget.
  • The widget is a ObjectListWidget with widgetOptions: { pattern_options: {selectableTypes: ['News Item'] }}

As this is something I'll need too in near future, I've set up an add-on: @rohberg/volto-newsblock - npm

1 Like

While we're on the topic, I've just had a partial similar use case, I had to use a RelationChoice with a catalogue source (filter on content type), in Volto.

My field is define like:

    partner = RelationChoice(
        title=_(u"Partner"),
        vocabulary="partners"
    )

For the vocabulary I've had to do like:

class TitleCatalogVocabulary(CatalogVocabulary):

    @classmethod
    def createTerm(cls, brain, context):
        return SimpleTerm(brain, brain.UID, brain.Title)


@implementer(IVocabularyFactory)
class PartnersVocabulary(CatalogVocabularyFactory):
    def __call__(self, context, query=None):
        query = query or {}

        if 'criteria' not in query:
            query['criteria'] = []

        query['criteria'].append(
            {u'i': u'portal_type',
             u'o': u'plone.app.querystring.operation.selection.is',
             u'v': [u'partner']}
        )

        parsed = {}
        if query:
            parsed = parseQueryString(context, query['criteria'])
            if 'sort_on' in query:
                parsed['sort_on'] = query['sort_on']
            if 'sort_order' in query:
                parsed['sort_order'] = str(query['sort_order'])

        # If no path is specified check if we are in a sub-site and use that
        # as the path root for catalog searches
        if 'path' not in parsed:
            site = getSite()
            nav_root = getNavigationRootObject(context, site)
            site_path = site.getPhysicalPath()
            if nav_root and nav_root.getPhysicalPath() != site_path:
                parsed['path'] = {
                    'query': '/'.join(nav_root.getPhysicalPath()),
                    'depth': -1
                }

        vocab = TitleCatalogVocabulary.fromItems(parsed, context)
        return vocab

That's because the default CatalogVocabulary uses (brain, UID, UID) for the vocabulary items (value, token, title).

Now, my use case is not in Volto blocks field right now, but the DX field serializes nicely:

{
  "@id": "http://localhost:3000/my-first-partner",
  "@type": "partner",
  "description": "",
  "review_state": "private",
  "title": "My first partner"
}

@ksuess I saw that your block stores the news data in the block. So, if a content manager changes the title of news, it won't be reflected in the block. Would it be possible to store only the uid, and do a search, to bring up the current data?

Yes it makes definitly more sense to show the up to date data of the selected news items.

One way to do this is to get the uids of the selected items, make a fetch and use this for rendering.

Same for the Edit and View component of the block. Here a first shot on View:

import React from 'react';
import { Link } from 'react-router-dom';
import { keyBy } from 'lodash';
import { searchContent } from '@plone/volto/actions';
import { useDispatch, useSelector } from 'react-redux';
import { flattenToAppURL } from '@plone/volto/helpers';

import './newsblock.css';

const View = ({ data }) => {
  let uids = data.news_list.items.map(el => el.newsitem[0].UID)
  const newsandevents = useSelector(
    (state) => state.search.subrequests.newsandevents?.items,
  );
  let newsandevents_mapping = keyBy(newsandevents, '@id')
  const dispatch = useDispatch();

  React.useEffect(() => {
    dispatch(
      searchContent(
        '/',
        {
          UID: uids,
        },
        'newsandevents',
      ),
    );
  }, [dispatch]);
  
  return (
    <div className="block newsblock">
      {data.news_list?.items.length > 0
        ? data.news_list?.items.map((item, index) =>
            item.newsitem ? (
              <div key={index}>
                <Link to={flattenToAppURL(item.newsitem[0]?.getURL)}>
                  <img
                    src={flattenToAppURL(
                      item.newsitem[0]?.getURL + `/@@images/image/mini`,
                    )}
                    alt="news"
                  />
                  <div>{newsandevents_mapping[item.newsitem[0]['@id']].title}</div>
                </Link>
              </div>
            ) : null,
          )
        : 'News block without items selected'}
    </div>
  );
};

export default View;

1 Like