Move QR generation to the browser, saving some time and effort on the server.
This commit is contained in:
parent
22109c0e04
commit
dfc66991f4
6 changed files with 24 additions and 62 deletions
|
|
@ -1,17 +1,15 @@
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import base64
|
import base64
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
import typing
|
import typing
|
||||||
import urllib
|
import urllib
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
import loky
|
|
||||||
import jinja2
|
import jinja2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import qrcode
|
|
||||||
import json
|
|
||||||
|
|
||||||
from cara import models
|
from cara import models
|
||||||
from ... import monte_carlo as mc
|
from ... import monte_carlo as mc
|
||||||
|
|
@ -123,7 +121,7 @@ def calculate_report_data(model: models.ExposureModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def generate_qr_code(base_url, calculator_prefix, form: FormData):
|
def generate_permalink(base_url, calculator_prefix, form: FormData):
|
||||||
form_dict = FormData.to_dict(form, strip_defaults=True)
|
form_dict = FormData.to_dict(form, strip_defaults=True)
|
||||||
|
|
||||||
# Generate the calculator URL arguments that would be needed to re-create this
|
# Generate the calculator URL arguments that would be needed to re-create this
|
||||||
|
|
@ -136,20 +134,9 @@ def generate_qr_code(base_url, calculator_prefix, form: FormData):
|
||||||
qr_url = f"{base_url}/_c/{compressed_args}"
|
qr_url = f"{base_url}/_c/{compressed_args}"
|
||||||
url = f"{base_url}{calculator_prefix}?{args}"
|
url = f"{base_url}{calculator_prefix}?{args}"
|
||||||
|
|
||||||
qr = qrcode.QRCode(
|
|
||||||
version=1,
|
|
||||||
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
|
||||||
box_size=10,
|
|
||||||
border=4,
|
|
||||||
)
|
|
||||||
qr.add_data(qr_url)
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white").convert('RGB')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'image': img2base64(_img2bytes(img)),
|
|
||||||
'link': url,
|
'link': url,
|
||||||
'qr_url': qr_url,
|
'shortened': qr_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -313,7 +300,7 @@ class ReportGenerator:
|
||||||
context['alternative_scenarios'] = comparison_report(
|
context['alternative_scenarios'] = comparison_report(
|
||||||
alternative_scenarios, scenario_sample_times, executor_factory=executor_factory,
|
alternative_scenarios, scenario_sample_times, executor_factory=executor_factory,
|
||||||
)
|
)
|
||||||
context['qr_code'] = generate_qr_code(base_url, self.calculator_prefix, form)
|
context['permalink'] = generate_permalink(base_url, self.calculator_prefix, form)
|
||||||
context['calculator_prefix'] = self.calculator_prefix
|
context['calculator_prefix'] = self.calculator_prefix
|
||||||
context['scale_warning'] = {
|
context['scale_warning'] = {
|
||||||
'level': 'yellow-2',
|
'level': 'yellow-2',
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
function generate_pdf_version(qr_link) {
|
|
||||||
const pdf_version = this.document.getElementById("body");
|
|
||||||
|
|
||||||
// PDF styling
|
|
||||||
var opt = {
|
|
||||||
filename: 'myfile.pdf',
|
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
|
||||||
html2canvas: { scale: 2, width: 1200, windowWidth: 1200 },
|
|
||||||
enableLinks: false,
|
|
||||||
jsPDF: {
|
|
||||||
unit: 'pt',
|
|
||||||
format: 'letter',
|
|
||||||
orientation: 'portrait',
|
|
||||||
},
|
|
||||||
pagebreak: { mode: '', avoid: '.break-avoid' },
|
|
||||||
};
|
|
||||||
html2pdf().set(opt).from(pdf_version).toPdf().get('pdf').then(function(pdf) {
|
|
||||||
var totalPages = pdf.internal.getNumberOfPages();
|
|
||||||
pdf.setPage(1);
|
|
||||||
pdf.link(530, 25, 60, 60, { url: qr_link }); //Hyperlink to reproduce results
|
|
||||||
|
|
||||||
for (i = 1; i <= totalPages; i++) {
|
|
||||||
pdf.setPage(i);
|
|
||||||
pdf.setFontSize(10);
|
|
||||||
pdf.setTextColor(150);
|
|
||||||
pdf.text('Page ' + i + ' of ' + totalPages, (pdf.internal.pageSize.getWidth() / 2.25), (pdf.internal.pageSize.getHeight() - 10));
|
|
||||||
}
|
|
||||||
}).save();
|
|
||||||
};
|
|
||||||
|
|
@ -24,8 +24,6 @@
|
||||||
<p class="mb-0"> Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}</p>
|
<p class="mb-0"> Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-outline-dark align-self-center" style="margin-right: -100pt" id="download-pdf" onclick="print()">Print Report</button>
|
<button type="button" class="btn btn-outline-dark align-self-center" style="margin-right: -100pt" id="download-pdf" onclick="print()">Print Report</button>
|
||||||
{# To be replaced by "Generate PDF" #}
|
|
||||||
<img id="pdf-qr-code" class="align-self-center invisible" src="{{ qr_code.image }}"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock report_header %}
|
{% endblock report_header %}
|
||||||
|
|
@ -162,10 +160,10 @@
|
||||||
<div class="collapse show" id="collapseQRcode">
|
<div class="collapse show" id="collapseQRcode">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ qr_code.link }}" style="float: left;"><img style="width:250pt;" id="qr_code" src="{{ qr_code.image }}"/></a>
|
<a href="{{ permalink.link }}" style="float: left;"><div id="qrcode"></div></a>
|
||||||
<span style="float: left; min-height: 250pt; line-height: 250pt; vertical-align: middle; display: inline-block;">
|
<span style="float: left; min-height: 250pt; line-height: 250pt; vertical-align: middle; display: inline-block;">
|
||||||
<p style="display: inline-block; vertical-align: middle; line-height: normal;">
|
<p style="display: inline-block; vertical-align: middle; line-height: normal;">
|
||||||
Click the QR code to regenerate the report and get a shareable link.<br>Alternatively, scan to regenerate the report.<br> Mobile-friendly app coming soon!
|
Click the QR code to regenerate the report and get a shareable link.<br>Alternatively, scan to regenerate the report.<br>
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -433,11 +431,19 @@
|
||||||
</div>
|
</div>
|
||||||
{% endblock disclaimer_container %}
|
{% endblock disclaimer_container %}
|
||||||
|
|
||||||
<script src="{{ calculator_prefix }}/static/js/pdf.js"></script>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.2/html2pdf.bundle.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.2/html2pdf.bundle.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
new QRCode(document.getElementById("qrcode"), {
|
||||||
|
text: "{{ permalink.shortened }}",
|
||||||
|
width: 330,
|
||||||
|
height: 330}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import pytest
|
||||||
import tornado.testing
|
import tornado.testing
|
||||||
|
|
||||||
import cara.apps.calculator
|
import cara.apps.calculator
|
||||||
from cara.apps.calculator.report_generator import generate_qr_code
|
from cara.apps.calculator.report_generator import generate_permalink
|
||||||
|
|
||||||
_TIMEOUT = 20.
|
_TIMEOUT = 20.
|
||||||
|
|
||||||
|
|
@ -97,26 +97,26 @@ class TestOpenApp(tornado.testing.AsyncHTTPTestCase):
|
||||||
assert response.code == 404
|
assert response.code == 404
|
||||||
|
|
||||||
|
|
||||||
async def test_qrcode_urls(http_server_client, baseline_form):
|
async def test_permalink_urls(http_server_client, baseline_form):
|
||||||
base_url = 'proto://hostname/prefix'
|
base_url = 'proto://hostname/prefix'
|
||||||
qr_data = generate_qr_code(base_url, "/calculator", baseline_form)
|
permalink_data = generate_permalink(base_url, "/calculator", baseline_form)
|
||||||
expected = f'{base_url}/calculator?exposed_coffee_break_option={baseline_form.exposed_coffee_break_option}&'
|
expected = f'{base_url}/calculator?exposed_coffee_break_option={baseline_form.exposed_coffee_break_option}&'
|
||||||
assert qr_data['link'].startswith(expected)
|
assert permalink_data['link'].startswith(expected)
|
||||||
|
|
||||||
# We should get a 200 for the link.
|
# We should get a 200 for the link.
|
||||||
response = await http_server_client.fetch(qr_data['link'].replace(base_url, ''))
|
response = await http_server_client.fetch(permalink_data['link'].replace(base_url, ''))
|
||||||
assert response.code == 200
|
assert response.code == 200
|
||||||
|
|
||||||
# And a 302 for the QR url itself. The redirected URL should be the same as
|
# And a 302 for the QR url itself. The redirected URL should be the same as
|
||||||
# in the link.
|
# in the link.
|
||||||
assert qr_data['qr_url'].startswith(base_url)
|
assert permalink_data['shortened'].startswith(base_url)
|
||||||
response = await http_server_client.fetch(
|
response = await http_server_client.fetch(
|
||||||
qr_data['qr_url'].replace(base_url, ''),
|
permalink_data['shortened'].replace(base_url, ''),
|
||||||
max_redirects=0,
|
max_redirects=0,
|
||||||
raise_error=False,
|
raise_error=False,
|
||||||
)
|
)
|
||||||
assert response.code == 302
|
assert response.code == 302
|
||||||
assert response.headers['Location'] == qr_data['link'].replace(base_url, '')
|
assert response.headers['Location'] == permalink_data['link'].replace(base_url, '')
|
||||||
|
|
||||||
|
|
||||||
async def test_invalid_compressed_url(http_server_client, baseline_form):
|
async def test_invalid_compressed_url(http_server_client, baseline_form):
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ pyparsing==2.4.7
|
||||||
pyrsistent==0.18.0
|
pyrsistent==0.18.0
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
pyzmq==22.1.0
|
pyzmq==22.1.0
|
||||||
qrcode==7.2
|
|
||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
requests-unixsocket==0.2.0
|
requests-unixsocket==0.2.0
|
||||||
scikit-learn==0.24.2
|
scikit-learn==0.24.2
|
||||||
|
|
|
||||||
1
setup.py
1
setup.py
|
|
@ -30,7 +30,6 @@ REQUIREMENTS: dict = {
|
||||||
'numpy',
|
'numpy',
|
||||||
'psutil',
|
'psutil',
|
||||||
'python-dateutil',
|
'python-dateutil',
|
||||||
'qrcode[pil]',
|
|
||||||
'scipy',
|
'scipy',
|
||||||
'sklearn',
|
'sklearn',
|
||||||
'timezonefinder',
|
'timezonefinder',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue