示例#1
0
class SourceDetail(db.Base):
    __tablename__ = 'source_detail'
    __mapper_args__ = {'order_by': 'name'}

    name = Column(Unicode(75), unique=True)
    description = Column(UnicodeText)
    source_type = Column(types.Enum(values=[i[0] for i in source_type_values],
                                    translations=dict(source_type_values)),
                         default=None)

    def __str__(self):
        return utils.utf8(self.name)
class PropCutting(db.Base):
    """
    A cutting
    """
    __tablename__ = 'prop_cutting'
    cutting_type = Column(types.Enum(values=cutting_type_values.keys(),
                                     translations=cutting_type_values),
                          default=u'Other')
    tip = Column(types.Enum(values=tip_values.keys(), translations=tip_values))
    leaves = Column(
        types.Enum(values=leaves_values.keys(), translations=leaves_values))
    leaves_reduced_pct = Column(Integer, autoincrement=False)
    length = Column(Integer, autoincrement=False)
    length_unit = Column(
        types.Enum(values=length_unit_values.keys(),
                   translations=length_unit_values))

    # single/double/slice
    wound = Column(
        types.Enum(values=wound_values.keys(), translations=wound_values))

    # removed/None
    flower_buds = Column(
        types.Enum(values=flower_buds_values.keys(),
                   translations=flower_buds_values))

    fungicide = Column(UnicodeText)  # fungal soak
    hormone = Column(UnicodeText)  # powder/liquid/None....solution

    media = Column(UnicodeText)
    container = Column(UnicodeText)
    location = Column(UnicodeText)
    cover = Column(UnicodeText)  # vispore, poly, plastic dome, poly bag

    # temperature of bottom heat
    bottom_heat_temp = Column(Integer, autoincrement=False)

    # TODO: make the bottom heat unit required if bottom_heat_temp is
    # not null

    # F/C
    bottom_heat_unit = Column(types.Enum(values=bottom_heat_unit_values.keys(),
                                         translations=bottom_heat_unit_values),
                              nullable=True)
    rooted_pct = Column(Integer, autoincrement=False)
    #aftercare = Column(UnicodeText) # same as propgation.notes

    propagation_id = Column(Integer,
                            ForeignKey('propagation.id'),
                            nullable=False)

    rooted = relation('PropRooted',
                      cascade='all,delete-orphan',
                      backref=backref('cutting', uselist=False))
示例#3
0
class SourceDetail(db.Base):
    __tablename__ = 'source_detail'
    __mapper_args__ = {'order_by': 'name'}

    # ITF2 - E6 - Donor
    name = Column(Unicode(75), unique=True)
    # extra description, not included in E6
    description = Column(UnicodeText)
    email = Column(Unicode(254), default=u'')
    private = Column(Boolean, default=False)
    # ITF2 - E5 - Donor Type Flag
    source_type = Column(types.Enum(values=[i[0] for i in source_type_values],
                                    translations=dict(source_type_values)),
                         default=None)

    def __str__(self):
        return utils.utf8(self.name)

    def search_view_markup_pair(self):
        '''provide the two lines describing object for SearchView row.
        '''
        safe = utils.xml_safe
        return (str(self), safe(self.source_type or ''))
class Propagation(db.Base):
    """
    Propagation
    """
    __tablename__ = 'propagation'
    #recvd_as = Column(Unicode(10)) # seed, urcu, other
    #recvd_as_other = Column(UnicodeText) # maybe this should be in the notes
    prop_type = Column(types.Enum(values=prop_type_values.keys(),
                                  translations=prop_type_values),
                       nullable=False)
    notes = Column(UnicodeText)
    date = Column(types.Date)

    _cutting = relation(
        'PropCutting',
        primaryjoin='Propagation.id==PropCutting.propagation_id',
        cascade='all,delete-orphan',
        uselist=False,
        backref=backref('propagation', uselist=False))
    _seed = relation('PropSeed',
                     primaryjoin='Propagation.id==PropSeed.propagation_id',
                     cascade='all,delete-orphan',
                     uselist=False,
                     backref=backref('propagation', uselist=False))

    def _get_details(self):
        if self.prop_type == 'Seed':
            return self._seed
        elif self.prop_type == 'UnrootedCutting':
            return self._cutting
        elif self.notes:
            return self.notes
        else:
            raise NotImplementedError

    #def _set_details(self, details):
    #    return self._details

    details = property(_get_details)

    def get_summary(self):
        """
        """
        date_format = prefs.prefs[prefs.date_format_pref]

        def get_date(date):
            if isinstance(date, datetime.date):
                return date.strftime(date_format)
            return date

        s = str(self)
        if self.prop_type == u'UnrootedCutting':
            c = self._cutting
            values = []
            if c.cutting_type is not None:
                values.append(
                    _('Cutting type: %s') %
                    cutting_type_values[c.cutting_type])
            if c.length:
                values.append(
                    _('Length: %(length)s%(unit)s') %
                    dict(length=c.length,
                         unit=length_unit_values[c.length_unit]))
            if c.tip:
                values.append(_('Tip: %s') % tip_values[c.tip])
            if c.leaves:
                s = _('Leaves: %s') % leaves_values[c.leaves]
                if c.leaves == u'Removed' and c.leaves_reduced_pct:
                    s.append('(%s%%)' % c.leaves_reduced_pct)
                values.append(s)
            if c.flower_buds:
                values.append(
                    _('Flower buds: %s') % flower_buds_values[c.flower_buds])
            if c.wound is not None:
                values.append(_('Wounded: %s') % wound_values[c.wound])
            if c.fungicide:
                values.append(_('Fungal soak: %s') % c.fungicide)
            if c.hormone:
                values.append(_('Hormone treatment: %s') % c.hormone)
            if c.bottom_heat_temp:
                values.append(
                    _('Bottom heat: %(temp)s%(unit)s') %
                    dict(temp=c.bottom_heat_temp,
                         unit=bottom_heat_unit_values[c.bottom_heat_unit]))
            if c.container:
                values.append(_('Container: %s') % c.container)
            if c.media:
                values.append(_('Media: %s') % c.media)
            if c.location:
                values.append(_('Location: %s') % c.location)
            if c.cover:
                values.append(_('Cover: %s') % c.cover)

            if c.rooted_pct:
                values.append(_('Rooted: %s%%') % c.rooted_pct)
            s = ', '.join(values)
        elif self.prop_type == u'Seed':
            s = str(self)
            seed = self._seed
            values = []
            if seed.pretreatment:
                values.append(_('Pretreatment: %s') % seed.pretreatment)
            if seed.nseeds:
                values.append(_('# of seeds: %s') % seed.nseeds)
            date_sown = get_date(seed.date_sown)
            if date_sown:
                values.append(_('Date sown: %s') % date_sown)
            if seed.container:
                values.append(_('Container: %s') % seed.container)
            if seed.media:
                values.append(_('Media: %s') % seed.media)
            if seed.covered:
                values.append(_('Covered: %s') % seed.covered)
            if seed.location:
                values.append(_('Location: %s') % seed.location)
            germ_date = get_date(seed.germ_date)
            if germ_date:
                values.append(_('Germination date: %s') % germ_date)
            if seed.nseedlings:
                values.append(_('# of seedlings: %s') % seed.nseedlings)
            if seed.germ_pct:
                values.append(_('Germination rate: %s%%') % seed.germ_pct)
            date_planted = get_date(seed.date_planted)
            if date_planted:
                values.append(_('Date planted: %s') % date_planted)
            s = ', '.join(values)
        elif self.notes:
            s = utils.utf8(self.notes)

        return s
