Skip to content

Commit

Permalink
Merge 6609484 into ebfcae5
Browse files Browse the repository at this point in the history
  • Loading branch information
ifirmawan committed Mar 13, 2023
2 parents ebfcae5 + 6609484 commit a938cce
Show file tree
Hide file tree
Showing 47 changed files with 3,295 additions and 125 deletions.
9 changes: 9 additions & 0 deletions akvo/rest/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,15 @@
url(r'v1/program/(?P<program_pk>[0-9]+)/updates/$',
views.program_updates,
name='program_updates'),
url(r'v1/program/(?P<program_pk>[0-9]+)/results/$',
views.get_program_results,
name='get_program_results'),
url(r'v1/program/(?P<program_pk>[0-9]+)/indicator_period_by_ids/$',
views.indicator_period_by_ids,
name='indicator_period_by_ids'),
url(r'v1/program/(?P<program_pk>[0-9]+)/indicator_updates_by_period_id/$',
views.indicator_updates_by_period_id,
name='indicator_updates_by_period_id'),
)


Expand Down
8 changes: 6 additions & 2 deletions akvo/rest/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
from .indicator_period_aggregation_job import IndicatorPeriodAggregationJobViewSet
from .indicator_period_label import IndicatorPeriodLabelViewSet, project_period_labels
from .indicator_period import (IndicatorPeriodViewSet, IndicatorPeriodFrameworkViewSet,
set_periods_locked, bulk_add_periods, bulk_remove_periods)
set_periods_locked, bulk_add_periods, bulk_remove_periods,
indicator_period_by_ids)
from .indicator_period_data import (IndicatorPeriodDataViewSet, IndicatorPeriodDataFrameworkViewSet,
IndicatorPeriodDataCommentViewSet, indicator_upload_file,
period_update_files, period_update_photos, set_updates_status,
indicator_previous_cumulative_update)
indicator_previous_cumulative_update,
indicator_updates_by_period_id)
from .indicator_period_disaggregation import IndicatorPeriodDisaggregationViewSet
from .disaggregation import DisaggregationViewSet
from .disaggregation_target import DisaggregationTargetViewSet
Expand Down Expand Up @@ -110,6 +112,7 @@
from .user import UserViewSet, change_password, update_details, current_user
from .user_management import invite_user
from .project_overview import project_results, project_result_overview, project_indicator_overview
from .program_results import get_program_results
from .program_results_geo import get_program_results_geo
from .project_enumerators import assignment_send, project_enumerators
from .demo_request import demo_request
Expand Down Expand Up @@ -246,6 +249,7 @@
'upload_indicator_update_photo',
'UserViewSet',
'project_results',
'get_program_results',
'get_program_results_geo',
'project_result_overview',
'project_indicator_overview',
Expand Down
21 changes: 21 additions & 0 deletions akvo/rest/views/indicator_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,24 @@ def bulk_remove_periods(request, project_pk):
pass

return JsonResponse(dict(), status=status.HTTP_204_NO_CONTENT)


@api_view(['GET'])
@authentication_classes([SessionAuthentication, TastyTokenAuthentication])
def indicator_period_by_ids(request, program_pk):
program = get_object_or_404(Project, pk=program_pk)
user = request.user
if not user.has_perm('rsr.view_project', program):
return HttpResponseForbidden()
period_ids = {id for id in request.GET.get('ids', '').split(',') if id}
contributors = program.descendants()
queryset = IndicatorPeriod.objects\
.prefetch_related(
'disaggregations',
'disaggregation_targets',
).filter(
indicator__result__project__in=contributors,
id__in=period_ids
)
serializer = IndicatorPeriodSerializer(queryset, many=True)
return Response(serializer.data)
29 changes: 29 additions & 0 deletions akvo/rest/views/indicator_period_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,32 @@ def indicator_previous_cumulative_update(request, project_pk, indicator_pk):
return HttpResponseForbidden()
data = get_previous_cumulative_update_value(user, indicator)
return Response(data)


