def lookup(cls, name=None, title=None, for_keywords=False):
        """Lookup by name or title and return a Thesaurus instance.

        This is a factory method that tries to instantiate a Thesaurus object
        from a collection of well-known (mostly related to INSPIRE) vocabularies.
        """

        vocab = None

        if (name is None) and title:
            name = vocabularies.normalize_thesaurus_title(title, for_keywords)

        if name:
            vocab = vocabularies.by_name(name)
        else:
            raise ValueError("Expected a name/title lookup")

        if vocab:
            kwargs = {
                "title": vocab.get("title"),
                "name": vocab.get("name"),
                "reference_date": vocab.get("reference_date"),
                "version": vocab.get("version"),
                "date_type": vocab.get("date_type"),
            }
            return cls(**kwargs)
        else:
            raise ValueError('Cannot find a thesaurus named "%s"' % (name))
Esempio n. 2
0
class IFundingReference(IObject):

    funder_name = zope.schema.TextLine(title=_(u'Funder name'),
                                       description=_(u'funder'),
                                       required=False)

    funder_identifier = zope.schema.TextLine(
        title=_(u'Funder identifier'),
        description=_(u'e.g. https://doi.org/10.13039/100000936'),
        required=False)

    funder_identifier_type = zope.schema.Choice(
        title=_(u'Funder identifier type'),
        vocabulary=vocabularies.by_name('funder-identifier-type').get(
            'vocabulary'),
        #default = 'crossrefFunder',
        description=_(u'funder identifier type'),
        required=False)

    award_number = zope.schema.TextLine(title=_(u'Award number'),
                                        description=_(u'e.g. GBMF3859.01'),
                                        required=False)

    award_uri = zope.schema.TextLine(
        title=_(u'Award uri'),
        description=_(u'e.g. https://www.moore.org/grants/list/GBMF3859.01'),
        required=False)

    award_title = zope.schema.TextLine(title=_(u'Award title'),
                                       description=_(u'award title'),
                                       required=False)
Esempio n. 3
0
class IConformity(IObject):

    title = zope.schema.Text(title=_(u'Specification'), required=True)
    title.setTaggedValue('translatable', True)

    date = zope.schema.Date(title=_(u'Date'), required=True)

    date_type = zope.schema.Choice(
        title=_(u'Date Type'),
        vocabulary=vocabularies.by_name('date-types').get('vocabulary'),
        required=True)

    degree = zope.schema.Choice(
        title=_(u'Degree'),
        vocabulary=vocabularies.by_name('degrees').get('vocabulary'),
        description=
        _(u'This is the degree of conformity of the resource to the implementing rules adopted under Article 7(1) of Directive 2007/2/EC or other specification.'
          ),
        default="not-evaluated",
        required=True)
Esempio n. 4
0
class IRelatedIdentifier(IObject):

    related_identifier = zope.schema.TextLine(
        title=_(u'Related identifier'),
        description=_(u'related identifier'),
        required=False)

    related_identifier_type = zope.schema.Choice(
        title=_(u'Related identifier type'),
        vocabulary=vocabularies.by_name('related-identifier-type').get(
            'vocabulary'),
        #default = 'issn',
        description=_(u'related identifier type'),
        required=False)

    relation_type = zope.schema.Choice(
        title=_(u'Relation type'),
        vocabulary=vocabularies.by_name('relation-type').get('vocabulary'),
        #default = 'IsSupplementTo',
        description=_(u'relation type'),
        required=False)

    related_metadata_scheme = zope.schema.TextLine(
        title=_(u'Related metadata scheme'),
        description=_(u'related metadat scheme'),
        required=False)

    related_metadata_scheme_uri = zope.schema.TextLine(
        title=_(u'Related metadata scheme uri'),
        description=_(u'related metadat scheme uri'),
        required=False)

    related_metadata_scheme_type = zope.schema.TextLine(
        title=_(u'Related metadata scheme type'),
        description=_(u'related metadat scheme type'),
        required=False)
Esempio n. 5
0
class IResponsibleParty(IObject):

    organization = zope.schema.TextLine(title=_(u'Organization Name'),
                                        required=True,
                                        min_length=2)
    organization.setTaggedValue('translatable', True)

    email = z3c.schema.email.RFC822MailAddress(title=_(u'Email'),
                                               required=True)

    role = zope.schema.Choice(
        title=_(u'Responsible Party Role'),
        vocabulary=vocabularies.by_name('party-roles').get('vocabulary'),
        description=_(u'This is the role of the responsible organisation.'),
        default='pointOfContact',
        required=True)
