Plone Vocabulary works fine in Plone Classic but is empty in Volto

Good afternoon everyone,

I am in the process of preparing a Plone addon to work in the Volto frontend. Most of the features of the addon work except when I am using the following python code for the custom vocabulary listing:

# -*- coding: utf-8 -*-
from plone import api
from zope.interface import implementer
from zope.schema.interfaces import IVocabularyFactory
from zope.schema.vocabulary import SimpleTerm
from zope.schema.vocabulary import SimpleVocabulary


@implementer(IVocabularyFactory)
class AvailableUnitGoals(object):
    """
    """

    def __call__(self, context):
        results = []
        try:
            get_container_unit = '/'.join(context.getPhysicalPath()[:-1])
            unit = api.content.get(path=get_container_unit).define_unit
            brains = api.content.find(
                portal_type='unit_goal',
                sort_on='sortable_title',
                unit=unit
            )
            brains_unique = []
            for brain in brains:
                if brain.unit_goal not in brains_unique:
                    brains_unique.append(brain.unit_goal)
            for brain in brains_unique:
                results.append(
                    SimpleTerm(
                        value=brain,
                        title=brain,
                    )
                )
        except:
            pass

        # Create a SimpleVocabulary from the terms list and return it:
        return SimpleVocabulary(results)


AvailableUnitGoalsFactory = AvailableUnitGoals()

Again, it works fine with no issues in Plone Classic, but fails in Volto and shows up empty. The same response shows in Postman. Was there something that changed in Plone 6 that was once available in Plone 5.2 or earlier regarding Plone Dynamic Vocabularies?

This is the error that is produced when I comment out the try and except:

2023-04-11 13:14:03,101 ERROR   [Zope.SiteErrorLog:35][waitress-0] AttributeError: http://localhost:3000/@vocabularies/yc.assessment.AvailableUnitGoals
Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 181, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 390, in publish_module
  Module ZPublisher.WSGIPublisher, line 285, in publish
  Module ZPublisher.mapply, line 85, in mapply
  Module ZPublisher.WSGIPublisher, line 68, in call_object
  Module plone.rest.service, line 22, in __call__
  Module plone.restapi.services, line 19, in render
  Module plone.restapi.services.vocabularies.get, line 89, in reply
  Module yc.assessment.vocabularies.available_unit_goals, line 18, in __call__
AttributeError: 'NoneType' object has no attribute 'define_unit'

Thanks in advance!
rbrown12

What does line 18 say?

@jaroel , Line 18 is

unit = api.content.get(path=get_container_unit).define_unit

This returns the unit as defined and selected in the content type. It is a drop down menu. It was supposed to show the unit goals which were manually submitted in a previous content type called 'unit_goal'.

What does AttributeError: 'NoneType' object has no attribute 'define_unit' mean?

Probably, the line:

get_container_unit = '/'.join(context.getPhysicalPath()[:-1])

or

api.content.get(path=get_container_unit)

returns nothing

So, for some reason, path is not the same in both, so check what context/ context.getPhysicalPath is

PS: I am not sure if using the path to find the object is the best approach, if you have to use get, cant you just user api.content.get(UID=context.UID) or context.aq_parent.UID ?

@espenmn , this is what I saw in the console depending on the location of the objects in the site:

context by itself returns <UnitPlanProgram at b2019-2020-accounting-bs-assessment-plan>
context.getPhysicalPath returns ('', 'Plone', 'accounting-bs-program-assessment', 'b2019-2020-accounting-bs-assessment-plan')
context by itself returns <PloneSite at Plone>
context.getPhysicalPath returns ('', 'Plone')

I used the following custom code in Python:

base_context = context
print('context by itself returns', base_context)
get_physical_path = context.getPhysicalPath()
print('context.getPhysicalPath returns', get_physical_path)

It means that 'None' does not have 'define_unit'.

For example, if I had a search that found 'first folder'

first_folder = 'a search for first folder'

And then did

 first_folder.Description()

The result is 'Description of my folder'

If there are no folder in the site, the search will return nothing and

first_folder.Description())

