Ejemplo n.º 1
0
class Household(BaseDispatchSyncUuidModel, BaseSyncUuidModel):
    """A system model that represents the household asset. See also HouseholdStructure."""

    plot = models.ForeignKey(Plot, null=True)

    household_identifier = models.CharField(
        verbose_name='Household Identifier',
        max_length=25,
        unique=True,
        help_text="Household identifier",
        null=True,
        editable=False)

    household_sequence = models.IntegerField(
        editable=False,
        null=True,
        help_text=('is 1 for first household in plot, 2 for second, 3, etc. '
                   'Embedded in household identifier.'))

    hh_int = models.IntegerField(null=True,
                                 editable=False,
                                 help_text='not used')

    hh_seed = models.IntegerField(null=True,
                                  editable=False,
                                  help_text='not used')

    report_datetime = models.DateTimeField(verbose_name='Report Date/Time',
                                           null=True)

    gps_degrees_s = EncryptedDecimalField(verbose_name='GPS Degrees-South',
                                          max_digits=10,
                                          null=True,
                                          decimal_places=0,
                                          editable=False,
                                          help_text='comes from plot')

    gps_minutes_s = EncryptedDecimalField(verbose_name='GPS Minutes-South',
                                          max_digits=10,
                                          null=True,
                                          decimal_places=4,
                                          editable=False,
                                          help_text='comes from plot')

    gps_degrees_e = EncryptedDecimalField(verbose_name='GPS Degrees-East',
                                          null=True,
                                          max_digits=10,
                                          decimal_places=0,
                                          editable=False,
                                          help_text='comes from plot')

    gps_minutes_e = EncryptedDecimalField(verbose_name='GPS Minutes-East',
                                          max_digits=10,
                                          null=True,
                                          decimal_places=4,
                                          editable=False,
                                          help_text='comes from plot')

    gps_lon = models.FloatField(verbose_name='longitude',
                                null=True,
                                editable=False,
                                help_text='comes from plot')

    gps_lat = models.FloatField(verbose_name='latitude',
                                null=True,
                                editable=False,
                                help_text='comes from plot')

    gps_target_lon = models.FloatField(
        verbose_name='target waypoint longitude',
        null=True,
        editable=False,
        help_text='comes from plot')

    gps_target_lat = models.FloatField(verbose_name='target waypoint latitude',
                                       null=True,
                                       editable=False,
                                       help_text='comes from plot')

    target_radius = models.FloatField(default=.025,
                                      help_text='km',
                                      editable=False)

    community = models.CharField(
        max_length=25,
        help_text=
        'If the community is incorrect, please contact the DMC immediately.',
        null=True,
        editable=False)

    replaced_by = models.CharField(
        max_length=25,
        null=True,
        help_text=
        u'The identifier of the plot that this household is replaced by',
        editable=False)

    replaceable = models.NullBooleanField(
        verbose_name='Replaceable?',
        default=None,
        editable=False,
        help_text='Updated by replacement helper')

    comment = EncryptedTextField(
        max_length=250,
        help_text="You may provide a comment here or leave BLANK.",
        blank=True,
        null=True)

    uploaded_map = models.CharField(verbose_name="filename of uploaded map",
                                    max_length=25,
                                    null=True,
                                    blank=True)

    # not used!!
    action = models.CharField(max_length=25,
                              null=True,
                              default='unconfirmed',
                              editable=False)

    # updated by subject_consent save method
    enrolled = models.BooleanField(
        default=False,
        editable=False,
        help_text=('Set to true if one member is consented. '
                   'Updated by Household_structure post_save.'))

    enrolled_datetime = models.DateTimeField(
        null=True,
        editable=False,
        help_text=('datetime that household is enrolled. '
                   'Updated by Household_structure post_save.'))

    history = AuditTrail()

    objects = HouseholdManager()

    def save(self, *args, **kwargs):
        using = kwargs.get('using')
        try:
            if self.__class__.objects.using(using).get(id=self.id).replaced_by:
                raise AlreadyReplaced('Household {0} has been replaced '
                                      'by plot {1}.'.format(
                                          self.household_identifier,
                                          self.replaced_by))
        except self.__class__.DoesNotExist:
            pass
        self.allow_enrollment(using)
        return super(Household, self).save(*args, **kwargs)

    def get_identifier(self):
        return self.household_identifier

    def allow_enrollment(self, using, exception_cls=None, instance=None):
        """Raises an exception if the plot is not enrolled and
        BHS_FULL_ENROLLMENT_DATE is past."""
        instance = instance or self
        return self.plot.allow_enrollment(using,
                                          exception_cls,
                                          plot_instance=instance.plot)

    @property
    def mapper_name(self):
        return self.community

    def __unicode__(self):
        if site_mappers.get_current_mapper(
        ).clinic_plot_identifier[0:6] == self.household_identifier[0:6]:
            return self.plot.description
        return self.household_identifier

    def natural_key(self):
        return (self.household_identifier, )

    natural_key.dependencies = [
        'bcpp_household.household',
    ]

    def get_subject_identifier(self):
        return self.household_identifier

    def gps(self):
        return "S{0} {1} E{2} {3}".format(self.gps_degrees_s,
                                          self.gps_minutes_s,
                                          self.gps_degrees_e,
                                          self.gps_minutes_e)

    def dispatch_container_lookup(self, using=None):
        return (Plot, 'plot__plot_identifier')

    def bypass_for_edit_dispatched_as_item(self,
                                           using=None,
                                           update_fields=None):
        """Bypasses dispatched check if update_fields is set by the replacement_helper."""
        try:
            if 'replaced_by' in update_fields:
                return True
        except TypeError as type_error:
            if '\'NoneType\' is not iterable' in str(type_error):
                pass
        return False

    def structure(self):
        return """<a href="{url}" />structure</a>"""  # .format(url=url)

    structure.allow_tags = True

    def member_count(self):
        HouseholdMember = models.get_model('bcpp_household_member',
                                           'HouseholdMember')
        return HouseholdMember.objects.filter(
            household_structure__household__pk=self.pk).count()

    def deserialize_prep(self, **kwargs):
        # Household being deleted by an IncommingTransaction, we go ahead and delete it.
        # An extra household created by mistake.
        if kwargs.get('action', None) and kwargs.get('action', None) == 'D':
            self.delete()

    class Meta:
        app_label = 'bcpp_household'
        ordering = [
            '-household_identifier',
        ]
