Merge branch 'feature/zlib_url' into 'master'
Shorten the form URL by compressing and b64 encoding it See merge request cara/cara!147
This commit is contained in:
commit
3d3127845c
7 changed files with 100 additions and 26 deletions
|
|
@ -1,10 +1,12 @@
|
|||
import datetime
|
||||
import base64
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
import uuid
|
||||
import zlib
|
||||
|
||||
import jinja2
|
||||
from tornado.web import Application, RequestHandler, StaticFileHandler
|
||||
|
|
@ -86,8 +88,6 @@ class ConcentrationModel(BaseRequestHandler):
|
|||
|
||||
try:
|
||||
form = model_generator.FormData.from_dict(requested_model_config)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as err:
|
||||
if self.settings.get("debug", False):
|
||||
import traceback
|
||||
|
|
@ -131,6 +131,18 @@ class CalculatorForm(BaseRequestHandler):
|
|||
self.finish(report)
|
||||
|
||||
|
||||
class CompressedCalculatorFormInputs(BaseRequestHandler):
|
||||
def get(self, compressed_args: str):
|
||||
# Convert a base64 zlib encoded shortened URL into a non compressed
|
||||
# URL, and redirect.
|
||||
try:
|
||||
args = zlib.decompress(base64.b64decode(compressed_args)).decode()
|
||||
except Exception as err: # noqa
|
||||
self.set_status(400)
|
||||
return self.finish("Invalid calculator data: it seems incomplete. Was there an error copying & pasting the URL?")
|
||||
self.redirect(f'/calculator?{args}')
|
||||
|
||||
|
||||
class ReadmeHandler(BaseRequestHandler):
|
||||
def get(self):
|
||||
template = self.settings['template_environment'].get_template("userguide.html.j2")
|
||||
|
|
@ -146,6 +158,7 @@ def make_app(debug=False, prefix='/calculator'):
|
|||
calculator_static_dir = Path(__file__).absolute().parent / 'static'
|
||||
urls = [
|
||||
(r'/?', LandingPage),
|
||||
(r'/_c/(.*)', CompressedCalculatorFormInputs),
|
||||
(r'/static/(.*)', StaticFileHandler, {'path': static_dir}),
|
||||
(prefix + r'/?', CalculatorForm),
|
||||
(prefix + r'/report', ConcentrationModel),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from datetime import datetime
|
|||
import io
|
||||
from pathlib import Path
|
||||
import typing
|
||||
import zlib
|
||||
|
||||
import qrcode
|
||||
import urllib
|
||||
|
|
@ -70,7 +71,17 @@ def calculate_report_data(model: models.ExposureModel):
|
|||
|
||||
def generate_qr_code(prefix, form: FormData):
|
||||
form_dict = FormData.to_dict(form)
|
||||
url = prefix + "?" + urllib.parse.urlencode(form_dict)
|
||||
|
||||
# Generate the calculator URL arguments that would be needed to re-create this
|
||||
# form.
|
||||
args = urllib.parse.urlencode(form_dict)
|
||||
|
||||
# Then zlib compress + base64 encode the string. To be inverted by the
|
||||
# /_c/ endpoint.
|
||||
qr_url = prefix + "/_c/" + base64.b64encode(
|
||||
zlib.compress(args.encode())
|
||||
).decode()
|
||||
url = prefix + "/calculator?" + args
|
||||
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
|
|
@ -78,13 +89,14 @@ def generate_qr_code(prefix, form: FormData):
|
|||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(url)
|
||||
qr.add_data(qr_url)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white").convert('RGB')
|
||||
|
||||
return {
|
||||
'image': img2base64(_img2bytes(img)),
|
||||
'link': url,
|
||||
'qr_url': qr_url,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -272,7 +284,7 @@ def build_report(base_url: str, model: models.ExposureModel, form: FormData):
|
|||
context.update(calculate_report_data(model))
|
||||
alternative_scenarios = manufacture_alternative_scenarios(form)
|
||||
context['alternative_scenarios'] = comparison_report(alternative_scenarios)
|
||||
context['qr_code'] = generate_qr_code(f'{base_url}/calculator', form)
|
||||
context['qr_code'] = generate_qr_code(base_url, form)
|
||||
|
||||
cara_templates = Path(__file__).parent.parent / "templates"
|
||||
calculator_templates = Path(__file__).parent / "templates"
|
||||
|
|
|
|||
13
cara/tests/apps/calculator/conftest.py
Normal file
13
cara/tests/apps/calculator/conftest.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import pytest
|
||||
|
||||
from cara.apps.calculator import model_generator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_form_data():
|
||||
return model_generator.baseline_raw_form_data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_form(baseline_form_data):
|
||||
return model_generator.FormData.from_dict(baseline_form_data)
|
||||
|
|
@ -6,15 +6,6 @@ from cara import models
|
|||
from cara import data
|
||||
import numpy as np
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_form_data():
|
||||
return model_generator.baseline_raw_form_data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_form(baseline_form_data):
|
||||
return model_generator.FormData.from_dict(baseline_form_data)
|
||||
|
||||
|
||||
def test_model_from_dict(baseline_form_data):
|
||||
form = model_generator.FormData.from_dict(baseline_form_data)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,8 @@
|
|||
import pytest
|
||||
|
||||
from cara.apps.calculator import model_generator
|
||||
from cara.apps.calculator import report_generator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_form_data():
|
||||
return model_generator.baseline_raw_form_data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_form(baseline_form_data):
|
||||
return model_generator.FormData.from_dict(baseline_form_data)
|
||||
|
||||
|
||||
def test_generate_report(baseline_form):
|
||||
model = baseline_form.build_model()
|
||||
|
||||
|
|
@ -32,4 +21,4 @@ def test_generate_report(baseline_form):
|
|||
],
|
||||
)
|
||||
def test_readable_minutes(test_input, expected):
|
||||
assert report_generator.readable_minutes(test_input) == expected
|
||||
assert report_generator.readable_minutes(test_input) == expected
|
||||
|
|
|
|||
54
cara/tests/apps/calculator/test_webapp.py
Normal file
54
cara/tests/apps/calculator/test_webapp.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import pytest
|
||||
|
||||
from cara.apps.calculator import make_app
|
||||
from cara.apps.calculator.report_generator import generate_qr_code
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return make_app()
|
||||
|
||||
|
||||
async def test_homepage(http_server_client):
|
||||
response = await http_server_client.fetch('/')
|
||||
assert response.code == 200
|
||||
|
||||
|
||||
async def test_calculator(http_server_client):
|
||||
# Both with and without a trailing slash.
|
||||
response = await http_server_client.fetch('/calculator')
|
||||
assert response.code == 200
|
||||
|
||||
response = await http_server_client.fetch('/calculator/')
|
||||
assert response.code == 200
|
||||
|
||||
|
||||
async def test_qrcode_urls(http_server_client, baseline_form):
|
||||
prefix = 'proto://hostname/prefix'
|
||||
qr_data = generate_qr_code(prefix, baseline_form)
|
||||
expected = f'{prefix}/calculator?activity_type=office&air_changes=0.0'
|
||||
assert qr_data['link'].startswith(expected)
|
||||
|
||||
# We should get a 200 for the link.
|
||||
response = await http_server_client.fetch(qr_data['link'].replace(prefix, ''))
|
||||
assert response.code == 200
|
||||
|
||||
# And a 302 for the QR url itself. The redirected URL should be the same as
|
||||
# in the link.
|
||||
assert qr_data['qr_url'].startswith(prefix)
|
||||
response = await http_server_client.fetch(
|
||||
qr_data['qr_url'].replace(prefix, ''),
|
||||
max_redirects=0,
|
||||
raise_error=False,
|
||||
)
|
||||
assert response.code == 302
|
||||
assert response.headers['Location'] == qr_data['link'].replace(prefix, '')
|
||||
|
||||
|
||||
async def test_invalid_compressed_url(http_server_client, baseline_form):
|
||||
response = await http_server_client.fetch(
|
||||
'/_c/invalid-data',
|
||||
max_redirects=0,
|
||||
raise_error=False,
|
||||
)
|
||||
assert response.code == 400
|
||||
2
setup.py
2
setup.py
|
|
@ -71,6 +71,8 @@ setup(
|
|||
'all': [req for reqs in REQUIREMENTS.values() for req in reqs],
|
||||
},
|
||||
package_data={'cara': [
|
||||
'apps/templates/*.j2',
|
||||
'apps/calculator/templates/*.j2',
|
||||
'apps/calculator/*',
|
||||
'apps/calculator/*/*',
|
||||
'apps/calculator/*/*/*'
|
||||
|
|
|
|||
Loading…
Reference in a new issue