would error with 'has no Description'

So here, the search,

api.content.get(path=get_container_unit)

returns nothing

@jaroel and @espenmn , I made changes to the code using the above suggestions:

# -*- coding: utf-8 -*-
from plone import api
from zope.interface import implementer
from zope.schema.interfaces import IVocabularyFactory
from zope.schema.vocabulary import SimpleTerm
from zope.schema.vocabulary import SimpleVocabulary


@implementer(IVocabularyFactory)
class AvailableUnitGoals(object):
    """
    """

    def __call__(self, context):
        results = []
        # get_container_unit = '/'.join(context.getPhysicalPath()[:-1])
        # unit = api.content.get(path=get_container_unit).define_unit
        unit = api.content.get(UID=context.aq_parent.UID()).define_unit
        brains = api.content.find(
            portal_type='unit_goal',
            sort_on='sortable_title',
            unit=unit
        )
        brains_unique = []
        for brain in brains:
            if brain.unit_goal not in brains_unique:
                brains_unique.append(brain.unit_goal)
        for brain in brains_unique:
            results.append(
                SimpleTerm(
                    value=brain,
                    title=brain,
                )
            )

        # Create a SimpleVocabulary from the terms list and return it:
        return SimpleVocabulary(results)


AvailableUnitGoalsFactory = AvailableUnitGoals()

However, this is the error that was displayed in the console below:

2023-04-12 11:29:11,480 ERROR   [Zope.SiteErrorLog:35][waitress-3] AttributeError: http://localhost:3000/@vocabularies/yc.assessment.AvailableUnitGoals
Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 181, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 390, in publish_module
  Module ZPublisher.WSGIPublisher, line 285, in publish
  Module ZPublisher.mapply, line 85, in mapply
  Module ZPublisher.WSGIPublisher, line 68, in call_object
  Module plone.rest.service, line 22, in __call__
  Module plone.restapi.services, line 19, in render
  Module plone.restapi.services.vocabularies.get, line 89, in reply
  Module yc.assessment.vocabularies.available_unit_goals, line 18, in __call__
AttributeError: define_unit

Isn't unit = api.content.get(UID=context.aq_parent.UID()).define_unit and unit = context.aq_parent.define_unit the same thing?

Yes, they are the same thing. Just to clarify, I was getting results in the Plone Classic, and in the plone restapi through Postman within the content types. It is when i go to view the vocabulary through the plone restapi using Postman where the results are blank. Any suggestions? Thanks.

I understand; I'm trying to get you to give use debug info.

Do plone.api.portal.get() and plone.api.user.get_current() inside the __call__ do anything?
And I assume AvailableUnitGoalsFactory is register as the vocab, not AvailableUnitGoals right?

Can you explain exactly how the add-on is supposed to work.
There are some strange things in the code (see above). You probably dont want to add 'the brain', but the uid or something of the object instead.

@jaroel,

Using the following code:

portal = api.portal.get()
            print('portal', portal)
            get_current = api.user.get_current()
            print('get_current', get_current)

This is what is returned in the console:

portal <PloneSite at Plone>
get_current admin

Yes, AvailableUnitGoalsFactory is registered as the vocab.

@espenmn , this addon is designed to facilitate the process of assessing different units and departments within an institution.

Ok, cool, that seems sensible. I do notice the user admin. You might run into issues if that's a Zope user instead of a Plone user (which acl_users it came from.) - I've see many weird issues over the years where the only difference was Zope admin account vs Plone user with Manager role.

I'd recommend you to use a debugger in the __call__ and see if you can get any catalog results at all.

Something like

import pdb; pdb.set_trace()
# these next two lines should return at least one brain each.
api.content.find(portal_type='unit_goal')
api.portal.get_tool(name='portal_catalog').unrestrictedSearchResults(portal_type='unit_goal')

Also, please show what context.aq_parent.define_unit is. And what context.aq_parent.UID() is.

You can also use PDB to "step into" the call to api.content.get so you can see where in the code we cannot find the content object by its .UID().

