Skip to content

Commit

Permalink
Upgrade pyxform to v2.1.1 (#5126)
Browse files Browse the repository at this point in the history
## 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 <john@tmoj.net>
  • Loading branch information
RuthShryock and jnm authored Nov 6, 2024
1 parent 709e65a commit 1574ad2
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 401 deletions.
4 changes: 2 additions & 2 deletions dependencies/pip/dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ oauthlib==3.2.2
# -r dependencies/pip/requirements.in
# django-oauth-toolkit
# requests-oauthlib
openpyxl==3.0.9
openpyxl==3.1.3
# via
# -r dependencies/pip/requirements.in
# pyxform
Expand Down Expand Up @@ -537,7 +537,7 @@ pytz==2024.1
# via
# flower
# pandas
pyxform==1.9.0
pyxform==2.1.1
# via
# -r dependencies/pip/requirements.in
# formpack
Expand Down
2 changes: 1 addition & 1 deletion dependencies/pip/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ openpyxl
psycopg
pymongo
python-dateutil
pyxform==1.9.0
pyxform==2.1.1
requests
regex
responses
Expand Down
4 changes: 2 additions & 2 deletions dependencies/pip/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ oauthlib==3.2.2
# -r dependencies/pip/requirements.in
# django-oauth-toolkit
# requests-oauthlib
openpyxl==3.0.9
openpyxl==3.1.3
# via
# -r dependencies/pip/requirements.in
# pyxform
Expand Down Expand Up @@ -412,7 +412,7 @@ pytz==2024.1
# via
# flower
# pandas
pyxform==1.9.0
pyxform==2.1.1
# via
# -r dependencies/pip/requirements.in
# formpack
Expand Down
326 changes: 130 additions & 196 deletions kobo/apps/openrosa/apps/api/tests/fixtures/Transportation Form.xml

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion kobo/apps/openrosa/apps/viewer/models/data_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from kobo.apps.openrosa.libs.utils.model_tools import queryset_iterator, set_uuid
from kpi.constants import DEFAULT_SURVEY_NAME
from kpi.utils.mongo_helper import MongoHelper
from kpi.utils.pyxform_compatibility import NamedBytesIO


class ColumnRename(models.Model):
Expand Down Expand Up @@ -157,8 +158,9 @@ def add_instances(self):

def save(self, *args, **kwargs):
if self.xls:
xls_io = NamedBytesIO.fromfieldfile(self.xls)
survey = create_survey_from_xls(
self.xls, default_name=DEFAULT_SURVEY_NAME
xls_io, default_name=DEFAULT_SURVEY_NAME
)
if survey.name == DEFAULT_SURVEY_NAME:
survey.name = survey.id_string
Expand Down
2 changes: 1 addition & 1 deletion kpi/tests/test_asset_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_snapshots_allow_choice_duplicates(self):
'settings': {},
}
snap = AssetSnapshot.objects.create(source=content)
assert snap.xml.count('<value>ABC</value>') == 2
assert snap.xml.count('<name>ABC</name>') == 2


class AssetSnapshotHousekeeping(AssetSnapshotsTestCase):
Expand Down
38 changes: 38 additions & 0 deletions kpi/utils/pyxform_compatibility.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from io import BytesIO
from pyxform.constants import ALLOW_CHOICE_DUPLICATES


def allow_choice_duplicates(content: dict) -> None:
"""
Modify `content` to include `allow_choice_duplicates=Yes` in the settings
Expand All @@ -13,3 +15,39 @@ def allow_choice_duplicates(content: dict) -> None:
settings = content.setdefault('settings', {})
if ALLOW_CHOICE_DUPLICATES not in settings:
settings[ALLOW_CHOICE_DUPLICATES] = 'yes'


class NamedBytesIO(BytesIO):
"""
Changes in XLSForm/pyxform#718 prevent
`pyxform.builder.create_survey_from_xls()` from accepting a
`django.db.models.fields.files.FieldFile`. Only instances of
`bytes | BytesIO | IOBase` are now accepted for treatment as file-like
objects, and furthermore, anything that is not already a `BytesIO` will
have its contents placed inside a newly instantiated one.
Problem: `BytesIO`s do not have `name`s, and the constructor for
`pyxform.xls2json.SurveyReader` fails because of that.
Workaround: a `BytesIO` with a `name` 🙃
For more details, see
/~https://github.com/kobotoolbox/kpi/pull/5126#discussion_r1829763316
"""

def __init__(self, *args, name=None, **kwargs):
if name is None:
raise NotImplementedError('Use `BytesIO` if no `name` is needed')
super().__init__(*args, **kwargs)
self.name = name

@classmethod
def fromfieldfile(cls, django_fieldfile):
"""
Given a Django `FieldFile`, return an instance of `NamedBytesIO`
à la `datetime.datetime.fromtimestamp()`
"""
new_instance = cls(django_fieldfile.read(), name=django_fieldfile.name)
django_fieldfile.seek(0) # Be kind: rewind
return new_instance

0 comments on commit 1574ad2

Please sign in to comment.