Skip to content

Commit 1574ad2

Browse files
RuthShryockjnm
andauthored
Upgrade pyxform to v2.1.1 (#5126)
## Description The previous version was 1.9.0. Most workflows are not affected, but if you interact directly with XForm XML, you should refer to the changes listed at https://github.com/XLSForm/pyxform/blob/master/CHANGES.txt, particularly the major version v2.0.0. These do affect the generated XForm XML. ## Checklist 1. [ ] If you've added code that should be tested, add tests 2. [ ] If you've changed APIs, update (or create!) the documentation 3. [x] Ensure the tests pass 4. [x] Make sure that your code lints and that you've followed [our coding style](https://github.com/kobotoolbox/kpi/blob/master/CONTRIBUTING.md) 5. [x] Write a title and, if necessary, a description of your work suitable for publishing in our [release notes](https://community.kobotoolbox.org/tag/release-notes) 6. [ ] Mention any related issues in this repository (as #ISSUE) and in other repositories (as kobotoolbox/other#ISSUE) 7. [ ] Open an issue in the [docs](https://github.com/kobotoolbox/docs/issues/new) if there are UI/UX changes ## Notes Update pyxform to version [2.1.1](https://github.com/XLSForm/pyxform/releases/tag/v2.1.1). This also modifies the format of the xls file that we send to the pyxform method `create_survey_from_xls`. Before the xls was a Django FieldFile object but we now covert it into a binary stream using `io.BytesIO` to create a file-like object in order to be compatible with the pyxform updates. This update is deployed to [kf.du.kbtdev.org](https://kf.du.kbtdev.org/) for testing. --------- Co-authored-by: John N. Milner <[email protected]>
1 parent 709e65a commit 1574ad2

File tree

8 files changed

+308
-401
lines changed

8 files changed

+308
-401
lines changed

dependencies/pip/dev_requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ oauthlib==3.2.2
414414
# -r dependencies/pip/requirements.in
415415
# django-oauth-toolkit
416416
# requests-oauthlib
417-
openpyxl==3.0.9
417+
openpyxl==3.1.3
418418
# via
419419
# -r dependencies/pip/requirements.in
420420
# pyxform
@@ -537,7 +537,7 @@ pytz==2024.1
537537
# via
538538
# flower
539539
# pandas
540-
pyxform==1.9.0
540+
pyxform==2.1.1
541541
# via
542542
# -r dependencies/pip/requirements.in
543543
# formpack

dependencies/pip/requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ openpyxl
7575
psycopg
7676
pymongo
7777
python-dateutil
78-
pyxform==1.9.0
78+
pyxform==2.1.1
7979
requests
8080
regex
8181
responses

dependencies/pip/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ oauthlib==3.2.2
336336
# -r dependencies/pip/requirements.in
337337
# django-oauth-toolkit
338338
# requests-oauthlib
339-
openpyxl==3.0.9
339+
openpyxl==3.1.3
340340
# via
341341
# -r dependencies/pip/requirements.in
342342
# pyxform
@@ -412,7 +412,7 @@ pytz==2024.1
412412
# via
413413
# flower
414414
# pandas
415-
pyxform==1.9.0
415+
pyxform==2.1.1
416416
# via
417417
# -r dependencies/pip/requirements.in
418418
# formpack

kobo/apps/openrosa/apps/api/tests/fixtures/Transportation Form.xml

Lines changed: 130 additions & 196 deletions
Large diffs are not rendered by default.

kobo/apps/openrosa/apps/main/tests/fixtures/transportation/transportation.xml

Lines changed: 131 additions & 198 deletions
Large diffs are not rendered by default.

kobo/apps/openrosa/apps/viewer/models/data_dictionary.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from kobo.apps.openrosa.libs.utils.model_tools import queryset_iterator, set_uuid
2222
from kpi.constants import DEFAULT_SURVEY_NAME
2323
from kpi.utils.mongo_helper import MongoHelper
24+
from kpi.utils.pyxform_compatibility import NamedBytesIO
2425

2526

2627
class ColumnRename(models.Model):
@@ -157,8 +158,9 @@ def add_instances(self):
157158

158159
def save(self, *args, **kwargs):
159160
if self.xls:
161+
xls_io = NamedBytesIO.fromfieldfile(self.xls)
160162
survey = create_survey_from_xls(
161-
self.xls, default_name=DEFAULT_SURVEY_NAME
163+
xls_io, default_name=DEFAULT_SURVEY_NAME
162164
)
163165
if survey.name == DEFAULT_SURVEY_NAME:
164166
survey.name = survey.id_string

kpi/tests/test_asset_snapshots.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_snapshots_allow_choice_duplicates(self):
7878
'settings': {},
7979
}
8080
snap = AssetSnapshot.objects.create(source=content)
81-
assert snap.xml.count('<value>ABC</value>') == 2
81+
assert snap.xml.count('<name>ABC</name>') == 2
8282

8383

8484
class AssetSnapshotHousekeeping(AssetSnapshotsTestCase):

kpi/utils/pyxform_compatibility.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from io import BytesIO
12
from pyxform.constants import ALLOW_CHOICE_DUPLICATES
23

4+
35
def allow_choice_duplicates(content: dict) -> None:
46
"""
57
Modify `content` to include `allow_choice_duplicates=Yes` in the settings
@@ -13,3 +15,39 @@ def allow_choice_duplicates(content: dict) -> None:
1315
settings = content.setdefault('settings', {})
1416
if ALLOW_CHOICE_DUPLICATES not in settings:
1517
settings[ALLOW_CHOICE_DUPLICATES] = 'yes'
18+
19+
20+
class NamedBytesIO(BytesIO):
21+
"""
22+
Changes in XLSForm/pyxform#718 prevent
23+
`pyxform.builder.create_survey_from_xls()` from accepting a
24+
`django.db.models.fields.files.FieldFile`. Only instances of
25+
`bytes | BytesIO | IOBase` are now accepted for treatment as file-like
26+
objects, and furthermore, anything that is not already a `BytesIO` will
27+
have its contents placed inside a newly instantiated one.
28+
29+
Problem: `BytesIO`s do not have `name`s, and the constructor for
30+
`pyxform.xls2json.SurveyReader` fails because of that.
31+
32+
Workaround: a `BytesIO` with a `name` 🙃
33+
34+
For more details, see
35+
https://github.com/kobotoolbox/kpi/pull/5126#discussion_r1829763316
36+
"""
37+
38+
def __init__(self, *args, name=None, **kwargs):
39+
if name is None:
40+
raise NotImplementedError('Use `BytesIO` if no `name` is needed')
41+
super().__init__(*args, **kwargs)
42+
self.name = name
43+
44+
@classmethod
45+
def fromfieldfile(cls, django_fieldfile):
46+
"""
47+
Given a Django `FieldFile`, return an instance of `NamedBytesIO`
48+
49+
à la `datetime.datetime.fromtimestamp()`
50+
"""
51+
new_instance = cls(django_fieldfile.read(), name=django_fieldfile.name)
52+
django_fieldfile.seek(0) # Be kind: rewind
53+
return new_instance

0 commit comments

Comments
 (0)