[SOLVED] Need help with indexing a field of a subform

Is there a way to index a field from a subform?

My aim is to have a user list their educational background, which should be indexed and searchable, in order to find the user by their school(s) when using the Plone's global search box.

I'm using collective.z3cform.datagridfield for the subform.

The snippets below works for rendering and saving the data but it's not showing up in the index.

# Subform
class IEducation(model.Schema):

    dexteritytextindexer.searchable('school')

    school = schema.TextLine(
        title=_(u'School'),
        required=True,
    )
    directives.widget(
        'school',
        AjaxSelectFieldWidget,
        pattern_options={
            "maximumSelectionSize": 1
        },
        vocabulary="my.package.vocabularies.schools"
    )

    from_date = schema.Date(
        title=_(u'From'),
        required=True
    )

    to_date = schema.Date(
        title=_(u'To'),
        required=True
    )
# Parent form
class IMember(model.Schema):

    education = schema.List(
        title=_(u'Education'),
        value_type=DictRow(title=u"education", schema=IEducation),
        required=False
    )
    directives.widget(
        'education',
        BlockDataGridFieldFactory
    )

Reference the index point in the `indexers.py``

@indexer(IEducation)
def school(obj):
    return obj.school

Set the adapter for the school index

  <adapter factory=".indexers.school" name="school" />

Create a dynamic vocabulary for the school index

@provider(IContextSourceBinder)
def available_schools(context):
    catalog = api.portal.get_tool('portal_catalog')
    schools = catalog.uniqueValuesFor('school')
    return SimpleVocabulary([
        SimpleTerm(value=school, title=school) for school in schools
    ])

Register the utility for the dynamic school vocabulary

  <utility
      provides="zope.schema.interfaces.IVocabularyFactory"
      component=".vocabularies.available_schools"
      name="my.package.vocabularies.schools"
      />
1 Like

Again: this is not something that I have done …

That said, I am pretty sure that the 'school field' is actually saved inside the education field, so you need to make an indexer that indexes 'school inside education'.

Probably the 'education' field looks something like this:

[{'school': u'Something', fromdate: 'y', todate: 'yy'}, 
 {'school': u'SNother', fromdate: 'xy', todate: 'xyxy'}]

Ok... In this case, which index type would best be used? Currently, I'm using KeywordIndex. However, I was wondering if FieldIndex would be better.

Again: this is not something I am certain about, but I would use KeywordIndex.

Somehow you have to make an indexer that indexes just the school (not other fields).

If you go to the ZMI and /portal_catalog and add a new index TTW and choose school 'attribute' (and reindex the catalog), does that show the schools ?

If they do, you can just export the settings from /portal_setup and add them to your /profiles/default folder (and reinstall the profile)

First of all, your indexer is called school but the adapter is pointing to field_of_study.

Second, your indexer is registered for the IEducation interface, but I'm not sure if/when this will get called. When in doubt, put a pdb.set_trace() in it and try to reindex or save an object to see whether it gets hit.
Instead, you might want to register the indexer for IMember, and then return object.education.schol, or something like that. Again, use pdb to figure out the correct value you want to return.

Lastly, KeywordIndex is for multi-valued fields, such as lists or dictionaries. In your case, if you want to store multiple schools (IEducation objects) for each member, use KeywordIndex. If each IMember only can choose one school, use FieldIndex.

1 Like

Sorry, I copied the wrong adapter..... I updated the snippet.

It's working now. I changed the indexer to point to IMember and then build a list from the list of dictionary

@indexer(IMember)
def school(obj):
    if not obj.education:
        return []
    return [
        edu['school'] for edu in obj.education
    ]
1 Like

Thank you @fulv, @pigeonflight and @espenmn

:wave:t6::raised_hands:t6: great!

Quick Update

Even though I got the school DataGridField field inside of the subform to index, searching for the school wasn't working.

Solution for searching for the subform field

I Added a custom SearchableTextExtender based on collective.dexteritytextindexer documentation.

from collective import dexteritytextindexer
from zope.component import adapts
from zope.interface import implements


class JaDMemberSearchableTextExtender(object):
    adapts(IMember)
    implements(dexteritytextindexer.IDynamicTextIndexExtender)

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

    def __call__(self):
        """Extend the searchable text with a custom string"""
        return " ".join([edu['school'] for edu in self.context.education])

ZCML:

  <adapter factory=".indexers.MemberSearchableTextExtender" name="IMember" />

I think I could have used collective.dexteritytextindexer.IDexterityTextIndexFieldConverter based on the documentation to convert the DataGridField to searchable text, but it took more than 5 minutes to understand, therefore, I used dexteritytextindexer.IDynamicTextIndexExtender instead.

Hey @b4oshany
This may be worth adding to the docs as a recipe/example.
Especially since there will be a new release of the docs next week