Use pycountry in a vocabulary

Only for documentation:

Example: a list with sorted label of localized countries

# init.py
import gettext
import pycountry

german = gettext.translation(
    "iso3166",
    pycountry.LOCALES_DIR,
    languages=["de"])
german.install()
german_countries = german.gettext
# vocabulary.py
from your.addon import german_countries
from zope.interface import provider
from zope.schema.interfaces import IVocabularyFactory
from zope.schema.vocabulary import SimpleTerm
from zope.schema.vocabulary import SimpleVocabulary
import pycountry

def countries_factory(context=None):
    countries = [country for country in pycountry.countries]
    countries.sort(key=lambda country: german_countries(country.name))
    terms = [
        SimpleTerm(
            value=country.name,
            token=country.numeric,
            title=german_countries(country.name),
        )
        for country in countries
    ]
    return SimpleVocabulary(terms)


@provider(IVocabularyFactory)
def countries(context):
    return countries_factory(context)

<!-- vocabulary.zcml -->
<configure xmlns="http://namespaces.zope.org/zope"
  xmlns:five="http://namespaces.zope.org/five"
  xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
  i18n_domain="your.addon">

  <utility component=".vocabularies.countries"
    name="your.addon.vocabulary.countries" />

</configure>
# interfaces.py
class IMyCT(model.Schema):
    """Marker interface and Dexterity Python Schema for CT"""

    country = schema.Choice(
        title="Land",
        required=False,
        source="your.addon.vocabulary.countries",
    )

Thanks for the reminder to degrok my country vocabulary :wink:

class CountryVocabulary(object):
    grok.implements(IVocabularyFactory)
    def __call__(self, context):
        terms = [
            SimpleTerm(value=country.alpha3, token=country.alpha3, title=country.name)
            for country in pycountry.countries]
        return SimpleVocabulary(terms)

and slightly different implementation

def get_country_name(country_code, language):
    """ The _(country_name) was somehow returned untranslated (using the 'message' domain)
    so we define country_trans locally
    https://docs.python.org/2/library/gettext.html
    https://pypi.python.org/pypi/pycountry
    https://docs.plone.org/develop/plone/i18n/internationalisation.html
    """
    country_name = pycountry.countries.get(alpha3=country_code).name

    # country_name is in english and there is no iso3166.mo in the en locales folder
    if language != 'en':
        country_trans = gettext.translation('iso3166', pycountry.LOCALES_DIR, languages=[language])
        return country_trans.gettext(country_name)
    else:
        return country_name

There is also https://pypi.org/project/gocept.country/ which wraps pycountry into sources.

2 Likes

@icemac thanks, that looks really nice.

In my Plone6 site, I have an Invoice content type with an IInvoice schema. According to the gocept.country readme, I should use a field like:

# gocept.country.countries is a FactoredSource object
shipping_address_country = schema.Choice(
    title = _(
        'country_title',
        default = u'Country',
    ),
    source=gocept.country.countries,
    required=False,
)

Which gets me the following traceback. What am I missing here?

2023-03-16 16:49:10,891 ERROR   [Zope.SiteErrorLog:17][waitress-0] ComponentLookupError: http://localhost:8888/Plone/invoices/301554/@@view
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 Products.PDBDebugMode.wsgi_runcall, line 60, in pdb_runcall
  Module pnz.erpediem.core.browser.invoice, line 50, in __call__
  Module plone.autoform.view, line 40, in __call__
  Module plone.autoform.view, line 62, in _update
  Module z3c.form.group, line 52, in update
  Module z3c.form.group, line 48, in updateWidgets
  Module z3c.form.field, line 274, in update
  Module z3c.form.browser.select, line 51, in update
  Module z3c.form.browser.widget, line 171, in update
  Module z3c.form.widget, line 233, in update
  Module z3c.form.widget, line 225, in updateTerms
  Module zope.component._api, line 102, in getMultiAdapter
  Module zope.component._api, line 116, in queryMultiAdapter
  Module zope.interface.registry, line 364, in queryMultiAdapter
  Module zope.interface.adapter, line 844, in queryMultiAdapter
  Module z3c.form.term, line 110, in ChoiceTerms
  Module zope.component._api, line 116, in queryMultiAdapter
  Module zope.interface.registry, line 364, in queryMultiAdapter
  Module zope.interface.adapter, line 844, in queryMultiAdapter
  Module z3c.form.term, line 64, in __init__
  Module zope.component._api, line 104, in getMultiAdapter