示例#5
0
class Family(db.Base, db.Serializable):
    """
    :Table name: family

    :Columns:
        *family*:
            The name of the family. Required.

        *qualifier*:
            The family qualifier.

            Possible values:
                * s. lat.: aggregrate family (senso lato)

                * s. str.: segregate family (senso stricto)

                * '': the empty string

    :Properties:
        *synonyms*:
            An association to _synonyms that will automatically
            convert a Family object and create the synonym.

    :Constraints:
        The family table has a unique constraint on family/qualifier.
    """
    __tablename__ = 'family'
    __table_args__ = (UniqueConstraint('family'), {})
    __mapper_args__ = {'order_by': ['Family.family', 'Family.qualifier']}

    rank = 'familia'
    link_keys = ['accepted']

    @validates('genus')
    def validate_stripping(self, key, value):
        if value is None:
            return None
        return value.strip()

    @property
    def cites(self):
        '''the cites status of this taxon, or None
        '''

        cites_notes = [
            i.note for i in self.notes
            if i.category and i.category.upper() == 'CITES'
        ]
        if not cites_notes:
            return None
        return cites_notes[0]

    # columns
    family = Column(String(45), nullable=False, index=True)

    # we use the blank string here instead of None so that the
    # contraints will work properly,
    qualifier = Column(types.Enum(values=[u's. lat.', u's. str.', u'']),
                       default=u'')

    # relations
    # `genera` relation is defined outside of `Family` class definition
    synonyms = association_proxy('_synonyms', 'synonym')
    _synonyms = relation('FamilySynonym',
                         primaryjoin='Family.id==FamilySynonym.family_id',
                         cascade='all, delete-orphan',
                         uselist=True,
                         backref='family')

    # this is a dummy relation, it is only here to make cascading work
    # correctly and to ensure that all synonyms related to this family
    # get deleted if this family gets deleted
    __syn = relation('FamilySynonym',
                     primaryjoin='Family.id==FamilySynonym.synonym_id',
                     cascade='all, delete-orphan',
                     uselist=True)

    def __repr__(self):
        return Family.str(self)

    @staticmethod
    def str(family, qualifier=False, author=False):
        # author is not in the model but it really should
        if family.family is None:
            return db.Base.__repr__(family)
        else:
            return ' '.join([
                s for s in [family.family, family.qualifier]
                if s not in (None, '')
            ])

    @property
    def accepted(self):
        'Name that should be used if name of self should be rejected'
        session = object_session(self)
        syn = session.query(FamilySynonym).filter(
            FamilySynonym.synonym_id == self.id).first()
        accepted = syn and syn.family
        return accepted

    @accepted.setter
    def accepted(self, value):
        'Name that should be used if name of self should be rejected'
        assert isinstance(value, self.__class__)
        if self in value.synonyms:
            return
        # remove any previous `accepted` link
        session = object_session(self) or db.Session()
        session.query(FamilySynonym).filter(
            FamilySynonym.synonym_id == self.id).delete()
        session.commit()
        value.synonyms.append(self)

    def has_accessions(self):
        '''true if family is linked to at least one accession
        '''

        return False

    def as_dict(self, recurse=True):
        result = db.Serializable.as_dict(self)
        del result['family']
        del result['qualifier']
        result['object'] = 'taxon'
        result['rank'] = self.rank
        result['epithet'] = self.family
        if recurse and self.accepted is not None:
            result['accepted'] = self.accepted.as_dict(recurse=False)
        return result

    @classmethod
    def retrieve(cls, session, keys):
        try:
            return session.query(cls).filter(
                cls.family == keys['epithet']).one()
        except:
            return None

    @classmethod
    def correct_field_names(cls, keys):
        for internal, exchange in [('family', 'epithet')]:
            if exchange in keys:
                keys[internal] = keys[exchange]
                del keys[exchange]

    def top_level_count(self):
        genera = set(g for g in self.genera if g.species)
        species = [s for g in genera for s in g.species]
        accessions = [a for s in species for a in s.accessions]
        plants = [p for a in accessions for p in a.plants]
        return {
            (1, 'Families'):
            set([self.id]),
            (2, 'Genera'):
            genera,
            (3, 'Species'):
            set(species),
            (4, 'Accessions'):
            len(accessions),
            (5, 'Plantings'):
            len(plants),
            (6, 'Living plants'):
            sum(p.quantity for p in plants),
            (7, 'Locations'):
            set(p.location.id for p in plants),
            (8, 'Sources'):
            set([
                a.source.source_detail.id for a in accessions
                if a.source and a.source.source_detail
            ])
        }
示例#6
0
class Genus(db.Base, db.Serializable):
    """
    :Table name: genus

    :Columns:
        *genus*:
            The name of the genus.  In addition to standard generic
            names any additional hybrid flags or genera should included here.

        *qualifier*:
            Designates the botanical status of the genus.

            Possible values:
                * s. lat.: aggregrate genus (sensu lato)

                * s. str.: segregate genus (sensu stricto)

        *author*:
            The name or abbreviation of the author who published this genus.

    :Properties:
        *family*:
            The family of the genus.

        *synonyms*:
            The list of genera who are synonymous with this genus.  If
            a genus is listed as a synonym of this genus then this
            genus should be considered the current and valid name for
            the synonym.

    :Contraints:
        The combination of genus, author, qualifier
        and family_id must be unique.
    """
    __tablename__ = 'genus'
    __table_args__ = (UniqueConstraint('genus', 'author', 'qualifier',
                                       'family_id'), {})
    __mapper_args__ = {'order_by': ['genus', 'author']}

    rank = 'genus'
    link_keys = ['accepted']

    @property
    def cites(self):
        '''the cites status of this taxon, or None
        '''

        cites_notes = [
            i.note for i in self.notes
            if i.category and i.category.upper() == 'CITES'
        ]
        if not cites_notes:
            return self.family.cites
        return cites_notes[0]

    # columns
    genus = Column(String(64), nullable=False, index=True)

    # use '' instead of None so that the constraints will work propertly
    author = Column(Unicode(255), default=u'')

    @validates('genus', 'author')
    def validate_stripping(self, key, value):
        if value is None:
            return None
        return value.strip()

    qualifier = Column(types.Enum(values=['s. lat.', 's. str', u'']),
                       default=u'')

    family_id = Column(Integer, ForeignKey('family.id'), nullable=False)

    # relations
    # `species` relation is defined outside of `Genus` class definition
    synonyms = association_proxy('_synonyms', 'synonym')
    _synonyms = relation('GenusSynonym',
                         primaryjoin='Genus.id==GenusSynonym.genus_id',
                         cascade='all, delete-orphan',
                         uselist=True,
                         backref='genus')

    # this is a dummy relation, it is only here to make cascading work
    # correctly and to ensure that all synonyms related to this genus
    # get deleted if this genus gets deleted
    __syn = relation('GenusSynonym',
                     primaryjoin='Genus.id==GenusSynonym.synonym_id',
                     cascade='all, delete-orphan',
                     uselist=True)

    @property
    def accepted(self):
        'Name that should be used if name of self should be rejected'
        session = object_session(self)
        syn = session.query(GenusSynonym).filter(
            GenusSynonym.synonym_id == self.id).first()
        accepted = syn and syn.genus
        return accepted

    @accepted.setter
    def accepted(self, value):
        'Name that should be used if name of self should be rejected'
        assert isinstance(value, self.__class__)
        if self in value.synonyms:
            return
        # remove any previous `accepted` link
        session = db.Session()
        session.query(GenusSynonym).filter(
            GenusSynonym.synonym_id == self.id).delete()
        session.commit()
        value.synonyms.append(self)

    def __repr__(self):
        return Genus.str(self)

    @staticmethod
    def str(genus, author=False):
        # TODO: the genus should be italicized for markup
        if genus.genus is None:
            return repr(genus)
        elif not author or genus.author is None:
            return ' '.join([
                s for s in [genus.genus, genus.qualifier]
                if s not in ('', None)
            ])
        else:
            return ' '.join([
                s for s in [
                    genus.genus, genus.qualifier,
                    xml.sax.saxutils.escape(genus.author)
                ] if s not in ('', None)
            ])

    def has_accessions(self):
        '''true if genus is linked to at least one accession
        '''

        return False

    def as_dict(self, recurse=True):
        result = db.Serializable.as_dict(self)
        del result['genus']
        del result['qualifier']
        result['object'] = 'taxon'
        result['rank'] = 'genus'
        result['epithet'] = self.genus
        result['ht-rank'] = 'familia'
        result['ht-epithet'] = self.family.family
        if recurse and self.accepted is not None:
            result['accepted'] = self.accepted.as_dict(recurse=False)
        return result

    @classmethod
    def retrieve(cls, session, keys):
        try:
            return session.query(cls).filter(
                cls.genus == keys['epithet']).one()
        except:
            if 'author' not in keys:
                return None
        try:
            return session.query(cls).filter(
                cls.genus == keys['epithet'],
                cls.author == keys['author']).one()
        except:
            return None

    @classmethod
    def correct_field_names(cls, keys):
        for internal, exchange in [('genus', 'epithet'),
                                   ('family', 'ht-epithet')]:
            if exchange in keys:
                keys[internal] = keys[exchange]
                del keys[exchange]

    @classmethod
    def compute_serializable_fields(cls, session, keys):
        from family import Family
        result = {'family': None}
        ## retrieve family object
        if keys.get('ht-epithet'):
            result['family'] = Family.retrieve_or_create(
                session, {'epithet': keys['ht-epithet']}, create=True)
        if result['family'] is None:
            raise error.NoResultException()
        return result
