Source code for akvo.rsr.models.result.indicator_period_data

# -*- 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 >.

from decimal import Decimal

from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from sorl.thumbnail.fields import ImageField

from .utils import (calculate_percentage, file_path, image_path,
                    MultipleUpdateError, PERCENTAGE_MEASURE, QUALITATIVE,
                    QUANTITATIVE)
from akvo.rsr.fields import ValidXMLCharField, ValidXMLTextField
from akvo.rsr.mixins import TimestampsMixin, IndicatorUpdateMixin
from akvo.rsr.usecases.jobs.aggregation import schedule_aggregation_jobs
from akvo.utils import rsr_image_path


[docs]class IndicatorPeriodData(TimestampsMixin, IndicatorUpdateMixin, models.Model): """ Model for adding data to an indicator period. """ project_relation = 'results__indicators__periods__data__in' STATUS_DRAFT = str(_('draft')) STATUS_PENDING = str(_('pending approval')) STATUS_REVISION = str(_('return for revision')) STATUS_APPROVED = str(_('approved')) STATUS_DRAFT_CODE = 'D' STATUS_PENDING_CODE = 'P' STATUS_REVISION_CODE = 'R' STATUS_APPROVED_CODE = 'A' STATUS_CODES_LIST = [STATUS_DRAFT_CODE, STATUS_PENDING_CODE, STATUS_REVISION_CODE, STATUS_APPROVED_CODE] STATUSES_LABELS_LIST = [STATUS_DRAFT, STATUS_PENDING, STATUS_REVISION, STATUS_APPROVED] STATUSES = list(zip(STATUS_CODES_LIST, STATUSES_LABELS_LIST)) UPDATE_METHODS = ( ('W', _('web')), ('M', _('mobile')), ) period = models.ForeignKey('IndicatorPeriod', verbose_name=_('indicator period'), related_name='data', on_delete=models.PROTECT) # TODO: rename to created_by when old results framework page is no longer in use user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_('user'), db_index=True, related_name='created_period_updates') approved_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, verbose_name=_('approved by'), db_index=True, related_name='approved_period_updates', blank=True, null=True, ) narrative = ValidXMLTextField(_('qualitative indicator narrative'), blank=True) score_index = models.SmallIntegerField(_('score index'), null=True, blank=True) score_indices = ArrayField(models.SmallIntegerField(), default=list, blank=True) status = ValidXMLCharField(_('status'), max_length=1, choices=STATUSES, db_index=True, default=STATUS_DRAFT_CODE) text = ValidXMLTextField(_('text'), blank=True) review_note = ValidXMLTextField(_('text'), blank=True) photo = ImageField(_('photo'), blank=True, upload_to=image_path, max_length=255) file = models.FileField(_('file'), blank=True, upload_to=file_path, max_length=255) update_method = ValidXMLCharField(_('update method'), blank=True, max_length=1, choices=UPDATE_METHODS, db_index=True, default='W') class Meta: app_label = 'rsr' verbose_name = _('indicator period data') verbose_name_plural = _('indicator period data') ordering = ('-id', )
[docs] def save(self, recalculate=True, *args, **kwargs): # Allow only a single update for percentage measure indicators if not self.period.can_save_update(self.id): raise MultipleUpdateError('Cannot create multiple updates with percentages') if ( self.period.indicator.measure == PERCENTAGE_MEASURE and self.numerator is not None and self.denominator not in {0, '0', None} ): self.value = calculate_percentage(self.numerator, self.denominator) super(IndicatorPeriodData, self).save(*args, **kwargs) # In case the status is approved, recalculate the period if recalculate and self.status == self.STATUS_APPROVED_CODE: # FIXME: Should we call this even when status is not approved? schedule_aggregation_jobs(self.period) self.period.update_actual_comment() # Update score even when the update is not approved, yet. It handles the # case where an approved update is returned for revision, etc. self.period.update_score()
[docs] def delete(self, *args, **kwargs): old_status = self.status period = self.period super(IndicatorPeriodData, self).delete(*args, **kwargs) # In case the status was approved, recalculate the period if old_status == self.STATUS_APPROVED_CODE: schedule_aggregation_jobs(period) self.period.update_actual_comment() self.period.update_score()
[docs] def clean(self): """ Perform several checks before we can actually save the update data. """ validation_errors = {} project = self.period.indicator.result.project # Don't allow a data update to an unpublished project if not project.is_published(): validation_errors['period'] = str(_('Indicator period must be part of a published ' 'project to add data to it')) raise ValidationError(validation_errors) # Don't allow a data update to a non-Impact project if not project.is_impact_project: validation_errors['period'] = str(_('Indicator period must be part of an RSR ' 'Impact project to add data to it')) raise ValidationError(validation_errors) # Don't allow a data update to a locked period if self.period.locked: validation_errors['period'] = str(_('Indicator period must be unlocked to add ' 'data to it')) raise ValidationError(validation_errors) # Don't allow a data update to an aggregated parent period with 'percentage' as measurement if self.period.indicator.children_aggregate_percentage: validation_errors['period'] = str( _('Indicator period has an average aggregate of the child projects. Disable ' 'aggregations to add data to it')) raise ValidationError(validation_errors) if self.pk: orig = IndicatorPeriodData.objects.get(pk=self.pk) # Don't allow for the indicator period to change if orig.period != self.period: validation_errors['period'] = str(_('Not allowed to change indicator period ' 'in a data update')) if self.period.indicator.type == QUANTITATIVE: if self.narrative is not None: validation_errors['period'] = str( _('Narrative field should be empty in quantitative indicators')) if self.value is not None: try: self.value = Decimal(self.value) except Exception: validation_errors['period'] = str( _('Only numeric values are allowed in quantitative indicators')) if self.period.indicator.type == QUALITATIVE: if self.value is not None: validation_errors['period'] = str( _('Value field should be empty in qualitative indicators')) if validation_errors: raise ValidationError(validation_errors)
@property def status_display(self): """ Returns the display of the status. """ try: return dict(self.STATUSES)[self.status].capitalize() except KeyError: return '' @property def photo_url(self): """ Returns the full URL of the photo. """ return self.photo.url if self.photo else '' @property def file_url(self): """ Returns the full URL of the file. """ return self.file.url if self.file else ''
[docs] @classmethod def get_user_viewable_updates(cls, queryset, user): approved_updates = queryset.filter(status=cls.STATUS_APPROVED_CODE) if user.is_anonymous: f_queryset = approved_updates elif user.is_admin or user.is_superuser: f_queryset = queryset else: from akvo.rsr.models import Project projects = Project.objects\ .filter(results__indicators__periods__data__in=queryset)\ .distinct() project = projects.first() if projects.count() == 1 else None # Allow Nuffic users to see all updates, irrespective of what state they are in if project is not None and project.in_nuffic_hierarchy() and user.has_perm('rsr.view_project', project): f_queryset = queryset else: own_updates = queryset.filter(user=user) non_draft_updates = queryset.exclude(status=cls.STATUS_DRAFT_CODE) filter_ = user.get_permission_filter( 'rsr.view_indicatorperioddata', 'period__indicator__result__project__' ) others_updates = non_draft_updates.filter(filter_) f_queryset = ( approved_updates | own_updates | others_updates ) return f_queryset.distinct()
[docs]def update_image_path(instance, file_name): path = 'db/indicator_period_data/%d/data_photo/%%(instance_pk)s/%%(file_name)s' % instance.update.pk return rsr_image_path(instance, file_name, path)
[docs]class IndicatorPeriodDataPhoto(models.Model): update = models.ForeignKey('IndicatorPeriodData', on_delete=models.CASCADE) photo = ImageField(_('photo'), upload_to=update_image_path, max_length=255) class Meta: app_label = 'rsr'
[docs]def update_file_path(instance, file_name): path = 'db/indicator_period_data/%d/data_file/%%(instance_pk)s/%%(file_name)s' % instance.update.pk return rsr_image_path(instance, file_name, path)
[docs]class IndicatorPeriodDataFile(models.Model): update = models.ForeignKey('IndicatorPeriodData', on_delete=models.CASCADE) file = models.FileField(_('file'), upload_to=update_file_path, max_length=255) class Meta: app_label = 'rsr'
[docs]@receiver(post_save, sender=IndicatorPeriodData) def set_qualitative_narrative(sender, **kwargs): """Update the narrative field of a qualitative indicator on updates.""" # Disable signal handler when loading fixtures if kwargs.get('raw', False): return update = kwargs['instance'] if update.status != IndicatorPeriodData.STATUS_APPROVED_CODE: return if update.period.indicator.type != QUALITATIVE: return # Current update is the latest update? if update.period.approved_updates.last().id != update.id: return update.period.narrative = update.narrative update.period.save()
@receiver(post_save, sender=IndicatorPeriodData) def _send_return_for_revision_email(sender, **kwargs): """Send email to assigned enumerator when indicator is returned for revision.""" # Disable signal handler when loading fixtures if kwargs.get('raw', False): return update = kwargs['instance'] if update.status != IndicatorPeriodData.STATUS_REVISION_CODE: return from akvo.rest.views.project_enumerators import send_return_for_revision_email send_return_for_revision_email(update)