diff --git a/cadasta/questionnaires/renderer/__init__.py b/cadasta/questionnaires/renderer/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/cadasta/questionnaires/renderer/xform.py b/cadasta/questionnaires/renderer/xform.py
deleted file mode 100644
index b94ada48d..000000000
--- a/cadasta/questionnaires/renderer/xform.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from pyxform.builder import create_survey_element_from_dict
-from lxml import etree
-from rest_framework.renderers import BaseRenderer
-from ..models import Question
-from ..managers import fix_languages
-
-QUESTION_TYPES = dict(Question.TYPE_CHOICES)
-
-
-class XFormRenderer(BaseRenderer):
- format = 'xform'
- media_type = 'application/xml'
-
- def transform_questions(self, questions):
- children = []
- for q in questions:
- q['type'] = QUESTION_TYPES[q['type']]
-
- if q.get('label', -1) is None:
- del q['label']
-
- if 'options' in q:
- q['choices'] = q['options']
-
- bind = {}
- if q.get('required', False) is True:
- bind['required'] = 'yes'
- if q.get('relevant'):
- bind['relevant'] = q.get('relevant')
-
- if bind:
- q['bind'] = bind
-
- children.append(q)
- return children
-
- def transform_groups(self, groups):
- transformed_groups = []
- for g in groups:
- group = {
- 'type': 'group',
- 'name': g.get('name'),
- 'label': g.get('label'),
- 'children': self.transform_questions(g.get('questions')),
- 'index': g.get('index')
- }
- if group['label'] is None:
- del group['label']
-
- bind = {}
- if g.get('relevant'):
- bind['relevant'] = g.get('relevant')
-
- if bind:
- group['bind'] = bind
- transformed_groups.append(group)
- return transformed_groups
-
- def transform_to_xform_json(self, data):
- json = {
- 'default_language': 'default',
- 'name': data.get('id_string'),
- 'sms_keyword': data.get('id_string'),
- 'type': 'survey',
- 'id_string': data.get('id_string'),
- 'title': data.get('id_string')
- }
-
- questions = self.transform_questions(data.get('questions', []))
- question_groups = self.transform_groups(
- data.get('question_groups', []))
- json['children'] = sorted(questions + question_groups,
- key=lambda x: x['index'])
- return json
-
- def insert_version_attribute(self, xform, root_node, version):
- ns = {'xf': 'http://www.w3.org/2002/xforms'}
- root = etree.fromstring(xform)
- inst = root.find(
- './/xf:instance/xf:{root_node}'.format(
- root_node=root_node
- ), namespaces=ns
- )
- inst.set('version', str(version))
- xml = etree.tostring(
- root, method='xml', encoding='utf-8', pretty_print=True
- )
- return xml
-
- def insert_uuid_bind(self, xform, id_string):
- ns = {'xf': 'http://www.w3.org/2002/xforms'}
- root = etree.fromstring(xform)
- model = root.find('.//xf:model', namespaces=ns)
- etree.SubElement(model, 'bind', {
- 'calculate': "concat('uuid:', uuid())",
- 'nodeset': '/{}/meta/instanceID'.format(id_string),
- 'readonly': 'true()',
- 'type': 'string'
- })
- xml = etree.tostring(
- root, method='xml', encoding='utf-8', pretty_print=True
- )
- return xml
-
- def render(self, data, *args, **kwargs):
- json = self.transform_to_xform_json(data)
- survey = create_survey_element_from_dict(json)
- xml = survey.xml()
- fix_languages(xml)
- xml = xml.toxml()
-
- xml = self.insert_version_attribute(xml,
- data.get('id_string'),
- data.get('version'))
- xml = self.insert_uuid_bind(xml, data.get('id_string'))
-
- return xml
diff --git a/cadasta/questionnaires/tests/test_views_api.py b/cadasta/questionnaires/tests/test_views_api.py
index baee259ad..5d20d9378 100644
--- a/cadasta/questionnaires/tests/test_views_api.py
+++ b/cadasta/questionnaires/tests/test_views_api.py
@@ -51,16 +51,6 @@ def test_get_questionnaire(self):
assert response.status_code == 200
assert response.content['id'] == questionnaire.id
- def test_get_questionnaire_as_xform(self):
- questionnaire = QuestionnaireFactory.create(project=self.prj)
- response = self.request(user=self.user, get_data={'format': 'xform'})
- assert response.status_code == 200
- assert (response.headers['content-type'][1] ==
- 'application/xml; charset=utf-8')
- assert '<{id} id="{id}" version="{v}"/>'.format(
- id=questionnaire.id_string,
- v=questionnaire.version) in response.content
-
def test_get_questionnaire_that_does_not_exist(self):
response = self.request(user=self.user)
assert response.status_code == 404
diff --git a/cadasta/questionnaires/views/api.py b/cadasta/questionnaires/views/api.py
index 08117e46a..0c467d41c 100644
--- a/cadasta/questionnaires/views/api.py
+++ b/cadasta/questionnaires/views/api.py
@@ -3,25 +3,17 @@
from rest_framework import generics, mixins, status
from rest_framework.response import Response
-from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer
from tutelary.mixins import APIPermissionRequiredMixin
from organization.models import Project
from ..models import Questionnaire
from ..serializers import QuestionnaireSerializer
from ..exceptions import InvalidXLSForm
-from ..renderer.xform import XFormRenderer
class QuestionnaireDetail(APIPermissionRequiredMixin,
mixins.CreateModelMixin,
generics.RetrieveUpdateAPIView):
- renderer_classes = (JSONRenderer, BrowsableAPIRenderer, XFormRenderer, )
-
- def get_actions(self, request):
- if request.GET.get('format') == 'xform':
- return None
- return 'questionnaire.view'
def patch_actions(self, request):
try:
@@ -31,7 +23,7 @@ def patch_actions(self, request):
serializer_class = QuestionnaireSerializer
permission_required = {
- 'GET': get_actions,
+ 'GET': 'questionnaire.view',
'PUT': patch_actions,
}
diff --git a/cadasta/xforms/renderers.py b/cadasta/xforms/renderers.py
index e5cc77ac0..95d6228c7 100644
--- a/cadasta/xforms/renderers.py
+++ b/cadasta/xforms/renderers.py
@@ -4,6 +4,14 @@
from django.utils.encoding import smart_text
from rest_framework.compat import six
+from pyxform.builder import create_survey_element_from_dict
+from lxml import etree
+from rest_framework.renderers import BaseRenderer
+from questionnaires.models import Question
+from questionnaires.managers import fix_languages
+
+QUESTION_TYPES = dict(Question.TYPE_CHOICES)
+
class XFormListRenderer(renderers.BaseRenderer):
"""
@@ -56,3 +64,133 @@ def _to_xml(self, xml, data):
else:
xml.characters(smart_text(data))
+
+
+class XFormRenderer(BaseRenderer):
+ format = 'xform'
+ media_type = 'application/xml'
+
+ def transform_questions(self, questions):
+ children = []
+ for q in questions:
+ q['type'] = QUESTION_TYPES[q['type']]
+
+ if q.get('label', -1) is None:
+ del q['label']
+
+ if 'options' in q:
+ q['choices'] = q['options']
+
+ bind = {}
+ if q.get('required', False) is True:
+ bind['required'] = 'yes'
+ if q.get('relevant'):
+ bind['relevant'] = q.get('relevant')
+
+ if bind:
+ q['bind'] = bind
+
+ children.append(q)
+ return children
+
+ def transform_groups(self, groups):
+ transformed_groups = []
+ for g in groups:
+ group = {
+ 'type': 'group',
+ 'name': g.get('name'),
+ 'label': g.get('label'),
+ 'children': self.transform_questions(g.get('questions')),
+ 'index': g.get('index')
+ }
+ if group['label'] is None:
+ del group['label']
+
+ bind = {}
+ if g.get('relevant'):
+ bind['relevant'] = g.get('relevant')
+
+ if bind:
+ group['bind'] = bind
+ transformed_groups.append(group)
+ return transformed_groups
+
+ def transform_to_xform_json(self, data):
+ json = {
+ 'default_language': 'default',
+ 'name': data.get('id_string'),
+ 'sms_keyword': data.get('id_string'),
+ 'type': 'survey',
+ 'id_string': data.get('id_string'),
+ 'title': data.get('id_string')
+ }
+
+ questions = self.transform_questions(data.get('questions', []))
+ question_groups = self.transform_groups(
+ data.get('question_groups', []))
+ json['children'] = sorted(questions + question_groups,
+ key=lambda x: x['index'])
+ return json
+
+ def insert_version_attribute(self, xform, root_node, version):
+ ns = {'xf': 'http://www.w3.org/2002/xforms'}
+ root = etree.fromstring(xform)
+ inst = root.find(
+ './/xf:instance/xf:{root_node}'.format(
+ root_node=root_node
+ ), namespaces=ns
+ )
+ inst.set('version', str(version))
+ xml = etree.tostring(
+ root, method='xml', encoding='utf-8', pretty_print=True
+ )
+ return xml
+
+ def insert_uuid_bind(self, xform, id_string):
+ ns = {'xf': 'http://www.w3.org/2002/xforms'}
+ root = etree.fromstring(xform)
+ model = root.find('.//xf:model', namespaces=ns)
+ etree.SubElement(model, 'bind', {
+ 'calculate': "concat('uuid:', uuid())",
+ 'nodeset': '/{}/meta/instanceID'.format(id_string),
+ 'readonly': 'true()',
+ 'type': 'string'
+ })
+ xml = etree.tostring(
+ root, method='xml', encoding='utf-8', pretty_print=True
+ )
+ return xml
+
+ def render(self, data, *args, **kwargs):
+ charset = 'utf-8'
+ root_node = 'xforms'
+ xmlns = "http://openrosa.org/xforms/xformsList"
+
+ if 'detail' in data.keys():
+ stream = StringIO()
+
+ xml = SimplerXMLGenerator(stream, charset)
+ xml.startDocument()
+ xml.startElement(root_node, {'xmlns': xmlns})
+
+ for key, value in six.iteritems(data):
+ xml.startElement(key, {})
+ xml.characters(smart_text(value))
+ xml.endElement(key)
+
+ xml.endElement(root_node)
+ xml.endDocument()
+ return stream.getvalue()
+ else:
+ json = self.transform_to_xform_json(data)
+ survey = create_survey_element_from_dict(json)
+ xml = survey.xml()
+ fix_languages(xml)
+ xml = xml.toxml()
+
+ xml = self.insert_version_attribute(xml,
+ data.get('id_string'),
+ data.get('version'))
+ xml = self.insert_uuid_bind(xml, data.get('id_string'))
+
+ return xml
diff --git a/cadasta/xforms/serializers.py b/cadasta/xforms/serializers.py
index c31073d42..1ec33a437 100644
--- a/cadasta/xforms/serializers.py
+++ b/cadasta/xforms/serializers.py
@@ -21,13 +21,9 @@ class XFormListSerializer(FieldSelectorSerializer,
downloadUrl = serializers.SerializerMethodField('get_xml_form')
def get_xml_form(self, obj):
- url = reverse('api:v1:questionnaires:detail',
- kwargs={
- 'organization': obj.project.organization.slug,
- 'project': obj.project.slug
- }) + '?format=xform'
+ url = reverse('form-download', args=[obj.id])
url = self.context['request'].build_absolute_uri(url)
- if self.context['request'].META['SERVER_PROTOCOL'] != 'HTTP/1.1':
+ if self.context['request'].META['SERVER_PROTOCOL'] == 'HTTPS/1.1':
url = url.replace('http://', 'https://')
return url
diff --git a/cadasta/questionnaires/tests/test_renderer.py b/cadasta/xforms/tests/test_renderer.py
similarity index 99%
rename from cadasta/questionnaires/tests/test_renderer.py
rename to cadasta/xforms/tests/test_renderer.py
index 5c8d86f70..916211a7d 100644
--- a/cadasta/questionnaires/tests/test_renderer.py
+++ b/cadasta/xforms/tests/test_renderer.py
@@ -1,5 +1,5 @@
from django.test import TestCase
-from ..renderer.xform import XFormRenderer
+from ..renderers import XFormRenderer
class XFormRendererTest(TestCase):
diff --git a/cadasta/xforms/tests/test_serializers.py b/cadasta/xforms/tests/test_serializers.py
index d32cd3e1a..32c23ee94 100644
--- a/cadasta/xforms/tests/test_serializers.py
+++ b/cadasta/xforms/tests/test_serializers.py
@@ -1,6 +1,7 @@
import pytest
from django.test import TestCase
+from django.core.urlresolvers import reverse
from rest_framework.test import APIRequestFactory, force_authenticate
from core.tests.utils.cases import UserTestCase, FileStorageTestCase
@@ -40,16 +41,14 @@ def _test_serialize(self, https=False):
serializer = serializers.XFormListSerializer(
form, context={'request': request})
+ url_refix = 'https' if https else 'http'
assert serializer.data['formID'] == questionnaire.data['id_string']
assert serializer.data['name'] == questionnaire.data['title']
assert serializer.data['version'] == questionnaire.data['version']
assert (serializer.data['downloadUrl'] ==
- '{}://testserver/api/v1/organizations/{}/projects/{}'
- '/questionnaire/?format=xform'.format(
- ('https' if https else 'http'),
- project.organization.slug,
- project.slug))
+ url_refix + '://testserver' +
+ reverse('form-download', args=[form.id]))
assert serializer.data['hash'] == questionnaire.data['md5_hash']
def test_serialize(self):
diff --git a/cadasta/xforms/tests/test_urls_api.py b/cadasta/xforms/tests/test_urls_api.py
index 24b66387f..c77b485dd 100644
--- a/cadasta/xforms/tests/test_urls_api.py
+++ b/cadasta/xforms/tests/test_urls_api.py
@@ -11,6 +11,13 @@ def test_xforms_list(self):
resolved = resolve('/collect/')
assert resolved.func.__name__ == api.XFormListView.__name__
+ def test_xforms_download(self):
+ assert reverse('form-download', args=['a']) == '/collect/formList/a/'
+
+ resolved = resolve('/collect/formList/a/')
+ assert resolved.func.__name__ == api.XFormDownloadView.__name__
+ assert resolved.kwargs['questionnaire'] == 'a'
+
def test_xforms_submission(self):
assert reverse('submissions') == '/collect/submission'
diff --git a/cadasta/xforms/tests/test_views_api.py b/cadasta/xforms/tests/test_views_api.py
index 95eeb7bdc..831b9be86 100644
--- a/cadasta/xforms/tests/test_views_api.py
+++ b/cadasta/xforms/tests/test_views_api.py
@@ -1,3 +1,4 @@
+import json
import io
import pytest
@@ -8,6 +9,7 @@
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import InMemoryUploadedFile
from skivvy import APITestCase
+from tutelary.models import Policy
from accounts.tests.factories import UserFactory
from core.tests.utils.cases import UserTestCase, FileStorageTestCase
@@ -62,9 +64,8 @@ def test_get_xforms(self):
assert response.status_code == 200
assert questionnaire.md5_hash in response.content
- assert ('/v1/organizations/{}/projects/{}/questionnaire/'.format(
- questionnaire.project.organization.slug,
- questionnaire.project.slug) in response.content)
+ assert ('/collect/formList/{}/'.format(
+ questionnaire.id) in response.content)
assert str(questionnaire.version) in response.content
def test_get_xforms_with_unauthroized_user(self):
@@ -73,9 +74,8 @@ def test_get_xforms_with_unauthroized_user(self):
assert response.status_code == 200
assert questionnaire.md5_hash not in response.content
- assert ('/v1/organizations/{}/projects/{}/questionnaire/'.format(
- questionnaire.project.organization.slug,
- questionnaire.project.slug) not in response.content)
+ assert ('/collect/formList/{}/'.format(
+ questionnaire.id) not in response.content)
def test_get_xforms_with_superuser(self):
self.user.assign_policies(self.superuser_role)
@@ -84,9 +84,8 @@ def test_get_xforms_with_superuser(self):
assert response.status_code == 200
assert questionnaire.md5_hash in response.content
- assert ('/v1/organizations/{}/projects/{}/questionnaire/'.format(
- questionnaire.project.organization.slug,
- questionnaire.project.slug) in response.content)
+ assert ('/collect/formList/{}/'.format(
+ questionnaire.id) in response.content)
def test_get_xforms_with_no_superuser(self):
OrganizationRole.objects.create(
@@ -663,3 +662,56 @@ def test_form_repeat_party_minus_tenure(self):
self._test_resource('test_image_three', party_one)
self._test_resource('test_image_four', party_one)
self._test_resource('test_image_five', party_two)
+
+
+class XFormDownloadView(APITestCase, UserTestCase, TestCase):
+ view_class = api.XFormDownloadView
+
+ def setup_models(self):
+ clause = {
+ 'clause': [
+ {
+ 'effect': 'allow',
+ 'object': ['project/*/*'],
+ 'action': ['questionnaire.*']
+ }
+ ]
+ }
+ policy = Policy.objects.create(
+ name='test-policy',
+ body=json.dumps(clause))
+ self.user = UserFactory.create()
+ self.user.assign_policies(policy)
+
+ self.questionnaire = QuestionnaireFactory.create()
+
+ def setup_url_kwargs(self):
+ return {'questionnaire': self.questionnaire.id}
+
+ def test_get_questionnaire_as_xform(self):
+ response = self.request(user=self.user)
+ assert response.status_code == 200
+ assert (response.headers['content-type'][1] ==
+ 'application/xml; charset=utf-8')
+ assert '<{id} id="{id}" version="{v}"/>'.format(
+ id=self.questionnaire.id_string,
+ v=self.questionnaire.version) in response.content
+
+ def test_get_questionnaire_that_does_not_exist(self):
+ response = self.request(user=self.user,
+ url_kwargs={'questionnaire': 'abc'})
+ assert response.status_code == 404
+ assert 'Questionnaire not found.' in response.content
+
+ def test_get_questionnaire_with_unauthorized_user(self):
+ user = UserFactory.create()
+ response = self.request(user=user)
+ assert response.status_code == 403
+ assert ('You do not have permission to perform this '
+ 'action.' in response.content)
+
+ def test_get_questionnaire_with_authenticated_user(self):
+ response = self.request(user=None)
+ assert response.status_code == 401
+ assert ('Authentication credentials were '
+ 'not provided.' in response.content)
diff --git a/cadasta/xforms/urls/api.py b/cadasta/xforms/urls/api.py
index 86545c0e1..131e84d9a 100644
--- a/cadasta/xforms/urls/api.py
+++ b/cadasta/xforms/urls/api.py
@@ -4,8 +4,10 @@
urlpatterns = [
url(r'^$', api.XFormListView.as_view(
{'get': 'list'}), name='form-list'),
- url(r'^formList/', api.XFormListView.as_view(
+ url(r'^formList/$', api.XFormListView.as_view(
{'get': 'list'}), name='form-list'),
+ url(r'^formList/(?P[-\w]+)/$',
+ api.XFormDownloadView.as_view(), name='form-download'),
url(r'^submission$',
api.XFormSubmissionViewSet.as_view(
{'post': 'create', 'head': 'create'}),
diff --git a/cadasta/xforms/views/api.py b/cadasta/xforms/views/api.py
index 3683be9b6..4d39e4281 100644
--- a/cadasta/xforms/views/api.py
+++ b/cadasta/xforms/views/api.py
@@ -1,21 +1,25 @@
import logging
+from django.shortcuts import get_object_or_404
from django.utils.six import BytesIO
from django.utils.translation import ugettext as _
from questionnaires.models import Questionnaire
-from rest_framework import status, viewsets
+from rest_framework import status, viewsets, generics
from rest_framework.authentication import BasicAuthentication
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from tutelary.models import Role
+from tutelary.mixins import APIPermissionRequiredMixin
from xforms.models import XFormSubmission
from xforms.mixins.model_helper import ModelHelper
from xforms.mixins.openrosa_headers_mixin import OpenRosaHeadersMixin
from xforms.renderers import XFormListRenderer
from xforms.serializers import XFormListSerializer, XFormSubmissionSerializer
from xforms.exceptions import InvalidXMLSubmission
+from questionnaires.serializers import QuestionnaireSerializer
+from ..renderers import XFormRenderer
logger = logging.getLogger('xform.submissions')
@@ -135,3 +139,26 @@ def list(self, request, *args, **kwargs):
serializer = self.get_serializer(self.object_list, many=True)
return Response(
serializer.data, headers=self.get_openrosa_headers(request))
+
+
+class XFormDownloadView(APIPermissionRequiredMixin, generics.RetrieveAPIView):
+ authentication_classes = (BasicAuthentication,)
+ permission_classes = (IsAuthenticated,)
+ renderer_classes = (XFormRenderer,)
+ serializer_class = QuestionnaireSerializer
+ permission_required = 'questionnaire.view'
+
+ def get_perms_objects(self):
+ return [self.get_object().project]
+
+ def get_object(self):
+ if not hasattr(self, '_object'):
+ self._object = get_object_or_404(
+ Questionnaire, id=self.kwargs['questionnaire'])
+
+ return self._object
+
+ def get_serializer_context(self, *args, **kwargs):
+ context = super().get_serializer_context(*args, **kwargs)
+ context['project'] = self.get_object().project
+ return context