Ejemplo n.º 2
0
class PlotLogEntry(BaseDispatchSyncUuidModel, BaseSyncUuidModel):
    """A model completed by the user to track an RA\'s attempts to confirm a Plot."""
    plot_log = models.ForeignKey(PlotLog)

    report_datetime = models.DateTimeField(verbose_name="Report date",
                                           validators=[
                                               datetime_not_before_study_start,
                                               datetime_not_future,
                                               date_in_survey
                                           ])

    log_status = models.CharField(
        verbose_name='What is the status of this plot?',
        max_length=25,
        choices=PLOT_LOG_STATUS)

    reason = models.CharField(
        verbose_name='If inaccessible, please indicate the reason.',
        max_length=25,
        blank=True,
        null=True,
        choices=INACCESSIBILITY_REASONS)

    reason_other = models.CharField(verbose_name='If Other, specify',
                                    max_length=100,
                                    blank=True,
                                    null=True)

    comment = EncryptedTextField(
        verbose_name="Comments",
        max_length=250,
        null=True,
        blank=True,
        help_text=
        ('IMPORTANT: Do not include any names or other personally identifying '
         'information in this comment'))

    history = AuditTrail()

    objects = PlotLogEntryManager()

    def save(self, *args, **kwargs):
        using = kwargs.get('using,')
        self.allow_enrollment(using)
        super(PlotLogEntry, self).save(*args, **kwargs)

    def natural_key(self):
        return (self.report_datetime, ) + self.plot_log.natural_key()

    def dispatch_container_lookup(self, using=None):
        return (Plot, 'plot_log__plot__plot_identifier')

    def allow_enrollment(self, using, exception_cls=None, instance=None):
        """Stops enrollments."""
        instance = instance or self
        return self.plot_log.plot.allow_enrollment(
            using, exception_cls, plot_instance=instance.plot_log.plot)

    def __unicode__(self):
        return unicode(self.plot_log) + '(' + unicode(
            self.report_datetime) + ')'

    class Meta:
        app_label = 'bcpp_household'
        unique_together = ('plot_log', 'report_datetime')