示例#7
0
class Family(db.Base, db.Serializable, db.WithNotes):
    """
    :Table name: family

    :Columns:
        *epithet*:
            The name of the family. Required.

        *aggregate*: aggregate(complex)

    :Properties:
        *synonyms*:
            An association to _synonyms that will automatically
            convert a Family object and create the synonym.

    :Constraints:
        The family table has a unique constraint on family/qualifier.
    """
    __tablename__ = 'family'
    __table_args__ = (UniqueConstraint('epithet', 'author', 'aggregate'), {})
    __mapper_args__ = {'order_by': ['Family.epithet', 'Family.aggregate']}

    rank = 'familia'
    link_keys = ['accepted']

    # columns - common for all taxa
    epithet = Column(Unicode(45), nullable=False, index=True)
    hybrid_marker = Column(types.Enum(values=dict(itf2.hybrid_marker).keys()),
                           default=u'')
    author = Column(Unicode(255), default=u'')
    aggregate = Column(types.Enum(values=dict(itf2.aggregate).keys()),
                       default=u'')

    @validates('genus')
    def validate_stripping(self, key, value):
        if value is None:
            return None
        return value.strip()

    @property
    def cites(self):
        '''the cites status of this taxon, or None
        '''

        cites_notes = [
            i.note for i in self.notes
            if i.category and i.category.upper() == 'CITES'
        ]
        if not cites_notes:
            return None
        return cites_notes[0]

    # relations
    # `genera` relation is defined outside of `Family` class definition
    synonyms = association_proxy('_synonyms', 'synonym')
    _synonyms = relation('FamilySynonym',
                         primaryjoin='Family.id==FamilySynonym.family_id',
                         cascade='all, delete-orphan',
                         uselist=True,
                         backref='family')

    # this is a dummy relation, it is only here to make cascading work
    # correctly and to ensure that all synonyms related to this family
    # get deleted if this family gets deleted
    __syn = relation('FamilySynonym',
                     primaryjoin='Family.id==FamilySynonym.synonym_id',
                     cascade='all, delete-orphan',
                     uselist=True)

    def __repr__(self):
        return self.str()

    def str(self, aggregate=False, author=False):
        # author is not in the model but it really should
        if self.epithet is None:
            return db.Base.__repr__(self)
        else:
            return ' '.join([
                s for s in [self.epithet, self.aggregate]
                if s not in (None, '')
            ])

    @property
    def accepted(self):
        'Name that should be used if name of self should be rejected'
        session = object_session(self)
        if not session:
            logger.warn('family:accepted - object not in session')
            return None
        syn = session.query(FamilySynonym).filter(
            FamilySynonym.synonym_id == self.id).first()
        accepted = syn and syn.family
        return accepted

    @accepted.setter
    def accepted(self, value):
        'Name that should be used if name of self should be rejected'
        assert isinstance(value, self.__class__)
        if self in value.synonyms:
            return
        # remove any previous `accepted` link
        session = object_session(self)
        if not session:
            logger.warn('family:accepted.setter - object not in session')
            return
        session.query(FamilySynonym).filter(
            FamilySynonym.synonym_id == self.id).delete()
        session.commit()
        value.synonyms.append(self)

    def has_accessions(self):
        '''true if family is linked to at least one accession
        '''

        return False

    def as_dict(self, recurse=True):
        result = db.Serializable.as_dict(self)
        result['object'] = 'taxon'
        result['rank'] = self.rank
        result['epithet'] = self.epithet
        if recurse and self.accepted is not None:
            result['accepted'] = self.accepted.as_dict(recurse=False)
        return result

    @classmethod
    def retrieve(cls, session, keys):
        try:
            return session.query(cls).filter(
                cls.epithet == keys['epithet']).one()
        except:
            return None

    def top_level_count(self):
        genera = set(g for g in self.genera if g.species)
        species = [s for g in genera for s in g.species]
        accessions = [a for s in species for a in s.accessions]
        plants = [p for a in accessions for p in a.plants]
        return {
            (1, 'Families'):
            set([self.id]),
            (2, 'Genera'):
            genera,
            (3, 'Species'):
            set(species),
            (4, 'Accessions'):
            len(accessions),
            (5, 'Plantings'):
            len(plants),
            (6, 'Living plants'):
            sum(p.quantity for p in plants),
            (7, 'Locations'):
            set(p.location.id for p in plants),
            (8, 'Sources'):
            set([
                a.source.source_detail.id for a in accessions
                if a.source and a.source.source_detail
            ])
        }
