Skip to content

Commit

Permalink
Merge pull request #2207 from akvo/#2189-iati-status-field
Browse files Browse the repository at this point in the history
[#2189] Move to IATI statuses
  • Loading branch information
KasperBrandt committed Jun 1, 2016
2 parents eb880ab + 9aaa593 commit 49bcf93
Show file tree
Hide file tree
Showing 32 changed files with 301 additions and 132 deletions.
2 changes: 1 addition & 1 deletion akvo/iati/checks/fields/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def status(project):
:param project: Project object
:return: All checks passed boolean, [Check results]
"""
if project.status:
if project.iati_status:
return True, [(u'success', u'has status')]

else:
Expand Down
6 changes: 3 additions & 3 deletions akvo/iati/exports/elements/activity_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from lxml import etree

STATUS_TO_CODE = {
'N': '6',
'N': '0',
'H': '1',
'A': '2',
'C': '3',
Expand All @@ -23,9 +23,9 @@ def activity_status(project):
:param project: Project object
:return: A list of Etree elements
"""
if project.status in STATUS_TO_CODE.keys():
if project.iati_status:
element = etree.Element("activity-status")
element.attrib['code'] = STATUS_TO_CODE[project.status]
element.attrib['code'] = project.iati_status
return [element]

return []
12 changes: 5 additions & 7 deletions akvo/iati/imports/mappers/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,14 @@ def do_import(self):
:return: List; contains fields that have changed
"""

status = self.get_child_elem_attrib(
iati_status = self.get_child_elem_attrib(
self.parent_elem, 'activity-status', 'code', 'status')

if status in CODE_TO_STATUS.keys():
status = CODE_TO_STATUS[status]
else:
self.add_log('activity-status@code', 'status', 'invalid status code')
status = Project.STATUS_NONE
if not iati_status:
self.add_log('activity-status@code', 'iati_status', 'invalid status code')
iati_status = '0'

return self.update_project_field('status', status)
return self.update_project_field('iati_status', iati_status)


class Conditions(ImportMapper):
Expand Down
2 changes: 1 addition & 1 deletion akvo/rest/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ProjectSerializer(BaseRSRSerializer):
current_image = Base64ImageField(required=False, allow_empty_file=True)
sync_owner = serializers.Field(source='reporting_org.id')
sync_owner_secondary_reporter = serializers.Field(source='reporting_partner.is_secondary_reporter')
status_label = serializers.Field(source='get_status_display')
status_label = serializers.Field(source='show_plain_status')
keyword_labels = serializers.Field(source='keyword_labels')

class Meta:
Expand Down
2 changes: 1 addition & 1 deletion akvo/rsr/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,7 @@ class ProjectAdmin(TimestampsAdminDisplayMixin, ObjectPermissionsModelAdmin, Nes
u'This section should contain the top-level information about your project which will be publicly '
u'available and used within searches. Try to keep your Title and Subtitle short and snappy.'
),
'fields': ('title', 'subtitle', 'iati_activity_id', 'status', 'date_start_planned',
'fields': ('title', 'subtitle', 'iati_activity_id', 'iati_status', 'date_start_planned',
'date_start_actual', 'date_end_planned', 'date_end_actual', 'language',
'currency', 'donate_button', 'hierarchy', 'is_public', 'validations'),
}),
Expand Down
7 changes: 6 additions & 1 deletion akvo/rsr/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from copy import deepcopy
from django.utils.translation import ugettext_lazy as _
from akvo.codelists.store.codelists_v201 import SECTOR_CATEGORY
from akvo.codelists.store.codelists_v201 import ACTIVITY_STATUS, SECTOR_CATEGORY
from akvo.utils import codelist_choices
from .models import (Category, Organisation, OrganisationLocation, Project,
ProjectLocation, ProjectUpdate, ProjectUpdateLocation)
Expand Down Expand Up @@ -150,6 +150,11 @@ class ProjectFilter(django_filters.FilterSet):
label=_(u'status'),
choices=ANY_CHOICE + Project.STATUSES)

iati_status = django_filters.ChoiceFilter(
initial=_('All'),
label=_(u'iati status'),
choices=([('', _('All'))] + codelist_choices(ACTIVITY_STATUS)))

title = django_filters.CharFilter(
lookup_type='icontains',
label=_(u'Search'),
Expand Down
2 changes: 1 addition & 1 deletion akvo/rsr/management/commands/funds_needed.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def handle(self, *args, **options):
# Set projects to 'Complete' for the retrieved projects
if self.projects.count() > 0 and options['complete']:
for p in self.projects:
p.status = Project.STATUS_COMPLETE
p.iati_status = '3'
p.save()

self.stdout.write('')
Expand Down
22 changes: 22 additions & 0 deletions akvo/rsr/migrations/0075_auto_20160525_1006.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import akvo.rsr.fields


class Migration(migrations.Migration):

dependencies = [
# ('rsr', '0070_auto_20160519_1205'),
('rsr', '0074_auto_20160526_0938'),
]

operations = [
migrations.AddField(
model_name='project',
name='iati_status',
field=akvo.rsr.fields.ValidXMLCharField(default=b'0', choices=[('0', ''), ('1', '1 - Pipeline/identification'), ('2', '2 - Implementation'), ('3', '3 - Completion'), ('4', '4 - Post-completion'), ('5', '5 - Cancelled'), ('6', '6 - Suspended')], max_length=1, help_text='There are six different project statuses:<br/>1) Pipeline/identification: the project is being scoped or planned<br/>2) Implementation: the project is currently being implemented<br/>3) Completion: the project is complete or the final disbursement has been made<br/>4) Post-completion: the project is complete or the final disbursement has been made, but the project remains open pending financial sign off or M&E<br/>5) Cancelled: the project has been cancelled<br/>6) Suspended: the project has been temporarily suspended or the reporting partner no longer uses RSR.', verbose_name='status', db_index=True),
preserve_default=True,
),
]
39 changes: 39 additions & 0 deletions akvo/rsr/migrations/0076_auto_20160525_1008.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


# convert old rsr statuses to equivalent iati status codes
# # see http://iatistandard.org/202/codelists/ActivityStatus/
STATUS_TO_CODE = {
'N': '0',
'H': '1',
'A': '2',
'C': '3',
'L': '5',
'R': '6',
}

# update new field with existing values
def populate_iati_status(apps, schema_editor):
Project = apps.get_model('rsr', 'Project')
for p in Project.objects.all():
p.iati_status = STATUS_TO_CODE[p.status]
p.save()


class Migration(migrations.Migration):

dependencies = [
('rsr', '0075_auto_20160525_1006'),
]

operations = [
migrations.RunPython(populate_iati_status, populate_iati_status),
migrations.RunSQL("UPDATE rsr_projecteditorvalidation SET validation = 'rsr_project.iati_status' "
"WHERE validation = 'rsr_project.status';",
"UPDATE rsr_projecteditorvalidation SET validation = 'rsr_project.status' "
"WHERE validation = 'rsr_project.iati_status';"
)
]
7 changes: 1 addition & 6 deletions akvo/rsr/models/organisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,7 @@ def supportpartners_with_projects(self):
return self.filter(
partnerships__iati_organisation_role=Partnership.IATI_ACCOUNTABLE_PARTNER,
partnerships__project__publishingstatus__status=PublishingStatus.STATUS_PUBLISHED,
partnerships__project__status__in=[
Project.STATUS_ACTIVE,
Project.STATUS_COMPLETE,
Project.STATUS_NEEDS_FUNDING,
Project.STATUS_CANCELLED,
]
partnerships__project__iati_status__in=Project.NOT_SUSPENDED
).distinct()

def ngos(self):
Expand Down
127 changes: 91 additions & 36 deletions akvo/rsr/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@

from sorl.thumbnail.fields import ImageField

from akvo.codelists.models import (AidType, ActivityScope, CollaborationType, FinanceType, FlowType,
from akvo.codelists.models import (AidType, ActivityScope, ActivityStatus, CollaborationType, FinanceType, FlowType,
TiedStatus)
from akvo.codelists.store.codelists_v202 import (AID_TYPE, ACTIVITY_SCOPE, COLLABORATION_TYPE,
from akvo.codelists.store.codelists_v202 import (AID_TYPE, ACTIVITY_SCOPE, ACTIVITY_STATUS, COLLABORATION_TYPE,
FINANCE_TYPE, FLOW_TYPE, TIED_STATUS,
BUDGET_IDENTIFIER_VOCABULARY)
from akvo.utils import codelist_choices, codelist_value, rsr_image_path, rsr_show_keywords
from akvo.utils import codelist_choices, codelist_value, codelist_name, rsr_image_path, rsr_show_keywords

from ...iati.checks.iati_checks import IatiChecks

Expand Down Expand Up @@ -92,27 +92,54 @@ class Project(TimestampsMixin, models.Model):
)

STATUSES_COLORS = {
STATUS_NONE: 'black',
STATUS_NEEDS_FUNDING: 'orange',
STATUS_ACTIVE: '#AFF167',
STATUS_COMPLETE: 'grey',
STATUS_CANCELLED: 'red',
STATUS_ARCHIVED: 'grey',
'0': 'grey',
'1': 'orange',
'2': '#AFF167',
'3': 'grey',
'4': 'grey',
'5': 'red',
'6': 'grey',
}

CODE_TO_STATUS = {
'0': 'N',
'1': 'H',
'2': 'A',
'3': 'C',
'4': 'C',
'5': 'L',
'6': 'R'
}

STATUS_TO_CODE = {
'N': '0',
'H': '1',
'A': '2',
'C': '3',
'L': '5',
'R': '6'
}

# Status combinations used in conditionals
EDIT_DISABLED = ['3', '5']
DONATE_DISABLED = ['0', '3', '4', '5', '6']
NOT_SUSPENDED = ['1', '2', '3', '4', '5']

title = ValidXMLCharField(_(u'project title'), max_length=200, db_index=True, blank=True)
subtitle = ValidXMLCharField(_(u'project subtitle'), max_length=200, blank=True)
status = ValidXMLCharField(
_(u'status'), max_length=1, choices=STATUSES, db_index=True, default=STATUS_NONE,
help_text=_(u'There are five different project statuses:<br/>'
u'1) Needs funding: this project still needs funding and implementation has '
u'not yet started.<br/>'
u'2) Active: the implementation phase has begun.<br/>'
u'3) Completed: the project has been completed.<br/>'
u'4) Cancelled: the project never took place or work stopped before it was '
u'fully implemented.<br/>'
u'5) Archived: projects are archived when the reporting partner no longer uses '
u'RSR.')
status = ValidXMLCharField(_(u'status'), max_length=1, choices=STATUSES, db_index=True, default=STATUS_NONE)
iati_status = ValidXMLCharField(
_(u'status'), max_length=1, choices=([('0', '')] + codelist_choices(ACTIVITY_STATUS)),
db_index=True, default='0',
help_text=_(u'There are six different project statuses:<br/>'
u'1) Pipeline/identification: the project is being scoped or planned<br/>'
u'2) Implementation: the project is currently being implemented<br/>'
u'3) Completion: the project is complete or the final disbursement has been made<br/>'
u'4) Post-completion: the project is complete or the final disbursement has been made, '
u'but the project remains open pending financial sign off or M&E<br/>'
u'5) Cancelled: the project has been cancelled<br/>'
u'6) Suspended: the project has been temporarily suspended '
u'or the reporting partner no longer uses RSR.')
)
categories = models.ManyToManyField(
'Category', verbose_name=_(u'categories'), related_name='projects', blank=True
Expand Down Expand Up @@ -387,6 +414,18 @@ def save(self, last_updated=False, *args, **kwargs):
if self.iati_activity_id:
self.iati_activity_id = self.iati_activity_id.strip()

# Update legacy status field
if self.pk is not None:
orig = Project.objects.get(pk=self.pk)

if self.iati_status != orig.iati_status:
self.status = self.CODE_TO_STATUS[self.iati_status]
super(Project, self).save(update_fields=['status'])

if self.status != orig.status:
self.iati_status = self.STATUS_TO_CODE[self.status]
super(Project, self).save(update_fields=['iati_status'])

super(Project, self).save(*args, **kwargs)

def clean(self):
Expand Down Expand Up @@ -421,8 +460,8 @@ def accepts_donations(self):
"""Returns True if a project accepts donations, otherwise False.
A project accepts donations when the donate button settings is True, the project is published,
the project needs funding and is not cancelled or archived."""
if self.donate_button and self.is_published() and self.funds_needed > 0 and \
self.status in [Project.STATUS_NEEDS_FUNDING, Project.STATUS_ACTIVE, Project.STATUS_COMPLETE]:
if self.donate_button and self.is_published() and self.funds_needed > 0 and not \
self.iati_status in Project.DONATE_DISABLED:
return True
return False

Expand Down Expand Up @@ -626,31 +665,37 @@ def public(self):
return self.filter(is_public=True)

def status_none(self):
return self.filter(status__exact=Project.STATUS_NONE)
return self.filter(iati_status__exact='6')

def status_active(self):
return self.filter(status__exact=Project.STATUS_ACTIVE)
return self.filter(iati_status__exact='2')

def status_onhold(self):
return self.filter(status__exact=Project.STATUS_NEEDS_FUNDING)
return self.filter(iati_status__exact='1')

def status_complete(self):
return self.filter(status__exact=Project.STATUS_COMPLETE)
return self.filter(iati_status__exact='3')

def status_not_complete(self):
return self.exclude(status__exact=Project.STATUS_COMPLETE)
return self.exclude(iati_status__exact='3')

def status_post_complete(self):
return self.filter(iati_status__exact='4')

def status_not_post_complete(self):
return self.exclude(iati_status__exact='4')

def status_cancelled(self):
return self.filter(status__exact=Project.STATUS_CANCELLED)
return self.filter(iati_status__exact='5')

def status_not_cancelled(self):
return self.exclude(status__exact=Project.STATUS_CANCELLED)
return self.exclude(iati_status__exact='5')

def status_archived(self):
return self.filter(status__exact=Project.STATUS_ARCHIVED)
return self.filter(iati_status__exact='6')

def status_not_archived(self):
return self.exclude(status__exact=Project.STATUS_ARCHIVED)
return self.exclude(iati_status__exact='6')

def active(self):
"""Return projects that are published and not cancelled or archived"""
Expand Down Expand Up @@ -856,10 +901,20 @@ def latest_update(self):

def show_status(self):
"Show the current project status"
return mark_safe(
"<span style='color: %s;'>%s</span>" % (self.STATUSES_COLORS[self.status],
self.get_status_display())
)
if not self.iati_status == '0':
return mark_safe(
"<span style='color: %s;'>%s</span>" % (self.STATUSES_COLORS[self.iati_status],
codelist_name(ActivityStatus, self, 'iati_status'))
)
else:
return ''

def show_plain_status(self):
"Show the current project status value without styling"
if not self.iati_status == '0':
return codelist_name(ActivityStatus, self, 'iati_status')
else:
return ''

def show_current_image(self):
try:
Expand Down Expand Up @@ -1028,7 +1083,7 @@ def show_status_large(self):
return mark_safe(
"<span class='status_large' style='background-color:%s; color:inherit; "
"display:inline-block;'>%s</span>" % (
self.STATUSES_COLORS[self.status], self.get_status_display()
self.STATUSES_COLORS[self.iati_status], codelist_name(ActivityStatus, self, 'iati_status')
)
)

Expand Down
4 changes: 2 additions & 2 deletions akvo/rsr/models/publishing_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def clean(self):
code='subtitle')
)

if self.project.status == 'N':
if self.project.iati_status == '6':
validation_errors.append(
ValidationError(_('Project needs to have a status.'),
ValidationError(_('Project needs to have non-suspended status.'),
code='status')
)

Expand Down
Loading

0 comments on commit 49bcf93

Please sign in to comment.