Customize the fields of the content types

by Reinout van Rees last modified Dec 30, 2008 05:54 PM
Realestatebroker as-is can never be a "one size fits all" product. So many country-specific needs... This how-to explains how you can customize realestatebroker to your needs: easy!

Realestatebroker has two content types. One for homes and one for businesses.

>>> from collective.realestatebroker.content.residential import Residential
>>> from collective.realestatebroker.content.commercial import Commercial

Interfaces

Both implement interfaces:

>>> from collective.realestatebroker.interfaces import IRealEstateContent
>>> from collective.realestatebroker.interfaces import IResidential
>>> from collective.realestatebroker.interfaces import ICommercial
>>> IRealEstateContent.implementedBy(Residential)
True
>>> IResidential.implementedBy(Residential)
True
>>> IRealEstateContent.implementedBy(Commercial)
True
>>> ICommercial.implementedBy(Commercial)
True

For flash mass-upload we need to implement IUploadingCapable.

>>> from Products.PloneFlashUpload.interfaces import IUploadingCapable
>>> IUploadingCapable.implementedBy(Residential)
True
>>> IUploadingCapable.implementedBy(Commercial)
True

Fields

By nature, a real estate object deals with a sizeable number of fields (address, price details, kind of house, extras, etc.). A large number of them are country-specific. With plone 3.0, there's a fancy way to extend existing schemas (ISchema), which is made even simpler by the schemaextender product. We therefore have the luxury of restricting the number of fields and to suggest integrators to add their own extra fields.

Schemata allow a handy subdivision (especially with plone 3.0's user interface) of fields, so we'll offer a standard set that can be extended by custom fields.

  • Generic data such as address, description, main text.
  • Price information (which has lots of scope for country-specific additions).
  • Measurements.
  • Object details such as kind of building, construction year, heating system, insulation.
  • Environment: garden and outside details like description of the environment.
  • Location (= google maps).

To be future-proof, we're implementing the content types as archetypes with zope3 interface around them. Bit of a double work, but OK for now. Let's test some basic presense of fields:

>>> residential_schema = Residential.schema
>>> 'constructYear' in residential_schema
True
>>> 'garage' in residential_schema
True
>>> 'price' in residential_schema
True
>>> 'parking' in residential_schema # from commercial!
False
>>> 'parking' in Commercial.schema
True

There are a lot of fields, so we're subdividing them into schemata, even though there will probably be custom-made edit forms.

>>> some_fields = residential_schema.getSchemataFields('default')
>>> len(some_fields) > 0
True
>>> some_fields = residential_schema.getSchemataFields('financial')
>>> len(some_fields) > 0
True
>>> some_fields = residential_schema.getSchemataFields('non-existing')
>>> len(some_fields) > 0
False

Schema extension

Real estate objects are very locale-dependent. So it will be necessary to do customization to the fields. archetypes.schemaextender is a great tool for that.

First we ensure that the field isn't available yet.

>>> res = Residential('res')
Traceback (most recent call last):
...
TypeError: ('Could not adapt', <Residential at >, <InterfaceClass Products.Archetypes.interfaces._schema.ISchema>)

Ouch, this is a unittest, so the zcml-loaded ISchema adapter doesn't work yet. We'll have to enable it by hand. Just a default plone will do this for you.

>>> from archetypes.schemaextender.interfaces import IOrderableSchemaExtender
>>> from zope import interface
>>> from Products.Archetypes import atapi
>>> from Products.Archetypes.Schema.factory import instanceSchemaFactory
>>> from zope import component
>>> component.provideAdapter(instanceSchemaFactory)

To prevent that we need to make too much mock objects for this unittest, we'll need to remove the fieldproperties as they require too much wiring.

>>> from Products.Archetypes.atapi import ATFieldProperty
>>> for attr in ['address', 'description', 'zipcode', 'city', 'price', 'house_type', 'rooms', 'text', 'acceptance', 'area', 'floor_area', 'volume', 'construct_year', 'location', 'kind_of_building', 'heating', 'insulation', 'garden', 'kind_of_garden', 'storage', 'kind_of_storage', 'garage', 'kind_of_garage']:
...     delattr(Residential, attr)

So, now we can grab the schema the official way.

>>> res = Residential('res')
>>> schema = res.Schema()
>>> 'price' in schema
True
>>> 'extra_field' in schema
False

Now that the basics are in place, we can now start with the work you'll have to do yourself in your own extensions.

Step 1. archetypes.schemaextender's overrides.zcml isn't applied, so the default ISchema handling is still in place. To fix this (if you use buildout) include an entry archetypes.schemaextender-overrides in your zcml section. For this unittest we'll do the work by hand:

>>> from archetypes.schemaextender.extender import instanceSchemaFactory
>>> component.provideAdapter(instanceSchemaFactory)

Step 2. Create field classes for the fields you want to adapt. Say, a StringField. Make sure you inherit from ExtensionField, first.

>>> from archetypes.schemaextender.field import ExtensionField
>>> class ExtendedStringField(ExtensionField, atapi.StringField):
...     pass

Step 3. Create a schema extender. An extender is a named adapter behind the scenes, so you have to say which interface you adapt. In our case, IResidential and/or ICommercial.

>>> class MyExtender(object):
...     component.adapts(IResidential)
...     interface.implements(IOrderableSchemaExtender)
...     _fields = [ExtendedStringField(
...         'extra_field',
...         storage=atapi.AnnotationStorage(),
...         widget = atapi.StringWidget(label = u'Extra field')
...         ),
...                ]
...     def __init__(self, context):
...         self.context = context
...     def getFields(self):
...         return self._fields
...     def getOrder(self, original):
...         # Possibility to move fields.
...         return original

Step 4. Wire up your extender with some zcml by defining an adapter with your extender as the "factory" and give it some name (so that it is a named adapter). We'll again enable it manually here (it is a unit test):

>>> component.provideAdapter(MyExtender,
...                          name=u"dutch.test")
>>> res = Residential('res')
>>> schema = res.Schema()
>>> 'price' in schema
True
>>> 'extra_field' in schema
True

Influence on templates and pdf export

Unless you're doing real strange things, all templates and also the PDF export ought to work just fine with your additions. Most of the fields are rendered automatically by archetypes and this also works for everything you add with schemaextender.

You can mark a field with selfrendered=True, in that case the field is not included in the automatic rendering. By default, this is "on" for the address and the main body text, for instance. So if you add a field that you want to render yourself, this is the way.

Editor note

Note: this is the content/contenttypes.txt inside the realestatebroker product. So don't edit it directly on plone.org, but in svn.