class Plot(MapFeature): width = models.FloatField(null=True, blank=True, help_text=trans("Plot Width")) length = models.FloatField(null=True, blank=True, help_text=trans("Plot Length")) owner_orig_id = models.CharField(max_length=255, null=True, blank=True) objects = GeoHStoreUDFManager() @classproperty def benefits(cls): from treemap.ecobenefits import TreeBenefitsCalculator return TreeBenefitsCalculator() def nearby_plots(self, distance_in_meters=None): if distance_in_meters is None: distance_in_meters = settings.NEARBY_TREE_DISTANCE distance_filter = Plot.objects.filter( geom__distance_lte=(self.geom, D(m=distance_in_meters))) return distance_filter\ .filter(instance=self.instance)\ .exclude(pk=self.pk) def get_tree_history(self): """ Get a list of all tree ids that were ever assigned to this plot """ return Audit.objects.filter(instance=self.instance)\ .filter(model='Tree')\ .filter(field='plot')\ .filter(current_value=self.pk)\ .order_by('-model_id', '-updated')\ .distinct('model_id')\ .values_list('model_id', flat=True) def current_tree(self): """ This is a compatibility method that is used by the API to select the 'current tree'. Right now OTM only supports one tree per plot, so this method returns the 'first' tree """ trees = list(self.tree_set.all()) if trees: return trees[0] else: return None def delete_with_user(self, user, cascade=False, *args, **kwargs): if self.current_tree() and cascade is False: raise ValidationError(trans( "Cannot delete plot with existing trees.")) super(Plot, self).delete_with_user(user, *args, **kwargs) @classproperty def display_name(cls): return trans('Planting Site')
class Tree(Convertible, UDFModel, PendingAuditable, ValidationMixin): """ Represents a single tree, belonging to an instance """ instance = models.ForeignKey(Instance) plot = models.ForeignKey(Plot) species = models.ForeignKey(Species, null=True, blank=True, verbose_name=_("Species")) readonly = models.BooleanField(default=False) diameter = models.FloatField(null=True, blank=True, verbose_name=_("Tree Diameter")) height = models.FloatField(null=True, blank=True, verbose_name=_("Tree Height")) canopy_height = models.FloatField(null=True, blank=True, verbose_name=_("Canopy Height")) date_planted = models.DateField(null=True, blank=True, verbose_name=_("Date Planted")) date_removed = models.DateField(null=True, blank=True, verbose_name=_("Date Removed")) objects = GeoHStoreUDFManager() _stewardship_choices = [ 'Watered', 'Pruned', 'Mulched, Had Compost Added, or Soil Amended', 'Cleared of Trash or Debris' ] udf_settings = { 'Stewardship': { 'iscollection': True, 'range_field_key': 'Date', 'action_field_key': 'Action', 'action_verb': 'that have been', 'defaults': [{ 'name': 'Action', 'choices': _stewardship_choices, 'type': 'choice' }, { 'type': 'date', 'name': 'Date' }], }, 'Alerts': { 'iscollection': True, 'warning_message': _("Marking a tree with an alert does not serve as a way to " "report problems with a tree. If you have any emergency " "tree concerns, please contact your city directly."), 'range_field_key': 'Date Noticed', 'action_field_key': 'Action Needed', 'action_verb': _('with open alerts for'), } } def __unicode__(self): diameter_chunk = ("Diameter: %s, " % self.diameter if self.diameter else "") species_chunk = ("Species: %s - " % self.species if self.species else "") return "%s%s" % (diameter_chunk, species_chunk) _terminology = {'singular': _('Tree'), 'plural': _('Trees')} def dict(self): props = self.as_dict() props['species'] = self.species return props def photos(self): return self.treephoto_set.order_by('-created_at') @property def itree_code(self): return self.species.get_itree_code(self.plot.itree_region.code) ########################## # tree validation ########################## def validate_diameter(self): if self.species: max_value = self.species.max_diameter else: max_value = Species.DEFAULT_MAX_DIAMETER self.validate_positive_nullable_float_field('diameter', max_value) def validate_height(self): if self.species: max_value = self.species.max_height else: max_value = Species.DEFAULT_MAX_HEIGHT self.validate_positive_nullable_float_field('height', max_value) def validate_canopy_height(self): self.validate_positive_nullable_float_field('canopy_height') def clean(self): super(Tree, self).clean() if self.plot and self.plot.instance != self.instance: raise ValidationError('Cannot save to a plot in another instance') def save_with_user(self, user, *args, **kwargs): self.full_clean_with_user(user) # These validations must be done after the field values have # been converted to database units but `convert_to_database_units` # calls `clean`, so these validations cannot be part of `clean`. self.validate_diameter() self.validate_height() self.validate_canopy_height() self.plot.update_updated_at() super(Tree, self).save_with_user(user, *args, **kwargs) @property def hash(self): string_to_hash = super(Tree, self).hash # We need to include tree photos in this hash as well photos = [str(photo.pk) for photo in self.treephoto_set.all()] string_to_hash += ":" + ",".join(photos) return hashlib.md5(string_to_hash).hexdigest() def add_photo(self, image, user): tp = TreePhoto(tree=self, instance=self.instance) tp.set_image(image) tp.save_with_user(user) return tp @classmethod def action_format_string_for_audit(clz, audit): if audit.field in set(['plot', 'readonly']): if audit.field == 'plot': return _action_format_string_for_location(audit.action) else: # audit.field == 'readonly' return _action_format_string_for_readonly( audit.action, audit.clean_current_value) else: return super(Tree, clz).action_format_string_for_audit(audit) @transaction.atomic def delete_with_user(self, user, *args, **kwargs): photos = self.photos() for photo in photos: photo.delete_with_user(user) self.plot.update_updated_at() self.instance.update_universal_rev() super(Tree, self).delete_with_user(user, *args, **kwargs)
class Plot(MapFeature, ValidationMixin): width = models.FloatField(null=True, blank=True, verbose_name=_("Planting Site Width")) length = models.FloatField(null=True, blank=True, verbose_name=_("Planting Site Length")) owner_orig_id = models.CharField(max_length=255, null=True, blank=True) objects = GeoHStoreUDFManager() is_editable = True _terminology = { 'singular': _('Planting Site'), 'plural': _('Planting Sites') } udf_settings = { 'Stewardship': { 'iscollection': True, 'range_field_key': 'Date', 'action_field_key': 'Action', 'action_verb': _('that have been'), 'defaults': [{ 'name': 'Action', 'choices': [ 'Enlarged', 'Changed to Include a Guard', 'Changed to Remove a Guard', 'Filled with Herbaceous Plantings' ], 'type': 'choice' }, { 'type': 'date', 'name': 'Date' }], }, 'Alerts': { 'iscollection': True, 'warning_message': _("Marking a planting site with an alert does not serve as a " "way to report problems with that site. If you have any " "emergency concerns, please contact your city directly."), 'range_field_key': 'Date Noticed', 'action_field_key': 'Action Needed', 'action_verb': _('with open alerts for'), } } @classproperty def benefits(cls): from treemap.ecobenefits import TreeBenefitsCalculator return TreeBenefitsCalculator() def nearby_plots(self, distance_in_meters=None): if distance_in_meters is None: distance_in_meters = settings.NEARBY_TREE_DISTANCE distance_filter = Plot.objects.filter( geom__distance_lte=(self.geom, D(m=distance_in_meters))) return distance_filter\ .filter(instance=self.instance)\ .exclude(pk=self.pk) def get_tree_history(self): """ Get a list of all tree ids that were ever assigned to this plot """ return Audit.objects.filter(instance=self.instance)\ .filter(model='Tree')\ .filter(field='plot')\ .filter(current_value=self.pk)\ .order_by('-model_id', '-updated')\ .distinct('model_id')\ .values_list('model_id', flat=True) def current_tree(self): """ This is a compatibility method that is used by the API to select the 'current tree'. Right now OTM only supports one tree per plot, so this method returns the 'first' tree """ trees = list(self.tree_set.all()) if trees: return trees[0] else: return None def save_with_user(self, user, *args, **kwargs): self.full_clean_with_user(user) # These validations must be done after the field values have # been converted to database units but `convert_to_database_units` # calls `clean`, so these validations cannot be part of `clean`. self.validate_positive_nullable_float_field('width') self.validate_positive_nullable_float_field('length') super(Plot, self).save_with_user(user, *args, **kwargs) def delete_with_user(self, user, cascade=False, *args, **kwargs): if self.current_tree() and cascade is False: raise ValidationError(_("Cannot delete plot with existing trees.")) super(Plot, self).delete_with_user(user, *args, **kwargs)
class MapFeature(Convertible, UDFModel, PendingAuditable): "Superclass for map feature subclasses like Plot, RainBarrel, etc." instance = models.ForeignKey(Instance) geom = models.PointField(srid=3857, db_column='the_geom_webmercator') address_street = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Address")) address_city = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("City")) address_zip = models.CharField(max_length=30, blank=True, null=True, verbose_name=_("Postal Code")) readonly = models.BooleanField(default=False) # Although this can be retrieved with a MAX() query on the audit # table, we store a "cached" value here to keep filtering easy and # efficient. updated_at = models.DateTimeField(default=timezone.now, verbose_name=_("Last Updated")) # Tells the permission system that if any other field is writable, # updated_at is also writable joint_writable = {'updated_at', 'hide_at_zoom'} objects = GeoHStoreUDFManager() # subclass responsibilities area_field_name = None is_editable = None # When querying MapFeatures (as opposed to querying a subclass like Plot), # we get a heterogenous collection (some Plots, some RainBarrels, etc.). # The feature_type attribute tells us the type of each object. feature_type = models.CharField(max_length=255) hide_at_zoom = models.IntegerField(null=True, blank=True, default=None, db_index=True) def __init__(self, *args, **kwargs): super(MapFeature, self).__init__(*args, **kwargs) if self.feature_type == '': self.feature_type = self.map_feature_type self._do_not_track.add('feature_type') self._do_not_track.add('mapfeature_ptr') self._do_not_track.add('hide_at_zoom') self.populate_previous_state() @property def _is_generic(self): return self.__class__.__name__ == 'MapFeature' @classproperty def geom_field_name(cls): return "%s.geom" % to_object_name(cls.map_feature_type) @property def is_plot(self): return getattr(self, 'feature_type', None) == 'Plot' def update_updated_at(self): """Changing a child object of a map feature (tree, photo, etc.) demands that we update the updated_at field on the parent map_feature, however there is likely code throughout the application that saves updates to a child object without calling save on the parent MapFeature. This method intended to by called in the save method of those child objects.""" self.updated_at = timezone.now() MapFeature.objects.filter(pk=self.pk).update( updated_at=self.updated_at) def save_with_user(self, user, *args, **kwargs): self.full_clean_with_user(user) if self._is_generic: raise Exception( 'Never save a MapFeature -- only save a MapFeature subclass') self.updated_at = timezone.now() super(MapFeature, self).save_with_user(user, *args, **kwargs) def clean(self): super(MapFeature, self).clean() if self.geom is None: raise ValidationError( {"geom": [_("Feature location is not specified")]}) if not self.instance.bounds.geom.contains(self.geom): raise ValidationError({ "geom": [ _("%(model)s must be created inside the map boundaries") % { 'model': self.terminology(self.instance)['plural'] } ] }) def delete_with_user(self, user, *args, **kwargs): self.instance.update_revs('geo_rev', 'eco_rev', 'universal_rev') super(MapFeature, self).delete_with_user(user, *args, **kwargs) def photos(self): return self.mapfeaturephoto_set.order_by('-created_at') def add_photo(self, image, user): photo = MapFeaturePhoto(map_feature=self, instance=self.instance) photo.set_image(image) photo.save_with_user(user) return photo @classproperty def map_feature_type(cls): # Map feature type defaults to subclass name (e.g. 'Plot'). # Subclasses can override it if they want something different. # (But note that the value gets stored in the database, so should not # be changed for a subclass once objects have been saved.) return cls.__name__ @classmethod def subclass_dict(cls): return { C.map_feature_type: C for C in leaf_models_of_class(MapFeature) } @classmethod def has_subclass(cls, type): return type in cls.subclass_dict() @classmethod def get_subclass(cls, type): try: return cls.subclass_dict()[type] except KeyError as e: raise ValidationError('Map feature type %s not found' % e) @property def address_full(self): components = [] if self.address_street: components.append(self.address_street) if self.address_city: components.append(self.address_city) if self.address_zip: components.append(self.address_zip) return ', '.join(components) @classmethod def action_format_string_for_audit(clz, audit): if audit.field in set(['geom', 'readonly']): if audit.field == 'geom': return _action_format_string_for_location(audit.action) else: # field == 'readonly' return _action_format_string_for_readonly( audit.action, audit.clean_current_value) else: return super(MapFeature, clz).action_format_string_for_audit(audit) @property def hash(self): string_to_hash = super(MapFeature, self).hash if self.is_plot: # The hash for a plot includes the hash for its trees tree_hashes = [t.hash for t in self.plot.tree_set.all()] string_to_hash += "," + ",".join(tree_hashes) return hashlib.md5(string_to_hash).hexdigest() def title(self): # Cast allows the map feature subclass to handle generating # the display name feature = self.cast_to_subtype() if feature.is_plot: tree = feature.current_tree() if tree: if tree.species: title = tree.species.common_name else: title = _("Missing Species") else: title = _("Empty Planting Site") else: title = feature.display_name(self.instance) return title def contained_plots(self): if self.area_field_name is not None: plots = Plot.objects \ .filter(instance=self.instance) \ .filter(geom__within=getattr(self, self.area_field_name)) \ .prefetch_related('tree_set', 'tree_set__species') def key_sort(plot): tree = plot.current_tree() if tree is None: return (0, None) if tree.species is None: return (1, None) return (2, tree.species.common_name) return sorted(plots, key=key_sort) return None def cast_to_subtype(self): """ Return the concrete subclass instance. For example, if self is a MapFeature with subtype Plot, return self.plot """ if type(self) is not MapFeature: # This shouldn't really ever happen, but there's no point trying to # cast a subclass to itself return self ft = self.feature_type if hasattr(self, ft.lower()): return getattr(self, ft.lower()) else: return getattr(self.polygonalmapfeature, ft.lower()) def safe_get_current_tree(self): if hasattr(self, 'current_tree'): return self.current_tree() else: return None def __unicode__(self): x = self.geom.x if self.geom else "?" y = self.geom.y if self.geom else "?" address = self.address_street or "Address Unknown" text = "%s (%s, %s) %s" % (self.feature_type, x, y, address) return text @classproperty def _terminology(cls): return {'singular': cls.__name__} @classproperty def benefits(cls): from treemap.ecobenefits import CountOnlyBenefitCalculator return CountOnlyBenefitCalculator(cls) @property def itree_region(self): regions = self.instance.itree_regions(geometry__contains=self.geom) if regions: return regions[0] else: return ITreeRegionInMemory(None)
class Species(UDFModel, PendingAuditable): """ http://plants.usda.gov/adv_search.html """ DEFAULT_MAX_DIAMETER = 200 DEFAULT_MAX_HEIGHT = 800 ### Base required info instance = models.ForeignKey(Instance) # ``otm_code`` is the key used to link this instance # species row to a cannonical species. An otm_code # is usually the USDA code, but this is not guaranteed. otm_code = models.CharField(max_length=255) common_name = models.CharField(max_length=255, verbose_name='Common Name') genus = models.CharField(max_length=255, verbose_name='Genus') species = models.CharField(max_length=255, blank=True, verbose_name='Species') cultivar = models.CharField(max_length=255, blank=True, verbose_name='Cultivar') other_part_of_name = models.CharField(max_length=255, blank=True, verbose_name='Other Part of Name') ### From original OTM (some renamed) ### is_native = models.NullBooleanField(verbose_name='Native to Region') flowering_period = models.CharField(max_length=255, blank=True, verbose_name='Flowering Period') fruit_or_nut_period = models.CharField(max_length=255, blank=True, verbose_name='Fruit or Nut Period') fall_conspicuous = models.NullBooleanField(verbose_name='Fall Conspicuous') flower_conspicuous = models.NullBooleanField( verbose_name='Flower Conspicuous') palatable_human = models.NullBooleanField(verbose_name='Edible') has_wildlife_value = models.NullBooleanField( verbose_name='Has Wildlife Value') fact_sheet_url = models.URLField(max_length=255, blank=True, verbose_name='Fact Sheet URL') plant_guide_url = models.URLField(max_length=255, blank=True, verbose_name='Plant Guide URL') ### Used for validation max_diameter = models.IntegerField(default=DEFAULT_MAX_DIAMETER, verbose_name='Max Diameter') max_height = models.IntegerField(default=DEFAULT_MAX_HEIGHT, verbose_name='Max Height') updated_at = models.DateTimeField( # TODO: remove null=True null=True, auto_now=True, editable=False, db_index=True) objects = GeoHStoreUDFManager() @property def display_name(self): return "%s [%s]" % (self.common_name, self.scientific_name) @classmethod def get_scientific_name(clazz, genus, species, cultivar): name = genus if species: name += " " + species if cultivar: name += " '%s'" % cultivar return name @property def scientific_name(self): return Species.get_scientific_name(self.genus, self.species, self.cultivar) def dict(self): props = self.as_dict() props['scientific_name'] = self.scientific_name return props def get_itree_code(self, region_code=None): if not region_code: regions = self.instance.itree_regions() if len(regions) == 1: region_code = regions[0].code else: return None override = ITreeCodeOverride.objects.filter( instance_species=self, region=ITreeRegion.objects.get(code=region_code), ) if override.exists(): return override[0].itree_code else: return get_itree_code(region_code, self.otm_code) def __unicode__(self): return self.display_name class Meta: verbose_name = "Species" verbose_name_plural = "Species" unique_together = ( 'instance', 'common_name', 'genus', 'species', 'cultivar', 'other_part_of_name', )
class Tree(Convertible, UDFModel, PendingAuditable): """ Represents a single tree, belonging to an instance """ instance = models.ForeignKey(Instance) plot = models.ForeignKey(Plot) species = models.ForeignKey(Species, null=True, blank=True) readonly = models.BooleanField(default=False) diameter = models.FloatField(null=True, blank=True, help_text=trans("Tree Diameter")) height = models.FloatField(null=True, blank=True, help_text=trans("Tree Height")) canopy_height = models.FloatField(null=True, blank=True, help_text=trans("Canopy Height")) date_planted = models.DateField(null=True, blank=True, help_text=trans("Date Planted")) date_removed = models.DateField(null=True, blank=True, help_text=trans("Date Removed")) objects = GeoHStoreUDFManager() def __unicode__(self): diameter_chunk = ("Diameter: %s, " % self.diameter if self.diameter else "") species_chunk = ("Species: %s - " % self.species if self.species else "") return "%s%s" % (diameter_chunk, species_chunk) def dict(self): props = self.as_dict() props['species'] = self.species return props def photos(self): return self.treephoto_set.order_by('-created_at') @property def itree_region(self): if self.instance.itree_region_default: region = self.instance.itree_region_default else: regions = ITreeRegion.objects\ .filter(geometry__contains=self.plot.geom) if len(regions) > 0: region = regions[0].code else: region = None return region ########################## # tree validation ########################## def clean(self): super(Tree, self).clean() if self.plot and self.plot.instance != self.instance: raise ValidationError('Cannot save to a plot in another instance') def save_with_user(self, user, *args, **kwargs): self.full_clean_with_user(user) super(Tree, self).save_with_user(user, *args, **kwargs) @property def hash(self): string_to_hash = super(Tree, self).hash # We need to include tree photos in this hash as well photos = [str(photo.pk) for photo in self.treephoto_set.all()] string_to_hash += ":" + ",".join(photos) return hashlib.md5(string_to_hash).hexdigest() def add_photo(self, image, user): tp = TreePhoto(tree=self, instance=self.instance) tp.set_image(image) tp.save_with_user(user) return tp @classmethod def action_format_string_for_audit(clz, audit): if audit.field in set(['plot', 'readonly']): if audit.field == 'plot': return _action_format_string_for_location(audit.action) else: # audit.field == 'readonly' return _action_format_string_for_readonly( audit.action, audit.clean_current_value) else: return super(Tree, clz).action_format_string_for_audit(audit) @transaction.atomic def delete_with_user(self, user, *args, **kwargs): photos = self.photos() for photo in photos: photo.delete_with_user(user) super(Tree, self).delete_with_user(user, *args, **kwargs)
class MapFeature(Convertible, UDFModel, PendingAuditable): "Superclass for map feature subclasses like Plot, RainBarrel, etc." instance = models.ForeignKey(Instance) geom = models.PointField(srid=3857, db_column='the_geom_webmercator') address_street = models.CharField(max_length=255, blank=True, null=True) address_city = models.CharField(max_length=255, blank=True, null=True) address_zip = models.CharField(max_length=30, blank=True, null=True) readonly = models.BooleanField(default=False) objects = GeoHStoreUDFManager() area_field_name = None # subclass responsibility # When querying MapFeatures (as opposed to querying a subclass like Plot), # we get a heterogenous collection (some Plots, some RainBarrels, etc.). # The feature_type attribute tells us the type of each object. feature_type = models.CharField(max_length=255) def __init__(self, *args, **kwargs): super(MapFeature, self).__init__(*args, **kwargs) if self.feature_type == '': self.feature_type = self.map_feature_type self._do_not_track.add('feature_type') self._do_not_track.add('mapfeature_ptr') self.populate_previous_state() @property def _is_generic(self): return self.__class__.__name__ == 'MapFeature' @classproperty def geom_field_name(cls): return "%s.geom" % to_object_name(cls.map_feature_type) @property def is_plot(self): return getattr(self, 'feature_type', None) == 'Plot' def save_with_user(self, user, *args, **kwargs): self.full_clean_with_user(user) if self._is_generic: raise Exception( 'Never save a MapFeature -- only save a MapFeature subclass') super(MapFeature, self).save_with_user(user, *args, **kwargs) def clean(self): super(MapFeature, self).clean() if not self.instance.bounds.contains(self.geom): raise ValidationError({ "geom": [ trans( "%(model)ss must be created inside the map boundaries") % { 'model': self.display_name } ] }) def photos(self): return self.mapfeaturephoto_set.order_by('-created_at') def add_photo(self, image, user): photo = MapFeaturePhoto(map_feature=self, instance=self.instance) photo.set_image(image) photo.save_with_user(user) return photo @classproperty def map_feature_type(cls): # Map feature type defaults to subclass name (e.g. 'Plot'). # Subclasses can override it if they want something different. # (But note that the value gets stored in the database, so should not # be changed for a subclass once objects have been saved.) return cls.__name__ @classproperty def display_name(cls): # Subclasses should override with something useful return cls.map_feature_type @classmethod def subclass_dict(cls): return {C.map_feature_type: C for C in leaf_subclasses(MapFeature)} @classmethod def has_subclass(cls, type): return type in cls.subclass_dict() @classmethod def get_subclass(cls, type): try: return cls.subclass_dict()[type] except KeyError as e: raise ValidationError('Map feature type %s not found' % e) @classmethod def create(cls, type, instance): """ Create a map feature with the given type string (e.g. 'Plot') """ return cls.get_subclass(type)(instance=instance) @property def address_full(self): components = [] if self.address_street: components.append(self.address_street) if self.address_city: components.append(self.address_city) if self.address_zip: components.append(self.address_zip) return ', '.join(components) @classmethod def action_format_string_for_audit(clz, audit): if audit.field in set(['geom', 'readonly']): if audit.field == 'geom': return _action_format_string_for_location(audit.action) else: # field == 'readonly' return _action_format_string_for_readonly( audit.action, audit.clean_current_value) else: return super(MapFeature, clz).action_format_string_for_audit(audit) @property def hash(self): string_to_hash = super(MapFeature, self).hash if self.is_plot: # The hash for a plot includes the hash for its trees tree_hashes = [t.hash for t in self.plot.tree_set.all()] string_to_hash += "," + ",".join(tree_hashes) return hashlib.md5(string_to_hash).hexdigest() def __unicode__(self): x = self.geom.x if self.geom else "?" y = self.geom.y if self.geom else "?" address = self.address_street or "Address Unknown" text = "%s (%s, %s) %s" % (self.feature_type, x, y, address) return text
class Species(UDFModel, PendingAuditable): """ http://plants.usda.gov/adv_search.html """ ### Base required info instance = models.ForeignKey(Instance) # ``otm_code`` is the key used to link this instance # species row to a cannonical species. An otm_code # is usually the USDA code, but this is not guaranteed. otm_code = models.CharField(max_length=255) common_name = models.CharField(max_length=255) genus = models.CharField(max_length=255) species = models.CharField(max_length=255, blank=True) cultivar = models.CharField(max_length=255, blank=True) other_part_of_name = models.CharField(max_length=255, blank=True) ### From original OTM (some renamed) ### is_native = models.NullBooleanField() flowering_period = models.CharField(max_length=255, blank=True) fruit_or_nut_period = models.CharField(max_length=255, blank=True) fall_conspicuous = models.NullBooleanField() flower_conspicuous = models.NullBooleanField() palatable_human = models.NullBooleanField() has_wildlife_value = models.NullBooleanField() fact_sheet_url = models.URLField(max_length=255, blank=True) plant_guide_url = models.URLField(max_length=255, blank=True) ### Used for validation max_diameter = models.IntegerField(default=200) max_height = models.IntegerField(default=800) objects = GeoHStoreUDFManager() @property def display_name(self): return "%s [%s]" % (self.common_name, self.scientific_name) @classmethod def get_scientific_name(clazz, genus, species, cultivar): name = genus if species: name += " " + species if cultivar: name += " '%s'" % cultivar return name @property def scientific_name(self): return Species.get_scientific_name(self.genus, self.species, self.cultivar) def dict(self): props = self.as_dict() props['scientific_name'] = self.scientific_name return props def get_itree_code(self, region_code=None): if not region_code: region_codes = self.instance.itree_region_codes() if len(region_codes) == 1: region_code = region_codes[0] else: return None override = ITreeCodeOverride.objects.filter( instance_species=self, region=ITreeRegion.objects.get(code=region_code), ) if override.exists(): return override[0].itree_code else: return get_itree_code(region_code, self.otm_code) def __unicode__(self): return self.display_name class Meta: verbose_name_plural = "Species" unique_together = ( 'instance', 'common_name', 'genus', 'species', 'cultivar', 'other_part_of_name', )
class Species(UDFModel, Authorizable, Auditable): """ http://plants.usda.gov/adv_search.html """ ### Base required info instance = models.ForeignKey(Instance) # ``otm_code`` is the key used to link this instance # species row to a cannonical species. An otm_code # is usually the USDA code, but this is not guaranteed. otm_code = models.CharField(max_length=255) common_name = models.CharField(max_length=255) genus = models.CharField(max_length=255) species = models.CharField(max_length=255, null=True, blank=True) cultivar = models.CharField(max_length=255, null=True, blank=True) other = models.CharField(max_length=255, null=True, blank=True) ### Copied from original OTM ### native_status = models.NullBooleanField() gender = models.CharField(max_length=50, null=True, blank=True) bloom_period = models.CharField(max_length=255, null=True, blank=True) fruit_period = models.CharField(max_length=255, null=True, blank=True) fall_conspicuous = models.NullBooleanField() flower_conspicuous = models.NullBooleanField() palatable_human = models.NullBooleanField() wildlife_value = models.NullBooleanField() fact_sheet = models.URLField(max_length=255, null=True, blank=True) plant_guide = models.URLField(max_length=255, null=True, blank=True) ### Used for validation max_dbh = models.IntegerField(default=200) max_height = models.IntegerField(default=800) objects = GeoHStoreUDFManager() @property def display_name(self): return "%s [%s]" % (self.common_name, self.scientific_name) @classmethod def get_scientific_name(clazz, genus, species, cultivar): name = genus if species: name += " " + species if cultivar: name += " '%s'" % cultivar return name @property def scientific_name(self): return Species.get_scientific_name(self.genus, self.species, self.cultivar) def dict(self): props = self.as_dict() props['scientific_name'] = self.scientific_name return props def __unicode__(self): return self.display_name class Meta: verbose_name_plural = "Species"