class Species(db.Base, db.Serializable, db.DefiningPictures, db.WithNotes):
    """
    :Table name: species

    :Columns:
        *epithet*:
        *author*:

        *cv_group*:
        *trade_name*:

        *label_distribution*:
            UnicodeText
            This field is optional and can be used for the label in case
            str(self.distribution) is too long to fit on the label.

    :Properties:
        *accessions*:

        *vernacular_names*:

        *default_vernacular_name*:

        *synonyms*:

        *distribution*:

    :Constraints:
        The combination of epithet, author, hybrid, aggregate,
        cv_group, trade_name, genus_id
    """
    __tablename__ = 'species'
    __mapper_args__ = {'order_by': ['epithet', 'author']}

    rank = 'species'
    link_keys = ['accepted']

    # columns
    epithet = Column(Unicode(64), nullable=True, index=True)  # allows for `sp`
    hybrid_marker = Column(types.Enum(values=dict(itf2.hybrid_marker).keys()),
                           default=u'')
    author = Column(Unicode(255), default=u'')
    aggregate = Column(types.Enum(values=dict(itf2.aggregate).keys()),
                       default=u'')
    cv_group = Column(Unicode(50))
    trade_name = Column(Unicode(64))

    infrasp1 = Column(Unicode(64))
    infrasp1_rank = Column(types.Enum(values=infrasp_rank_values.keys(),
                                      translations=infrasp_rank_values))
    infrasp1_author = Column(Unicode(64))

    infrasp2 = Column(Unicode(64))
    infrasp2_rank = Column(types.Enum(values=infrasp_rank_values.keys(),
                                      translations=infrasp_rank_values))
    infrasp2_author = Column(Unicode(64))

    infrasp3 = Column(Unicode(64))
    infrasp3_rank = Column(types.Enum(values=infrasp_rank_values.keys(),
                                      translations=infrasp_rank_values))
    infrasp3_author = Column(Unicode(64))

    infrasp4 = Column(Unicode(64))
    infrasp4_rank = Column(types.Enum(values=infrasp_rank_values.keys(),
                                      translations=infrasp_rank_values))
    infrasp4_author = Column(Unicode(64))

    genus_id = Column(Integer, ForeignKey('genus.id'), nullable=False)
    ## the Species.genus property is defined as backref in Genus.species

    label_distribution = Column(UnicodeText)
    bc_distribution = Column(UnicodeText)

    def search_view_markup_pair(self):
        '''provide the two lines describing object for SearchView row.
        '''
        try:
            if len(self.vernacular_names) > 0:
                substring = (
                    '%s -- %s' %
                    (self.genus.family,
                     ', '.join([str(v) for v in self.vernacular_names])))
            else:
                substring = '%s' % self.genus.family
            trail = self.author and (' <span weight="light">%s</span>' %
                                     utils.xml_safe(self.author)) or ''
            if self.accepted:
                trail += ('<span foreground="#555555" size="small" '
                          'weight="light"> - ' + _("synonym of %s") + "</span>"
                          ) % self.accepted.markup(authors=True)
            return self.markup(authors=False) + trail, substring
        except:
            return u'...', u'...'

    @property
    def cites(self):
        '''the cites status of this taxon, or None

        cites appendix number, one of I, II, or III.
        not enforced by the software in v1.0.x
        '''

        cites_notes = [i.note for i in self.notes
                       if i.category and i.category.upper() == u'CITES']
        if not cites_notes:
            return self.genus.cites
        return cites_notes[0]

    @property
    def conservation(self):
        '''the IUCN conservation status of this taxon, or DD

        one of: EX, RE, CR, EN, VU, NT, LC, DD
        not enforced by the software in v1.0.x
        '''

        {'EX': _('Extinct (EX)'),
         'EW': _('Extinct Wild (EW)'),
         'RE': _('Regionally Extinct (RE)'),
         'CR': _('Critically Endangered (CR)'),
         'EN': _('Endangered (EN)'),
         'VU': _('Vulnerable (VU)'),
         'NT': _('Near Threatened (NT)'),
         'LV': _('Least Concern (LC)'),
         'DD': _('Data Deficient (DD)'),
         'NE': _('Not Evaluated (NE)')}

        notes = [i.note for i in self.notes
                 if i.category and i.category.upper() == u'IUCN']
        return (notes + ['DD'])[0]

    @property
    def condition(self):
        '''the condition of this taxon, or None

        this is referred to what the garden conservator considers the
        area of interest. it is really an interpretation, not a fact.
        '''
        # one of, but not forcibly so:
        [_('endemic'), _('indigenous'), _('native'), _('introduced')]

        notes = [i.note for i in self.notes
                 if i.category.lower() == u'condition']
        return (notes + [None])[0]

    @staticmethod
    def match_func(completion, key, treeiter, data=None):
        """
        """
        species = completion.get_model()[treeiter][0]
        if str(species).lower().startswith(key.lower()) \
                or str(species.genus.epithet).lower().startswith(key.lower()):
            return True
        return False

    @staticmethod
    def cell_data_func(column, renderer, model, treeiter, data=None):
        """
        """
        v = model[treeiter][0]
        renderer.set_property(
            'text', '%s (%s)' % (v.str(authors=True), v.genus.family))
    

    def __lowest_infraspecific(self):
        infrasp = [(self.infrasp1_rank, self.infrasp1,
                    self.infrasp1_author),
                   (self.infrasp2_rank, self.infrasp2,
                    self.infrasp2_author),
                   (self.infrasp3_rank, self.infrasp3,
                    self.infrasp3_author),
                   (self.infrasp4_rank, self.infrasp4,
                    self.infrasp4_author)]
        infrasp = [i for i in infrasp if i[0] not in [u'cv.', '', None]]
        if infrasp == []:
            return (u'', u'', u'')
        return sorted(infrasp, cmp=lambda a, b: compare_rank(a[0], b[0]))[-1]

    @property
    def infraspecific_rank(self):
        return self.__lowest_infraspecific()[0] or u''

    @property
    def infraspecific_epithet(self):
        return self.__lowest_infraspecific()[1] or u''

    @property
    def infraspecific_author(self):
        return self.__lowest_infraspecific()[2] or u''

    @property
    def cultivar_epithet(self):
        infrasp = ((self.infrasp1_rank, self.infrasp1,
                    self.infrasp1_author),
                   (self.infrasp2_rank, self.infrasp2,
                    self.infrasp2_author),
                   (self.infrasp3_rank, self.infrasp3,
                    self.infrasp3_author),
                   (self.infrasp4_rank, self.infrasp4,
                    self.infrasp4_author))
        for rank, epithet, author in infrasp:
            if rank == u'cv.':
                return epithet
        return u''

    # relations
    synonyms = association_proxy('_synonyms', 'synonym')
    _synonyms = relation('SpeciesSynonym',
                         primaryjoin='Species.id==SpeciesSynonym.species_id',
                         cascade='all, delete-orphan', uselist=True,
                         backref='species')

    # this is a dummy relation, it is only here to make cascading work
    # correctly and to ensure that all synonyms related to this genus
    # get deleted if this genus gets deleted
    _syn = relation('SpeciesSynonym',
                    primaryjoin='Species.id==SpeciesSynonym.synonym_id',
                    cascade='all, delete-orphan', uselist=True)

    ## VernacularName.species gets defined here too.
    vernacular_names = relation('VernacularName', cascade='all, delete-orphan',
                                collection_class=VNList,
                                backref=backref('species', uselist=False))
    _default_vernacular_name = relation('DefaultVernacularName', uselist=False,
                                        cascade='all, delete-orphan',
                                        backref=backref('species',
                                                        uselist=False))
    distribution = relation('SpeciesDistribution',
                            cascade='all, delete-orphan',
                            backref=backref('species', uselist=False))

    habit_id = Column(Integer, ForeignKey('habit.id'), default=None)
    habit = relation('Habit', uselist=False, backref='species')

    flower_color_id = Column(Integer, ForeignKey('color.id'), default=None)
    flower_color = relation('Color', uselist=False, backref='species')

    #hardiness_zone = Column(Unicode(4))

    awards = Column(UnicodeText)

    def __init__(self, *args, **kwargs):
        self.author = self.aggregate = self.hybrid_marker = u''
        super(Species, self).__init__(*args, **kwargs)

    def __str__(self):
        'return the default string representation for self.'
        return self.str()

    def _get_default_vernacular_name(self):
        if self._default_vernacular_name is None:
            return None
        return self._default_vernacular_name.vernacular_name

    def _set_default_vernacular_name(self, vn):
        if vn is None:
            del self.default_vernacular_name
            return
        if vn not in self.vernacular_names:
            self.vernacular_names.append(vn)
        d = DefaultVernacularName()
        d.vernacular_name = vn
        self._default_vernacular_name = d

    def _del_default_vernacular_name(self):
        utils.delete_or_expunge(self._default_vernacular_name)
        del self._default_vernacular_name
    default_vernacular_name = property(_get_default_vernacular_name,
                                       _set_default_vernacular_name,
                                       _del_default_vernacular_name)

    def distribution_str(self):
        if self.distribution is None:
            return ''
        else:
            dist = ['%s' % d for d in self.distribution]
            return unicode(', ').join(sorted(dist))

    def markup(self, authors=False, genus=True):
        '''
        returns this object as a string with markup

        :param authors: flag to toggle whethe the author names should be
        included
        '''
        return self.str(authors, markup=True, genus=genus)

    # in PlantPlugins.init() we set this to 'x' for win32
    hybrid_char = u'×'

    def str(self, authors=False, markup=False, remove_zws=False, genus=True,
            qualification=None, sensu=None):
        '''
        returns a string for species

        :param authors: flag to toggle whether authorship should be included
        :param markup: flag to toggle whether the returned text is marked up
        to show italics on the epithets
        :param remove_zws: flag to toggle zero width spaces, helping
        semantically correct lexicographic order.
        :param genus: flag to toggle leading genus name.
        :param qualification: pair or None. if specified, first is the
        qualified rank, second is the qualification.
        '''
        # TODO: this method will raise an error if the session is none
        # since it won't be able to look up the genus....we could
        # probably try to query the genus directly with the genus_id
        if genus is True:
            genus = self.genus.str(author=False, use_hybrid_marker=True)
        else:
            genus = ''
        if self.epithet and not remove_zws:
            epithet = u'\u200b' + self.epithet  # prepend with zero_width_space
        else:
            epithet = self.epithet
        if markup:
            escape = utils.xml_safe
            italicize = lambda s: s and (  # all but the multiplication signs
                u'<i>%s</i>' % escape(s).replace(u'×', u'</i>×<i>')) or u''
            genus = italicize(genus)
            if epithet is not None:
                epithet = italicize(epithet)
        else:
            italicize = escape = lambda x: x

        author = None
        if authors and self.author:
            author = escape(self.author)

        infrasp = ((self.infrasp1_rank, self.infrasp1,
                    self.infrasp1_author),
                   (self.infrasp2_rank, self.infrasp2,
                    self.infrasp2_author),
                   (self.infrasp3_rank, self.infrasp3,
                    self.infrasp3_author),
                   (self.infrasp4_rank, self.infrasp4,
                    self.infrasp4_author))

        infrasp_parts = []
        group_added = False
        for irank, iepithet, iauthor in infrasp:
            if irank == 'cv.' and iepithet:
                if self.cv_group and not group_added:
                    group_added = True
                    infrasp_parts.append(_("(%(group)s Group)") %
                                         dict(group=self.cv_group))
                infrasp_parts.append("'%s'" % escape(iepithet))
            else:
                if irank:
                    infrasp_parts.append(irank)
                if iepithet and irank:
                    infrasp_parts.append(italicize(iepithet))
                elif iepithet:
                    infrasp_parts.append(escape(iepithet))

            if authors and iauthor:
                infrasp_parts.append(escape(iauthor))
        if self.cv_group and not group_added:
            infrasp_parts.append(_("%(group)s Group") %
                                 dict(group=self.cv_group))

        if self.hybrid_marker == u'H':
            # totally do something else!
            # and return
            genera = set(i.genus for i in self.hybrid_operands)
            if len(genera) == 1:
                operands_str = [i.str(remove_zws=True, genus=False,
                                      markup=markup)
                                for i in self.hybrid_operands]
                prefix = italicize(genera.pop().str()) + u' '
            else:
                operands_str = [i.str(remove_zws=True, genus=True,
                                      markup=markup)
                                for i in self.hybrid_operands]
                prefix = u''
            return prefix + u' × '.join(operands_str)

        # create the binomial part
        binomial = [genus, self.hybrid_marker, epithet, author]

        # create the tail, ie: anything to add on to the end
        tail = []
        if self.aggregate:
            if sensu is not None:
                tail.append(sensu)
            else:
                tail.append(self.aggregate)

        if qualification is None:
            pass
        else:
            rank, qual = qualification
            if qual in ['incorrect']:
                rank = None
            if rank == 'sp':
                binomial.insert(2, qual)
            elif not rank:
                binomial[2] += ' (' + qual + ')'
            elif rank == 'genus':
                binomial.insert(0, qual)
            elif rank == 'infrasp':
                if infrasp_parts:
                    infrasp_parts.insert(0, qual)
            else:
                for r, e, a in infrasp:
                    if r == 'cv.':
                        e = "'%s'" % e
                    if rank == r:
                        pos = infrasp_parts.index(e)
                        infrasp_parts.insert(pos, qual)
                else:
                    logger.info('cannot find specified rank %s' % e)

        parts = chain(binomial, infrasp_parts, tail)
        s = utils.utf8(u' '.join(i for i in parts if i))
        return s

    @property
    def accepted(self):
        'Name that should be used if name of self should be rejected'
        from sqlalchemy.orm.session import object_session
        session = object_session(self)
        if not session:
            logger.warn('species:accepted - object not in session')
            return None
        syn = session.query(SpeciesSynonym).filter(
            SpeciesSynonym.synonym_id == self.id).first()
        accepted = syn and syn.species
        return accepted

    @accepted.setter
    def accepted(self, value):
        'Name that should be used if name of self should be rejected'
        logger.debug("Accepted taxon: %s %s" % (type(value), value))
        assert isinstance(value, self.__class__)
        if self in value.synonyms:
            return
        # remove any previous `accepted` link
        from sqlalchemy.orm.session import object_session
        session = object_session(self)
        if not session:
            logger.warn('species:accepted.setter - object not in session')
            return
        session.query(SpeciesSynonym).filter(
            SpeciesSynonym.synonym_id == self.id).delete()
        session.commit()
        value.synonyms.append(self)

    def has_accessions(self):
        '''true if species is linked to at least one accession
        '''

        return False

    infrasp_attr = {1: {'rank': 'infrasp1_rank',
                        'epithet': 'infrasp1',
                        'author': 'infrasp1_author'},
                    2: {'rank': 'infrasp2_rank',
                        'epithet': 'infrasp2',
                        'author': 'infrasp2_author'},
                    3: {'rank': 'infrasp3_rank',
                        'epithet': 'infrasp3',
                        'author': 'infrasp3_author'},
                    4: {'rank': 'infrasp4_rank',
                        'epithet': 'infrasp4',
                        'author': 'infrasp4_author'}}

    def get_infrasp(self, level):
        """
        level should be 1-4
        """
        return getattr(self, self.infrasp_attr[level]['rank']), \
            getattr(self, self.infrasp_attr[level]['epithet']), \
            getattr(self, self.infrasp_attr[level]['author'])

    def set_infrasp(self, level, rank, epithet, author=None):
        """
        level should be 1-4
        """
        setattr(self, self.infrasp_attr[level]['rank'], rank)
        setattr(self, self.infrasp_attr[level]['epithet'], epithet)
        setattr(self, self.infrasp_attr[level]['author'], author)

    def as_dict(self, recurse=True):
        result = dict((col, getattr(self, col))
                      for col in self.__table__.columns.keys()
                      if col not in ['id', 'epithet']
                      and col[0] != '_'
                      and getattr(self, col) is not None
                      and not col.endswith('_id'))
        result['object'] = 'taxon'
        result['rank'] = 'species'
        result['epithet'] = self.epithet
        result['ht-rank'] = 'genus'
        result['ht-epithet'] = self.genus.epithet
        if recurse and self.accepted is not None:
            result['accepted'] = self.accepted.as_dict(recurse=False)
        return result

    @classmethod
    def retrieve(cls, session, keys):
        from genus import Genus
        try:
            return session.query(cls).filter(
                cls.epithet == keys['epithet']).join(Genus).filter(
                Genus.epithet == keys['ht-epithet']).one()
        except:
            return None

    @classmethod
    def correct_field_names(cls, keys):
        for internal, exchange in [('genus', 'ht-epithet')]:
            if exchange in keys:
                keys[internal] = keys[exchange]
                del keys[exchange]

    @classmethod
    def compute_serializable_fields(cls, session, keys):
        from genus import Genus
        result = {'genus': None}
        ## retrieve genus object
        specifies_family = keys.get('familia')
        result['genus'] = Genus.retrieve_or_create(
            session, {'epithet': keys['ht-epithet'],
                      'ht-epithet': specifies_family},
            create=(specifies_family is not None))
        if result['genus'] is None:
            raise error.NoResultException()
        return result

    def top_level_count(self):
        plants = [p for a in self.accessions for p in a.plants]
        return {(1, 'Species'): 1,
                (2, 'Genera'): set([self.genus.id]),
                (3, 'Families'): set([self.genus.family.id]),
                (4, 'Accessions'): len(self.accessions),
                (5, 'Plantings'): len(plants),
                (6, 'Living plants'): sum(p.quantity for p in plants),
                (7, 'Locations'): set(p.location.id for p in plants),
                (8, 'Sources'): set([a.source.source_detail.id
                                     for a in self.accessions
                                     if a.source and a.source.source_detail])}
                (8, 'Sources'): set([a.source.source_detail.id
                                     for a in self.accessions
                                     if a.source and a.source.source_detail])}