Ejemplo n.º 3
0
class SubjectMoved(BaseMemberStatusModel):
    """A model completed by the user to indicate a subject has moved from the household and or community."""

    moved_household = models.CharField(
        max_length=7,
        verbose_name=
        'Has the participant moved out of the household where last seen',
        choices=YES_NO_UNKNOWN,
        null=True,
        blank=False,
        help_text="")

    moved_community = models.CharField(
        max_length=7,
        verbose_name='Has the participant moved out of the community',
        choices=YES_NO_UNKNOWN,
        null=True,
        blank=False,
        help_text="")

    new_community = models.CharField(
        max_length=50,
        verbose_name=
        'If the participant has moved, provide the name of the new community',
        null=True,
        blank=True,
        help_text=
        "If moved out of the community, provide a new community name or \'UNKNOWN\'"
    )

    update_locator = models.CharField(
        max_length=7,
        verbose_name='Has the locator information changed',
        choices=YES_NO_UNKNOWN,
        null=True,
        blank=False,
        help_text=('If YES, please enter the changed information '
                   'the locator form'))

    comment = EncryptedTextField(verbose_name="Comment",
                                 max_length=250,
                                 blank=True,
                                 help_text=(''))

    history = AuditTrail()

    def save(self, *args, **kwargs):
        household = models.get_model(
            'bcpp_household', 'Household').objects.get(
                household_identifier=self.household_member.household_structure.
                household.household_identifier)
        if household.replaced_by:
            raise AlreadyReplaced('Household {0} replaced.'.format(
                household.household_identifier))
        self.survey = self.household_member.survey
        super(SubjectMoved, self).save(*args, **kwargs)

    class Meta:
        app_label = 'bcpp_household_member'
        verbose_name = "Subject Moved"
        verbose_name_plural = "Subject Moved"
        ordering = ['household_member']
Ejemplo n.º 4
0
class HouseholdLogEntry(BaseDispatchSyncUuidModel, BaseSyncUuidModel):
    """A model completed by the user each time the household is visited."""
    household_log = models.ForeignKey(HouseholdLog)

    report_datetime = models.DateField(
        verbose_name="Report date",
        validators=[date_not_before_study_start, date_not_future])

    household_status = models.CharField(verbose_name='Household Status',
                                        max_length=50,
                                        choices=HOUSEHOLD_LOG_STATUS,
                                        null=True,
                                        blank=False)

    next_appt_datetime = models.DateTimeField(
        verbose_name="Re-Visit On",
        help_text="The date and time to revisit household",
        null=True,
        blank=True)

    next_appt_datetime_source = models.CharField(
        verbose_name="Source",
        max_length=25,
        choices=NEXT_APPOINTMENT_SOURCE,
        help_text='source of information for the appointment date',
        null=True,
        blank=True)

    comment = EncryptedTextField(null=True, blank=True)

    history = AuditTrail()

    objects = HouseholdLogEntryManager()

    def natural_key(self):
        return (self.report_datetime, ) + self.household_log.natural_key()

    def save(self, *args, **kwargs):
        household = models.get_model(
            'bcpp_household', 'Household').objects.get(
                household_identifier=self.household_log.household_structure.
                household.household_identifier)
        if household.replaced_by:
            raise AlreadyReplaced('Household {0} replaced.'.format(
                household.household_identifier))
        super(HouseholdLogEntry, self).save(*args, **kwargs)

    def dispatch_container_lookup(self, using=None):
        return (
            Plot,
            'household_log__household_structure__household__plot__plot_identifier'
        )

    def __unicode__(self):
        household_log = self.household_log or None
        return unicode(household_log) + '(' + unicode(
            self.report_datetime) + ')'

    class Meta:
        app_label = 'bcpp_household'
        unique_together = ('household_log', 'report_datetime')
