Пример #1
0
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')
Пример #2
0
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)
Пример #3
0
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)
Пример #4
0
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)
Пример #5
0
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',
        )
Пример #6
0
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)
Пример #7
0
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
Пример #8
0
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',
        )
Пример #9
0
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"