Esempio n. 6
0
 def vocabulary_get(self, name):
     name = str(name)
     r = None
     
     vocab = vocabularies.by_name(name) 
     if vocab:
         r = {
             'date_type': vocab.get('date_type'),
             'reference_date': vocab.get('reference_date'),
             'title': vocab.get('title'),
             'name': vocab.get('name'),
             'terms': [{'token': t.token, 'value': t.value, 'title': t.title} 
                 for t in vocab['vocabulary']],
         }
     
     response.headers['Content-Type'] = content_types['json']
     return [to_json(r)]
Esempio n. 7
0
class IReferenceSystem(IObject):

    code = zope.schema.Choice(
        title=_(u'System'),
        description=_(u'Coordinate Reference System'),
        vocabulary=vocabularies.by_name('reference-systems').get('vocabulary'),
        default='http://www.opengis.net/def/crs/EPSG/0/2100',
        required=True)

    code_space = zope.schema.NativeStringLine(
        title=_(u'Code-Space'),
        description=_(u'Reference System Code-Space'),
        #default = 'urn:ogc:def:crs:EPSG',
        required=False)

    version = zope.schema.NativeStringLine(
        title=_(u'Version'),
        description=_(u'Reference System version'),
        #default = '6.11.2',
        required=False)
Esempio n. 8
0
class IInspireMetadata(IMetadata):

    zope.interface.taggedValue('recurse-on-invariants', True)

    # Metadata on metadata

    contact = zope.schema.List(
        title=_(u'Metadata Point of Contact'),
        description=
        _(u'The organisations responsible for the creation and maintenance of the metadata.'
          ),
        required=True,
        min_length=1,
        max_length=4,
        value_type=zope.schema.Object(IResponsibleParty,
                                      title=_(u'Contact'),
                                      required=True))

    datestamp = zope.schema.Date(
        title=_(u'Metadata Date'),
        description=
        _(u'The date which specifies when the metadata record was created or updated. This date shall be expressed in conformity with ISO 8601.'
          ),
        required=False,
        defaultFactory=datetime.date.today)

    languagecode = zope.schema.Choice(
        title=_(u'Metadata Language'),
        vocabulary=vocabularies.by_name('languages-iso-639-2').get(
            'vocabulary'),
        description=
        _(u'This is the language in which the metadata elements are expressed. The value domain of this metadata element is limited to the official languages of the Community expressed in conformity with ISO 639-2.'
          ),
        required=True,
        default='eng')

    # Identification

    identtype = zope.schema.DottedName(
        title=_(u'Type'),
        required=True,
        default='dataset',
    )

    title = zope.schema.TextLine(
        title=_(u'Resource Title'),
        description=
        _(u'This a characteristic (and often unique) name by which the resource is known.'
          ),
        required=True)
    title.setTaggedValue('translatable', True)

    identifier = zope.schema.NativeStringLine(
        title=_(u'Identifier'),
        description=
        _(u'A value uniquely identifying the dataset. The value domain of this metadata element is a mandatory character string code, generally assigned by the data owner, and a character string namespace uniquely identifying the context of the identifier code (for example, the data owner).'
          ),
        required=True)
    identifier.setTaggedValue('links-to', 'id')

    abstract = zope.schema.Text(
        title=_(u'Resource Abstract'),
        description=
        _(u'This is a brief narrative summary of the contents of this dataset.'
          ),
        required=True)
    abstract.setTaggedValue('translatable', True)
    abstract.setTaggedValue('links-to', 'notes')

    locator = zope.schema.List(
        title=_(u'Resource Locator'),
        description=
        _(u'The resource locator defines the link(s) to the resource and/or the link to additional information about the resource. The value domain of this metadata element is a character string, commonly expressed as uniform resource locator (URL).'
          ),
        required=True,
        min_length=1,
        max_length=6,
        value_type=zope.schema.URI(title=_(u'Linkage'), required=True))

    resource_language = zope.schema.List(
        title=_(u'Resource Languages'),
        description=
        _(u'The language(s) used within the resource. The value domain of this metadata element is limited to the languages defined in ISO 639-2.'
          ),
        required=False,
        max_length=5,
        value_type=zope.schema.Choice(
            title=_(u'Resource Language'),
            vocabulary=vocabularies.by_name('languages-iso-639-2').get(
                'vocabulary'),
        ))

    # Classification

    topic_category = zope.schema.List(
        title=_(u'Topic Categories'),
        description=
        _(u'The topic category is a high-level classification scheme to assist in the grouping and topic-based search of available spatial data resources. The value domain of this metadata element is defined in Part D.2.'
          ),
        required=True,
        min_length=1,
        max_length=6,
        value_type=zope.schema.Choice(
            title=_(u'Topic Category'),
            vocabulary=vocabularies.by_name('topic-category').get(
                'vocabulary'),
        ))
    topic_category.setTaggedValue('format:markup',
                                  {'descend-if-dictized': False})

    # Keywords

    keywords = zope.schema.Dict(
        title=_(u'Keywords'),
        description=
        _(u'The keyword value is a commonly used word, formalised word or phrase used to describe the subject. While the topic category is too coarse for detailed queries, keywords help narrowing a full text search and they allow for structured keyword search.'
          ),
        required=True,
        min_length=1,
        key_type=zope.schema.Choice(vocabulary=SimpleVocabulary(
            tuple(
                SimpleTerm(k, k,
                           vocabularies.by_name(k).get('title'))
                for k in keyword_thesaurus_names)),
                                    title=_(u'Keyword Thesaurus')),
        value_type=zope.schema.Object(IThesaurusTerms, title=_(u'Keywords')))
    keywords.setTaggedValue('format:markup', {'descend-if-dictized': False})

    @zope.interface.invariant
    def check_keywords(obj):
        if obj.keywords:
            if not ('keywords-gemet-inspire-themes' in obj.keywords):
                raise zope.interface.Invalid(
                    _(u'You need to select at least one keyword from INSPIRE data themes'
                      ))

    free_keywords = zope.schema.List(
        title=_(u'Free Keywords'),
        description=
        _(u'The keyword value is a commonly used word, formalised word or phrase used to describe the subject. While the topic category is too coarse for detailed queries, keywords help narrowing a full text search and they allow for structured keyword search.'
          ),
        required=False,
        max_length=20,
        value_type=zope.schema.Object(IFreeKeyword, title=_(u'Free Keyword')))
    free_keywords.setTaggedValue('format:markup',
                                 {'descend-if-dictized': False})

    # Geographic

    bounding_box = zope.schema.List(
        title=_(u'Geographic Bounding Box'),
        description=
        _(u'This is the extent of the resource in the geographic space, given as a bounding box. The bounding box shall be expressed with westbound and eastbound longitudes, and southbound and northbound latitudes in decimal degrees, with a precision of at least two decimals.'
          ),
        required=True,
        min_length=1,
        max_length=4,
        value_type=zope.schema.Object(IGeographicBoundingBox,
                                      title=_(u'Bounding Box')))
    bounding_box.setTaggedValue('format:markup', {'descend-if-dictized': True})

    # Temporal

    temporal_extent = zope.schema.List(
        title=_(u'Temporal Extents'),
        description=
        _(u'The temporal extent defines the time period covered by the content of the resource. This time period may be expressed as any of the following: - an individual date, - an interval of dates expressed through the starting date and end date of the interval,- a mix of individual dates and intervals of dates.'
          ),
        required=False,
        max_length=3,
        value_type=zope.schema.Object(ITemporalExtent, title=_(u'Extent')))

    creation_date = zope.schema.Date(
        title=_(u'Creation Date'),
        description=
        _(u'This is the date of creation of the resource. There shall not be more than one date of creation.'
          ),
        required=False)

    publication_date = zope.schema.Date(
        title=_(u'Publication Date'),
        description=
        _(u'This is the date of publication of the resource when available, or the date of entry into force. There may be more than one date of publication.'
          ),
        required=False)

    revision_date = zope.schema.Date(
        title=_(u'Revision Date'),
        description=
        _(u'This is the date of last revision of the resource, if the resource has been revised. There shall not be more than one date of last revision.'
          ),
        required=False)

    @zope.interface.invariant
    def check_creation_publication_order(obj):
        errs = []

        if obj.creation_date and obj.publication_date:
            if obj.creation_date > obj.publication_date:
                errs.append(
                    'Creation date (%s) is later than publication date (%s)' %
                    (obj.creation_date, obj.publication_date))

        if obj.publication_date and obj.revision_date:
            if obj.publication_date > obj.revision_date:
                errs.append(
                    'Publication date (%s) is later than last revision date (%s)'
                    % (obj.publication_date, obj.revision_date))

        if obj.creation_date and obj.revision_date:
            if obj.creation_date > obj.revision_date:
                errs.append(
                    'Creation date (%s) is later than last revision date (%s)'
                    % (obj.creation_date, obj.revision_date))

        if errs:
            raise zope.interface.Invalid(errs)

    # Quality & Validity

    lineage = zope.schema.Text(
        title=_(u'Lineage'),
        description=
        _(u'This is a statement on process history and/or overall quality of the spatial data set. Where appropriate it may include a statement whether the data set has been validated or quality assured, whether it is the official version (if multiple versions exist), and whether it has legal validity. The value domain of this metadata element is free text.'
          ),
        required=False)
    lineage.setTaggedValue('translatable', True)

    spatial_resolution = zope.schema.List(
        title=_(u'Spatial Resolution'),
        description=
        _(u'''Spatial resolution refers to the level of detail of the data set. It shall be expressed as a set of zero to many resolution distances (typically for gridded data and imagery-derived products) or equivalent scales (typically for maps or map-derived products). An equivalent scale is generally expressed as an integer value expressing the scale denominator. A resolution distance shall be expressed as a numerical value associated with a unit of length.'''
          ),
        required=False,
        max_length=6,
        value_type=zope.schema.Object(ISpatialResolution,
                                      title=_(u'Resolution')))
    spatial_resolution.value_type.setTaggedValue('allow-partial-update', False)

    reference_system = zope.schema.Object(
        IReferenceSystem,
        title=_(u'Coordinate Reference System'),
        description=_(u'Coordinate Reference System'),
        required=False)
    reference_system.setTaggedValue('format:markup',
                                    {'descend-if-dictized': False})

    # Conformity

    conformity = zope.schema.List(
        title=_(u'Conformity'),
        description=
        _(u'''This is a citation of the implementing rules adopted under Article 7(1) of Directive 2007/2/EC or other specification to which a particular resource conforms. A resource may conform to more than one implementing rules adopted under Article 7(1) of Directive 2007/2/EC or other specification. This citation shall include at least the title and a reference date (date of publication, date of last revision or of creation) of the implementing rules adopted under Article 7(1) of Directive 2007/2/EC or of the specification.'''
          ),
        required=False,
        max_length=4,
        value_type=zope.schema.Object(IConformity, title=_(u'Specification')))

    # Constraints

    # Todo: The following fields should have suggestions (autocompleted?) values.

    access_constraints = zope.schema.List(
        title=_(u'Access Constraints'),
        description=
        _(u'Define the conditions for access and use of spatial data sets and services, and where applicable, corresponding fees as required by Article 5(2)(b) and Article 11(2)(f) of Directive 2007/2/EC. The value domain of this metadata element is free text. The element must have values. If no conditions apply to the access and use of the resource, "no conditions apply" shall be used. If conditions are unknown, "conditions unknown" shall be used. This element shall also provide information on any fees necessary to access and use the resource, if applicable, or refer to a uniform resource locator (URL) where information on fees is available.'
          ),
        required=True,
        min_length=1,
        max_length=4,
        value_type=zope.schema.TextLine(title=_(u'Condition')))

    limitations = zope.schema.List(
        title=_(u'Limitations'),
        description=
        _(u'When member states limit public access to spatial data sets and spatial data services under Article 13 of Directive 2007/2/EC, this metadata element shall provide information on the limitations and the reasons for them. If there are no limitations on public access, this metadata element shall indicate that fact. The value domain of this metadata element is free text.'
          ),
        required=True,
        min_length=1,
        max_length=4,
        value_type=zope.schema.TextLine(title=_(u'Limitation')))

    # Responsible Party

    responsible_party = zope.schema.List(
        title=_(u'Responsible Party'),
        description=
        _(u'The responsible party names the organisation responsible for the establishment, management, maintenance and distribution of resources contained in this dataset.'
          ),
        required=True,
        min_length=1,
        max_length=3,
        value_type=zope.schema.Object(IResponsibleParty, title=_(u'Party')))
    def from_xml(self, e):
        '''Build and return an InspireMetadata object from a (serialized) etree Element e.
        '''

        def to_date(s):
            return strptime(s, '%Y-%m-%d').date() if isinstance(s, str) else None

        def to_responsible_party(alist):
            result = []
            for it in alist:
                result.append(ResponsibleParty(
                    organization = unicode(it.organization),
                    email = unicode(it.email),
                    role = it.role))
            return result

        # Parse object

        md = MD_Metadata(e)

        datestamp = to_date(md.datestamp)
        id_list = md.identification.uricode

        url_list = []
        if md.distribution:
            for it in md.distribution.online:
                url_list.append(it.url)

        topic_list = []
        for topic in md.identification.topiccategory:
            topic_list.append(topic)
        
        free_keywords = []
        keywords = {}
        for it in md.identification.keywords:
            thes_title = it['thesaurus']['title']
            # Lookup and instantiate a named thesaurus
            thes = None
            if thes_title:
                try:
                    thes_title, thes_version = thes_title.split(',')
                except:
                    thes_version = None
                else:
                    thes_version = re.sub(r'^[ ]*version[ ]+(\d\.\d)$', r'\1', thes_version)
                # Note thes_version can be used to enforce a specific thesaurus version
                try:
                    thes = Thesaurus.lookup(title=thes_title, for_keywords=True)
                except ValueError:
                    thes = None
            # Treat present keywords depending on if they belong to a thesaurus
            if thes:
                # Treat as thesaurus terms; discard unknown terms
                terms = []
                for keyword in it['keywords']:
                    term = thes.vocabulary.by_value.get(keyword)
                    if not term:
                        term = thes.vocabulary.by_token.get(keyword)
                    if term:
                        terms.append(term.value)
                keywords[thes.name] = ThesaurusTerms(thesaurus=thes, terms=terms)
            else:
                # Treat as free keywords (not really a thesaurus)
                vocab_date = to_date(it['thesaurus']['date'])
                vocab_datetype = it['thesaurus']['datetype']
                if thes_title:
                    thes_title = unicode(thes_title)
                for keyword in it['keywords']:
                    free_keywords.append(FreeKeyword(
                        value = keyword,
                        reference_date = vocab_date,
                        date_type = vocab_datetype,
                        originating_vocabulary = thes_title))

        temporal_extent = []
        if md.identification.temporalextent_start or md.identification.temporalextent_end:
            temporal_extent = [TemporalExtent(
                start = to_date(md.identification.temporalextent_start),
                end = to_date(md.identification.temporalextent_end))]

        bbox = []
        if md.identification.extent:
            if md.identification.extent.boundingBox:
                bbox = [GeographicBoundingBox(
                    nblat = float(md.identification.extent.boundingBox.maxy),
                    sblat = float(md.identification.extent.boundingBox.miny),
                    eblng = float(md.identification.extent.boundingBox.maxx),
                    wblng = float(md.identification.extent.boundingBox.minx))]

        creation_date = None
        publication_date = None
        revision_date = None

        for it in md.identification.date:
            if it.type == 'creation':
                creation_date = to_date(it.date)
            elif it.type == 'publication':
                publication_date = to_date(it.date)
            elif it.type == 'revision':
                revision_date = to_date(it.date)

        spatial_list = []

        if len(md.identification.distance) != len(md.identification.uom):
            raise Exception(_('Found unequal list lengths distance,uom (%s, %s)' % (
                    md.identification.distance,md.identification.uom)))
        else:
                for i in range(0,len(md.identification.distance)):
                    spatial_list.append(SpatialResolution(
                        distance = int(md.identification.distance[i]),
                        uom = unicode(md.identification.uom[i])))

                for i in range(0, len(md.identification.denominators)):
                    spatial_list.append(SpatialResolution(
                        denominator = int(md.identification.denominators[i])))
        conf_list = []
        invalid_degree = False
        #if md.referencesystem.codeSpace:
        #    code_space = md.referenceSystem.codeSpace
        reference_system = None
        if md.referencesystem:
            code = md.referencesystem.code
            reference_systems = vocabularies.by_name('reference-systems').get('vocabulary')
            if code in reference_systems:
                # Check whether the URI is provided 
                reference_system = ReferenceSystem(code = code)
            else:
                # Check whether just the EPSG code suffix is provided
                code_full = 'http://www.opengis.net/def/crs/EPSG/0/{code}'.format(code=code)
                if code_full in reference_systems:
                    reference_system = ReferenceSystem(code = code_full)
                else:
                    raise Exception(_('Reference system not recognizable'))

            if md.referencesystem.codeSpace:
                reference_system.code_space = md.referencesystem.codeSpace
            if md.referencesystem.version:
                reference_system.version = md.referencesystem.version

        if len(md.dataquality.conformancedate) != len(md.dataquality.conformancedatetype):
            # Date list is unequal to datetype list, this means wrong XML so exception is thrown
            raise Exception(_('Found unequal list lengths: conformance date, conformancedatetype'))
        if len(md.dataquality.conformancedegree) != len(md.dataquality.conformancedate):
            # Degree list is unequal to date/datetype lists, so we are unable to conclude
            # to which conformity item each degree value corresponds, so all are set to 
            # not-evaluated (Todo: MD_Metadata bug #63)
            invalid_degree = True

        if md.dataquality.conformancedate:
        #and len(md.dataquality.conformancedate) == len(md.dataquality.degree):
            for i in range(0,len(md.dataquality.conformancedate)):

                date = to_date(md.dataquality.conformancedate[i])

                date_type = md.dataquality.conformancedatetype[i]
                # TODO md.dataquality.conformancedatetype returns empty
                if invalid_degree:
                    degree = 'not-evaluated'
                else:
                    try:
                        if md.dataquality.conformancedegree[i] == 'true':
                            degree = 'conformant'
                        elif md.dataquality.conformancedegree[i] == 'false':
                            degree = 'not-conformant'
                    except:
                        degree = "not-evaluated"
                title = unicode(md.dataquality.conformancetitle[i])
                if title != 'None': 
                    conf_list.append(Conformity(title=title, date=date, date_type=date_type, degree=degree))

                # TODO: is title required fields? If so the following is unnecessary
                else:
                    conf_list.append(Conformity(date=date, date_type=date_type, degree=degree))

        limit_list = []
        for it in md.identification.uselimitation:
                limit_list.append(unicode(it))
        constr_list = []
        for it in md.identification.otherconstraints:
                constr_list.append(unicode(it))

        obj = InspireMetadata()

        obj.contact = to_responsible_party(md.contact)
        obj.datestamp = datestamp
        obj.languagecode = md.languagecode
        obj.title = unicode(md.identification.title)
        obj.abstract = unicode(md.identification.abstract)
        obj.identifier = id_list[0]
        obj.locator = url_list
        #obj.resource_language = md.identification.resourcelanguage
        obj.topic_category = topic_list
        obj.keywords = keywords
        obj.free_keywords = free_keywords
        obj.bounding_box = bbox
        obj.temporal_extent = temporal_extent
        obj.creation_date = creation_date
        obj.publication_date = publication_date
        obj.revision_date = revision_date
        obj.lineage = unicode(md.dataquality.lineage)
        obj.spatial_resolution = spatial_list
        obj.reference_system = reference_system
        obj.conformity = conf_list
        obj.access_constraints = limit_list
        obj.limitations = constr_list
        obj.responsible_party = to_responsible_party(md.identification.contact)

        return obj
Esempio n. 10
0
 def vocabulary(self):
     vocab = vocabularies.by_name(self.name)
     return vocab.get("vocabulary") if vocab else None
Esempio n. 11
0
import zope.interface
import zope.schema
from zope.interface import Interface

from ckanext.publicamundi.lib import vocabularies
from ckanext.publicamundi.lib.util import check_uuid
from ckanext.publicamundi.lib.metadata import IMetadata
from ckanext.publicamundi.lib.metadata.ibase import IFieldWithContext

language_vocabulary = vocabularies.by_name('languages-iso-639-1').get('vocabulary')

__all__ = [
    'IFieldTranslation',
    'IKeyBasedFieldTranslation',
    'IValueBasedFieldTranslation',
    'ITranslatedField',
    'ITranslatedMetadata',
    'ITranslator',
    'IMetadataTranslator',
]

class IFieldTranslation(Interface):
    
    def get(field, language, state='active'):
        '''Return a bound field translated for the given pair (field, language).
        
        This method should always return a bound field, or None if no translation exists.
        The  translated value (found at .context.value) should be unicode text.
        
        *Note* Maybe return just unicode text (avoid wrapping to a field) ?