diff --git a/README.md b/README.md
index 6006534..35e22c2 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ FLASK_APP=app/main.py:app FLASK_DEBUG=1 flask run
# Run in Docker
```
docker build --tag pyxform-http .
-docker run --detach --publish 5000:80 pyxform-http
+docker run --detach --publish 5001:80 pyxform-http
```
# Test forms
@@ -23,4 +23,8 @@ docker run --detach --publish 5000:80 pyxform-http
bash test.sh
```
-The test script builds, runs, stops, and removes a pyxform-http-tester container
\ No newline at end of file
+The test script builds, runs, stops, and removes a pyxform-http-tester container
+
+# Notes
+
+* We use port 5001 because 5000 is used by ControlCenter on macOS.
\ No newline at end of file
diff --git a/app/main.py b/app/main.py
index f41c74e..4b6b526 100644
--- a/app/main.py
+++ b/app/main.py
@@ -4,7 +4,8 @@
from tempfile import TemporaryDirectory
import os.path
-from flask import Flask, jsonify, request, escape
+from flask import Flask, jsonify, request
+from markupsafe import escape
from pyxform import xls2xform
from uuid import uuid4 as uuid
from urllib.parse import unquote
diff --git a/requirements.txt b/requirements.txt
index 43e8970..90616e0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-Flask==2.3.3
-pyxform==1.12.2
+Flask==3.0.0
+pyxform==2.0.0
gunicorn==21.2.0
diff --git a/test.sh b/test.sh
index 99b43cd..b8de17f 100755
--- a/test.sh
+++ b/test.sh
@@ -5,67 +5,67 @@ set -o nounset
set -o pipefail
docker build --quiet --tag pyxform-http . >/dev/null
-docker run --detach --publish 5000:80 --name pyxform-http-tester pyxform-http >/dev/null
+docker run --detach --publish 5001:80 --name pyxform-http-tester pyxform-http >/dev/null
# wait for docker container to come up
sleep 1
test_failed="false"
-test_1_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: pyxform-clean" --header 'Transfer-Encoding: chunked' --data-binary @test/pyxform-clean.xlsx http://127.0.0.1:5000/api/v1/convert)
-test_1_expected='{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}'
+test_1_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: pyxform-clean" --header 'Transfer-Encoding: chunked' --data-binary @test/pyxform-clean.xlsx http://127.0.0.1:5001/api/v1/convert)
+test_1_expected='{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}'
if [ "$test_1_actual" != "$test_1_expected" ]; then
echo "test 1 failed: form that converts (with chunked encoding)"
test_failed="true"
fi
-test_2_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: pyxform-error" --data-binary @test/pyxform-error.xlsx http://127.0.0.1:5000/api/v1/convert)
+test_2_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: pyxform-error" --data-binary @test/pyxform-error.xlsx http://127.0.0.1:5001/api/v1/convert)
test_2_expected='{"error":"Unknown question type '\''textX'\''.","itemsets":null,"result":null,"status":400,"warnings":null}'
if [ "$test_2_actual" != "$test_2_expected" ]; then
echo "test 2 failed: form that fails to convert and returns a pyxform error"
test_failed="true"
fi
-test_3_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: pyxform-warning" --data-binary @test/pyxform-warning.xlsx http://127.0.0.1:5000/api/v1/convert)
-test_3_expected='{"error":null,"itemsets":null,"result":"pyxform-warning","status":200,"warnings":["[row : 3] Group has no label: {'\''name'\'': '\''group'\'', '\''type'\'': '\''begin group'\''}"]}'
+test_3_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: pyxform-warning" --data-binary @test/pyxform-warning.xlsx http://127.0.0.1:5001/api/v1/convert)
+test_3_expected='{"error":null,"itemsets":null,"result":"pyxform-warning","status":200,"warnings":["[row : 3] Group has no label: {'\''name'\'': '\''group'\'', '\''type'\'': '\''begin group'\''}"]}'
if [ "$test_3_actual" != "$test_3_expected" ]; then
echo "test 3 failed: form that converts and also returns pyxform warnings"
test_failed="true"
fi
-test_4_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: validate-error" --data-binary @test/validate-error.xlsx http://127.0.0.1:5000/api/v1/convert)
+test_4_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: validate-error" --data-binary @test/validate-error.xlsx http://127.0.0.1:5001/api/v1/convert)
test_4_expected='{"error":"ODK Validate Errors:\n>> Something broke the parser. See above for a hint.\nError evaluating field '\''concat'\'' (${concat}[1]): The problem was located in Calculate expression for ${concat}\nXPath evaluation: cannot handle function '\''concatx'\''\nCaused by: org.javarosa.xpath.XPathUnhandledException: The problem was located in Calculate expression for ${concat}\nXPath evaluation: cannot handle function '\''concatx'\''\n\t... 10 more\n\nThe following files failed validation:\n${validate-error}.xml\n\nResult: Invalid","itemsets":null,"result":null,"status":400,"warnings":null}'
if [ "$test_4_actual" != "$test_4_expected" ]; then
echo "test 4 failed: form that passes pyxform's internal checks, but fails ODK Validate's checks"
test_failed="true"
fi
-test_5_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: external-choices" --data-binary @test/external-choices.xlsx http://127.0.0.1:5000/api/v1/convert)
-test_5_expected='{"error":null,"itemsets":"\"list_name\",\"name\",\"label\",\"province\",\"district\"\n\"districts\",\"district_a\",\"District A (in Province 1)\",\"province_1\",\"None\"\n\"districts\",\"district_b\",\"District B (in Province 1)\",\"province_1\",\"None\"\n\"districts\",\"district_c\",\"District C (in Province 2)\",\"province_2\",\"None\"\n\"None\",\"None\",\"None\",\"None\",\"None\"\n\"lots\",\"lot_10\",\"Lot 10 (in District A)\",\"province_1\",\"district_a\"\n\"lots\",\"lot_20\",\"Lot 20 (in District A)\",\"province_1\",\"district_a\"\n\"lots\",\"lot_30\",\"Lot 30 (In District B)\",\"province_1\",\"district_b\"\n\"lots\",\"lot_40\",\"Lot 40 (In District C)\",\"province_2\",\"district_c\"\n","result":"external-choicesprovince_1province_2","status":200,"warnings":[]}'
+test_5_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: external-choices" --data-binary @test/external-choices.xlsx http://127.0.0.1:5001/api/v1/convert)
+test_5_expected='{"error":null,"itemsets":"\"list_name\",\"name\",\"label\",\"province\",\"district\"\n\"districts\",\"district_a\",\"District A (in Province 1)\",\"province_1\",\"None\"\n\"districts\",\"district_b\",\"District B (in Province 1)\",\"province_1\",\"None\"\n\"districts\",\"district_c\",\"District C (in Province 2)\",\"province_2\",\"None\"\n\"None\",\"None\",\"None\",\"None\",\"None\"\n\"lots\",\"lot_10\",\"Lot 10 (in District A)\",\"province_1\",\"district_a\"\n\"lots\",\"lot_20\",\"Lot 20 (in District A)\",\"province_1\",\"district_a\"\n\"lots\",\"lot_30\",\"Lot 30 (In District B)\",\"province_1\",\"district_b\"\n\"lots\",\"lot_40\",\"Lot 40 (In District C)\",\"province_2\",\"district_c\"\n","result":"external-choicesprovince_1province_2","status":200,"warnings":[]}'
if [ "$test_5_actual" != "$test_5_expected" ]; then
echo "test 5 failed: form that converts (with external choices)"
test_failed="true"
fi
# test removes uuid from actual and expected
-test_6_actual=$(curl --silent --request POST --data-binary @test/pyxform-clean.xlsx http://127.0.0.1:5000/api/v1/convert | sed 's/[0-9a-f-]\{36\}//g')
-test_6_expected=$(echo '{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}' | sed 's/[0-9a-f-]\{36\}//g')
+test_6_actual=$(curl --silent --request POST --data-binary @test/pyxform-clean.xlsx http://127.0.0.1:5001/api/v1/convert | sed 's/[0-9a-f-]\{36\}//g')
+test_6_expected=$(echo '{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}' | sed 's/[0-9a-f-]\{36\}//g')
if [ "$test_6_actual" != "$test_6_expected" ]; then
echo "test 6 failed: form that converts (with no id)"
test_failed="true"
fi
# test removes uuid from actual and expected
-test_7_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: example%40example.org" --data-binary @test/pyxform-clean.xlsx http://127.0.0.1:5000/api/v1/convert | sed 's/[0-9a-f-]\{36\}//g')
-test_7_expected=$(echo '{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}' | sed 's/[0-9a-f-]\{36\}//g')
+test_7_actual=$(curl --silent --request POST --header "X-XlsForm-FormId-Fallback: example%40example.org" --data-binary @test/pyxform-clean.xlsx http://127.0.0.1:5001/api/v1/convert | sed 's/[0-9a-f-]\{36\}//g')
+test_7_expected=$(echo '{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}' | sed 's/[0-9a-f-]\{36\}//g')
if [ "$test_7_actual" != "$test_7_expected" ]; then
echo "test 7 failed: form that converts (with percent encoded id)"
test_failed="true"
fi
# test removes uuid from actual and expected
-test_8_actual=$(curl --silent --request POST --data-binary @test/pyxform-clean.xls http://127.0.0.1:5000/api/v1/convert | sed 's/[0-9a-f-]\{36\}//g')
-test_8_expected=$(echo '{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}' | sed 's/[0-9a-f-]\{36\}//g')
+test_8_actual=$(curl --silent --request POST --data-binary @test/pyxform-clean.xls http://127.0.0.1:5001/api/v1/convert | sed 's/[0-9a-f-]\{36\}//g')
+test_8_expected=$(echo '{"error":null,"itemsets":null,"result":"pyxform-clean","status":200,"warnings":[]}' | sed 's/[0-9a-f-]\{36\}//g')
if [ "$test_8_actual" != "$test_8_expected" ]; then
echo "test 8 failed: form that converts (with no id, in XLS format)"
test_failed="true"
@@ -75,5 +75,7 @@ docker container stop pyxform-http-tester >/dev/null
docker container rm pyxform-http-tester >/dev/null
if [ "$test_failed" == "true" ] ; then
- exit 1
-fi
\ No newline at end of file
+ exit 1
+else
+ echo "tests passed"
+fi
\ No newline at end of file