@api_view(["GET"])
@authentication_classes([SessionAuthentication, TastyTokenAuthentication])
def indicator_updates_by_period_id(request, program_pk):
program = get_object_or_404(Project, pk=program_pk)
user = request.user
if not user.has_perm("rsr.view_project", program):
return HttpResponseForbidden()
period_ids = {id for id in request.GET.get("ids", "").split(",") if id}
contributors = program.descendants()
queryset = (
IndicatorPeriodData.objects.select_related(
"period",
"user",
"approved_by",
)
.prefetch_related(
"comments",
"disaggregations",
)
.filter(
status=IndicatorPeriodData.STATUS_APPROVED_CODE,
period__indicator__result__project__in=contributors,
period__in=period_ids,
)
)
serializer = IndicatorPeriodDataFrameworkSerializer(queryset, many=True)
return Response(serializer.data)
185 changes: 185 additions & 0 deletions akvo/rest/views/program_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# -*- 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 akvo.rest.authentication import TastyTokenAuthentication
from akvo.rsr.dataclasses import ResultData, IndicatorData, PeriodData, ContributorData
from akvo.rsr.models import Project, IndicatorPeriod
from akvo.rsr.models.result.utils import QUANTITATIVE
from django.shortcuts import get_object_or_404
from akvo.rest.views.project_overview import _get_indicator_target, is_aggregating_targets
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response
from rest_framework.status import HTTP_403_FORBIDDEN


@api_view(['GET'])
@authentication_classes([SessionAuthentication, TastyTokenAuthentication])
def get_program_results(request, program_pk):
queryset = Project.objects.prefetch_related('results')
program = get_object_or_404(queryset, pk=program_pk)
if not request.user.has_perm('rsr.view_project', program):
return Response('Request not allowed', status=HTTP_403_FORBIDDEN)
results = get_results_framework(program)
aggregate_targets = is_aggregating_targets(program)
data = {
'id': program.id,
'title': program.title,
'targets_at': program.targets_at,
'results': [
{
'id': result.id,
'title': result.title,
'type': result.iati_type_name,
'indicators': [
{
'id': indicator.id,
'title': indicator.title,
'type': 'quantitative' if indicator.type == QUANTITATIVE else 'qualitative',
'target_value': _get_indicator_target(indicator, program.targets_at, aggregate_targets),
'score_options': indicator.scores,
'cumulative': indicator.is_cumulative,
'periods': [
{
'id': period.id,
'period_start': period.period_start,
'period_end': period.period_end,
'target_value': period.target_value,
'contributors': format_contributors(period.contributors),
}
for period in indicator.periods
],
} for indicator in result.indicators
],
}
for result in results
],
}
return Response(data)


def format_contributors(contributors):
return [format_contributor(c) for c in contributors if c.project.aggregate_to_parent]


def format_contributor(contributor):
return {
'id': contributor.id,
'project_id': contributor.project.id,
'project_title': contributor.project.title,
'project_subtitle': contributor.project.subtitle,
'country': {'iso_code': contributor.project.country_code} if contributor.project.country_code else None,
'partners': {k: v for k, v in contributor.project.partners.items()},
'contributors': format_contributors(contributor.contributors) if contributor.project.aggregate_children else []
}


def get_results_framework(program):
raw_periods = fetch_periods(program)
lookup = {
'results': {},
'indicators': {},
'periods': {},
}
for r in raw_periods:
result_id = r['indicator__result__id']
indicator_id = r['indicator__id']
period_id = r['id']
if result_id not in lookup['results']:
lookup['results'][result_id] = ResultData.make(r, 'indicator__result__')
result = lookup['results'][result_id]
if indicator_id not in lookup['indicators']:
indicator = IndicatorData.make(r, 'indicator__')
result.indicators.append(indicator)
lookup['indicators'][indicator_id] = indicator
else:
indicator = lookup['indicators'][indicator_id]
if period_id not in lookup['periods']:
period = PeriodData.make(r)
indicator.periods.append(period)
lookup['periods'][period_id] = period
contributors = get_contributors(lookup['periods'].keys())
for c in contributors:
period_id = c.parent
if period_id in lookup['periods']:
lookup['periods'][period_id].contributors.append(c)
return lookup['results'].values()