zope.interface.interfaces.ComponentLookupError: ((<zc.sourcefactory.source.FactoredSource object at 0x7f2beb415a30>, <WSGIRequest, URL=http://localhost:8888/Plone/invoices/301554/@@view>), <InterfaceClass zope.browser.interfaces.ITerms>, '')
[24] > /usr/local/Plone6/lib/python3.8/site-packages/zope/component/_api.py(104)getMultiAdapter()
-> raise ComponentLookupError(objects, interface, name)
(Pdb++) 

My view is

@implementer(IDexterityContent)
class View(DefaultView):
    """ Default view for Invoice content
    """

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

    def __call__(self, content_type=None, *args, **kw):
        context = self.context
        request = self.request
        
        return super(View, self).__call__()

    def getContent(self):
        obj = self.context.getObject()
        data = {'id' : self.context.getId()}
        for field_id in schema.getFields(IInvoice):
            
            data[field_id] = getattr(obj, field_id)
            #log.info("%s - %s" % (field_id, data[field_id]))
        return data

@mtrebron The sources provided by gocept.country use zc.sourcefactory under the hood. Did you load the ZCML files of zc.sourcefactory?

@icemac thanks for that hint, I am now loading the zc.sourcefactory zcml for Zope2 and am getting a bit further:

2023-03-17 12:53:32,761 ERROR   [Zope.SiteErrorLog:17][waitress-0] AttributeError: http://localhost:8888/Plone/invoices/301554/@@view
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 Products.PDBDebugMode.wsgi_runcall, line 60, in pdb_runcall
  Module pnz.erpediem.core.browser.invoice, line 48, in __call__
  Module plone.autoform.view, line 40, in __call__
  Module plone.autoform.view, line 62, in _update
  Module z3c.form.group, line 52, in update
  Module z3c.form.group, line 48, in updateWidgets
  Module z3c.form.field, line 274, in update
  Module z3c.form.browser.select, line 51, in update
  Module z3c.form.browser.widget, line 171, in update
  Module z3c.form.widget, line 234, in update
  Module Products.CMFPlone.patches.z3c_form, line 46, in _wrapped
  Module z3c.form.widget, line 132, in update
  Module z3c.form.converter, line 295, in toWidgetValue
  Module z3c.form.term, line 167, in getTerm
  Module z3c.form.term, line 70, in getTerm
  Module z3c.form.term, line 35, in getTerm
  Module zc.sourcefactory.browser.source, line 38, in getTerm
  Module gocept.country.sources, line 24, in getTitle
AttributeError: 'str' object has no attribute 'name'
[21] > /usr/local/Plone6/lib/python3.8/site-packages/gocept/country/sources.py(24)getTitle()
-> return value.name
(Pdb++) value
'DE'

The value stored on your model object has to be a gocept.country.db.Country object. It seems that you are storing a str instead. If you have already existing data you either have to migrate it or create a new source class which yields the country codes from its getValues method and is able to handle the country code in getTitle by creating an instance of gocept.country.db.Country from the country code and then returning the name attribute of this instance. getToken should return the value unchanged.

Untested implementation of the suggested source class, see gocept.country/sources.py at main · gocept/gocept.country · GitHub for the original implementation:

class StrCountrySource(BasicSource):

    def getValues(self):
        for country in pycountry.countries:
            if country in self:
                yield country.alpha_2

    def getTitle(self, value):
        return gocept.country.db.Country(value).name

    def getToken(self, value):
        return value

The reason why a gocept.country.db.Country is expected to be stored is, that getTitle and getToken are called more than once on each country in the list of countries, so it is more performant to access the values from an instance than creating this instance multiple times.

Aha, understood! Thanks. I wondered if I had to create my own converter: we're creating Plone content representations from data stored in a relational database.