diff --git a/docs/profiles.rst b/docs/profiles.rst index 95c4e50409..cc2434c27f 100644 --- a/docs/profiles.rst +++ b/docs/profiles.rst @@ -234,3 +234,47 @@ Response :: HTTP 200 OK + +Get the total number of monthly submissions +------------------------------------------- + +This gets the total number of submissions made in a month to a specific user's forms. +The result is a count of the submissions to both private and public forms. + +If there are no private forms then only the number of submissions to the public forms is returned, and vice versa. +If there are no submissions, then an empty dictionary is returned. + +Use the month and year as query parameters to get the total number of submissions for a specific month. +If no query parameters are used, the result is the number of submissions of the current month. +If only month is used, then the year is assumed to be the current year. And if only year is passed, then the month is +assumed to be the current year. + + +Example +^^^^^^^ + +:: + curl -X GET https://api.ona.io/api/v1/profiles/demouser/monthly_submissions + +Response +^^^^^^^^ + +:: + { + "public": 41, + "private": 185 + } + +Example +^^^^^^^ + +:: + curl -X GET https://api.ona.io/api/v1/profiles/demouser/monthly_submissions?month=5&year=2018 + +Response +^^^^^^^^ + +:: + { + "public": 240 + } diff --git a/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py b/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py index 0573cad0e0..ecbc287306 100644 --- a/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py @@ -1,24 +1,29 @@ +import datetime import json -import requests +import os from builtins import str -from django_digest.test import DigestAuth +from django.contrib.auth.models import User from django.db.models import signals from django.test.utils import override_settings -from httmock import all_requests, HTTMock +from django.utils.dateparse import parse_datetime + +import requests +from django_digest.test import DigestAuth +from httmock import HTTMock, all_requests from mock import patch -from onadata.apps.api.tests.viewsets.test_abstract_viewset import\ +from onadata.apps.api.tests.viewsets.test_abstract_viewset import \ TestAbstractViewSet +from onadata.apps.api.viewsets.connect_viewset import ConnectViewSet from onadata.apps.api.viewsets.user_profile_viewset import UserProfileViewSet +from onadata.apps.logger.models.instance import Instance from onadata.apps.main.models import UserProfile -from django.contrib.auth.models import User -from onadata.libs.serializers.user_profile_serializer import ( - _get_first_last_names) -from onadata.apps.api.viewsets.connect_viewset import ConnectViewSet -from onadata.libs.authentication import DigestAuthentication -from onadata.apps.main.models.user_profile import\ +from onadata.apps.main.models.user_profile import \ set_kpi_formbuilder_permissions +from onadata.libs.authentication import DigestAuthentication +from onadata.libs.serializers.user_profile_serializer import \ + _get_first_last_names def _profile_data(): @@ -832,3 +837,108 @@ def test_create_user_with_given_name(self): with self.settings(KPI_FORMBUILDER_URL='http://test_formbuilder$'): extra_data = {"username": "rust"} self._login_user_and_profile(extra_post_data=extra_data) + + def test_get_monthly_submissions(self): + """ + Test getting monthly submissions for a user + """ + view = UserProfileViewSet.as_view({'get': 'monthly_submissions'}) + # publish form and make submissions + self._publish_xls_form_to_project() + self._make_submissions() + count1 = Instance.objects.filter(xform=self.xform).count() + + request = self.factory.get('/', **self.extra) + response = view(request, user=self.user.username) + self.assertEquals(response.status_code, 200) + self.assertFalse(self.xform.shared) + self.assertEquals(response.data, {'private': count1}) + + # publish another form, make submission and make it public + self._publish_form_with_hxl_support() + self.assertEquals(self.xform.id_string, 'hxl_example') + count2 = Instance.objects.filter(xform=self.xform).filter( + date_created__year=datetime.datetime.now().year).filter( + date_created__month=datetime.datetime.now().month).filter( + date_created__day=datetime.datetime.now().day).count() + + self.xform.shared = True + self.xform.save() + request = self.factory.get('/', **self.extra) + response = view(request, user=self.user.username) + self.assertEquals(response.status_code, 200) + self.assertEquals(response.data, {'private': count1, 'public': count2}) + + def test_get_monthly_submissions_with_year_and_month_params(self): + """ + Test passing both month and year params + """ + view = UserProfileViewSet.as_view({'get': 'monthly_submissions'}) + # publish form and make a submission dated 2013-02-18 + self._publish_xls_form_to_project() + survey = self.surveys[0] + submission_time = parse_datetime('2013-02-18 15:54:01Z') + self._make_submission( + os.path.join(self.main_directory, 'fixtures', 'transportation', + 'instances', survey, survey + '.xml'), + forced_submission_time=submission_time) + count = Instance.objects.filter(xform=self.xform).filter( + date_created__month=2).filter(date_created__year=2013).count() + + # get submission count and assert the response is correct + data = {'month': 2, 'year': 2013} + request = self.factory.get('/', data=data, **self.extra) + response = view(request, user=self.user.username) + self.assertEquals(response.status_code, 200) + self.assertFalse(self.xform.shared) + self.assertEquals(response.data, {'private': count}) + + def test_monthly_submissions_with_month_param(self): + """ + Test that by passing only the value for month, + the year is assumed to be the current year + """ + view = UserProfileViewSet.as_view({'get': 'monthly_submissions'}) + month = datetime.datetime.now().month + year = datetime.datetime.now().year + + # publish form and make submissions + self._publish_xls_form_to_project() + self._make_submissions() + count = Instance.objects.filter(xform=self.xform).filter( + date_created__year=year).filter(date_created__month=month).count() + + data = {'month': month} + request = self.factory.get('/', data=data, **self.extra) + response = view(request, user=self.user.username) + self.assertEquals(response.status_code, 200) + self.assertFalse(self.xform.shared) + self.assertEquals(response.data, {'private': count}) + + def test_monthly_submissions_with_year_param(self): + """ + Test that by passing only the value for year + the month is assumed to be the current month + """ + view = UserProfileViewSet.as_view({'get': 'monthly_submissions'}) + month = datetime.datetime.now().month + + # publish form and make submissions dated the year 2013 + # and the current month + self._publish_xls_form_to_project() + survey = self.surveys[0] + _time = parse_datetime('2013-' + str(month) + '-18 15:54:01Z') + self._make_submission( + os.path.join(self.main_directory, 'fixtures', 'transportation', + 'instances', survey, survey + '.xml'), + forced_submission_time=_time) + count = Instance.objects.filter(xform=self.xform).filter( + date_created__year=2013).filter( + date_created__month=month).count() + + data = {'year': 2013} + request = self.factory.get('/', data=data, **self.extra) + response = view(request, user=self.user.username) + self.assertEquals(response.status_code, 200) + self.assertFalse(self.xform.shared) + self.assertEquals(response.data, {'private': count}) diff --git a/onadata/apps/api/viewsets/user_profile_viewset.py b/onadata/apps/api/viewsets/user_profile_viewset.py index 03eb75ca30..98e7325a24 100644 --- a/onadata/apps/api/viewsets/user_profile_viewset.py +++ b/onadata/apps/api/viewsets/user_profile_viewset.py @@ -3,11 +3,14 @@ UserProfileViewSet module. """ +import datetime import json from past.builtins import basestring # pylint: disable=redefined-builtin from django.conf import settings +from django.core.validators import ValidationError +from django.db.models import Count from rest_framework import serializers, status from rest_framework.decorators import detail_route @@ -19,6 +22,7 @@ from onadata.apps.api.permissions import UserProfilePermissions from onadata.apps.api.tools import get_baseviewset_class, load_class +from onadata.apps.logger.models.instance import Instance from onadata.apps.main.models import UserProfile from onadata.libs import filters from onadata.libs.mixins.authenticate_header_mixin import \ @@ -26,6 +30,8 @@ from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.mixins.object_lookup_mixin import ObjectLookupMixin +from onadata.libs.serializers.monthly_submissions_serializer import \ + MonthlySubmissionsSerializer from onadata.libs.serializers.user_profile_serializer import \ UserProfileSerializer @@ -164,3 +170,33 @@ def partial_update(self, request, *args, **kwargs): return super(UserProfileViewSet, self).partial_update(request, *args, **kwargs) + + @detail_route(methods=['GET']) + def monthly_submissions(self, request, *args, **kwargs): + """ Get the total number of submissions for a user """ + profile = self.get_object() + month_param = self.request.query_params.get('month', None) + year_param = self.request.query_params.get('year', None) + + # check if parameters are valid + if month_param: + if not month_param.isdigit() or \ + int(month_param) not in range(1, 13): + raise ValidationError(u'Invalid month provided as parameter') + if year_param: + if not year_param.isdigit() or len(year_param) != 4: + raise ValidationError(u'Invalid year provided as parameter') + + # Use query parameter values for month and year + # if none, use the current month and year + now = datetime.datetime.now() + month = month_param if month_param else now.month + year = year_param if year_param else now.year + + instance_count = Instance.objects.filter( + xform__user=profile.user, xform__deleted_at__isnull=True, + date_created__year=year, date_created__month=month).values( + 'xform__shared').annotate(num_instances=Count('id')) + + serializer = MonthlySubmissionsSerializer(instance_count, many=True) + return Response(serializer.data[0]) diff --git a/onadata/libs/serializers/monthly_submissions_serializer.py b/onadata/libs/serializers/monthly_submissions_serializer.py new file mode 100644 index 0000000000..2fa779f690 --- /dev/null +++ b/onadata/libs/serializers/monthly_submissions_serializer.py @@ -0,0 +1,28 @@ +""" +Monthly submissions serializer +""" +from rest_framework import serializers + + +class MonthlySubmissionsListSerializer(serializers.ListSerializer): + + def to_representation(self, data): + result = super(MonthlySubmissionsListSerializer, + self).to_representation(data) + result_dictionary = {} + for i in result: + label = 'public' if i['xform__shared'] else 'private' + result_dictionary[label] = i['num_instances'] + return [result_dictionary] + + +class MonthlySubmissionsSerializer(serializers.Serializer): + + class Meta: + list_serializer_class = MonthlySubmissionsListSerializer + + def to_representation(self, instance): + """ + Returns the total number of private/public submissions for a user + """ + return instance