From 8d5767797c4c978bf35ba135f73b3447be1d79ca Mon Sep 17 00:00:00 2001 From: Kasper Brandt Date: Mon, 1 Feb 2016 17:29:53 +0100 Subject: [PATCH] [#1897] New result indicator models --- akvo/rest/serializers/__init__.py | 7 ++- akvo/rest/serializers/indicator.py | 19 +++++- akvo/rest/views/__init__.py | 7 ++- akvo/rest/views/indicator.py | 21 ++++++- akvo/rsr/admin.py | 19 ++++++ .../rsr/migrations/0050_auto_20160201_1513.py | 62 +++++++++++++++++++ .../rsr/migrations/0051_auto_20160201_1534.py | 43 +++++++++++++ .../rsr/migrations/0052_auto_20160201_1556.py | 22 +++++++ akvo/rsr/models/__init__.py | 4 +- akvo/rsr/models/indicator.py | 4 +- akvo/rsr/models/project_update.py | 52 ---------------- 11 files changed, 199 insertions(+), 61 deletions(-) create mode 100644 akvo/rsr/migrations/0050_auto_20160201_1513.py create mode 100644 akvo/rsr/migrations/0051_auto_20160201_1534.py create mode 100644 akvo/rsr/migrations/0052_auto_20160201_1556.py diff --git a/akvo/rest/serializers/__init__.py b/akvo/rest/serializers/__init__.py index b23a758e25..1c806bb2f4 100644 --- a/akvo/rest/serializers/__init__.py +++ b/akvo/rest/serializers/__init__.py @@ -17,7 +17,8 @@ from .employment import EmploymentSerializer from .focus_area import FocusAreaSerializer from .goal import GoalSerializer -from .indicator import IndicatorPeriodSerializer, IndicatorSerializer +from .indicator import (IndicatorSerializer, IndicatorPeriodSerializer, + IndicatorPeriodDataSerializer, IndicatorPeriodDataCommentSerializer) from .internal_organisation_id import InternalOrganisationIDSerializer from .invoice import InvoiceSerializer from .keyword import KeywordSerializer @@ -67,8 +68,10 @@ 'EmploymentSerializer', 'FocusAreaSerializer', 'GoalSerializer', - 'IndicatorPeriodSerializer', 'IndicatorSerializer', + 'IndicatorPeriodSerializer', + 'IndicatorPeriodDataSerializer', + 'IndicatorPeriodDataCommentSerializer', 'InternalOrganisationIDSerializer', 'InvoiceSerializer', 'KeywordSerializer', diff --git a/akvo/rest/serializers/indicator.py b/akvo/rest/serializers/indicator.py index 075e83bdd9..f35d511dc0 100644 --- a/akvo/rest/serializers/indicator.py +++ b/akvo/rest/serializers/indicator.py @@ -5,13 +5,30 @@ # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >. -from akvo.rsr.models import IndicatorPeriod, Indicator +from akvo.rsr.models import (Indicator, IndicatorPeriod, IndicatorPeriodData, + IndicatorPeriodDataComment) from .rsr_serializer import BaseRSRSerializer +class IndicatorPeriodDataCommentSerializer(BaseRSRSerializer): + + class Meta: + model = IndicatorPeriodDataComment + + +class IndicatorPeriodDataSerializer(BaseRSRSerializer): + + comments = IndicatorPeriodDataCommentSerializer(many=True, required=False, + allow_add_remove=True) + + class Meta: + model = IndicatorPeriodData + class IndicatorPeriodSerializer(BaseRSRSerializer): + data = IndicatorPeriodDataSerializer(many=True, required=False, allow_add_remove=True) + class Meta: model = IndicatorPeriod diff --git a/akvo/rest/views/__init__.py b/akvo/rest/views/__init__.py index fba637b368..4c1257e3d4 100644 --- a/akvo/rest/views/__init__.py +++ b/akvo/rest/views/__init__.py @@ -17,7 +17,8 @@ from .employment import EmploymentViewSet, approve_employment, set_group from .focus_area import FocusAreaViewSet from .goal import GoalViewSet -from .indicator import IndicatorViewSet, IndicatorPeriodViewSet +from .indicator import (IndicatorViewSet, IndicatorPeriodViewSet, IndicatorPeriodDataViewSet, + IndicatorPeriodDataCommentViewSet) from .internal_organisation_id import InternalOrganisationIDViewSet from .invoice import InvoiceViewSet from .keyword import KeywordViewSet @@ -83,8 +84,10 @@ 'EmploymentViewSet', 'FocusAreaViewSet', 'GoalViewSet', - 'IndicatorPeriodViewSet', 'IndicatorViewSet', + 'IndicatorPeriodViewSet', + 'IndicatorPeriodDataViewSet', + 'IndicatorPeriodDataCommentViewSet', 'InternalOrganisationIDViewSet', 'invite_user', 'InvoiceViewSet', diff --git a/akvo/rest/views/indicator.py b/akvo/rest/views/indicator.py index 9de3c95c8e..09fd775b6a 100644 --- a/akvo/rest/views/indicator.py +++ b/akvo/rest/views/indicator.py @@ -5,7 +5,8 @@ # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >. -from akvo.rsr.models import Indicator, IndicatorPeriod +from akvo.rsr.models import (Indicator, IndicatorPeriod, IndicatorPeriodData, + IndicatorPeriodDataComment) from ..serializers import IndicatorSerializer, IndicatorPeriodSerializer from ..viewsets import PublicProjectViewSet @@ -27,3 +28,21 @@ class IndicatorPeriodViewSet(PublicProjectViewSet): serializer_class = IndicatorPeriodSerializer filter_fields = ('indicator', ) project_relation = 'indicator__result__project__' + + +class IndicatorPeriodDataViewSet(PublicProjectViewSet): + """ + """ + queryset = IndicatorPeriodData.objects.all() + serializer_class = IndicatorPeriodSerializer + filter_fields = ('period', 'user', 'relative_data', 'status', 'update_method') + project_relation = 'period__indicator__result__project__' + + +class IndicatorPeriodDataCommentViewSet(PublicProjectViewSet): + """ + """ + queryset = IndicatorPeriodDataComment.objects.all() + serializer_class = IndicatorPeriodSerializer + filter_fields = ('data', 'user') + project_relation = 'period__indicator__result__project__' diff --git a/akvo/rsr/admin.py b/akvo/rsr/admin.py index 850c73a6df..709c3fa211 100644 --- a/akvo/rsr/admin.py +++ b/akvo/rsr/admin.py @@ -1244,3 +1244,22 @@ class ValidationSetAdmin(admin.ModelAdmin): inlines = (ValidationInline, ) admin.site.register(get_model('rsr', 'ProjectEditorValidationSet'), ValidationSetAdmin) + + +class IndicatorPeriodDataCommentInline(admin.TabularInline): + model = get_model('rsr', 'IndicatorPeriodDataComment') + + def get_extra(self, request, obj=None, **kwargs): + if obj: + return 1 if obj.comments.count() == 0 else 0 + else: + return 1 + + +class IndicatorPeriodDataAdmin(admin.ModelAdmin): + model = get_model('rsr', 'IndicatorPeriodData') + list_display = ('period', 'user', 'data', 'relative_data', 'status') + readonly_fields = ('created_at', 'last_modified_at') + inlines = (IndicatorPeriodDataCommentInline, ) + +admin.site.register(get_model('rsr', 'IndicatorPeriodData'), IndicatorPeriodDataAdmin) diff --git a/akvo/rsr/migrations/0050_auto_20160201_1513.py b/akvo/rsr/migrations/0050_auto_20160201_1513.py new file mode 100644 index 0000000000..e3508bece9 --- /dev/null +++ b/akvo/rsr/migrations/0050_auto_20160201_1513.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import sorl.thumbnail.fields +import akvo.rsr.fields +from django.conf import settings +import akvo.rsr.models.indicator + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsr', '0049_auto_20160128_1605'), + ] + + operations = [ + migrations.CreateModel( + name='IndicatorPeriodData', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created_at', models.DateTimeField(db_index=True, auto_now_add=True, null=True)), + ('last_modified_at', models.DateTimeField(db_index=True, auto_now=True, null=True)), + ('relative_data', models.BooleanField(default=False, verbose_name='relative data')), + ('data', akvo.rsr.fields.ValidXMLCharField(max_length=300, verbose_name='data')), + ('status', akvo.rsr.fields.ValidXMLCharField(default=b'D', choices=[(b'D', 'draft'), (b'P', 'pending approval'), (b'R', 'return for revision'), (b'A', 'approved')], max_length=1, blank=True, verbose_name='status', db_index=True)), + ('text', akvo.rsr.fields.ValidXMLTextField(verbose_name='text', blank=True)), + ('photo', sorl.thumbnail.fields.ImageField(upload_to=akvo.rsr.models.indicator.image_path, verbose_name='photo', blank=True)), + ('file', models.FileField(upload_to=akvo.rsr.models.indicator.file_path, verbose_name='file', blank=True)), + ('update_method', akvo.rsr.fields.ValidXMLCharField(default=b'W', choices=[(b'W', 'web'), (b'M', 'mobile')], max_length=1, blank=True, verbose_name='update method', db_index=True)), + ('period', models.ForeignKey(related_name='data', verbose_name='indicator period', to='rsr.IndicatorPeriod')), + ('user', models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'indicator period data', + 'verbose_name_plural': 'indicator period data', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='IndicatorPeriodDataComment', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created_at', models.DateTimeField(db_index=True, auto_now_add=True, null=True)), + ('last_modified_at', models.DateTimeField(db_index=True, auto_now=True, null=True)), + ('comment', akvo.rsr.fields.ValidXMLTextField(verbose_name='comment', blank=True)), + ('data', models.ForeignKey(related_name='comments', verbose_name='indicator period data', to='rsr.IndicatorPeriodData')), + ('user', models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'indicator period data comment', + 'verbose_name_plural': 'indicator period data comments', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='indicatorperiod', + name='locked', + field=models.BooleanField(default=True, db_index=True, verbose_name='locked'), + preserve_default=True, + ), + ] diff --git a/akvo/rsr/migrations/0051_auto_20160201_1534.py b/akvo/rsr/migrations/0051_auto_20160201_1534.py new file mode 100644 index 0000000000..61a7d6e2ca --- /dev/null +++ b/akvo/rsr/migrations/0051_auto_20160201_1534.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +def move_indicator_period_data(apps, schema_editor): + """ + Move the indicator period data from the ProjectUpdate model to the IndicatorPeriodData model + and delete the old indicator 'updates'. + """ + ProjectUpdate = apps.get_model('rsr', 'ProjectUpdate') + IndicatorPeriodData = apps.get_model('rsr', 'IndicatorPeriodData') + + indicator_updates = ProjectUpdate.objects.exclude(indicator_period=None) + for update in indicator_updates: + # Create new indicater period data object + IndicatorPeriodData.objects.create( + period=update.indicator_period, + user=update.user, + relative_data=True, + data=str(update.period_update), + text=update.text, + photo=update.photo, + update_method=update.update_method, + created_at=update.created_at, + last_modified_at=update.last_modified_at, + ) + # Delete all (old) indicator 'updates' + indicator_updates.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsr', '0050_auto_20160201_1513'), + ] + + operations = [ + migrations.RunPython( + move_indicator_period_data + ), + ] diff --git a/akvo/rsr/migrations/0052_auto_20160201_1556.py b/akvo/rsr/migrations/0052_auto_20160201_1556.py new file mode 100644 index 0000000000..88b6a6d428 --- /dev/null +++ b/akvo/rsr/migrations/0052_auto_20160201_1556.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsr', '0051_auto_20160201_1534'), + ] + + operations = [ + migrations.RemoveField( + model_name='projectupdate', + name='indicator_period', + ), + migrations.RemoveField( + model_name='projectupdate', + name='period_update', + ), + ] diff --git a/akvo/rsr/models/__init__.py b/akvo/rsr/models/__init__.py index 4cd820394e..141e61bb79 100644 --- a/akvo/rsr/models/__init__.py +++ b/akvo/rsr/models/__init__.py @@ -37,7 +37,7 @@ from .iati_import import IatiImport from .iati_import_job import IatiImportJob, CordaidZipIatiImportJob from .iati_import_log import IatiImportLog -from .indicator import Indicator, IndicatorPeriod +from .indicator import Indicator, IndicatorPeriod, IndicatorPeriodData, IndicatorPeriodDataComment from .invoice import Invoice from .internal_organisation_id import InternalOrganisationID from .keyword import Keyword @@ -91,6 +91,8 @@ 'IatiImportLog', 'Indicator', 'IndicatorPeriod', + 'IndicatorPeriodData', + 'IndicatorPeriodDataComment', 'Invoice', 'InternalOrganisationID', 'Keyword', diff --git a/akvo/rsr/models/indicator.py b/akvo/rsr/models/indicator.py index 1fb3fb51fb..06b1f2cec4 100644 --- a/akvo/rsr/models/indicator.py +++ b/akvo/rsr/models/indicator.py @@ -22,7 +22,6 @@ class Indicator(models.Model): result = models.ForeignKey('Result', verbose_name=_(u'result'), related_name='indicators') - locked = models.BooleanField(_(u'locked'), default=True) title = ValidXMLCharField( _(u'indicator title'), blank=True, max_length=500, help_text=_(u'Within each result indicators can be defined. Indicators should be items ' @@ -204,6 +203,7 @@ class Meta: class IndicatorPeriod(models.Model): indicator = models.ForeignKey(Indicator, verbose_name=_(u'indicator'), related_name='periods') + locked = models.BooleanField(_(u'locked'), default=True, db_index=True) period_start = models.DateField( _(u'period start'), null=True, blank=True, help_text=_(u'The start date of the reporting period for this indicator.') @@ -584,7 +584,7 @@ def delete(self, *args, **kwargs): deleted, because it could lead to strange scenarios. """ if self.status == self.STATUS_APPROVED: - raise FieldError(unicode(_(u'It is not possible to delete an approved update'))) + raise FieldError(unicode(_(u'It is not possible to delete an approved data update'))) super(IndicatorPeriodData, self).delete(*args, **kwargs) diff --git a/akvo/rsr/models/project_update.py b/akvo/rsr/models/project_update.py index 2c554e364c..b20cdb0b21 100644 --- a/akvo/rsr/models/project_update.py +++ b/akvo/rsr/models/project_update.py @@ -68,12 +68,6 @@ class ProjectUpdate(TimestampsMixin, models.Model): help_text=_(u'Universally unique ID set by creating user agent')) notes = ValidXMLTextField(verbose_name=_(u"Notes and comments"), blank=True, default='') - # Indicator updates - indicator_period = models.ForeignKey('IndicatorPeriod', related_name='updates', - verbose_name=_(u'indicator period'), blank=True, null=True) - period_update = models.DecimalField(_(u'period update'), blank=True, null=True, max_digits=14, - decimal_places=2) - class Meta: app_label = 'rsr' get_latest_by = "created_at" @@ -81,52 +75,6 @@ class Meta: verbose_name_plural = _(u'project updates') ordering = ['-id', ] - def save(self, *args, **kwargs): - if self.indicator_period and self.period_update: - if not self.pk: - # Newly created update to indicator period, update the actual value. - self.indicator_period.update_actual_value(self.period_update) - - else: - # Update to already existing indicator period, check if values have been changed. - orig_update = ProjectUpdate.objects.get(pk=self.pk) - if orig_update.indicator_period != self.indicator_period: - # Indicator period has changed. Substract value from old period, and add new - # value to new period. - try: - orig_update.update_actual_value(Decimal(orig_update.period_update) * -1) - except (InvalidOperation, TypeError): - pass - self.indicator_period.update_actual_value(self.period_update) - - elif orig_update.period_update != self.period_update: - # Indicator value has changed. Add the difference to it. - try: - self.indicator_period.update_actual_value( - Decimal(self.period_update) - Decimal(orig_update.period_update) - ) - except (InvalidOperation, TypeError): - self.indicator_period.update_actual_value(self.period_update) - - super(ProjectUpdate, self).save(*args, **kwargs) - - def delete(self, *args, **kwargs): - if self.indicator_period and self.period_update: - # Subsctract the value of the update from the actual value of the indicator period - if ProjectUpdate.objects.filter(indicator_period=self.indicator_period).\ - exclude(pk=self.pk).exists(): - try: - self.indicator_period.update_actual_value( - Decimal(self.period_update) * -1 - ) - except (InvalidOperation, TypeError): - pass - else: - # There's no other updates for this indicator period, remove actual value - self.indicator_period.actual_value = '' - self.indicator_period.save() - super(ProjectUpdate, self).delete(*args, **kwargs) - def img(self, value=''): try: return self.photo.thumbnail_tag