def fetch_periods(program):
return IndicatorPeriod.objects\
.select_related('indicator', 'indicator__result')\
.filter(indicator__result__project=program)\
.order_by('indicator__result__order', 'indicator__order', '-period_start')\
.values(
'id', 'period_start', 'period_end', 'target_value', 'indicator__id',
'indicator__title', 'indicator__type', 'indicator__target_value', 'indicator__scores',
'indicator__result__id', 'indicator__result__title', 'indicator__result__type'
)


def get_contributors(root_period_ids):
flat_contributors = get_flat_contributors(root_period_ids)
return hierarchize_contributors(flat_contributors)


def get_flat_contributors(root_period_ids):
lookup = {}
raw_contributors = fetch_contributors(root_period_ids)
for c in raw_contributors:
contributor_id = c['id']
partner_id = c['indicator__result__project__partners__id']
if contributor_id not in lookup:
contributor = ContributorData.make(c)
lookup[contributor_id] = contributor
contributor = lookup[contributor_id]
if partner_id not in contributor.project.partners:
contributor.project.partners[partner_id] = c['indicator__result__project__partners__name']
return lookup.values()


def fetch_contributors(root_period_ids):
contributor_ids = fetch_contributor_ids(root_period_ids)
return IndicatorPeriod.objects\
.select_related('indicator__result__project')\
.prefetch_related('indicator__result__project__partners')\
.filter(id__in=contributor_ids)\
.values(
'id', 'parent_period',
'indicator__result__project__id',
'indicator__result__project__title',
'indicator__result__project__subtitle',
'indicator__result__project__aggregate_children',
'indicator__result__project__aggregate_to_parent',
'indicator__result__project__primary_location__country__iso_code',
'indicator__result__project__partners__id',
'indicator__result__project__partners__name',
)


def fetch_contributor_ids(root_period_ids):
family = set(root_period_ids)
while True:
children = IndicatorPeriod.objects.filter(parent_period__in=family).values_list('id', flat=True)
if family.union(children) == family:
break
family = family.union(children)
return family - root_period_ids


def hierarchize_contributors(contributors):
tops = []
lookup = {it.id: it for it in contributors}
ids = lookup.keys()
for contributor in contributors:
parent = contributor.parent
if not parent or parent not in ids:
tops.append(contributor)
else:
lookup[parent].contributors.append(contributor)
return tops
7 changes: 6 additions & 1 deletion akvo/rsr/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from datetime import date
from decimal import Decimal
from functools import cached_property, lru_cache
from typing import Optional, List, Set
from typing import Optional, List, Set, Dict

from akvo.rsr.models import IndicatorPeriodData
from akvo.rsr.models.result.utils import QUANTITATIVE, QUALITATIVE, PERCENTAGE_MEASURE, calculate_percentage
Expand Down Expand Up @@ -97,8 +97,10 @@ class ContributorProjectData(object):
title: str = ''
subtitle: str = ''
country: Optional[str] = None
country_code: Optional[str] = None
aggregate_children: bool = True
aggregate_to_parent: bool = True
partners: Dict[int, str] = field(default_factory=dict)
sectors: Set[str] = field(default_factory=set)

@classmethod
Expand All @@ -108,6 +110,7 @@ def make(cls, data, prefix=''):
title=data.get(f"{prefix}title", ''),
subtitle=data.get(f"{prefix}subtitle", ''),
country=data.get(f"{prefix}primary_location__country__name", None),
country_code=data.get(f"{prefix}primary_location__country__iso_code", None),
aggregate_children=data.get(f"{prefix}aggregate_children", True),
aggregate_to_parent=data.get(f"{prefix}aggregate_to_parent", True),
)
Expand Down Expand Up @@ -485,6 +488,7 @@ class IndicatorData(object):
baseline_comment: str = ''
target_value: Optional[Decimal] = None
target_comment: str = ''
scores: List[str] = field(default_factory=list)
periods: List[PeriodData] = field(default_factory=list)

@classmethod
Expand All @@ -501,6 +505,7 @@ def make(cls, data, prefix=''):
baseline_comment=data.get(f"{prefix}baseline_comment", ''),
target_value=data.get(f"{prefix}target_value", None),
target_comment=data.get(f"{prefix}target_comment", None),
scores=data.get(f"{prefix}scores", []),
)

@cached_property
Expand Down

0 comments on commit a938cce

Please sign in to comment.