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))
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
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 ]) }
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
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
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 ]) }