hybrid_parent_role = (
    ('?', 'not specified'),
    ('m', 'pollen donor'),
    ('f', 'seed parent'),
    )

hybrid_operands_table = Table(
    'hybrid_operands', db.Base.metadata,
    Column('child_id', Integer, ForeignKey('species.id'), primary_key=True,
           nullable=False),
    Column('parent_id', Integer, ForeignKey('species.id'), primary_key=True,
           nullable=False),
    Column('role', types.Enum(values=dict(hybrid_parent_role).keys()),
           default=u'?'),
)

Species.hybrid_operands = relation(
    Species,
    secondary=lambda: hybrid_operands_table,
    primaryjoin=Species.id == hybrid_operands_table.c.child_id,
    secondaryjoin=Species.id == hybrid_operands_table.c.parent_id)


class SpeciesNote(db.Base, db.Serializable):
    """
    Notes for the species table
    """
    __tablename__ = 'species_note'
class Species(db.Base, db.Serializable, db.DefiningPictures):
    """
    :Table name: species

    :Columns:
        *sp*:
        *sp2*:
        *sp_author*:

        *hybrid*:
            Hybrid flag

        *infrasp1*:
        *infrasp1_rank*:
        *infrasp1_author*:

        *infrasp2*:
        *infrasp2_rank*:
        *infrasp2_author*:

        *infrasp3*:
        *infrasp3_rank*:
        *infrasp3_author*:

        *infrasp4*:
        *infrasp4_rank*:
        *infrasp4_author*:

        *cv_group*:
        *trade_name*:

        *sp_qual*:
            Species qualifier

            Possible values:
                *agg.*: An aggregate species

                *s. lat.*: aggregrate species (sensu lato)

                *s. str.*: segregate species (sensu stricto)

        *label_distribution*:
            UnicodeText
            This field is optional and can be used for the label in case
            str(self.distribution) is too long to fit on the label.

    :Properties:
        *accessions*:

        *vernacular_names*:

        *default_vernacular_name*:

        *synonyms*:

        *distribution*:

    :Constraints:
        The combination of sp, sp_author, hybrid, sp_qual,
        cv_group, trade_name, genus_id
    """
    __tablename__ = 'species'
    __mapper_args__ = {'order_by': ['sp', 'sp_author']}

    rank = 'species'
    link_keys = ['accepted']

    @property
    def cites(self):
        '''the cites status of this taxon, or None

        cites appendix number, one of I, II, or III.
        not enforced by the software in v1.0.x
        '''

        cites_notes = [
            i.note for i in self.notes
            if i.category and i.category.upper() == u'CITES'
        ]
        if not cites_notes:
            return self.genus.cites
        return cites_notes[0]

    @property
    def conservation(self):
        '''the IUCN conservation status of this taxon, or DD

        one of: EX, RE, CR, EN, VU, NT, LC, DD
        not enforced by the software in v1.0.x
        '''

        {
            'EX': _('Extinct (EX)'),
            'EW': _('Extinct Wild (EW)'),
            'RE': _('Regionally Extinct (RE)'),
            'CR': _('Critically Endangered (CR)'),
            'EN': _('Endangered (EN)'),
            'VU': _('Vulnerable (VU)'),
            'NT': _('Near Threatened (NT)'),
            'LV': _('Least Concern (LC)'),
            'DD': _('Data Deficient (DD)'),
            'NE': _('Not Evaluated (NE)')
        }

        notes = [
            i.note for i in self.notes
            if i.category and i.category.upper() == u'IUCN'
        ]
        return (notes + ['DD'])[0]

    @property
    def condition(self):
        '''the condition of this taxon, or None

        this is referred to what the garden conservator considers the
        area of interest. it is really an interpretation, not a fact.
        '''
        # one of, but not forcibly so:
        [_('endemic'), _('indigenous'), _('native'), _('introduced')]

        notes = [
            i.note for i in self.notes if i.category.lower() == u'condition'
        ]
        return (notes + [None])[0]

    # columns
    sp = Column(Unicode(64), index=True)
    sp2 = Column(Unicode(64), index=True)  # in case hybrid=True
    sp_author = Column(Unicode(128))
    hybrid = Column(Boolean, default=False)
    sp_qual = Column(types.Enum(values=['agg.', 's. lat.', 's. str.', None]),
                     default=None)
    cv_group = Column(Unicode(50))
    trade_name = Column(Unicode(64))

    infrasp1 = Column(Unicode(64))
    infrasp1_rank = Column(
        types.Enum(values=infrasp_rank_values.keys(),
                   translations=infrasp_rank_values))
    infrasp1_author = Column(Unicode(64))

    infrasp2 = Column(Unicode(64))
    infrasp2_rank = Column(
        types.Enum(values=infrasp_rank_values.keys(),
                   translations=infrasp_rank_values))
    infrasp2_author = Column(Unicode(64))

    infrasp3 = Column(Unicode(64))
    infrasp3_rank = Column(
        types.Enum(values=infrasp_rank_values.keys(),
                   translations=infrasp_rank_values))
    infrasp3_author = Column(Unicode(64))

    infrasp4 = Column(Unicode(64))
    infrasp4_rank = Column(
        types.Enum(values=infrasp_rank_values.keys(),
                   translations=infrasp_rank_values))
    infrasp4_author = Column(Unicode(64))

    genus_id = Column(Integer, ForeignKey('genus.id'), nullable=False)
    ## the Species.genus property is defined as backref in Genus.species

    label_distribution = Column(UnicodeText)
    bc_distribution = Column(UnicodeText)

    # relations
    synonyms = association_proxy('_synonyms', 'synonym')
    _synonyms = relation('SpeciesSynonym',
                         primaryjoin='Species.id==SpeciesSynonym.species_id',
                         cascade='all, delete-orphan',
                         uselist=True,
                         backref='species')

    # this is a dummy relation, it is only here to make cascading work
    # correctly and to ensure that all synonyms related to this genus
    # get deleted if this genus gets deleted
    _syn = relation('SpeciesSynonym',
                    primaryjoin='Species.id==SpeciesSynonym.synonym_id',
                    cascade='all, delete-orphan',
                    uselist=True)

    ## VernacularName.species gets defined here too.
    vernacular_names = relation('VernacularName',
                                cascade='all, delete-orphan',
                                collection_class=VNList,
                                backref=backref('species', uselist=False))
    _default_vernacular_name = relation('DefaultVernacularName',
                                        uselist=False,
                                        cascade='all, delete-orphan',
                                        backref=backref('species',
                                                        uselist=False))
    distribution = relation('SpeciesDistribution',
                            cascade='all, delete-orphan',
                            backref=backref('species', uselist=False))

    habit_id = Column(Integer, ForeignKey('habit.id'), default=None)
    habit = relation('Habit', uselist=False, backref='species')

    flower_color_id = Column(Integer, ForeignKey('color.id'), default=None)
    flower_color = relation('Color', uselist=False, backref='species')

    #hardiness_zone = Column(Unicode(4))

    awards = Column(UnicodeText)

    def __init__(self, *args, **kwargs):
        super(Species, self).__init__(*args, **kwargs)

    def __str__(self):
        '''
        returns a string representation of this species,
        calls Species.str(self)
        '''
        return Species.str(self)

    def _get_default_vernacular_name(self):
        if self._default_vernacular_name is None:
            return None
        return self._default_vernacular_name.vernacular_name

    def _set_default_vernacular_name(self, vn):
        if vn is None:
            del self.default_vernacular_name
            return
        if vn not in self.vernacular_names:
            self.vernacular_names.append(vn)
        d = DefaultVernacularName()
        d.vernacular_name = vn
        self._default_vernacular_name = d

    def _del_default_vernacular_name(self):
        utils.delete_or_expunge(self._default_vernacular_name)
        del self._default_vernacular_name

    default_vernacular_name = property(_get_default_vernacular_name,
                                       _set_default_vernacular_name,
                                       _del_default_vernacular_name)

    def distribution_str(self):
        if self.distribution is None:
            return ''
        else:
            dist = ['%s' % d for d in self.distribution]
            return unicode(', ').join(sorted(dist))

    def markup(self, authors=False):
        '''
        returns this object as a string with markup

        :param authors: flag to toggle whethe the author names should be
        included
        '''
        return Species.str(self, authors, True)

    # in PlantPlugins.init() we set this to 'x' for win32
    hybrid_char = utils.utf8(u'\u2a09')  # U+2A09

    @staticmethod
    def str(species, authors=False, markup=False):
        '''
        returns a string for species

        :param species: the species object to get the values from
        :param authors: flags to toggle whether the author names should be
        included
        :param markup: flags to toggle whether the returned text is marked up
        to show italics on the epithets
        '''
        # TODO: this method will raise an error if the session is none
        # since it won't be able to look up the genus....we could
        # probably try to query the genus directly with the genus_id
        genus = str(species.genus)
        sp = species.sp
        sp2 = species.sp2
        if markup:
            escape = utils.xml_safe
            italicize = lambda s: u'<i>%s</i>' % escape(s)
            genus = italicize(genus)
            if sp is not None:
                sp = italicize(species.sp)
            if sp2 is not None:
                sp2 = italicize(species.sp2)
        else:
            italicize = lambda s: u'%s' % s
            escape = lambda x: x

        author = None
        if authors and species.sp_author:
            author = escape(species.sp_author)

        infrasp = ((species.infrasp1_rank, species.infrasp1,
                    species.infrasp1_author),
                   (species.infrasp2_rank, species.infrasp2,
                    species.infrasp2_author),
                   (species.infrasp3_rank, species.infrasp3,
                    species.infrasp3_author), (species.infrasp4_rank,
                                               species.infrasp4,
                                               species.infrasp4_author))

        infrasp_parts = []
        group_added = False
        for rank, epithet, iauthor in infrasp:
            if rank == 'cv.' and epithet:
                if species.cv_group and not group_added:
                    group_added = True
                    infrasp_parts.append(
                        _("(%(group)s Group)") % dict(group=species.cv_group))
                infrasp_parts.append("'%s'" % escape(epithet))
            else:
                if rank:
                    infrasp_parts.append(rank)
                if epithet and rank:
                    infrasp_parts.append(italicize(epithet))
                elif epithet:
                    infrasp_parts.append(escape(epithet))

            if authors and iauthor:
                infrasp_parts.append(escape(iauthor))
        if species.cv_group and not group_added:
            infrasp_parts.append(
                _("%(group)s Group") % dict(group=species.cv_group))

        # create the binomial part
        binomial = []
        if species.hybrid:
            if species.sp2:
                binomial = [genus, sp, species.hybrid_char, sp2, author]
            else:
                binomial = [genus, species.hybrid_char, sp, author]
        else:
            binomial = [genus, sp, sp2, author]

        # create the tail a.k.a think to add on to the end
        tail = []
        if species.sp_qual:
            tail = [species.sp_qual]

        parts = chain(binomial, infrasp_parts, tail)
        s = utils.utf8(' '.join(filter(lambda x: x not in ('', None), parts)))
        return s

    @property
    def accepted(self):
        'Name that should be used if name of self should be rejected'
        from sqlalchemy.orm.session import object_session
        session = object_session(self)
        syn = session.query(SpeciesSynonym).filter(
            SpeciesSynonym.synonym_id == self.id).first()
        accepted = syn and syn.species
        return accepted

    @accepted.setter
    def accepted(self, value):
        'Name that should be used if name of self should be rejected'
        assert isinstance(value, self.__class__)
        if self in value.synonyms:
            return
        value.synonyms.append(self)

    def has_accessions(self):
        '''true if species is linked to at least one accession
        '''

        return False

    infrasp_attr = {
        1: {
            'rank': 'infrasp1_rank',
            'epithet': 'infrasp1',
            'author': 'infrasp1_author'
        },
        2: {
            'rank': 'infrasp2_rank',
            'epithet': 'infrasp2',
            'author': 'infrasp2_author'
        },
        3: {
            'rank': 'infrasp3_rank',
            'epithet': 'infrasp3',
            'author': 'infrasp3_author'
        },
        4: {
            'rank': 'infrasp4_rank',
            'epithet': 'infrasp4',
            'author': 'infrasp4_author'
        }
    }

    def get_infrasp(self, level):
        """
        level should be 1-4
        """
        return getattr(self, self.infrasp_attr[level]['rank']), \
            getattr(self, self.infrasp_attr[level]['epithet']), \
            getattr(self, self.infrasp_attr[level]['author'])

    def set_infrasp(self, level, rank, epithet, author=None):
        """
        level should be 1-4
        """
        setattr(self, self.infrasp_attr[level]['rank'], rank)
        setattr(self, self.infrasp_attr[level]['epithet'], epithet)
        setattr(self, self.infrasp_attr[level]['author'], author)

    def as_dict(self, recurse=True):
        result = dict(
            (col, getattr(self, col)) for col in self.__table__.columns.keys()
            if col not in ['id', 'sp'] and col[0] != '_'
            and getattr(self, col) is not None and not col.endswith('_id'))
        result['object'] = 'taxon'
        result['rank'] = 'species'
        result['epithet'] = self.sp
        result['ht-rank'] = 'genus'
        result['ht-epithet'] = self.genus.genus
        if recurse and self.accepted is not None:
            result['accepted'] = self.accepted.as_dict(recurse=False)
        return result

    @classmethod
    def correct_field_names(cls, keys):
        for internal, exchange in [('sp_author', 'author'), ('sp', 'epithet')]:
            if exchange in keys:
                keys[internal] = keys[exchange]
                del keys[exchange]

    @classmethod
    def retrieve(cls, session, keys):
        from genus import Genus
        try:
            return session.query(cls).filter(
                cls.sp == keys['epithet']).join(Genus).filter(
                    Genus.genus == keys['ht-epithet']).one()
        except:
            return None

    @classmethod
    def compute_serializable_fields(cls, session, keys):
        from genus import Genus
        result = {'genus': None}
        ## retrieve genus object
        specifies_family = keys.get('familia')
        result['genus'] = Genus.retrieve_or_create(
            session, {
                'epithet': keys['ht-epithet'],
                'ht-epithet': specifies_family
            },
            create=(specifies_family is not None))
        if result['genus'] is None:
            raise error.NoResultException()
        return result