Also, where you said "Plone Classic" did you mean your current production setup and by "Volto" and "Postman" did you mean your newly-in-development-setup? ie are those two different codebases and database?
Please rebuild all catalogs you can find if so.

@jaroel ,
This is the code I used:

# -*- coding: utf-8 -*-
from plone import api
from zope.interface import implementer
from zope.schema.interfaces import IVocabularyFactory
from zope.schema.vocabulary import SimpleTerm
from zope.schema.vocabulary import SimpleVocabulary
import pdb

pdb.set_trace()


@implementer(IVocabularyFactory)
class AvailableUnitGoals(object):
    """
    """

    def __call__(self, context):
        results = []
        try:
            # get_container_unit = '/'.join(context.getPhysicalPath()[:-1])
            # unit = api.content.get(path=get_container_unit).define_unit
            unit = api.content.get(UID=context.UID()).define_unit
            # print('unit', unit)
            portal = api.portal.get()
            print('portal', portal)
            get_current = api.user.get_current()
            print('get_current', get_current)
            unit_goal1 = api.content.find(portal_type='unit_goal')
            print('unit_goal (api.content.find)', unit_goal1)
            unit_goal2 = api.portal.get_tool(name='portal_catalog').unrestrictedSearchResults(portal_type='unit_goal')
            print('unit_goal (api.portal.get_tool)', unit_goal2)
            define_unit = context.aq_parent.define_unit
            print('define_unit', define_unit)
            context_aq_parent = context.aq_parent.UID()
            print('context_aq_parent', context_aq_parent)
            brains = api.content.find(
                portal_type='unit_goal',
                sort_on='sortable_title',
                unit=unit
            )
            brains_unique = []
            for brain in brains:
                goal = brain.getObject()
                # print('goal', str(goal.unit_goal))
                if brain.unit_goal not in brains_unique:
                    brains_unique.append(str(goal.unit_goal))
            for brain in brains_unique:
                results.append(
                    SimpleTerm(
                        value=brain,
                        title=str(brain),
                    )
                )
        except:
            pass

        # Create a SimpleVocabulary from the terms list and return it:
        return SimpleVocabulary(results)


AvailableUnitGoalsFactory = AvailableUnitGoals()

After using pdb debugger I got this output:

(Pdb) api.portal.get_tool(name='portal_catalog').unrestrictedSearchResults(portal_type='unit_goal')
[<Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c39cfc0>, <Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c39c480>, <Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c39cdc0>, <Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c3b0880>]
(Pdb) api.content.find(portal_type='unit_goal')
[<Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c2a50c0>, <Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c39cdc0>, <Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c3b0f80>, <Products.ZCatalog.Catalog.Catalog.useBrains.<locals>.mybrains object at 0x10c3b0d00>]
(Pdb) context.aq_parent.define_unit
'Academic Advisement Center'
(Pdb) context.aq_parent.UID()
'b453cb7bc2584343b6a4293fc3ef0521'

@jaroel , In addition, I am using Plone 6.0.3 and Volto 16.19.0 in foreground mode or developer mode.

would be nice with a better explanation.
Excactly what should the vocabulary do / be used for.

For example: 1) Should the vocabulary show all 'something' on the site, or just in one section
2) The 'brain' part of your code is strange to me, the 'brain object' is 'the indexed part of a content item'. you dont want that for 'value'.

Could you replace unit = api.content.get(UID=context.UID()).define_unit with unit = context.aq_parent.define_unit and see if you get anything out of api.content.find(unit=unit) and api.content.find(portal_type='unit_goal', unit=unit)?
The .find should return at least some brains.

I have run out of simple things for you to try. Maybe someone else has some ideas?

@espenmn, 1) The vocabulary mentioned in this post is meant to show the unit goals entered in the site, depending on the parent content type being used. The two parent content types are 'assessment_unit' and 'assessment_unit_program'. In other words, this vocabulary only shows the goals entered through the parent content type (e.g. assessment_unit, assessment_unit_program). It works fine in the Plone Classic (non-Volto) frontend. However, it is when I go to serialize the content in the Volto frontend where I am unable to get anything for the mentioned goals.