# -*- coding: utf-8 -*-
# Akvo RSR is covered by the GNU Affero General Public License.
# See more details in the license.txt file located at the root folder of the Akvo RSR module.
# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
import logging
from django.db import models
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from ..fields import LatitudeField, LongitudeField, ValidXMLCharField
from akvo.codelists.models import (Country, GeographicExactness, GeographicLocationClass,
GeographicLocationReach, GeographicVocabulary, LocationType)
from akvo.codelists.store.default_codelists import (
COUNTRY, GEOGRAPHIC_EXACTNESS, GEOGRAPHIC_LOCATION_CLASS, GEOGRAPHIC_LOCATION_REACH,
GEOGRAPHIC_VOCABULARY, LOCATION_TYPE
)
from akvo.utils import codelist_choices, codelist_value
[docs]class BaseLocation(models.Model):
latitude = LatitudeField(
_('latitude'), null=True, blank=True, db_index=True, default=None,
help_text=_('Use a period to denote decimals.')
)
longitude = LongitudeField(
_('longitude'), null=True, blank=True, db_index=True, default=None,
help_text=_('Use a period to denote decimals.')
)
city = ValidXMLCharField(_('city'), blank=True, max_length=255)
state = ValidXMLCharField(_('state'), blank=True, max_length=255)
address_1 = ValidXMLCharField(_('address 1'), max_length=255, blank=True)
address_2 = ValidXMLCharField(_('address 2'), max_length=255, blank=True)
postcode = ValidXMLCharField(_('postal code'), max_length=10, blank=True)
country = models.ForeignKey('Country', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('country'))
def __str__(self):
return '{0}, {1}, {2}{3}'.format(
'{0}: {1}'.format(
_('Latitude'),
str(self.latitude) if self.latitude else _('No latitude specified')),
'{0}: {1}'.format(
_('Longitude'),
str(self.longitude) if self.longitude else _('No longitude specified')),
'{0}: {1}'.format(
_('Country'),
str(self.country.name) if self.country else _('No country specified')),
' ({0})'.format(self.name) if getattr(self, 'name', None) else ''
)
[docs] def delete(self, *args, **kwargs):
super(BaseLocation, self).delete(*args, **kwargs)
# If location_target has more locations, set the first as primary location
location_target = self.location_target
other_locations = location_target.locations.all()
if other_locations.count() > 0:
location_target.primary_location = other_locations.first()
else:
location_target.primary_location = None
location_target.save()
[docs] def save(self, *args, **kwargs):
super(BaseLocation, self).save(*args, **kwargs)
# Set location as primary location if it is the first location
location_target = self.location_target
if location_target.primary_location is None or location_target.primary_location.pk > self.pk:
location_target.primary_location = self
location_target.save()
[docs] def is_valid(self):
if (self.latitude is None or self.longitude is None) or \
(self.latitude == 0 and self.longitude == 0) or \
(self.latitude > 90 or self.latitude < -90) or \
(self.longitude > 180 or self.latitude < -180):
return False
return True
[docs]class OrganisationLocation(BaseLocation):
location_target = models.ForeignKey('Organisation', on_delete=models.CASCADE, related_name='locations')
iati_country = ValidXMLCharField(
_('country'), blank=True, max_length=2, choices=codelist_choices(COUNTRY, show_code=False),
help_text=_('The country in which the organisation is located.')
)
[docs] def iati_country_value(self):
return codelist_value(Country, self, 'iati_country')
[docs] def iati_country_value_unicode(self):
return str(self.iati_country_value())
[docs]class ProjectLocation(BaseLocation):
project_relation = 'locations__in'
location_target = models.ForeignKey('Project', on_delete=models.CASCADE, related_name='locations')
# Additional IATI fields
reference = ValidXMLCharField(
_('reference'), blank=True, max_length=50,
help_text=_('An internal reference that describes the location in the reporting '
'organisation\'s own system. For reference see: '
'<a href="http://iatistandard.org/202/activity-standard/iati-activities/'
'iati-activity/location/#attributes" target="_blank">'
'http://iatistandard.org/202/activity-standard/iati-activities/iati-activity/'
'location/#attributes</a>.')
)
location_code = ValidXMLCharField(
_('code'), blank=True, max_length=25,
help_text=_('Enter a code to identify the region. Codes are based on DAC region codes. '
'Where an activity is considered global, the code 998 can be used. For '
'reference: <a href="http://www.oecd.org/dac/stats/dacandcrscodelists.htm" '
'target="_blank">http://www.oecd.org/dac/stats/dacandcrscodelists.htm</a>.')
)
vocabulary = ValidXMLCharField(_('vocabulary'), blank=True, max_length=2,
choices=codelist_choices(GEOGRAPHIC_VOCABULARY))
name = ValidXMLCharField(
_('name'), blank=True, max_length=100,
help_text=_('The human-readable name for the location.')
)
description = ValidXMLCharField(
_('location description'), blank=True, max_length=2000,
help_text=_('This provides free text space for providing an additional description, if '
'needed, of the actual target of the activity. A description that qualifies '
'the location, not the activity.')
)
activity_description = ValidXMLCharField(
_('activity description'), blank=True, max_length=2000,
help_text=_('A description that qualifies the activity taking place at the location. '
'This should not duplicate information provided in the main activity '
'description, and should typically be used to distinguish between activities '
'at multiple locations within a single iati-activity record.')
)
exactness = ValidXMLCharField(
_('location precision'), blank=True, max_length=1,
choices=codelist_choices(GEOGRAPHIC_EXACTNESS),
help_text=_('Defines whether the location represents the most distinct point reasonably '
'possible for this type of activity or is an approximation due to lack of '
'more detailed information.')
)
location_reach = ValidXMLCharField(
_('reach'), blank=True, max_length=1, choices=codelist_choices(GEOGRAPHIC_LOCATION_REACH),
help_text=_('Does this location describe where the activity takes place or where the '
'intended beneficiaries reside?')
)
location_class = ValidXMLCharField(
_('class'), blank=True, max_length=1, choices=codelist_choices(GEOGRAPHIC_LOCATION_CLASS),
help_text=_('Does the location refer to a physical structure such as a building, a '
'populated place (e.g. city or village), an administrative division, or '
'another topological feature (e.g. river, nature reserve)? For reference: '
'<a href="http://iatistandard.org/202/codelists/GeographicLocationClass/" '
'target="_blank">http://iatistandard.org/202/codelists/'
'GeographicLocationClass/</a>.')
)
feature_designation = ValidXMLCharField(
_('feature designation'), blank=True, max_length=5,
choices=codelist_choices(LOCATION_TYPE),
help_text=_('A more refined coded classification of the type of feature referred to by '
'this location. For reference: <a href="http://iatistandard.org/202/codelists/'
'LocationType/" target="_blank">http://iatistandard.org/202/codelists/'
'LocationType/</a>.')
)
[docs] def iati_country(self):
return codelist_value(Country, self, 'country')
[docs] def iati_country_unicode(self):
return str(self.iati_country())
[docs] def iati_vocabulary(self):
return codelist_value(GeographicVocabulary, self, 'vocabulary')
[docs] def iati_vocabulary_unicode(self):
return str(self.iati_vocabulary())
[docs] def iati_exactness(self):
return codelist_value(GeographicExactness, self, 'exactness')
[docs] def iati_exactness_unicode(self):
return str(self.iati_exactness())
[docs] def iati_reach(self):
return codelist_value(GeographicLocationReach, self, 'location_reach')
[docs] def iati_reach_unicode(self):
return str(self.iati_reach())
[docs] def iati_class(self):
return codelist_value(GeographicLocationClass, self, 'location_class')
[docs] def iati_class_unicode(self):
return str(self.iati_class())
[docs] def iati_designation(self):
return codelist_value(LocationType, self, 'feature_designation')
[docs] def iati_designation_unicode(self):
return str(self.iati_designation())
# Over-riding fields doesn't work in Django < 1.10, and hence this hack.
ProjectLocation._meta.get_field('country').help_text = _(
'The country or countries that benefit(s) from the activity.'
)
logger = logging.getLogger(__name__)
[docs]@receiver(pre_delete, sender=ProjectLocation)
def on_projectlocation_delete(sender, instance: ProjectLocation, using, **kwargs):
logger.warning(
"About to delete ProjectLocation(%s) %s of project(%s) %s",
instance.id, instance,
instance.location_target.id, instance.location_target,
stack_info=True
)
[docs]class AdministrativeLocation(models.Model):
project_relation = 'locations__administratives__in'
location = models.ForeignKey(
'ProjectLocation', on_delete=models.CASCADE, verbose_name=_('location'), related_name='administratives'
)
code = ValidXMLCharField(
_('administrative code'), blank=True, max_length=25,
help_text=_('Coded identification of national and sub-national divisions according to '
'recognised administrative boundary repositories. Multiple levels may be '
'reported.')
)
vocabulary = ValidXMLCharField(
_('administrative vocabulary'), blank=True, max_length=2,
choices=codelist_choices(GEOGRAPHIC_VOCABULARY),
help_text=_('For reference: <a href="http://iatistandard.org/202/codelists/'
'GeographicVocabulary/" target="_blank">http://iatistandard.org/202/codelists/'
'GeographicVocabulary/</a>.')
)
level = models.PositiveSmallIntegerField(_('administrative level'), blank=True, null=True)
def __str__(self):
return str(self.code) if self.code else '%s' % _('No code specified')
[docs] def iati_vocabulary(self):
return codelist_value(GeographicVocabulary, self, 'vocabulary')
[docs] def iati_vocabulary_unicode(self):
return str(self.iati_vocabulary())
class Meta:
app_label = 'rsr'
verbose_name = _('location administrative')
verbose_name_plural = _('location administratives')
ordering = ('pk',)
[docs]class ProjectUpdateLocation(BaseLocation):
location_target = models.ForeignKey('ProjectUpdate', on_delete=models.CASCADE, related_name='locations')