In an interface for a form, why is my defaultFactory function being called in BrowserView?

I have a function used for defaultFactory parameter of a schema.Float field (from zope.schema). The field is supposed to be read only, so I set the readonly parameter to True. Also, the field is part of an interface for a behavior.

In my class, I have:

from zope.interface import alsoProvides, Interface, provider
from zope.schema.interfaces import IContextAwareDefaultFactory, IContextSourceBinder

from plone.autoform.interfaces import IFormFieldProvider

@provider(IContextAwareDefaultFactory)
def getExchangeRate(context):
    """Request is made to an api that provides an exchange rate for a specific day"""
    .....
    return exchangeRate

@provider(IContextSourceBinder)
class IMyBehavior(Interface):
    value = schema.Float(title=_(u"Value"),
                                default=0.00,
                                required=True,
              )

    currency = schema.Choice(title_(u"Currency",
                                            source=currencies,
                                            required=True,
                                      )

    exchange_rate = schema.Float(title=_(u"Exchange Rate"),
                                             required=True,
                                             readonly=True,
                                             defaultFactory=getExchangeRate,
                            )

alsoProvides(IMyInterface, IFormFieldProvider)

I have a content type that implements this behavior and there is a property that makes use of this function:

class IFundingSource(Interface):
....

class FundingSource(Container):
    implements(IFundingSource)

    @property
    def us_dollar_value(self):
        amount = self.value * self.exchange_rate
        return amount

In my template for viewing the object using the property us_dollar_value:
...
<div tal:content="context/us_dollar_value"></div>

I guess I can understand it being called in an edit form? But when viewing a BrowserView, the defaultFactory function is called. Why is this? Am I better off using an event that sets the field's value upon creation?

My version of Plone is 4.3

Is there any reason why you store this field?

Unless I misunderstand things: Why don't you store the exchange rate somewhere (I would have stored it in the registry (and have a control panel in case you need to manually change it).

Then you will not have to update the exchange rate 'for every item'.

then you can probably do something like

amount = (plone.api.portal.get_registry_record(name=Something, interface=my.Interface)) * self.value

MonetaryType was a bad name for my example of what I was trying to do. I replaced it with FundingSource.

I forgot to include a field in the behavior, 'currency', which is used to store the currency type assigned to a funding source.

A FundingSource is assigned a currency (yen, pound, etc) based on what the user selects, but I need to convert the value entered of the funding source to USD.
Unfortunately, exchange rates aren't static. So we're storing the exchange rate between USD and the selected currency for the date the funding source was created.

Here's how I format the url I use to call the api (api.fixer.io):

currency = getattr(self, 'currency', 'USD')
date_of_creation = datetime.now().strftime('%Y-%m-%d')
api_uri = "http://api.fixer.io/{}?base={}&symbols={}".format(date_of_creation, currency, 'USD')

In the getExchangeRate function, I make this url and then make a request to get the exchange rate.
Since I don't want to abuse the api, I want to only use it when the funding source is created. I thought maybe making the exchange rate field readonly with defaultFactory set would've been a solution.

Edit: Actually, this probably a bad approach. In 'Edit': If they change the currency, I have to recalculate the exchange rate.

But still, I am curious why defaultFactory was firing when I wasn't even in a form.

I think the problem may be that you're misunderstanding how a behavior works: your content type is adapted by the behavior and the behavior must be applied to it, manually or in the FundingSource.xml file on the profile. you must not try to access the field if your content type doesn't provide the behavior.

so, my question is why don't you add those fields directly to the IFundingSource schema?

1 Like

You might have hit a weakness (likely in zope.schema): one important approach in software development is to reduce the number of cases through transformation of a set of related cases to a single one and then handle only this one afterwards. With zope.schema you have two ways to specify the default: giving a default value or a default value factory. In the latter case, the value is computed based on some context. As soon as the context is known, the factory can be used to compute the value and after that handle only a single way to specify the default -- a direct value. However, this could cause the computation of a default value, even if no such value is required.

This should only be an issue if the factory call is very expensive (which is quite unlikely). Otherwise, do not worry about an "API abuse" in your case - your views are likely not activated sufficiently often to worry about a redundant call of a mostly inexpensive default factory.

2 Likes

I had a similar problem, it looks like the field never store a value so every time your view needs the value it call the defaultFactory.

This will be ok if you test for context in the factory

if context is None:
    return  some_value
return exchangeRate

This is because when you create an object applyChanges in z3c.form.form is called, here the value is updated only if the data is different from the stored value. In this case the data we want to save is the default value. To get the current field value it calls the defaultFactory. When this two values are compared they are equal so no value is saved.
It turned out that when applyChanges calls defaultFactory context is None.

2 Likes

I see, thank you for the explanation.

And unfortunately, I discovered that the api was being called a lot (the api only accepts so many requests per period of time) and that's why I was trying to figure out how to set it once.

Thank you for the suggestion and explanation.

i hit the same issue. You are trying to access the value with self.value inside the behavior this should be self.context.value instead ;). The defaultFactory only gets called more often when you don't have a value set. Which should be the case after saving it the first time if the property is working corretly.