Ejemplo n.º 5
0
class Plot(BaseDispatchSyncUuidModel, BaseSyncUuidModel):
    """A model completed by the user (and initially by the system) to represent a Plot
    in the community."""
    plot_identifier = models.CharField(verbose_name='Plot Identifier',
                                       max_length=25,
                                       unique=True,
                                       help_text="Plot identifier",
                                       editable=False,
                                       db_index=True)

    eligible_members = models.IntegerField(
        verbose_name="Approximate number of age eligible members",
        blank=True,
        null=True,
        help_text=(("Provide an approximation of the number of people "
                    "who live in this residence who are age eligible.")))

    description = EncryptedTextField(
        verbose_name="Description of plot/residence",
        max_length=250,
        blank=True,
        null=True)

    comment = EncryptedTextField(verbose_name="Comment",
                                 max_length=250,
                                 blank=True,
                                 null=True)

    cso_number = EncryptedCharField(
        verbose_name="CSO Number",
        blank=True,
        null=True,
        db_index=True,
        help_text=("provide the CSO number or leave BLANK."))

    household_count = models.IntegerField(
        verbose_name="Number of Households on this plot.",
        default=0,
        null=True,
        validators=[MaxValueValidator(9)],
        help_text=("Provide the number of households in this plot."))

    time_of_week = models.CharField(
        verbose_name=
        'Time of week when most of the eligible members will be available',
        max_length=25,
        choices=TIME_OF_WEEK,
        blank=True,
        null=True)

    time_of_day = models.CharField(
        verbose_name=
        'Time of day when most of the eligible members will be available',
        max_length=25,
        choices=TIME_OF_DAY,
        blank=True,
        null=True)

    gps_degrees_s = EncryptedDecimalField(verbose_name='GPS Degrees-South',
                                          max_digits=10,
                                          null=True,
                                          decimal_places=0)

    gps_minutes_s = EncryptedDecimalField(verbose_name='GPS Minutes-South',
                                          max_digits=10,
                                          null=True,
                                          decimal_places=4)

    gps_degrees_e = EncryptedDecimalField(verbose_name='GPS Degrees-East',
                                          null=True,
                                          max_digits=10,
                                          decimal_places=0)

    gps_minutes_e = EncryptedDecimalField(verbose_name='GPS Minutes-East',
                                          max_digits=10,
                                          null=True,
                                          decimal_places=4)

    gps_lon = EncryptedDecimalField(verbose_name='longitude',
                                    max_digits=10,
                                    null=True,
                                    decimal_places=6)

    gps_lat = EncryptedDecimalField(verbose_name='latitude',
                                    max_digits=10,
                                    null=True,
                                    decimal_places=6)

    gps_target_lon = EncryptedDecimalField(
        verbose_name='target waypoint longitude',
        max_digits=10,
        null=True,
        decimal_places=6)

    gps_target_lat = EncryptedDecimalField(
        verbose_name='target waypoint latitude',
        max_digits=10,
        null=True,
        decimal_places=6)

    status = models.CharField(verbose_name='Plot status',
                              max_length=35,
                              null=True,
                              choices=PLOT_STATUS)

    target_radius = models.FloatField(default=.025,
                                      help_text='km',
                                      editable=False)

    distance_from_target = models.FloatField(null=True,
                                             editable=True,
                                             help_text='distance in meters')

    # 20 percent plots is reperesented by 1 and 5 percent of by 2, the rest of
    # the plots which is 75 percent selected value is None
    selected = models.CharField(max_length=25,
                                null=True,
                                verbose_name='selected',
                                choices=SELECTED,
                                editable=False)

    device_id = models.CharField(max_length=2, null=True, editable=False)

    action = models.CharField(max_length=25,
                              null=True,
                              default=UNCONFIRMED,
                              editable=False)

    access_attempts = models.IntegerField(
        default=0,
        editable=False,
        help_text=
        'Number of attempts to access a plot to determine it\'s status.')

    # Google map static images for this plots with different zoom levels.
    # uploaded_map_16, uploaded_map_17, uploaded_map_18 zoom level 16, 17, 18 respectively
    uploaded_map_16 = models.CharField(
        verbose_name="Map image at zoom level 16",
        max_length=25,
        null=True,
        blank=True,
        editable=False)

    uploaded_map_17 = models.CharField(
        verbose_name="Map image at zoom level 17",
        max_length=25,
        null=True,
        blank=True,
        editable=False)

    uploaded_map_18 = models.CharField(
        verbose_name="Map image at zoom level 18",
        max_length=25,
        null=True,
        blank=True,
        editable=False)

    community = models.CharField(
        max_length=25,
        help_text=
        'If the community is incorrect, please contact the DMC immediately.',
        validators=[
            is_valid_community,
        ],
        editable=False)

    section = models.CharField(max_length=25,
                               null=True,
                               verbose_name='Section',
                               editable=False)

    sub_section = models.CharField(max_length=25,
                                   null=True,
                                   verbose_name='Sub-section',
                                   help_text=u'',
                                   editable=False)

    bhs = models.NullBooleanField(
        default=None,
        editable=False,
        help_text=('True indicates that plot is enrolled into BHS. '
                   'Updated by bcpp_subject.subject_consent post_save'))

    enrolled_datetime = models.DateTimeField(
        null=True,
        editable=False,
        help_text=('datetime that plot is enrolled into BHS. '
                   'Updated by bcpp_subject.subject_consent post_save'))

    htc = models.NullBooleanField(default=False, editable=False)

    replaces = models.CharField(
        max_length=25,
        null=True,
        blank=True,
        editable=False,
        help_text=("plot or household identifier that this plot replaces."))

    replaced_by = models.CharField(
        max_length=25,
        null=True,
        blank=True,
        editable=False,
        help_text=u'The identifier of the plot that this plot was replaced by')

    replaceable = models.NullBooleanField(
        verbose_name='Replaceable?',
        default=None,
        editable=False,
        help_text='Updated by replacement helper')

    history = AuditTrail()

    objects = PlotManager()

    def __unicode__(self):
        if site_mappers.get_current_mapper(
        ).clinic_plot_identifier == self.plot_identifier:
            return 'BCPP-CLINIC'
        else:
            return self.plot_identifier

    def natural_key(self):
        return (self.plot_identifier, )

    def save(self, *args, **kwargs):
        using = kwargs.get('using')
        update_fields = kwargs.get('update_fields')
        if not self.plot_identifier == site_mappers.get_current_mapper(
        ).clinic_plot_identifier:
            self.allow_enrollment(using, update_fields=update_fields)
        if self.replaced_by and update_fields != ['replaced_by', 'htc']:
            raise AlreadyReplaced(
                'Plot {0} is no longer part of BHS. It has been replaced '
                'by plot {1}.'.format(self.plot_identifier, self.replaced_by))
        if not self.community:
            # plot data is imported and not entered, so community must be provided on import
            raise ValidationError(
                'Attribute \'community\' may not be None for model {0}'.format(
                    self))
        if self.household_count > settings.MAX_HOUSEHOLDS_PER_PLOT:
            raise ValidationError('Number of households cannot exceed {}. '
                                  'Perhaps catch this in the forms.py. See '
                                  'settings.MAX_HOUSEHOLDS_PER_PLOT'.format(
                                      settings.MAX_HOUSEHOLDS_PER_PLOT))
        # unless overridden, if self.community != to mapper.map_area, raise
        self.verify_plot_community_with_current_mapper(self.community)
        # if self.community does not get valid mapper, will raise an error that should be caught in forms.pyx
        mapper_cls = site_mappers.registry.get(self.community)
        mapper = mapper_cls()
        if not self.plot_identifier:
            self.plot_identifier = PlotIdentifier(mapper.map_code,
                                                  using).get_identifier()
            if not self.plot_identifier:
                raise IdentifierError(
                    'Expected a value for plot_identifier. Got None')
        # if user added/updated gps_degrees_[es] and gps_minutes_[es], update gps_lat, gps_lon
        if (self.gps_degrees_e and self.gps_degrees_s and self.gps_minutes_e
                and self.gps_minutes_s):
            self.gps_lat = mapper.get_gps_lat(self.gps_degrees_s,
                                              self.gps_minutes_s)
            self.gps_lon = mapper.get_gps_lon(self.gps_degrees_e,
                                              self.gps_minutes_e)
            mapper.verify_gps_location(self.gps_lat, self.gps_lon, MapperError)
            mapper.verify_gps_to_target(
                self.gps_lat,
                self.gps_lon,
                self.gps_target_lat,
                self.gps_target_lon,
                self.target_radius,
                MapperError,
                radius_bypass_instance=self.increase_radius_instance)
            self.distance_from_target = mapper.gps_distance_between_points(
                self.gps_lat, self.gps_lon, self.gps_target_lat,
                self.gps_target_lon) * 1000
        self.action = self.get_action()
        try:
            update_fields = update_fields + [
                'action', 'distance_from_target', 'plot_identifier',
                'user_modified'
            ]
            kwargs.update({'update_fields': update_fields})
        except TypeError:
            pass
        super(Plot, self).save(*args, **kwargs)

    def get_identifier(self):
        return self.plot_identifier

    @property
    def identifier_segment(self):
        return self.plot_identifier[:-3]

    @property
    def increase_radius_instance(self):
        IncreasePlotRadius = models.get_model('bcpp_household',
                                              'IncreasePlotRadius')
        try:
            return IncreasePlotRadius.objects.get(plot=self)
        except IncreasePlotRadius.DoesNotExist:
            return None

    def create_household(self, count, instance=None, using=None):
        instance = instance or self
        using = using or 'default'
        if instance.pk:
            for _ in range(0, count):
                Household = models.get_model('bcpp_household', 'Household')
                Household.objects.create(
                    **{
                        'plot': instance,
                        'gps_target_lat': instance.gps_target_lat,
                        'gps_target_lon': instance.gps_target_lon,
                        'gps_lat': instance.gps_lat,
                        'gps_lon': instance.gps_lon,
                        'gps_degrees_e': instance.gps_degrees_e,
                        'gps_degrees_s': instance.gps_degrees_s,
                        'gps_minutes_e': instance.gps_minutes_e,
                        'gps_minutes_s': instance.gps_minutes_s
                    })

    def allow_enrollment(self,
                         using,
                         exception_cls=None,
                         plot_instance=None,
                         update_fields=None):
        """Raises an exception if an attempt is made to edit a plot after
        the BHS_FULL_ENROLLMENT_DATE or if the plot has been allocated to HTC."""
        plot_instance = plot_instance or self
        using = using or 'default'
        update_fields = update_fields or []
        exception_cls = exception_cls or ValidationError
        if using == 'default':  # do not check on remote systems
            mapper_instance = site_mappers.get_current_mapper()
            if plot_instance.id:
                if plot_instance.htc and 'htc' not in update_fields:
                    raise exception_cls(
                        'Modifications not allowed, this plot has been assigned to the HTC campaign.'
                    )
            if settings.ALLOW_ENROLLMENT:
                if not mapper_instance.map_code == '00' and not plot_instance.bhs and date.today(
                ) > mapper_instance.current_survey_dates.full_enrollment_date:
                    raise exception_cls(
                        'BHS enrollment for {0} ended on {1}. This plot, and the '
                        'data related to it, may not be modified. '
                        'See site_mappers'.format(
                            self.community, mapper_instance.
                            current_survey_dates.full_enrollment_date))
        return True

    def safe_delete_households(self, existing_no, instance=None, using=None):
        """ Deletes households and HouseholdStructure if member_count==0 and no log entry.
            If there is a household log entry, this DOES NOT delete the household
        """
        Household = models.get_model('bcpp_household', 'Household')
        HouseholdStructure = models.get_model('bcpp_household',
                                              'HouseholdStructure')
        HouseholdLog = models.get_model('bcpp_household', 'HouseholdLog')
        HouseholdLogEntry = models.get_model('bcpp_household',
                                             'HouseholdLogEntry')
        instance = instance or self
        using = using or 'default'

        def household_valid_to_delete(instance):
            """ Checks whether there is a plot log entry for each log. If it does not exists the
            household can be deleted. """
            allowed_to_delete = []
            for hh in Household.objects.using(using).filter(plot=instance):
                if (HouseholdLogEntry.objects.filter(
                        household_log__household_structure__household=hh).
                        exists() or hh in allowed_to_delete):
                    continue
                allowed_to_delete.append(hh)
            return allowed_to_delete

        def delete_confirmed_household(instance, existing_no):
            """ Deletes required number of households. """
            def validate_number_to_delete(instance, existing_no):
                if (existing_no in [0, 1]
                        or instance.household_count == existing_no
                        or instance.household_count > existing_no
                        or instance.household_count == 0):
                    return False
                else:
                    if household_valid_to_delete(instance):
                        del_valid = existing_no - instance.household_count
                        if len(household_valid_to_delete(
                                instance)) < del_valid:
                            return len(household_valid_to_delete(instance))
                        else:
                            return del_valid
                    return False

            def delete_households_for_non_residential(instance, existing_no):
                household_to_delete = household_valid_to_delete(instance)
                try:
                    if len(household_to_delete
                           ) == existing_no and existing_no > 0:
                        hh = HouseholdStructure.objects.filter(
                            household__in=household_to_delete)
                        hl = HouseholdLog.objects.filter(
                            household_structure__in=hh)
                        hl.delete()  # delete household_logs
                        hh.delete()  # delete household_structure
                        for hh in household_to_delete:
                            with transaction.atomic():
                                Household.objects.get(
                                    id=hh.id).delete()  # delete household
                        return True
                    else:
                        return False
                except IntegrityError:
                    return False
                except DatabaseError:
                    return False

            def delete_household(instance, existing_no):
                try:
                    delete_no = validate_number_to_delete(
                        instance, existing_no) if validate_number_to_delete(
                            instance, existing_no) else 0
                    if not delete_no == 0:
                        deletes = household_valid_to_delete(
                            instance)[:delete_no]
                        hh = HouseholdStructure.objects.filter(
                            household__in=deletes)
                        hl = HouseholdLog.objects.filter(
                            household_structure__in=hh)
                        hl.delete()  # delete household_logs
                        hh.delete()  # delete household_structure
                        for hh in deletes:
                            with transaction.atomic():
                                Household.objects.get(
                                    id=hh.id).delete()  # delete household
                        return True
                    else:
                        return False
                except IntegrityError:
                    return False
                except DatabaseError:
                    return False

            if instance.status in [RESIDENTIAL_NOT_HABITABLE, NON_RESIDENTIAL]:
                return delete_households_for_non_residential(
                    instance, existing_no)
            else:
                return delete_household(instance, existing_no)

        return delete_confirmed_household(instance, existing_no)

    def create_or_delete_households(self, instance=None, using=None):
        """Creates or deletes households to try to equal the number of households reported on the plot instance.

        This gets called by a household post_save signal and on the plot save method on change.

            * If number is greater than actual household instances, households are created.
            * If number is less than actual household instances, households are deleted as long as
              there are no household members and the household log does not have entries.
            * bcpp_clinic is a special case to allow for a plot to represent the BCPP Clinic."""
        instance = instance or self
        using = using or 'default'
        Household = models.get_model('bcpp_household', 'Household')
        # check that tuple has not changed and has "residential_habitable"
        if instance.status:
            if instance.status not in [
                    item[0] for item in list(PLOT_STATUS) +
                [('bcpp_clinic', 'BCPP Clinic')]
            ]:
                raise AttributeError(
                    '{0} not found in choices tuple PLOT_STATUS. {1}'.format(
                        instance.status, PLOT_STATUS))
        existing_household_count = Household.objects.using(using).filter(
            plot__pk=instance.pk).count()
        instance.create_household(instance.household_count -
                                  existing_household_count,
                                  using=using)
        instance.safe_delete_households(existing_household_count, using=using)
        with transaction.atomic():
            count = Household.objects.using(using).filter(
                plot__pk=instance.pk).count()
        return count

    def get_action(self):
        retval = UNCONFIRMED
        if self.gps_lon and self.gps_lat:
            retval = CONFIRMED
        return retval

    @property
    def validate_plot_accessible(self):
        if self.plot_log_entry and (
                self.plot_inaccessible is
                False) and self.plot_log_entry.log_status == ACCESSIBLE:
            return True
        return False

    def gps(self):
        return "S{0} {1} E{2} {3}".format(self.gps_degrees_s,
                                          self.gps_minutes_s,
                                          self.gps_degrees_e,
                                          self.gps_minutes_e)

    def is_dispatch_container_model(self):
        return True

    def dispatched_as_container_identifier_attr(self):
        return 'plot_identifier'

    def dispatch_container_lookup(self):
        return (self.__class__, 'plot_identifier')

    def include_for_dispatch(self):
        return True

    def bypass_for_edit_dispatched_as_item(self,
                                           using=None,
                                           update_fields=None):
        """Bypasses dispatched check if update_fields is set by the replacement_helper."""
        if update_fields:
            if 'replaces' in update_fields or 'htc' in update_fields or 'replaced_by' in update_fields:
                return True
        return False

    def get_contained_households(self):
        from bhp066.apps.bcpp_household.models import Household
        households = Household.objects.filter(
            plot__plot_identifier=self.plot_identifier)
        return households

    @property
    def log_form_label(self):
        # TODO: where is this used?
        using = 'default'
        PlotLog = models.get_model('bcpp_household', 'PlotLog')
        PlotLogEntry = models.get_model('bcpp_household', 'PlotLogEntry')
        form_label = []
        try:
            plot_log = PlotLog.objects.using(using).get(plot=self)
            for plot_log_entry in PlotLogEntry.objects.using(using).filter(
                    plot_log=plot_log).order_by('report_datetime'):
                try:
                    form_label.append(
                        (plot_log_entry.log_status.lower() + '-' +
                         plot_log_entry.report_datetime.strftime('%Y-%m-%d'),
                         plot_log_entry.id))
                except AttributeError:  # log_status is None ??
                    form_label.append(
                        (plot_log_entry.report_datetime.strftime('%Y-%m-%d'),
                         plot_log_entry.id))
        except PlotLog.DoesNotExist:
            pass
        if self.access_attempts < 3 and self.action != CONFIRMED:
            form_label.append(('add new entry', 'add new entry'))
        if not form_label and self.action != CONFIRMED:
            form_label.append(('add new entry', 'add new entry'))
        return form_label

    @property
    def log_entry_form_urls(self):
        # TODO: where is this used?
        # TODO: this does not belong on the plot model!
        """Returns a url or urls to the plotlogentry(s) if an instance(s) exists."""
        using = 'default'
        PlotLog = models.get_model('bcpp_household', 'PlotLog')
        PlotLogEntry = models.get_model('bcpp_household', 'PlotLogEntry')
        entry_urls = {}
        try:
            plot_log = PlotLog.objects.using(using).get(plot=self)
            for entry in PlotLogEntry.objects.using(using).filter(
                    plot_log=plot_log).order_by('report_datetime'):
                entry_urls[entry.pk] = self._get_form_url(
                    'plotlogentry', entry.pk)
            add_url_2 = self._get_form_url('plotlogentry',
                                           model_pk=None,
                                           add_url=True)
            entry_urls['add new entry'] = add_url_2
        except PlotLog.DoesNotExist:
            pass
        return entry_urls

    def _get_form_url(self, model, model_pk=None, add_url=None):
        url = ''
        pk = None
        app_label = 'bcpp_household'
        if add_url:
            url = reverse('admin:{0}_{1}_add'.format(app_label, model))
            return url
        if not model_pk:  # This is a like a SubjectAbsentee
            model_class = models.get_model(app_label, model)
            try:
                pk = model_class.objects.get(plot=self).pk
            except model_class.DoesNotExist:
                pk = None
        else:
            pk = model_pk
        if pk:
            url = reverse('admin:{0}_{1}_change'.format(app_label, model),
                          args=(pk, ))
        else:
            url = reverse('admin:{0}_{1}_add'.format(app_label, model))
        return url

    @property
    def location(self):
        if self.plot_identifier.endswith('0000-00'):
            return 'clinic'
        else:
            return 'household'

    @property
    def plot_inaccessible(self):
        """Returns True if the plot is inaccessible as defined by its status and number of attempts."""
        PlotLogEntry = models.get_model('bcpp_household', 'plotlogentry')
        return PlotLogEntry.objects.filter(
            plot_log__plot__id=self.id, log_status=INACCESSIBLE).count() >= 3

    @property
    def target_radius_in_meters(self):
        return self.target_radius * 1000

    @property
    def increase_plot_radius(self):
        """Returns an instance of IncreasePlotRadius if the user should be
        allowed to change the target_radius otherwise returns None.

        Plot must be inaccessible and the last reason (of 3) be either "dogs"
        or "locked gate" """
        PlotLogEntry = models.get_model('bcpp_household', 'PlotLogEntry')
        IncreasePlotRadius = models.get_model('bcpp_household',
                                              'IncreasePlotRadius')
        created = False
        increase_plot_radius = None
        try:
            increase_plot_radius = IncreasePlotRadius.objects.get(plot=self)
        except IncreasePlotRadius.DoesNotExist:
            if self.plot_inaccessible:
                plot_log_entries = PlotLogEntry.objects.filter(
                    plot_log__plot__id=self.id).order_by('report_datetime')
                if plot_log_entries[2].reason in ['dogs', 'locked_gate']:
                    increase_plot_radius = IncreasePlotRadius.objects.create(
                        plot=self)
                    created = True
        except IndexError:
            pass
        return increase_plot_radius, created

    def verify_plot_community_with_current_mapper(self,
                                                  community,
                                                  exception_cls=None):
        """Returns True if the plot.community = the current mapper.map_area.

        This check can be disabled using the settings attribute VERIFY_PLOT_COMMUNITY_WITH_CURRENT_MAPPER.
        """
        verify_plot_community_with_current_mapper = True  # default
        exception_cls = exception_cls or ValidationError
        try:
            verify_plot_community_with_current_mapper = settings.VERIFY_PLOT_COMMUNITY_WITH_CURRENT_MAPPER
        except AttributeError:
            pass
        if verify_plot_community_with_current_mapper:
            if community != site_mappers.get_current_mapper().map_area:
                raise exception_cls(
                    'Plot community does not correspond with the current mapper '
                    'community of \'{}\'. Got \'{}\'. '
                    'See settings.VERIFY_PLOT_COMMUNITY_WITH_CURRENT_MAPPER'.
                    format(site_mappers.get_current_mapper().map_area,
                           community))

    @property
    def plot_log(self):
        """Returns an instance of the plot log."""
        PlotLog = models.get_model('bcpp_household', 'plotlog')
        try:
            instance = PlotLog.objects.get(plot__id=self.id)
        except PlotLog.DoesNotExist:
            instance = None
        return instance

    @property
    def plot_log_entry(self):
        PlotLogEntry = models.get_model('bcpp_household', 'plotlogentry')
        try:
            return PlotLogEntry.objects.filter(
                plot_log__plot__id=self.id).latest('report_datetime')
        except PlotLogEntry.DoesNotExist:
            print "PlotLogEntry.DoesNotExist"

    class Meta:
        app_label = 'bcpp_household'
        ordering = [
            '-plot_identifier',
        ]
        unique_together = (('gps_target_lat', 'gps_target_lon'), )