示例#11
0
class Genus(db.Base, db.Serializable, db.WithNotes):
    """
    :Table name: genus

    :Contraints:
        The combination of genus, author, aggregate
        and family_id must be unique.
    """
    __tablename__ = 'genus'
    __table_args__ = (UniqueConstraint('epithet', 'author', 'aggregate',
                                       'family_id'), {})
    __mapper_args__ = {'order_by': ['genus', 'author']}

    rank = 'genus'
    link_keys = ['accepted']

    # columns - common for all taxa
    epithet = Column(Unicode(64), nullable=False, index=True)
    hybrid_marker = Column(types.Enum(values=dict(itf2.hybrid_marker).keys()),
                           default=u'')
    author = Column(Unicode(255), default=u'')
    aggregate = Column(types.Enum(values=dict(itf2.aggregate).keys()),
                       default=u'')

    family_id = Column(Integer, ForeignKey('family.id'), nullable=False)

    def search_view_markup_pair(self):
        '''provide the two lines describing object for SearchView row.
        '''
        return utils.xml_safe(self.str(use_hybrid_marker=True),
                              utils.xml_safe(self.family))

    @property
    def cites(self):
        '''the cites status of this taxon, or None
        '''

        cites_notes = [
            i.note for i in self.notes
            if i.category and i.category.upper() == 'CITES'
        ]
        if not cites_notes:
            return self.family.cites
        return cites_notes[0]

    @validates('genus', 'author')
    def validate_stripping(self, key, value):
        if value is None:
            return None
        return value.strip()

    # relations
    # `species` relation is defined outside of `Genus` class definition
    synonyms = association_proxy('_synonyms', 'synonym')
    _synonyms = relation('GenusSynonym',
                         primaryjoin='Genus.id==GenusSynonym.genus_id',
                         cascade='all, delete-orphan',
                         uselist=True,
                         backref='genus')

    # this is a dummy relation, it is only here to make cascading work
    # correctly and to ensure that all synonyms related to this genus
    # get deleted if this genus gets deleted
    __syn = relation('GenusSynonym',
                     primaryjoin='Genus.id==GenusSynonym.synonym_id',
                     cascade='all, delete-orphan',
                     uselist=True)

    @property
    def accepted(self):
        'Name that should be used if name of self should be rejected'
        session = object_session(self)
        if not session:
            logger.warn('genus:accepted - object not in session')
            return None
        syn = session.query(GenusSynonym).filter(
            GenusSynonym.synonym_id == self.id).first()
        accepted = syn and syn.genus
        return accepted

    @accepted.setter
    def accepted(self, value):
        'Name that should be used if name of self should be rejected'
        assert isinstance(value, self.__class__)
        if self in value.synonyms:
            return
        # remove any previous `accepted` link
        session = object_session(self)
        if not session:
            logger.warn('genus:accepted.setter - object not in session')
            return
        session.query(GenusSynonym).filter(
            GenusSynonym.synonym_id == self.id).delete()
        session.commit()
        value.synonyms.append(self)

    def __repr__(self):
        return self.str()

    def str(self, author=False, use_hybrid_marker=False):
        # string representation of genus, good for markup.
        prepend = use_hybrid_marker and self.hybrid_marker or ''
        if self.epithet is None:
            return repr(self)
        elif not author or self.author is None:
            return (prepend + ' '.join([
                s
                for s in [self.epithet, self.aggregate] if s not in ('', None)
            ]))
        else:
            return (prepend + ' '.join([
                s for s in [
                    self.epithet, self.aggregate,
                    xml.sax.saxutils.escape(self.author)
                ] if s not in ('', None)
            ]))

    def has_accessions(self):
        '''true if genus is linked to at least one accession
        '''

        return False

    def as_dict(self, recurse=True):
        result = db.Serializable.as_dict(self)
        result['object'] = 'taxon'
        result['rank'] = 'genus'
        result['epithet'] = self.epithet
        result['ht-rank'] = 'familia'
        result['ht-epithet'] = self.family.epithet
        if recurse and self.accepted is not None:
            result['accepted'] = self.accepted.as_dict(recurse=False)
        return result

    @classmethod
    def retrieve(cls, session, keys):
        # hybrid spec is contained in epithet
        firstchar = keys['epithet'][0]
        if firstchar in [u'×', u'x', u'+']:
            keys['hybrid_marker'] = {'x': u'×'}.get(firstchar, firstchar)
            keys['epithet'] = keys['epithet'][1:]
        if keys['epithet'].find(u'×') != -1:
            keys['hybrid_marker'] = u'H'
        try:
            return session.query(cls).filter(
                cls.epithet == keys['epithet']).one()
        except:
            if 'author' not in keys:
                return None
        try:
            return session.query(cls).filter(
                cls.epithet == keys['epithet'],
                cls.author == keys['author']).one()
        except:
            return None

    @classmethod
    def correct_field_names(cls, keys):
        for internal, exchange in [('family', 'ht-epithet')]:
            if exchange in keys:
                keys[internal] = keys[exchange]
                del keys[exchange]

    @classmethod
    def compute_serializable_fields(cls, session, keys):
        from family import Family
        result = {'family': None}
        ## retrieve family object
        if keys.get('ht-epithet'):
            result['family'] = Family.retrieve_or_create(
                session, {'epithet': keys['ht-epithet']}, create=True)
        if result['family'] is None:
            raise error.NoResultException()
        return result

    def top_level_count(self):
        accessions = [a for s in self.species for a in s.accessions]
        plants = [p for a in accessions for p in a.plants]
        return {
            (1, 'Genera'):
            set([self.id]),
            (2, 'Families'):
            set([self.family.id]),
            (3, 'Species'):
            len(self.species),
            (4, 'Accessions'):
            len(accessions),
            (5, 'Plantings'):
            len(plants),
            (6, 'Living plants'):
            sum(p.quantity for p in plants),
            (7, 'Locations'):
            set(p.location.id for p in plants),
            (8, 'Sources'):
            set([
                a.source.source_detail.id for a in accessions
                if a.source and a.source.source_detail
            ])
        }