diff --git a/README.md b/README.md index b01fb94..4d75998 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # REST Framework XML -[![build-status-image]][github-action] +[![build-status-image]][travis] [![pypi-version]][pypi] **XML support for Django REST Framework** @@ -15,9 +15,11 @@ XML support extracted as a third party package directly from the official Django ## Requirements -* Python 3.5+ -* Django 2.2+ -* Django REST Framework 3.11+ +* Python (2.7, 3.4, 3.5, 3.6) +* Django (1.8 - 1.11, 2.0 - 2.1) +* Django REST Framework (2.4, 3.0 - 3.9) + +This project is tested on the combinations of Python and Django that are supported by each version of Django REST Framework. ## Installation @@ -75,6 +77,59 @@ class UserViewSet(viewsets.ModelViewSet): ``` +## SOAP Renderer + +You can also set the SOAP renderer for an individual view, or viewset, using the APIView class based views. You must reload soap schema. + +```python +from rest_framework import routers, serializers, viewsets +from rest_framework_xml.parsers import XMLParser +from rest_framework_xml.renderers import SOAPRenderer + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ('url', 'username', 'email', 'is_staff') + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + parser_classes = (XMLParser,) + + soap_tag = "SOAP-TEST" + soap_endpoint = "https://xml.com/soap" + soap_service = "soapService" + + renderer = SOAPRenderer + renderer.set_schema_attrs(soap_tag, soap_endpoint, soap_service) + renderer_classes = (renderer,) +``` + +### Sample output + +```xml +\n + + + + + Some words + + 1 + tag one + 2 + tag two + + 2020-04-14 12:45:00 + + + +``` + ## Documentation & Support Full documentation for the project is available at [http://jpadilla.github.io/django-rest-framework-xml][docs]. @@ -82,8 +137,8 @@ Full documentation for the project is available at [http://jpadilla.github.io/dj You may also want to follow the [author][jpadilla] on Twitter. -[build-status-image]: https://github.com/jpadilla/django-rest-framework-xml/workflows/CI/badge.svg -[github-action]: https://github.com/jpadilla/django-rest-framework-xml/actions?query=workflow%3ACI +[build-status-image]: https://secure.travis-ci.org/jpadilla/django-rest-framework-xml.svg?branch=master +[travis]: http://travis-ci.org/jpadilla/django-rest-framework-xml?branch=master [pypi-version]: https://img.shields.io/pypi/v/djangorestframework-xml.svg [pypi]: https://pypi.python.org/pypi/djangorestframework-xml [defusedxml]: https://pypi.python.org/pypi/defusedxml diff --git a/docs/renderers.md b/docs/renderers.md index 7916053..6ff8f25 100644 --- a/docs/renderers.md +++ b/docs/renderers.md @@ -62,3 +62,17 @@ If you are considering using `XML` for your API, you may want to consider implem **item_tag_name**: `list-item` **.root_tag_name**: `root` + +## SOAP Renderer + +If you are considering using `SOAP` renderer for your API, you may must need to create your own custom SOAP envelope structure. + +**soap_tag**: `SOAP-TEST` +**soap_endpoint**: `https://xml.com/soap` +**soap_service**: `soapService` + +For creating your SOAP envelope structure you must need to reload the schema: + + renderer = SOAPRenderer + renderer.set_schema_attrs(soap_tag, soap_endpoint, soap_service) + renderer_classes = (renderer,) \ No newline at end of file diff --git a/rest_framework_xml/renderers.py b/rest_framework_xml/renderers.py index 81765e8..f9c7d36 100644 --- a/rest_framework_xml/renderers.py +++ b/rest_framework_xml/renderers.py @@ -13,18 +13,18 @@ class XMLRenderer(BaseRenderer): Renderer which serializes to XML. """ - media_type = "application/xml" - format = "xml" - charset = "utf-8" - item_tag_name = "list-item" - root_tag_name = "root" + media_type = 'application/xml' + format = 'xml' + charset = 'utf-8' + item_tag_name = 'list-item' + root_tag_name = 'root' def render(self, data, accepted_media_type=None, renderer_context=None): """ Renders `data` into serialized XML. """ if data is None: - return "" + return '' stream = StringIO() @@ -57,3 +57,81 @@ def _to_xml(self, xml, data): else: xml.characters(force_str(data)) + + +class SOAPRenderer(BaseRenderer): + """ + Renderer which serialize to SOAP Envelope + """ + media_type = 'application/xml' + format = 'xml' + charset = 'utf-8' + + soap_tag = 'SOAP-ENV' + service_endpoint = 'http://dummyservice.com/endpoint' + service_name = 'dummyService' + + schema_attrs = { + 'xmlns:%s' % soap_tag: 'http://schemas.xmlsoap.org/soap/envelope/', + 'xmlns:%s' % service_name: '%s' % service_endpoint + } + + envelope_tag_name = '%s:Envelope' % soap_tag + header_tag_name = '%s:Header' % soap_tag + body_tag_name = '%s:Body' % soap_tag + response_tag_name = '%s:Response' % service_name + + @classmethod + def set_schema_attrs(cls, soap_tag, service_endpoint, service_name): + cls.envelope_tag_name = '%s:Envelope' % soap_tag + cls.header_tag_name = '%s:Header' % soap_tag + cls.body_tag_name = '%s:Body' % soap_tag + cls.response_tag_name = '%s:Response' % service_name + + cls.schema_attrs = { + 'xmlns:%s' % soap_tag: 'http://schemas.xmlsoap.org/soap/envelope/', + 'xmlns:%s' % service_name: '%s' % service_endpoint + } + + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Renders `data` into serialized SOAP Envelope. + """ + if not data: + return '' + + stream = StringIO() + + xml = SimplerXMLGenerator(stream, self.charset) + xml.startDocument() + + xml.startElement(self.envelope_tag_name, self.schema_attrs) + xml.addQuickElement(self.header_tag_name) + xml.startElement(self.body_tag_name, {}) + xml.startElement(self.response_tag_name, {}) + + self._to_xml(xml, data) + + xml.endElement(self.response_tag_name) + xml.endElement(self.body_tag_name) + xml.endElement(self.envelope_tag_name) + + return stream.getvalue() + + def _to_xml(self, xml, data): + if isinstance(data, (list, tuple)): + for item in data: + self._to_xml(xml, item) + + elif isinstance(data, dict): + for key, value in data.items(): + xml.startElement(key, {}) + self._to_xml(xml, value) + xml.endElement(key) + + elif data is None: + # Don't output any value + pass + + else: + xml.characters(force_str(data)) \ No newline at end of file diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 597aa2c..d18993b 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -11,6 +11,7 @@ from rest_framework_xml.compat import etree from rest_framework_xml.parsers import XMLParser from rest_framework_xml.renderers import XMLRenderer +from rest_framework_xml.renderers import SOAPRenderer class XMLRendererTestCase(TestCase): @@ -22,9 +23,15 @@ class XMLRendererTestCase(TestCase): "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00), "name": "name", "sub_data_list": [ - {"sub_id": 1, "sub_name": "first"}, - {"sub_id": 2, "sub_name": "second"}, - ], + { + "sub_id": 1, + "sub_name": "first" + }, + { + "sub_id": 2, + "sub_name": "second" + } + ] } def test_render_string(self): @@ -32,96 +39,199 @@ def test_render_string(self): Test XML rendering. """ renderer = XMLRenderer() - content = renderer.render({"field": "astring"}, "application/xml") - self.assertXMLContains(content, "astring") + content = renderer.render({'Field': 'astring'}, 'application/xml') + self.assertXMLContains(content, 'astring') def test_render_integer(self): """ Test XML rendering. """ renderer = XMLRenderer() - content = renderer.render({"field": 111}, "application/xml") - self.assertXMLContains(content, "111") + content = renderer.render({'Field': 111}, 'application/xml') + self.assertXMLContains(content, '111') def test_render_datetime(self): """ Test XML rendering. """ renderer = XMLRenderer() - content = renderer.render( - {"field": datetime.datetime(2011, 12, 25, 12, 45, 00)}, - "application/xml", - ) - self.assertXMLContains(content, "2011-12-25 12:45:00") + content = renderer.render({ + 'Field': datetime.datetime(2011, 12, 25, 12, 45, 00) + }, 'application/xml') + self.assertXMLContains(content, '2011-12-25 12:45:00') def test_render_float(self): """ Test XML rendering. """ renderer = XMLRenderer() - content = renderer.render({"field": 123.4}, "application/xml") - self.assertXMLContains(content, "123.4") + content = renderer.render({'Field': 123.4}, 'application/xml') + self.assertXMLContains(content, '123.4') def test_render_decimal(self): """ Test XML rendering. """ renderer = XMLRenderer() - content = renderer.render( - {"field": Decimal("111.2")}, "application/xml" - ) - self.assertXMLContains(content, "111.2") + content = renderer.render({'Field': Decimal('111.2')}, 'application/xml') + self.assertXMLContains(content, '111.2') def test_render_none(self): """ Test XML rendering. """ renderer = XMLRenderer() - content = renderer.render({"field": None}, "application/xml") - self.assertXMLContains(content, "") + content = renderer.render({'Field': None}, 'application/xml') + self.assertXMLContains(content, '') def test_render_complex_data(self): """ Test XML rendering. """ renderer = XMLRenderer() - content = renderer.render(self._complex_data, "application/xml") - self.assertXMLContains(content, "first") - self.assertXMLContains(content, "second") + content = renderer.render(self._complex_data, 'application/xml') + self.assertXMLContains(content, 'first') + self.assertXMLContains(content, 'second') def test_render_list(self): renderer = XMLRenderer() - content = renderer.render(self._complex_data, "application/xml") - self.assertXMLContains(content, "") - self.assertXMLContains(content, "") + content = renderer.render(self._complex_data, 'application/xml') + self.assertXMLContains(content, '') + self.assertXMLContains(content, '') def test_render_lazy(self): renderer = XMLRenderer() - lazy = gettext_lazy("hello") - content = renderer.render({"field": lazy}, "application/xml") - self.assertXMLContains(content, "hello") + lazy = gettext_lazy('hello') + content = renderer.render({'Field': lazy}, 'application/xml') + self.assertXMLContains(content, 'hello') - @skipUnless(etree, "defusedxml not installed") + @skipUnless(etree, 'defusedxml not installed') def test_render_and_parse_complex_data(self): """ Test XML rendering. """ renderer = XMLRenderer() - content = StringIO( - renderer.render(self._complex_data, "application/xml") - ) + content = StringIO(renderer.render(self._complex_data, 'application/xml')) parser = XMLParser() complex_data_out = parser.parse(content) - error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % ( - repr(self._complex_data), - repr(complex_data_out), - ) + error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out)) self.assertEqual(self._complex_data, complex_data_out, error_msg) def assertXMLContains(self, xml, string): - self.assertTrue( - xml.startswith('\n') - ) - self.assertTrue(xml.endswith("")) - self.assertTrue(string in xml, "%r not in %r" % (string, xml)) + self.assertTrue(xml.startswith('\n')) + self.assertTrue(xml.endswith('')) + self.assertTrue(string in xml, '%r not in %r' % (string, xml)) + + +class SOAPRendererTestCase(TestCase): + """ + Test speecific to the SOAP Renderer + """ + _complex_data = { + "TextSearch": "Some words", + "ComparsionResult": [ + { + "ResultId": 1, + "ResultTag": "tag one" + }, + { + "ResultId": 2, + "ResultTag": "tag two" + } + ], + "RecordDate": datetime.datetime(2020, 4, 14, 12, 45, 00) + } + + def test_set_schema(self): + """ + Test set soap schema + """ + test_tag = "SOAP-TEST" + test_endpoint = "https://xml.com/soap" + test_service = "soapService" + + expected_vals = { + 'xmlns:%s' % test_tag: 'http://schemas.xmlsoap.org/soap/envelope/', + 'xmlns:%s' % test_service: '%s' % test_endpoint + } + + renderer = SOAPRenderer() + renderer.set_schema_attrs(test_tag, test_endpoint, test_service) + content = renderer.render({'Field': 'astring'}, 'application/xml') + + self.assertEqual(renderer.schema_attrs, expected_vals) + print(content) + self.assertTrue(content.startswith('\n')) + + def test_render_string(self): + """ + Test SOAP rendering. + """ + renderer = SOAPRenderer() + + content = renderer.render({'Field': 'astring'}, 'application/xml') + self.assertSOAPContains(content, 'astring') + + def test_render_integer(self): + """ + Test SOAP rendering. + """ + renderer = SOAPRenderer() + content = renderer.render({'Digit': 111}, 'application/xml') + self.assertSOAPContains(content, '111') + + def test_render_datetime(self): + """ + Test SOAP rendering. + """ + renderer = SOAPRenderer() + content = renderer.render({ + 'Field': datetime.datetime(2011, 12, 25, 12, 45, 00) + }, 'application/xml') + self.assertSOAPContains(content, '2011-12-25 12:45:00') + + def test_render_float(self): + """ + Test SOAP rendering. + """ + renderer = SOAPRenderer() + content = renderer.render({'Field': 123.4}, 'application/xml') + self.assertSOAPContains(content, '123.4') + + def test_render_decimal(self): + """ + Test SOAP rendering. + """ + renderer = SOAPRenderer() + content = renderer.render({'Field': Decimal('111.2')}, 'application/xml') + self.assertSOAPContains(content, '111.2') + + def test_render_none(self): + """ + Test SOAP rendering. + """ + renderer = SOAPRenderer() + content = renderer.render({'Field': None}, 'application/xml') + self.assertSOAPContains(content, '') + + def test_render_complex_data(self): + """ + Test SOAP rendering. + """ + renderer = SOAPRenderer() + content = renderer.render(self._complex_data, 'application/xml') + self.assertSOAPContains(content, '') + self.assertSOAPContains(content, '') + + def test_render_lazy(self): + renderer = SOAPRenderer() + lazy = gettext_lazy('hello') + content = renderer.render({'Field': lazy}, 'application/xml') + self.assertSOAPContains(content, 'hello') + + def assertSOAPContains(self, xml, string): + self.assertTrue(xml.startswith('\n')) + self.assertTrue(string in xml, '%r not in %r' % (string, xml)) \ No newline at end of file