From cddc559cfe27769ba3e458bb641aaf19b30560e2 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 13 Jun 2023 10:57:22 +0300 Subject: [PATCH 1/5] fix open-data endpoint timeout bug there currently exists a peculiar intermittent bug where after ordering the queryset and the first item is accessed such as instances[0] or by slicing instances[0:1] (as in the the pagination implementation) the execution freezes and no result is returned. This causes the server to timeout. The workaround only ensures we order and paginate the results only when the queryset returns more than 1 item --- .../tests/viewsets/test_tableau_viewset.py | 384 +++++++++--------- .../apps/api/viewsets/v2/tableau_viewset.py | 24 +- onadata/settings/github_actions_test.py | 2 +- 3 files changed, 207 insertions(+), 203 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py index 6d7b0d54d8..d991962c48 100644 --- a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py @@ -11,39 +11,39 @@ from onadata.apps.main.tests.test_base import TestBase from onadata.apps.logger.models.open_data import get_or_create_opendata from onadata.apps.api.viewsets.v2.tableau_viewset import ( - TableauViewSet, unpack_select_multiple_data, - unpack_gps_data, clean_xform_headers) + TableauViewSet, + unpack_select_multiple_data, + unpack_gps_data, + clean_xform_headers, +) from onadata.libs.renderers.renderers import pairing def streaming_data(response): - return json.loads(u''.join( - [i.decode('utf-8') for i in response.streaming_content])) + return json.loads("".join([i.decode("utf-8") for i in response.streaming_content])) class TestTableauViewSet(TestBase): - def setUp(self): super(TestTableauViewSet, self).setUp() self._create_user_and_login() - self._submission_time = parse_datetime('2020-02-18 15:54:01Z') - self.fixture_dir = os.path.join( - self.this_directory, 'fixtures', 'csv_export') - path = os.path.join(self.fixture_dir, 'tutorial_w_repeats.xlsx') + self._submission_time = parse_datetime("2020-02-18 15:54:01Z") + self.fixture_dir = os.path.join(self.this_directory, "fixtures", "csv_export") + path = os.path.join(self.fixture_dir, "tutorial_w_repeats.xlsx") self._publish_xls_file_and_set_xform(path) - path = os.path.join(self.fixture_dir, 'repeats_sub.xml') + path = os.path.join(self.fixture_dir, "repeats_sub.xml") self.factory = RequestFactory() - self.extra = { - 'HTTP_AUTHORIZATION': 'Token %s' % self.user.auth_token} - self._make_submission( - path, forced_submission_time=self._submission_time) + self.extra = {"HTTP_AUTHORIZATION": "Token %s" % self.user.auth_token} + self._make_submission(path, forced_submission_time=self._submission_time) - self.view = TableauViewSet.as_view({ - 'post': 'create', - 'patch': 'partial_update', - 'delete': 'destroy', - 'get': 'data' - }) + self.view = TableauViewSet.as_view( + { + "post": "create", + "patch": "partial_update", + "delete": "destroy", + "get": "data", + } + ) def get_open_data_object(self): return get_or_create_opendata(self.xform)[0] @@ -52,154 +52,111 @@ def test_tableau_data_and_fetch(self): # pylint: disable=invalid-name """ Test the schema and data endpoint and data returned by each. """ - self.view = TableauViewSet.as_view({ - 'get': 'schema' - }) + self.view = TableauViewSet.as_view({"get": "schema"}) _open_data = get_or_create_opendata(self.xform) uuid = _open_data[0].uuid expected_schema = [ { - 'table_alias': 'data', - 'connection_name': f'{self.xform.project_id}_{self.xform.id_string}', # noqa - 'column_headers': [ + "table_alias": "data", + "connection_name": f"{self.xform.project_id}_{self.xform.id_string}", # noqa + "column_headers": [ + {"id": "_id", "dataType": "int", "alias": "_id"}, + {"id": "name", "dataType": "string", "alias": "name"}, + {"id": "age", "dataType": "int", "alias": "age"}, + {"id": "picture", "dataType": "string", "alias": "picture"}, { - 'id': '_id', - 'dataType': 'int', - 'alias': '_id' + "id": "has_children", + "dataType": "string", + "alias": "has_children", }, { - 'id': 'name', - 'dataType': 'string', - 'alias': 'name' + "id": "_gps_latitude", + "dataType": "string", + "alias": "_gps_latitude", }, { - 'id': 'age', - 'dataType': 'int', - 'alias': 'age' + "id": "_gps_longitude", + "dataType": "string", + "alias": "_gps_longitude", }, { - 'id': 'picture', - 'dataType': 'string', - 'alias': 'picture' + "id": "_gps_altitude", + "dataType": "string", + "alias": "_gps_altitude", }, { - 'id': 'has_children', - 'dataType': 'string', - 'alias': 'has_children' + "id": "_gps_precision", + "dataType": "string", + "alias": "_gps_precision", }, { - 'id': '_gps_latitude', - 'dataType': 'string', - 'alias': '_gps_latitude' + "id": "browsers_firefox", + "dataType": "string", + "alias": "browsers_firefox", }, { - 'id': '_gps_longitude', - 'dataType': 'string', - 'alias': '_gps_longitude' + "id": "browsers_chrome", + "dataType": "string", + "alias": "browsers_chrome", }, + {"id": "browsers_ie", "dataType": "string", "alias": "browsers_ie"}, { - 'id': '_gps_altitude', - 'dataType': 'string', - 'alias': '_gps_altitude' + "id": "browsers_safari", + "dataType": "string", + "alias": "browsers_safari", }, { - 'id': '_gps_precision', - 'dataType': 'string', - 'alias': '_gps_precision' + "id": "meta_instanceID", + "dataType": "string", + "alias": "meta_instanceID", }, - { - 'id': 'browsers_firefox', - 'dataType': 'string', - 'alias': 'browsers_firefox' - }, - { - 'id': 'browsers_chrome', - 'dataType': 'string', - 'alias': 'browsers_chrome' - }, - { - 'id': 'browsers_ie', - 'dataType': 'string', - 'alias': 'browsers_ie' - }, - { - 'id': 'browsers_safari', - 'dataType': 'string', - 'alias': 'browsers_safari' - }, - { - 'id': 'meta_instanceID', - 'dataType': 'string', - 'alias': 'meta_instanceID' - } - ] + ], }, { - 'table_alias': 'children', - 'connection_name': f'{self.xform.project_id}_{self.xform.id_string}_children', # noqa - 'column_headers': [ + "table_alias": "children", + "connection_name": f"{self.xform.project_id}_{self.xform.id_string}_children", # noqa + "column_headers": [ + {"id": "_id", "dataType": "int", "alias": "_id"}, + {"id": "__parent_id", "dataType": "int", "alias": "__parent_id"}, { - 'id': '_id', - 'dataType': 'int', - 'alias': '_id' + "id": "__parent_table", + "dataType": "string", + "alias": "__parent_table", }, - { - 'id': '__parent_id', - 'dataType': 'int', - 'alias': '__parent_id' - }, - { - 'id': '__parent_table', - 'dataType': 'string', - 'alias': '__parent_table' - }, - { - 'id': 'childs_name', - 'dataType': 'string', - 'alias': 'childs_name' - }, - { - 'id': 'childs_age', - 'dataType': 'int', - 'alias': 'childs_age' - } - ] - }] + {"id": "childs_name", "dataType": "string", "alias": "childs_name"}, + {"id": "childs_age", "dataType": "int", "alias": "childs_age"}, + ], + }, + ] - request1 = self.factory.get('/', **self.extra) + request1 = self.factory.get("/", **self.extra) response1 = self.view(request1, uuid=uuid) self.assertEqual(response1.status_code, 200) self.assertEqual(response1.data, expected_schema) # Test that multiple schemas are generated for each repeat self.assertEqual(len(response1.data), 2) self.assertListEqual( - ['column_headers', 'connection_name', 'table_alias'], - sorted(list(response1.data[0].keys())) + ["column_headers", "connection_name", "table_alias"], + sorted(list(response1.data[0].keys())), ) - connection_name = f'{self.xform.project_id}_{self.xform.id_string}' - self.assertEqual( - connection_name, response1.data[0].get('connection_name')) + connection_name = f"{self.xform.project_id}_{self.xform.id_string}" + self.assertEqual(connection_name, response1.data[0].get("connection_name")) # Test that the table alias field being sent to Tableau # for each schema contains the right table name - self.assertEqual( - u'data', response1.data[0].get('table_alias') - ) - self.assertEqual( - u'children', response1.data[1].get('table_alias') - ) + self.assertEqual("data", response1.data[0].get("table_alias")) + self.assertEqual("children", response1.data[1].get("table_alias")) _id_datatype = [ - a.get('dataType') - for a in response1.data[0]['column_headers'] - if a.get('id') == '_id'][0] - self.assertEqual(_id_datatype, 'int') + a.get("dataType") + for a in response1.data[0]["column_headers"] + if a.get("id") == "_id" + ][0] + self.assertEqual(_id_datatype, "int") - self.view = TableauViewSet.as_view({ - 'get': 'data' - }) - request2 = self.factory.get('/', **self.extra) + self.view = TableauViewSet.as_view({"get": "data"}) + request2 = self.factory.get("/", **self.extra) response2 = self.view(request2, uuid=uuid) self.assertEqual(response2.status_code, 200) @@ -207,41 +164,44 @@ def test_tableau_data_and_fetch(self): # pylint: disable=invalid-name row_data = streaming_data(response2) expected_data = [ { - '_gps_altitude': '0', - '_gps_latitude': '26.431228', - '_gps_longitude': '58.157921', - '_gps_precision': '0', - '_id': self.xform.instances.first().id, - 'age': 32, - 'browsers_chrome': 'TRUE', - 'browsers_firefox': 'TRUE', - 'browsers_ie': 'TRUE', - 'browsers_safari': 'TRUE', - 'children': [ + "_gps_altitude": "0", + "_gps_latitude": "26.431228", + "_gps_longitude": "58.157921", + "_gps_precision": "0", + "_id": self.xform.instances.first().id, + "age": 32, + "browsers_chrome": "TRUE", + "browsers_firefox": "TRUE", + "browsers_ie": "TRUE", + "browsers_safari": "TRUE", + "children": [ { - '__parent_id': self.xform.instances.first().id, - '__parent_table': 'data', - '_id': int(pairing( - self.xform.instances.first().id, 1)), - 'childs_age': 2, - 'childs_name': 'Harry'}, + "__parent_id": self.xform.instances.first().id, + "__parent_table": "data", + "_id": int(pairing(self.xform.instances.first().id, 1)), + "childs_age": 2, + "childs_name": "Harry", + }, { - '__parent_id': self.xform.instances.first().id, - '__parent_table': 'data', - '_id': int(pairing( - self.xform.instances.first().id, 2)), - 'childs_age': 5, - 'childs_name': 'Potter'}], - 'has_children': '1', - 'name': 'Tom', - 'picture': 'wotm_01_green_desktop-10_36_1.jpg' - }] + "__parent_id": self.xform.instances.first().id, + "__parent_table": "data", + "_id": int(pairing(self.xform.instances.first().id, 2)), + "childs_age": 5, + "childs_name": "Potter", + }, + ], + "has_children": "1", + "name": "Tom", + "picture": "wotm_01_green_desktop-10_36_1.jpg", + } + ] # Test to confirm that the repeat tables generated # are related to the main table self.assertEqual( - row_data[0]['children'][0]['__parent_table'], - response1.data[0]['table_alias']) + row_data[0]["children"][0]["__parent_table"], + response1.data[0]["table_alias"], + ) self.assertEqual(row_data, expected_data) def test_unpack_select_multiple_data(self): @@ -249,34 +209,36 @@ def test_unpack_select_multiple_data(self): Test expected output when `unpack_select_multiple_data` function is run. """ - picked_choices = ['firefox', 'chrome', 'ie', 'safari'] - list_name = 'browsers' - choices_names = ['firefox', 'chrome', 'ie', 'safari'] - prefix = '' + picked_choices = ["firefox", "chrome", "ie", "safari"] + list_name = "browsers" + choices_names = ["firefox", "chrome", "ie", "safari"] + prefix = "" expected_data = { - 'browsers_chrome': 'TRUE', - 'browsers_firefox': 'TRUE', - 'browsers_ie': 'TRUE', - 'browsers_safari': 'TRUE' - } + "browsers_chrome": "TRUE", + "browsers_firefox": "TRUE", + "browsers_ie": "TRUE", + "browsers_safari": "TRUE", + } select_multiple_data = unpack_select_multiple_data( - picked_choices, list_name, choices_names, prefix) + picked_choices, list_name, choices_names, prefix + ) self.assertEqual(select_multiple_data, expected_data) # Confirm expected data when 2 choices are selected - picked_choices = ['firefox', 'safari'] + picked_choices = ["firefox", "safari"] select_multiple_data = unpack_select_multiple_data( - picked_choices, list_name, choices_names, prefix) + picked_choices, list_name, choices_names, prefix + ) expected_data = { - 'browsers_chrome': 'FALSE', - 'browsers_firefox': 'TRUE', - 'browsers_ie': 'FALSE', - 'browsers_safari': 'TRUE' - } + "browsers_chrome": "FALSE", + "browsers_firefox": "TRUE", + "browsers_ie": "FALSE", + "browsers_safari": "TRUE", + } self.assertEqual(select_multiple_data, expected_data) @@ -287,18 +249,17 @@ def test_unpack_gps_data(self): """ # We receive gps data as a string # with 4 space separated values - gps_data = '26.431228 58.157921 0 0' + gps_data = "26.431228 58.157921 0 0" qstn_name = "gps" prefix = "" - data = unpack_gps_data( - gps_data, qstn_name, prefix) + data = unpack_gps_data(gps_data, qstn_name, prefix) expected_data = { - '_gps_latitude': '26.431228', - '_gps_longitude': '58.157921', - '_gps_altitude': '0', - '_gps_precision': '0' - } + "_gps_latitude": "26.431228", + "_gps_longitude": "58.157921", + "_gps_altitude": "0", + "_gps_precision": "0", + } self.assertEqual(data, expected_data) def test_clean_xform_headers(self): @@ -308,15 +269,13 @@ def test_clean_xform_headers(self): are being pushed to Tableau. """ headers = self.xform.get_headers(repeat_iterations=1) - group_columns = [ - field for field in headers if search(r"\[+\d+\]", field)] - self.assertEqual(group_columns, - ['children[1]/childs_name', - 'children[1]/childs_age']) + group_columns = [field for field in headers if search(r"\[+\d+\]", field)] + self.assertEqual( + group_columns, ["children[1]/childs_name", "children[1]/childs_age"] + ) cleaned_data = clean_xform_headers(group_columns) - self.assertEqual(cleaned_data, - ['childs_name', 'childs_age']) + self.assertEqual(cleaned_data, ["childs_name", "childs_age"]) def test_replace_media_links(self): """ @@ -330,27 +289,60 @@ def test_replace_media_links(self): """ xform_w_attachments = self._publish_markdown(images_md, self.user) submission_file = NamedTemporaryFile(delete=False) - with open(submission_file.name, 'w') as xml_file: + with open(submission_file.name, "w") as xml_file: xml_file.write( "" "1335783522563.jpg" "1442323232322.jpg" "uuid:729f173c688e482486a48661700455ff" - "" % - (xform_w_attachments.id_string)) + "" % (xform_w_attachments.id_string) + ) media_file = "1335783522563.jpg" self._make_submission_w_attachment( submission_file.name, - os.path.join(self.this_directory, 'fixtures', 'transportation', - 'instances', self.surveys[0], media_file)) + os.path.join( + self.this_directory, + "fixtures", + "transportation", + "instances", + self.surveys[0], + media_file, + ), + ) submission_data = xform_w_attachments.instances.first().json _open_data = get_or_create_opendata(xform_w_attachments) uuid = _open_data[0].uuid - request = self.factory.get('/', **self.extra) + request = self.factory.get("/", **self.extra) response = self.view(request, uuid=uuid) self.assertEqual(response.status_code, 200) # cast generator response to list for easy manipulation row_data = streaming_data(response) self.assertEqual( - row_data[0]['image1'], - f"example.com{submission_data['_attachments'][0]['download_url']}") + row_data[0]["image1"], + f"example.com{submission_data['_attachments'][0]['download_url']}", + ) + + def test_pagination(self): + """Pagination works correctly""" + self.view = TableauViewSet.as_view({"get": "data"}) + # test 1 submission + _open_data = get_or_create_opendata(self.xform) + uuid = _open_data[0].uuid + request = self.factory.get( + "/", data={"page": 1, "page_size": 100}, **self.extra + ) + response = self.view(request, uuid=uuid) + self.assertEqual(response.status_code, 200) + row_data = streaming_data(response) + self.assertEqual(len(row_data), 1) + + # multiple submissions are ordered by primary key + path = os.path.join(self.fixture_dir, "repeats_sub.xml") + self._make_submission(path, forced_submission_time=self._submission_time) + response = self.view(request, uuid=uuid) + self.assertEqual(response.status_code, 200) + row_data = streaming_data(response) + self.assertEqual(len(row_data), 2) + instances = self.xform.instances.all().order_by("pk") + self.assertEqual(row_data[0]["_id"], instances[0].pk) + self.assertEqual(row_data[1]["_id"], instances[1].pk) diff --git a/onadata/apps/api/viewsets/v2/tableau_viewset.py b/onadata/apps/api/viewsets/v2/tableau_viewset.py index 80de83a8c5..b318e0920d 100644 --- a/onadata/apps/api/viewsets/v2/tableau_viewset.py +++ b/onadata/apps/api/viewsets/v2/tableau_viewset.py @@ -53,7 +53,7 @@ def process_tableau_data( else: flat_dict[ID] = row_id - for (key, value) in row.items(): + for key, value in row.items(): qstn = xform.get_element(key) if qstn: qstn_type = qstn.get("type") @@ -181,8 +181,8 @@ def data(self, request, **kwargs): ] query_param_keys = request.query_params should_paginate = any(k in query_param_keys for k in pagination_keys) - data = [] + if isinstance(self.object.content_object, XForm): if not self.object.active: return Response(status=status.HTTP_404_NOT_FOUND) @@ -202,16 +202,28 @@ def data(self, request, **kwargs): # Filter out deleted submissions instances = Instance.objects.filter( **qs_kwargs, deleted_at__isnull=True - ).order_by("pk") + ).only("json") + # we prefer to use len(instances) instead of instances.count() as using + # len is less expensive as no db query is made. Read more + # https://docs.djangoproject.com/en/4.2/topics/db/optimization/ + num_instances = len(instances) if count: - return Response({"count": instances.count()}) + return Response({"count": num_instances}) + + # there currently exists a peculiar intermittent bug where after ordering the queryset and + # the first item is accessed such as instances[0] or by slicing instances[0:1] (as in the + # the pagination implementation) the execution freezes and no result is returned. + # This causes the server to timeout. The workaround below only ensures we order and paginate + # the results only when the queryset returns more than 1 item + if num_instances > 1: + instances = instances.order_by("pk") - if should_paginate: + if should_paginate and num_instances > 1: instances = self.paginate_queryset(instances) + # Switch out media file names for url links in queryset data = replace_attachment_name_with_url(instances) - data = process_tableau_data( TableauDataSerializer(data, many=True).data, xform ) diff --git a/onadata/settings/github_actions_test.py b/onadata/settings/github_actions_test.py index 28a2cafdb2..68d2dd503f 100644 --- a/onadata/settings/github_actions_test.py +++ b/onadata/settings/github_actions_test.py @@ -15,7 +15,7 @@ "NAME": "onadata", "USER": "onadata", "PASSWORD": "onadata", - "HOST": "localhost", + "HOST": os.environ.get("DB_HOST", "localhost"), } } From 26a312d5f1bd47abb5597f1b882c651324f16f44 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 13 Jun 2023 11:16:26 +0300 Subject: [PATCH 2/5] address linting error --- onadata/apps/api/viewsets/v2/tableau_viewset.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/onadata/apps/api/viewsets/v2/tableau_viewset.py b/onadata/apps/api/viewsets/v2/tableau_viewset.py index b318e0920d..74aeff03ba 100644 --- a/onadata/apps/api/viewsets/v2/tableau_viewset.py +++ b/onadata/apps/api/viewsets/v2/tableau_viewset.py @@ -211,10 +211,11 @@ def data(self, request, **kwargs): if count: return Response({"count": num_instances}) - # there currently exists a peculiar intermittent bug where after ordering the queryset and - # the first item is accessed such as instances[0] or by slicing instances[0:1] (as in the - # the pagination implementation) the execution freezes and no result is returned. - # This causes the server to timeout. The workaround below only ensures we order and paginate + # there currently exists a peculiar intermittent bug where after ordering + # the queryset and the first item is accessed such as instances[0] or by + # slicing instances[0:1] (as in the the pagination implementation) the + # execution freezes and no result is returned. This causes the server to + # timeout. The workaround below only ensures we order and paginate # the results only when the queryset returns more than 1 item if num_instances > 1: instances = instances.order_by("pk") From dd9261068d8d6597f03f21ae8616823097395c99 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 13 Jun 2023 11:23:55 +0300 Subject: [PATCH 3/5] add test case add test case for count query parameter on endpoint /api/v2/open-data//data --- .../apps/api/tests/viewsets/test_tableau_viewset.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py index d991962c48..c07d05ce6f 100644 --- a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py @@ -346,3 +346,16 @@ def test_pagination(self): instances = self.xform.instances.all().order_by("pk") self.assertEqual(row_data[0]["_id"], instances[0].pk) self.assertEqual(row_data[1]["_id"], instances[1].pk) + + def test_count_query_param(self): + """count query param works""" + self.view = TableauViewSet.as_view({"get": "data"}) + path = os.path.join(self.fixture_dir, "repeats_sub.xml") + # make submission number 2 + self._make_submission(path, forced_submission_time=self._submission_time) + _open_data = get_or_create_opendata(self.xform) + uuid = _open_data[0].uuid + request = self.factory.get("/", data={"count": True}, **self.extra) + response = self.view(request, uuid=uuid) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, {"count": 2}) From 7cd641bfc6650c94eaea51c8974268c2211e98ec Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 13 Jun 2023 14:15:39 +0300 Subject: [PATCH 4/5] address linting errors --- onadata/apps/api/tests/viewsets/test_tableau_viewset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py index c07d05ce6f..1d3ad66292 100644 --- a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py @@ -33,7 +33,7 @@ def setUp(self): self._publish_xls_file_and_set_xform(path) path = os.path.join(self.fixture_dir, "repeats_sub.xml") self.factory = RequestFactory() - self.extra = {"HTTP_AUTHORIZATION": "Token %s" % self.user.auth_token} + self.extra = {"HTTP_AUTHORIZATION": f"Token {self.user.auth_token}"} self._make_submission(path, forced_submission_time=self._submission_time) self.view = TableauViewSet.as_view( @@ -59,7 +59,7 @@ def test_tableau_data_and_fetch(self): # pylint: disable=invalid-name expected_schema = [ { "table_alias": "data", - "connection_name": f"{self.xform.project_id}_{self.xform.id_string}", # noqa + "connection_name": f"{self.xform.project_id}_{self.xform.id_string}", # noqa pylint: disable=line-too-long "column_headers": [ {"id": "_id", "dataType": "int", "alias": "_id"}, {"id": "name", "dataType": "string", "alias": "name"}, @@ -289,7 +289,7 @@ def test_replace_media_links(self): """ xform_w_attachments = self._publish_markdown(images_md, self.user) submission_file = NamedTemporaryFile(delete=False) - with open(submission_file.name, "w") as xml_file: + with open(submission_file.name, "w", encoding="utf-8") as xml_file: xml_file.write( "" "1335783522563.jpg" From fb07aa9a672ef664975b40a5be30b6e6e6247cad Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 13 Jun 2023 14:51:28 +0300 Subject: [PATCH 5/5] address linting errors --- onadata/apps/api/tests/viewsets/test_tableau_viewset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py index 1d3ad66292..8c860bdced 100644 --- a/onadata/apps/api/tests/viewsets/test_tableau_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_tableau_viewset.py @@ -115,7 +115,7 @@ def test_tableau_data_and_fetch(self): # pylint: disable=invalid-name }, { "table_alias": "children", - "connection_name": f"{self.xform.project_id}_{self.xform.id_string}_children", # noqa + "connection_name": f"{self.xform.project_id}_{self.xform.id_string}_children", # noqa pylint: disable=line-too-long "column_headers": [ {"id": "_id", "dataType": "int", "alias": "_id"}, {"id": "__parent_id", "dataType": "int", "alias": "__parent_id"},