Compare View
Commits (25)
-
This is a stepping stone towards python 3 support, that we need to fix the geocoder.
-
Spent time is not attached to this commit only. /spend 11h
-
/spend 2min
-
and continue moving to python3 /spend 8h
-
Thanks @Jan for the issue report and fix suggestion!
-
This is a monkey fix, and we're not supporting tabs either.
Showing
15 changed files
Show diff stats
Dockerfile
1 | -#FROM python:2.7 | ||
2 | -FROM tiangolo/uwsgi-nginx-flask:python2.7 | 1 | +FROM tiangolo/uwsgi-nginx-flask:python3.8 |
3 | ADD . /app | 2 | ADD . /app |
4 | WORKDIR /app | 3 | WORKDIR /app |
4 | +RUN apt-get update && apt-get install -y cython3 | ||
5 | RUN pip install -r requirements.txt | 5 | RUN pip install -r requirements.txt |
6 | ENV FLASK_APP "flaskr" | 6 | ENV FLASK_APP "flaskr" |
7 | ENV FLASK_ENV "production" | 7 | ENV FLASK_ENV "production" |
README.md
@@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
2 | # Travel Carbon Footprint Calculator | 2 | # Travel Carbon Footprint Calculator |
3 | 3 | ||
4 | - https://travel-footprint-calculator.apps.goutenoir.com (private demo) | 4 | - https://travel-footprint-calculator.apps.goutenoir.com (private demo) |
5 | -- http://travel-footprint-calculator.irap.omp.eu (official, for later) | 5 | +- http://travel-footprint-calculator.irap.omp.eu (official) |
6 | 6 | ||
7 | 7 | ||
8 | ## Overview | 8 | ## Overview |
@@ -16,20 +16,25 @@ | @@ -16,20 +16,25 @@ | ||
16 | 16 | ||
17 | ## Installation | 17 | ## Installation |
18 | 18 | ||
19 | -Tested only on Python `2.7`. _Sprint._ | 19 | +Works on Python `2.7` up to `1.2` version. |
20 | +Works on Python `3.7` from `1.3` version. | ||
21 | + | ||
20 | 22 | ||
21 | ### Create a virtual environment | 23 | ### Create a virtual environment |
22 | 24 | ||
23 | You don't _have to_. But it's useful for development. | 25 | You don't _have to_. But it's useful for development. |
24 | 26 | ||
25 | - virtualenv venv | 27 | + virtualenv .venv |
26 | 28 | ||
27 | Then, source it to enable it. | 29 | Then, source it to enable it. |
28 | 30 | ||
29 | - source venv/bin/activate | 31 | + source .venv/bin/activate |
30 | 32 | ||
31 | ### Install the python dependencies | 33 | ### Install the python dependencies |
32 | 34 | ||
35 | +You will need Cython. | ||
36 | + | ||
37 | + sudo apt install cython | ||
33 | pip install -r requirements.txt | 38 | pip install -r requirements.txt |
34 | 39 | ||
35 | ### Create an empty database | 40 | ### Create an empty database |
@@ -44,18 +49,16 @@ Then, source it to enable it. | @@ -44,18 +49,16 @@ Then, source it to enable it. | ||
44 | ### Configure permissions | 49 | ### Configure permissions |
45 | 50 | ||
46 | `var/runs` must be writeable by the application. | 51 | `var/runs` must be writeable by the application. |
52 | +It is the file-based part of the database. | ||
47 | 53 | ||
48 | - | ||
49 | -## Build CSS and JS ()for prod) | 54 | +## Build CSS and JS (for prod) |
50 | 55 | ||
51 | flask assets build | 56 | flask assets build |
52 | 57 | ||
53 | 58 | ||
54 | - | ||
55 | - | ||
56 | ## Development | 59 | ## Development |
57 | 60 | ||
58 | - source venv/bin/activate | 61 | + source .venv/bin/activate |
59 | source .env.flaskrun | 62 | source .env.flaskrun |
60 | flask run | 63 | flask run |
61 | 64 |
VERSION
docker-compose.yml
@@ -4,8 +4,8 @@ | @@ -4,8 +4,8 @@ | ||
4 | 4 | ||
5 | version: '2.0' | 5 | version: '2.0' |
6 | services: | 6 | services: |
7 | - com_goutenoir_apps_travel-footprint-calculator: | ||
8 | - container_name: com_goutenoir_apps_travel-footprint-calculator | 7 | + travel_footprint_calculator: |
8 | + container_name: travel_footprint_calculator | ||
9 | restart: always | 9 | restart: always |
10 | build: . | 10 | build: . |
11 | volumes: | 11 | volumes: |
@@ -22,13 +22,12 @@ services: | @@ -22,13 +22,12 @@ services: | ||
22 | LETSENCRYPT_HOST: travel-footprint-calculator.apps.goutenoir.com | 22 | LETSENCRYPT_HOST: travel-footprint-calculator.apps.goutenoir.com |
23 | LETSENCRYPT_EMAIL: antoine@goutenoir.com | 23 | LETSENCRYPT_EMAIL: antoine@goutenoir.com |
24 | VIRTUAL_HOST: travel-footprint-calculator.apps.goutenoir.com | 24 | VIRTUAL_HOST: travel-footprint-calculator.apps.goutenoir.com |
25 | - # 80 is the default port | ||
26 | VIRTUAL_PORT: 80 | 25 | VIRTUAL_PORT: 80 |
27 | ################################################### | 26 | ################################################### |
28 | 27 | ||
29 | 28 | ||
30 | - com_goutenoir_apps_travel-footprint-calculator_mahcron: | ||
31 | - container_name: com_goutenoir_apps_travel-footprint-calculator_mahcron | 29 | + travel_footprint_calculator_mahcron: |
30 | + container_name: travel_footprint_calculator_mahcron | ||
32 | image: jsonfry/curl-cron | 31 | image: jsonfry/curl-cron |
33 | restart: always | 32 | restart: always |
34 | environment: | 33 | environment: |
flaskr/__init__.py
1 | #! ../venv/bin/python | 1 | #! ../venv/bin/python |
2 | import os | 2 | import os |
3 | -from markdown import markdown | ||
4 | 3 | ||
5 | from dotenv import load_dotenv, find_dotenv, dotenv_values | 4 | from dotenv import load_dotenv, find_dotenv, dotenv_values |
5 | +from markdown import markdown | ||
6 | + | ||
6 | # Load config from .env ; do this before importing local libs (and flask) | 7 | # Load config from .env ; do this before importing local libs (and flask) |
7 | # 1. Write into OS environment -- if this fails you forgot to create .env file | 8 | # 1. Write into OS environment -- if this fails you forgot to create .env file |
8 | load_dotenv(find_dotenv(raise_error_if_not_found=True), override=True) | 9 | load_dotenv(find_dotenv(raise_error_if_not_found=True), override=True) |
@@ -63,7 +64,7 @@ def create_app(object_name): | @@ -63,7 +64,7 @@ def create_app(object_name): | ||
63 | app.config['BASIC_AUTH_USERNAME'] = os.getenv('ADMIN_USERNAME') | 64 | app.config['BASIC_AUTH_USERNAME'] = os.getenv('ADMIN_USERNAME') |
64 | app.config['BASIC_AUTH_PASSWORD'] = os.getenv('ADMIN_PASSWORD') | 65 | app.config['BASIC_AUTH_PASSWORD'] = os.getenv('ADMIN_PASSWORD') |
65 | 66 | ||
66 | - app.config['CAPTCHA_ENABLE'] = True | 67 | + app.config['CAPTCHA_ENABLE'] = False |
67 | app.config['CAPTCHA_LENGTH'] = 5 | 68 | app.config['CAPTCHA_LENGTH'] = 5 |
68 | app.config['CAPTCHA_WIDTH'] = 256 | 69 | app.config['CAPTCHA_WIDTH'] = 256 |
69 | app.config['CAPTCHA_HEIGHT'] = 158 | 70 | app.config['CAPTCHA_HEIGHT'] = 158 |
flaskr/content.py
1 | from collections import namedtuple | 1 | from collections import namedtuple |
2 | -from yaml import safe_load as yaml_safe_load | ||
3 | from os.path import abspath, dirname, join | 2 | from os.path import abspath, dirname, join |
4 | 3 | ||
4 | +from yaml import safe_load as yaml_safe_load | ||
5 | + | ||
6 | +# Move this to ENV, perhaps | ||
7 | +base_url = "https://travel-footprint-calculator.irap.omp.eu" | ||
5 | 8 | ||
6 | PROJECT_DIRECTORY = abspath(dirname(dirname(__file__))) | 9 | PROJECT_DIRECTORY = abspath(dirname(dirname(__file__))) |
7 | 10 | ||
@@ -21,7 +24,7 @@ class Struct(object): | @@ -21,7 +24,7 @@ class Struct(object): | ||
21 | def __new__(cls, data): | 24 | def __new__(cls, data): |
22 | if isinstance(data, dict): | 25 | if isinstance(data, dict): |
23 | return namedtuple( | 26 | return namedtuple( |
24 | - 'Struct', data.iterkeys() | 27 | + 'Struct', data.keys() |
25 | )( | 28 | )( |
26 | *(Struct(val) for val in data.values()) | 29 | *(Struct(val) for val in data.values()) |
27 | ) | 30 | ) |
@@ -33,9 +36,6 @@ class Struct(object): | @@ -33,9 +36,6 @@ class Struct(object): | ||
33 | 36 | ||
34 | content = Struct(content_dict) | 37 | content = Struct(content_dict) |
35 | 38 | ||
36 | -# Move this to ENV, perhaps | ||
37 | -base_url = "https://travel-footprint-calculator.irap.omp.eu" | ||
38 | - | ||
39 | 39 | ||
40 | # For Python3? | 40 | # For Python3? |
41 | # def dict2obj(d): | 41 | # def dict2obj(d): |
flaskr/controllers/main_controller.py
1 | import csv | 1 | import csv |
2 | import re | 2 | import re |
3 | -# from io import StringIO | ||
4 | -from cStringIO import StringIO | 3 | +# from cStringIO import StringIO |
5 | from copy import deepcopy | 4 | from copy import deepcopy |
6 | -from os import unlink | 5 | +from io import StringIO |
6 | +from os import unlink, getenv | ||
7 | from os.path import join | 7 | from os.path import join |
8 | 8 | ||
9 | import geopy | 9 | import geopy |
@@ -13,13 +13,14 @@ from flask import ( | @@ -13,13 +13,14 @@ from flask import ( | ||
13 | Blueprint, | 13 | Blueprint, |
14 | Response, | 14 | Response, |
15 | render_template, | 15 | render_template, |
16 | + request, | ||
16 | flash, | 17 | flash, |
17 | redirect, | 18 | redirect, |
18 | url_for, | 19 | url_for, |
19 | abort, | 20 | abort, |
20 | send_from_directory, | 21 | send_from_directory, |
21 | ) | 22 | ) |
22 | -from pandas.compat import StringIO as PandasStringIO | 23 | +# from pandas.compat import StringIO as PandasStringIO |
23 | from wtforms import validators | 24 | from wtforms import validators |
24 | from yaml import safe_dump as yaml_dump | 25 | from yaml import safe_dump as yaml_dump |
25 | 26 | ||
@@ -28,7 +29,7 @@ from flaskr.core import ( | @@ -28,7 +29,7 @@ from flaskr.core import ( | ||
28 | get_emission_models, | 29 | get_emission_models, |
29 | increment_hit_counter, | 30 | increment_hit_counter, |
30 | ) | 31 | ) |
31 | -from flaskr.extensions import cache, send_email | 32 | +from flaskr.extensions import send_email |
32 | from flaskr.forms import EstimateForm | 33 | from flaskr.forms import EstimateForm |
33 | from flaskr.geocoder import CachedGeocoder | 34 | from flaskr.geocoder import CachedGeocoder |
34 | from flaskr.models import db, Estimation, StatusEnum, ScenarioEnum | 35 | from flaskr.models import db, Estimation, StatusEnum, ScenarioEnum |
@@ -36,7 +37,7 @@ from flaskr.models import db, Estimation, StatusEnum, ScenarioEnum | @@ -36,7 +37,7 @@ from flaskr.models import db, Estimation, StatusEnum, ScenarioEnum | ||
36 | main = Blueprint('main', __name__) | 37 | main = Blueprint('main', __name__) |
37 | 38 | ||
38 | 39 | ||
39 | -OUT_ENCODING = 'utf-8' | 40 | +#OUT_ENCODING = 'utf-8' |
40 | 41 | ||
41 | 42 | ||
42 | # ----------------------------------------------------------------------------- | 43 | # ----------------------------------------------------------------------------- |
@@ -48,7 +49,7 @@ pi_email = "didier.barret@gmail.com" # todo: move to content YAML or .env | @@ -48,7 +49,7 @@ pi_email = "didier.barret@gmail.com" # todo: move to content YAML or .env | ||
48 | 49 | ||
49 | 50 | ||
50 | @main.route('/favicon.ico') | 51 | @main.route('/favicon.ico') |
51 | -@cache.cached(timeout=10000) | 52 | +# @cache.cached(timeout=10000) |
52 | def favicon(): # we want it served from the root, not from static/ | 53 | def favicon(): # we want it served from the root, not from static/ |
53 | return send_from_directory( | 54 | return send_from_directory( |
54 | join(main.root_path, '..', 'static', 'img'), | 55 | join(main.root_path, '..', 'static', 'img'), |
@@ -82,14 +83,16 @@ def gather_addresses(from_list, from_file): | @@ -82,14 +83,16 @@ def gather_addresses(from_list, from_file): | ||
82 | addresses = [] | 83 | addresses = [] |
83 | if from_file: | 84 | if from_file: |
84 | file_mimetype = from_file.mimetype | 85 | file_mimetype = from_file.mimetype |
85 | - file_contents = from_file.read() | 86 | + file_contents = from_file.read().decode() |
86 | 87 | ||
87 | rows_dicts = None | 88 | rows_dicts = None |
88 | 89 | ||
89 | if 'text/csv' == file_mimetype: | 90 | if 'text/csv' == file_mimetype: |
90 | - | 91 | + delimiter = ',' |
92 | + if ';' in file_contents: | ||
93 | + delimiter = ';' | ||
91 | rows_dicts = pandas \ | 94 | rows_dicts = pandas \ |
92 | - .read_csv(PandasStringIO(file_contents)) \ | 95 | + .read_csv(StringIO(file_contents), delimiter=delimiter) \ |
93 | .rename(str.lower, axis='columns') \ | 96 | .rename(str.lower, axis='columns') \ |
94 | .to_dict(orient="row") | 97 | .to_dict(orient="row") |
95 | 98 | ||
@@ -110,7 +113,7 @@ def gather_addresses(from_list, from_file): | @@ -110,7 +113,7 @@ def gather_addresses(from_list, from_file): | ||
110 | or from_file.filename.endswith('xlsx'): | 113 | or from_file.filename.endswith('xlsx'): |
111 | 114 | ||
112 | rows_dicts = pandas \ | 115 | rows_dicts = pandas \ |
113 | - .read_excel(PandasStringIO(file_contents)) \ | 116 | + .read_excel(StringIO(file_contents)) \ |
114 | .rename(str.lower, axis='columns') \ | 117 | .rename(str.lower, axis='columns') \ |
115 | .to_dict(orient="row") | 118 | .to_dict(orient="row") |
116 | 119 | ||
@@ -153,8 +156,9 @@ def gather_addresses(from_list, from_file): | @@ -153,8 +156,9 @@ def gather_addresses(from_list, from_file): | ||
153 | for address in addresses: | 156 | for address in addresses: |
154 | if not address: | 157 | if not address: |
155 | continue | 158 | continue |
156 | - if type(address).__name__ == 'str': | ||
157 | - address = unicode(address, 'utf-8') | 159 | + # if type(address).__name__ == 'str': |
160 | + # address = str(address).encode('utf-8') | ||
161 | + address = str(address) | ||
158 | if to_ignore.match(address) is not None: | 162 | if to_ignore.match(address) is not None: |
159 | continue | 163 | continue |
160 | clean_addresses.append(address) | 164 | clean_addresses.append(address) |
@@ -192,7 +196,7 @@ def estimate(): # register new estimation request, more accurately | @@ -192,7 +196,7 @@ def estimate(): # register new estimation request, more accurately | ||
192 | form.origin_addresses_file.data | 196 | form.origin_addresses_file.data |
193 | ) | 197 | ) |
194 | except validators.ValidationError as e: | 198 | except validators.ValidationError as e: |
195 | - form.origin_addresses_file.errors.append(e.message) | 199 | + form.origin_addresses_file.errors.append(str(e)) |
196 | return show_form() | 200 | return show_form() |
197 | 201 | ||
198 | try: | 202 | try: |
@@ -201,7 +205,7 @@ def estimate(): # register new estimation request, more accurately | @@ -201,7 +205,7 @@ def estimate(): # register new estimation request, more accurately | ||
201 | form.destination_addresses_file.data | 205 | form.destination_addresses_file.data |
202 | ) | 206 | ) |
203 | except validators.ValidationError as e: | 207 | except validators.ValidationError as e: |
204 | - form.destination_addresses_file.errors.append(e.message) | 208 | + form.destination_addresses_file.errors.append(str(e)) |
205 | return show_form() | 209 | return show_form() |
206 | 210 | ||
207 | estimation.use_train_below_km = form.use_train_below_km.data | 211 | estimation.use_train_below_km = form.use_train_below_km.data |
@@ -372,7 +376,7 @@ def compute(): # process the queue of estimation requests | @@ -372,7 +376,7 @@ def compute(): # process the queue of estimation requests | ||
372 | continue | 376 | continue |
373 | 377 | ||
374 | try: | 378 | try: |
375 | - origin = geocoder.geocode(origin_address.encode('utf-8')) | 379 | + origin = geocoder.geocode(origin_address) |
376 | except geopy.exc.GeopyError as e: | 380 | except geopy.exc.GeopyError as e: |
377 | warning = u"Ignoring origin `%s` " \ | 381 | warning = u"Ignoring origin `%s` " \ |
378 | u"since we failed to geocode it.\n%s\n" % ( | 382 | u"since we failed to geocode it.\n%s\n" % ( |
@@ -428,9 +432,7 @@ def compute(): # process the queue of estimation requests | @@ -428,9 +432,7 @@ def compute(): # process the queue of estimation requests | ||
428 | continue | 432 | continue |
429 | 433 | ||
430 | try: | 434 | try: |
431 | - destination = geocoder.geocode( | ||
432 | - destination_address.encode('utf-8') | ||
433 | - ) | 435 | + destination = geocoder.geocode(destination_address) |
434 | except geopy.exc.GeopyError as e: | 436 | except geopy.exc.GeopyError as e: |
435 | warning = u"Ignoring destination `%s` " \ | 437 | warning = u"Ignoring destination `%s` " \ |
436 | u"since we failed to geocode it.\n%s\n" % ( | 438 | u"since we failed to geocode it.\n%s\n" % ( |
@@ -489,6 +491,13 @@ def compute(): # process the queue of estimation requests | @@ -489,6 +491,13 @@ def compute(): # process the queue of estimation requests | ||
489 | 491 | ||
490 | # UTILITY PRIVATE FUNCTION(S) ######################################### | 492 | # UTILITY PRIVATE FUNCTION(S) ######################################### |
491 | 493 | ||
494 | + # _locations | ||
495 | + def _get_location_key(_location): | ||
496 | + return "%s, %s" % ( | ||
497 | + _get_city_key(_location), | ||
498 | + _get_country_key(_location), | ||
499 | + ) | ||
500 | + | ||
492 | def _get_city_key(_location): | 501 | def _get_city_key(_location): |
493 | return _location.address.split(',')[0] | 502 | return _location.address.split(',')[0] |
494 | 503 | ||
@@ -512,7 +521,7 @@ def compute(): # process the queue of estimation requests | @@ -512,7 +521,7 @@ def compute(): # process the queue of estimation requests | ||
512 | _results = {} | 521 | _results = {} |
513 | footprints = {} | 522 | footprints = {} |
514 | 523 | ||
515 | - destinations_by_city_key = {} | 524 | + destinations_by_key = {} |
516 | 525 | ||
517 | cities_sum_foot = {} | 526 | cities_sum_foot = {} |
518 | cities_sum_dist = {} | 527 | cities_sum_dist = {} |
@@ -528,13 +537,13 @@ def compute(): # process the queue of estimation requests | @@ -528,13 +537,13 @@ def compute(): # process the queue of estimation requests | ||
528 | extra_config=_extra_config, | 537 | extra_config=_extra_config, |
529 | ) | 538 | ) |
530 | 539 | ||
531 | - _key = _get_city_key(_destination) | ||
532 | - | ||
533 | - destinations_by_city_key[_key] = _destination | 540 | + _key = _get_location_key(_destination) |
541 | + destinations_by_key[_key] = _destination | ||
534 | 542 | ||
535 | if _key not in cities_dict: | 543 | if _key not in cities_dict: |
536 | cities_dict[_key] = { | 544 | cities_dict[_key] = { |
537 | - 'city': _key, | 545 | + 'location': _get_location_key(_destination), |
546 | + 'city': _get_city_key(_destination), | ||
538 | 'country': _get_country_key(_destination), | 547 | 'country': _get_country_key(_destination), |
539 | 'address': _destination.address, | 548 | 'address': _destination.address, |
540 | 'latitude': _destination.latitude, | 549 | 'latitude': _destination.latitude, |
@@ -578,11 +587,12 @@ def compute(): # process the queue of estimation requests | @@ -578,11 +587,12 @@ def compute(): # process the queue of estimation requests | ||
578 | city_train_trips = cities_dict_first_model[city]['train_trips'] | 587 | city_train_trips = cities_dict_first_model[city]['train_trips'] |
579 | city_plane_trips = cities_dict_first_model[city]['plane_trips'] | 588 | city_plane_trips = cities_dict_first_model[city]['plane_trips'] |
580 | cities_mean_dict[city] = { | 589 | cities_mean_dict[city] = { |
581 | - 'city': city, | ||
582 | - 'country': _get_country_key(destinations_by_city_key[city]), | ||
583 | - 'address': destinations_by_city_key[city].address, | ||
584 | - 'latitude': destinations_by_city_key[city].latitude, | ||
585 | - 'longitude': destinations_by_city_key[city].longitude, | 590 | + 'location': _get_location_key(destinations_by_key[city]), |
591 | + 'city': _get_city_key(destinations_by_key[city]), | ||
592 | + 'country': _get_country_key(destinations_by_key[city]), | ||
593 | + 'address': destinations_by_key[city].address, | ||
594 | + 'latitude': destinations_by_key[city].latitude, | ||
595 | + 'longitude': destinations_by_key[city].longitude, | ||
586 | 'footprint': city_mean_foot, | 596 | 'footprint': city_mean_foot, |
587 | 'distance': city_mean_dist, | 597 | 'distance': city_mean_dist, |
588 | 'train_trips': city_train_trips, | 598 | 'train_trips': city_train_trips, |
@@ -638,19 +648,21 @@ def compute(): # process the queue of estimation requests | @@ -638,19 +648,21 @@ def compute(): # process the queue of estimation requests | ||
638 | # SCENARIO C : At Least One Origin, At Least One Destination ########## | 648 | # SCENARIO C : At Least One Origin, At Least One Destination ########## |
639 | # | 649 | # |
640 | # Run Scenario A for each Destination, and expose optimum Destination. | 650 | # Run Scenario A for each Destination, and expose optimum Destination. |
651 | + # Skip destinations already visited. (collapse duplicate destinations) | ||
641 | # | 652 | # |
642 | else: | 653 | else: |
643 | estimation.scenario = ScenarioEnum.many_to_many | 654 | estimation.scenario = ScenarioEnum.many_to_many |
644 | - unique_city_keys = [] | 655 | + unique_location_keys = [] |
645 | result_cities = [] | 656 | result_cities = [] |
646 | for destination in destinations: | 657 | for destination in destinations: |
658 | + location_key = _get_location_key(destination) | ||
647 | city_key = _get_city_key(destination) | 659 | city_key = _get_city_key(destination) |
648 | country_key = _get_country_key(destination) | 660 | country_key = _get_country_key(destination) |
649 | 661 | ||
650 | - if city_key in unique_city_keys: | 662 | + if location_key in unique_location_keys: |
651 | continue | 663 | continue |
652 | else: | 664 | else: |
653 | - unique_city_keys.append(city_key) | 665 | + unique_location_keys.append(location_key) |
654 | 666 | ||
655 | city_results = compute_one_to_many( | 667 | city_results = compute_one_to_many( |
656 | _origin=destination, | 668 | _origin=destination, |
@@ -659,6 +671,7 @@ def compute(): # process the queue of estimation requests | @@ -659,6 +671,7 @@ def compute(): # process the queue of estimation requests | ||
659 | ) | 671 | ) |
660 | city_results['city'] = city_key | 672 | city_results['city'] = city_key |
661 | city_results['country'] = country_key | 673 | city_results['country'] = country_key |
674 | + city_results['location'] = location_key | ||
662 | city_results['address'] = destination.address | 675 | city_results['address'] = destination.address |
663 | city_results['latitude'] = destination.latitude | 676 | city_results['latitude'] = destination.latitude |
664 | city_results['longitude'] = destination.longitude | 677 | city_results['longitude'] = destination.longitude |
@@ -699,7 +712,9 @@ def compute(): # process the queue of estimation requests | @@ -699,7 +712,9 @@ def compute(): # process the queue of estimation requests | ||
699 | 712 | ||
700 | except Exception as e: | 713 | except Exception as e: |
701 | errmsg = u"Computation failed : %s" % (e,) | 714 | errmsg = u"Computation failed : %s" % (e,) |
702 | - # errmsg = u"%s\n\n%s" % (errmsg, traceback.format_exc()) | 715 | + if 'production' != getenv('FLASK_ENV', 'production'): |
716 | + import traceback | ||
717 | + errmsg = u"%s\n\n%s" % (errmsg, traceback.format_exc()) | ||
703 | if estimation: | 718 | if estimation: |
704 | _handle_failure(estimation, errmsg) | 719 | _handle_failure(estimation, errmsg) |
705 | return _respond(errmsg) | 720 | return _respond(errmsg) |
@@ -717,7 +732,7 @@ def consult_estimation(public_id, extension): | @@ -717,7 +732,7 @@ def consult_estimation(public_id, extension): | ||
717 | except sqlalchemy.orm.exc.NoResultFound: | 732 | except sqlalchemy.orm.exc.NoResultFound: |
718 | return abort(404) | 733 | return abort(404) |
719 | except Exception as e: | 734 | except Exception as e: |
720 | - # TODO: log? | 735 | + # log? (or not) |
721 | return abort(500) | 736 | return abort(500) |
722 | 737 | ||
723 | # allowed_formats = ['html'] | 738 | # allowed_formats = ['html'] |
@@ -761,6 +776,7 @@ def consult_estimation(public_id, extension): | @@ -761,6 +776,7 @@ def consult_estimation(public_id, extension): | ||
761 | si = StringIO() | 776 | si = StringIO() |
762 | cw = csv.writer(si, quoting=csv.QUOTE_ALL) | 777 | cw = csv.writer(si, quoting=csv.QUOTE_ALL) |
763 | cw.writerow([ | 778 | cw.writerow([ |
779 | + u"location", | ||
764 | u"city", u"country", u"address", | 780 | u"city", u"country", u"address", |
765 | u"latitude", u"longitude", | 781 | u"latitude", u"longitude", |
766 | u"co2_kg", | 782 | u"co2_kg", |
@@ -772,9 +788,11 @@ def consult_estimation(public_id, extension): | @@ -772,9 +788,11 @@ def consult_estimation(public_id, extension): | ||
772 | results = estimation.get_output_dict() | 788 | results = estimation.get_output_dict() |
773 | for city in results['cities']: | 789 | for city in results['cities']: |
774 | cw.writerow([ | 790 | cw.writerow([ |
775 | - city['city'].encode(OUT_ENCODING), | ||
776 | - city['country'].encode(OUT_ENCODING), | ||
777 | - city['address'].encode(OUT_ENCODING), | 791 | + # city['location'].encode(OUT_ENCODING), |
792 | + city['location'], | ||
793 | + city['city'], | ||
794 | + city['country'], | ||
795 | + city['address'], | ||
778 | city.get('latitude', 0.0), | 796 | city.get('latitude', 0.0), |
779 | city.get('longitude', 0.0), | 797 | city.get('longitude', 0.0), |
780 | round(city['footprint'], 3), | 798 | round(city['footprint'], 3), |
@@ -948,6 +966,49 @@ def get_scaling_laws_csv(): | @@ -948,6 +966,49 @@ def get_scaling_laws_csv(): | ||
948 | ) | 966 | ) |
949 | 967 | ||
950 | 968 | ||
969 | +@main.route('/geocode') | ||
970 | +@main.route('/geocode.html') | ||
971 | +def query_geocode(): | ||
972 | + requested = request.args.getlist('address') | ||
973 | + if not requested: | ||
974 | + requested = request.args.getlist('address[]') | ||
975 | + if not requested: | ||
976 | + requested = request.args.getlist('a') | ||
977 | + if not requested: | ||
978 | + requested = request.args.getlist('a[]') | ||
979 | + # requested = _collect_request_args_list(('address', 'a')) | ||
980 | + if not requested: | ||
981 | + return Response( | ||
982 | + response=""" | ||
983 | +<p> | ||
984 | +Usage example: <a href="/geocode.html?address=Toulouse,France&address=Paris,France">/geocode?address=Toulouse,France</a> | ||
985 | +</p> | ||
986 | + | ||
987 | +<p> | ||
988 | +Please do not request this endpoint more than every two seconds. | ||
989 | +</p> | ||
990 | +""" | ||
991 | + ) | ||
992 | + | ||
993 | + response = u"" | ||
994 | + | ||
995 | + geocoder = CachedGeocoder() | ||
996 | + for address in requested: | ||
997 | + location = geocoder.geocode(address) | ||
998 | + | ||
999 | + response += """ | ||
1000 | +<pre> | ||
1001 | +Requested: `%s' | ||
1002 | +Geocoded: `%s' | ||
1003 | +Longitude: `%f` | ||
1004 | +Latitude: `%f` | ||
1005 | +Altitude: `%f` (unreliable) | ||
1006 | +</pre> | ||
1007 | +""" % (address, location, location.longitude, location.latitude, location.altitude) | ||
1008 | + | ||
1009 | + return Response(response=response) | ||
1010 | + | ||
1011 | + | ||
951 | @main.route("/test") | 1012 | @main.route("/test") |
952 | # @basic_auth.required | 1013 | # @basic_auth.required |
953 | def dev_test(): | 1014 | def dev_test(): |
flaskr/forms.py
@@ -11,7 +11,6 @@ from wtforms import validators | @@ -11,7 +11,6 @@ from wtforms import validators | ||
11 | 11 | ||
12 | from .content import content_dict as content | 12 | from .content import content_dict as content |
13 | from .core import models | 13 | from .core import models |
14 | -from .extensions import captcha | ||
15 | from .models import User | 14 | from .models import User |
16 | 15 | ||
17 | form_content = content['estimate']['form'] | 16 | form_content = content['estimate']['form'] |
@@ -134,14 +133,14 @@ class EstimateForm(FlaskForm): | @@ -134,14 +133,14 @@ class EstimateForm(FlaskForm): | ||
134 | # ) | 133 | # ) |
135 | ], | 134 | ], |
136 | ) | 135 | ) |
137 | - captcha = StringField( | ||
138 | - label=form_content['captcha']['label'], | ||
139 | - description=form_content['captcha']['description'], | ||
140 | - validators=[ | ||
141 | - validators.InputRequired(), | ||
142 | - validators.Length(max=16), | ||
143 | - ], | ||
144 | - ) | 136 | + # captcha = StringField( |
137 | + # label=form_content['captcha']['label'], | ||
138 | + # description=form_content['captcha']['description'], | ||
139 | + # validators=[ | ||
140 | + # validators.InputRequired(), | ||
141 | + # validators.Length(max=16), | ||
142 | + # ], | ||
143 | + # ) | ||
145 | 144 | ||
146 | upload_set = ['csv', 'xls', 'xlsx'] | 145 | upload_set = ['csv', 'xls', 'xlsx'] |
147 | 146 | ||
@@ -169,12 +168,12 @@ class EstimateForm(FlaskForm): | @@ -169,12 +168,12 @@ class EstimateForm(FlaskForm): | ||
169 | if not check_validate: | 168 | if not check_validate: |
170 | return False | 169 | return False |
171 | 170 | ||
172 | - if self.captcha.data != IS_HUMAN: | ||
173 | - if not captcha.validate(): | ||
174 | - self.captcha.errors.append( | ||
175 | - "Captcha do not match. Try again." | ||
176 | - ) | ||
177 | - return False | 171 | + # if self.captcha.data != IS_HUMAN: |
172 | + # if not captcha.validate(): | ||
173 | + # self.captcha.errors.append( | ||
174 | + # "Captcha do not match. Try again." | ||
175 | + # ) | ||
176 | + # return False | ||
178 | 177 | ||
179 | # Origins must be set either by field or file | 178 | # Origins must be set either by field or file |
180 | if ( | 179 | if ( |
flaskr/geocoder.py
1 | -import geopy | ||
2 | import shelve | 1 | import shelve |
2 | +import ssl | ||
3 | import time | 3 | import time |
4 | 4 | ||
5 | -from core import get_path | 5 | +import certifi |
6 | +import geopy | ||
7 | +import geopy.geocoders | ||
8 | + | ||
9 | +from flaskr.core import get_path | ||
10 | + | ||
11 | +ssl_ctx = ssl.create_default_context(cafile=certifi.where()) | ||
12 | +geopy.geocoders.options.default_ssl_context = ssl_ctx | ||
6 | 13 | ||
7 | 14 | ||
8 | class CachedGeocoder: | 15 | class CachedGeocoder: |
9 | 16 | ||
10 | def __init__(self, source="Nominatim", geocache="geocache.db"): | 17 | def __init__(self, source="Nominatim", geocache="geocache.db"): |
11 | - self.geocoder = getattr(geopy.geocoders, source)() | 18 | + self.geocoder = getattr(geopy.geocoders, source)(scheme='https') |
12 | self.cache = shelve.open(get_path(geocache), writeback=True) | 19 | self.cache = shelve.open(get_path(geocache), writeback=True) |
13 | # self.timestamp = time.time() + 1.5 | 20 | # self.timestamp = time.time() + 1.5 |
14 | 21 | ||
@@ -25,5 +32,8 @@ class CachedGeocoder: | @@ -25,5 +32,8 @@ class CachedGeocoder: | ||
25 | ) | 32 | ) |
26 | return self.cache[address] | 33 | return self.cache[address] |
27 | 34 | ||
35 | + def __del__(self): | ||
36 | + self.close() | ||
37 | + | ||
28 | def close(self): | 38 | def close(self): |
29 | self.cache.close() | 39 | self.cache.close() |
flaskr/models.py
@@ -8,7 +8,7 @@ from flask_sqlalchemy import SQLAlchemy | @@ -8,7 +8,7 @@ from flask_sqlalchemy import SQLAlchemy | ||
8 | from werkzeug.security import generate_password_hash, check_password_hash | 8 | from werkzeug.security import generate_password_hash, check_password_hash |
9 | from yaml import safe_load as yaml_load | 9 | from yaml import safe_load as yaml_load |
10 | 10 | ||
11 | -from content import get_path, base_url | 11 | +from flaskr.content import get_path, base_url |
12 | from flaskr.core import generate_unique_id, models | 12 | from flaskr.core import generate_unique_id, models |
13 | 13 | ||
14 | # These are not the emission "models" in the scientific meaning of the word. | 14 | # These are not the emission "models" in the scientific meaning of the word. |
flaskr/static/img/recap_scaling_laws.jpg
flaskr/templates/base.html
@@ -70,6 +70,43 @@ | @@ -70,6 +70,43 @@ | ||
70 | {% endif %} | 70 | {% endif %} |
71 | {% endwith %} | 71 | {% endwith %} |
72 | 72 | ||
73 | + <style> | ||
74 | + .nojs { | ||
75 | + opacity: 0; | ||
76 | + animation: 16.18s fadeIn; | ||
77 | + animation-delay: 1.618s; | ||
78 | + animation-fill-mode: forwards; | ||
79 | + | ||
80 | + visibility: hidden; | ||
81 | + pointer-events: none; | ||
82 | + } | ||
83 | + | ||
84 | + @keyframes fadeIn { | ||
85 | + 0% { | ||
86 | + opacity: 0; | ||
87 | + } | ||
88 | + 25% { | ||
89 | + visibility: visible; | ||
90 | + opacity: 1; | ||
91 | + } | ||
92 | + 93% { | ||
93 | + visibility: visible; | ||
94 | + opacity: 1; | ||
95 | + } | ||
96 | + 99% { | ||
97 | + visibility: visible; | ||
98 | + opacity: 0; | ||
99 | + } | ||
100 | + 100% { | ||
101 | + visibility: hidden; | ||
102 | + } | ||
103 | + } | ||
104 | + </style> | ||
105 | + <div class="alert alert-danger nojs"> | ||
106 | + Javascript appears disabled, and we totally get that. | ||
107 | + You'll need javascript to render the plots, though. | ||
108 | + </div> | ||
109 | + | ||
73 | {% block body %} | 110 | {% block body %} |
74 | {% endblock %} | 111 | {% endblock %} |
75 | </div> | 112 | </div> |
flaskr/templates/estimation-request.html
@@ -176,30 +176,30 @@ | @@ -176,30 +176,30 @@ | ||
176 | <small class="form-text text-muted">{{ content.estimate.help.run_name | safe }}</small> | 176 | <small class="form-text text-muted">{{ content.estimate.help.run_name | safe }}</small> |
177 | </div> | 177 | </div> |
178 | 178 | ||
179 | - <div class="form-group"> | ||
180 | - <dt> | ||
181 | - {{ form.captcha.label }} | ||
182 | - <span class="required-asterisk" title="This field is required.">*</span> | ||
183 | - </dt> | ||
184 | - <dd> | ||
185 | - {{ captcha() }} | ||
186 | - <br><br> | ||
187 | - | ||
188 | - {{ form.captcha( | ||
189 | - title=form.captcha.description, | ||
190 | - class_="form-control", | ||
191 | - placeholder="Please write the numbers you see in the image above." | ||
192 | - ) | safe }} | ||
193 | - | ||
194 | - {% if form.captcha.errors -%} | ||
195 | - <ul class="errors text-danger has-error"> | ||
196 | - {% for error in form.captcha.errors %} | ||
197 | - <li>{{ error }}</li> | ||
198 | - {% endfor %} | ||
199 | - </ul> | ||
200 | - {%- endif %} | ||
201 | - </dd> | ||
202 | - </div> | 179 | +{# <div class="form-group">#} |
180 | +{# <dt>#} | ||
181 | +{# {{ form.captcha.label }}#} | ||
182 | +{# <span class="required-asterisk" title="This field is required.">*</span>#} | ||
183 | +{# </dt>#} | ||
184 | +{# <dd>#} | ||
185 | +{# {{ captcha() }}#} | ||
186 | +{# <br><br>#} | ||
187 | +{##} | ||
188 | +{# {{ form.captcha(#} | ||
189 | +{# title=form.captcha.description,#} | ||
190 | +{# class_="form-control",#} | ||
191 | +{# placeholder="Please write the numbers you see in the image above."#} | ||
192 | +{# ) | safe }}#} | ||
193 | +{##} | ||
194 | +{# {% if form.captcha.errors -%}#} | ||
195 | +{# <ul class="errors text-danger has-error">#} | ||
196 | +{# {% for error in form.captcha.errors %}#} | ||
197 | +{# <li>{{ error }}</li>#} | ||
198 | +{# {% endfor %}#} | ||
199 | +{# </ul>#} | ||
200 | +{# {%- endif %}#} | ||
201 | +{# </dd>#} | ||
202 | +{# </div>#} | ||
203 | 203 | ||
204 | <button type="submit" class="btn btn-primary"> | 204 | <button type="submit" class="btn btn-primary"> |
205 | Submit a Request | 205 | Submit a Request |
flaskr/templates/estimation.html
@@ -122,7 +122,7 @@ | @@ -122,7 +122,7 @@ | ||
122 | {% endif %} | 122 | {% endif %} |
123 | {#############################################################################} | 123 | {#############################################################################} |
124 | 124 | ||
125 | -{# SORTED EMISSIONS INEQUALITY ##########################################} | 125 | +{# SORTED EMISSIONS INEQUALITY ###############################################} |
126 | {# That plot makes no sense with our many to many data. #} | 126 | {# That plot makes no sense with our many to many data. #} |
127 | {% if not estimation.is_many_to_many() %} | 127 | {% if not estimation.is_many_to_many() %} |
128 | <div class="row"> | 128 | <div class="row"> |
@@ -378,13 +378,12 @@ jQuery(document).ready(function($){ | @@ -378,13 +378,12 @@ jQuery(document).ready(function($){ | ||
378 | console.info("[Footprint Lollipop] Starting…"); | 378 | console.info("[Footprint Lollipop] Starting…"); |
379 | var vizid = "#cities_footprints_d3viz_lollipop"; | 379 | var vizid = "#cities_footprints_d3viz_lollipop"; |
380 | var csvUrl = "/estimation/{{ estimation.public_id }}.csv"; | 380 | var csvUrl = "/estimation/{{ estimation.public_id }}.csv"; |
381 | - var y_key = 'city'; | 381 | + var y_key = 'address'; |
382 | var x_key = 'co2_kg'; | 382 | var x_key = 'co2_kg'; |
383 | 383 | ||
384 | - var margin = {top: 40, right: 40, bottom: 150, left: 180}, | ||
385 | - height = Math.max(300, 100 + 16*plots_config['cities_count']) - margin.top - margin.bottom; | ||
386 | - var width = Math.max(800, $(vizid).parent().width()); | ||
387 | - width = width - margin.left - margin.right; | 384 | + var margin = {top: 40, right: 40, bottom: 150, left: 180}; |
385 | + var height = Math.max(300, 100 + 16*plots_config['cities_count']) - margin.top - margin.bottom; | ||
386 | + var width = Math.max(800, $(vizid).parent().width()) - margin.left - margin.right; | ||
388 | 387 | ||
389 | var svg_tag = d3.select(vizid) | 388 | var svg_tag = d3.select(vizid) |
390 | .append("svg") | 389 | .append("svg") |
@@ -404,6 +403,20 @@ jQuery(document).ready(function($){ | @@ -404,6 +403,20 @@ jQuery(document).ready(function($){ | ||
404 | 403 | ||
405 | d3.csv(csvUrl).then(function (data) { | 404 | d3.csv(csvUrl).then(function (data) { |
406 | console.info("[Footprint Lollipop] Generating…"); | 405 | console.info("[Footprint Lollipop] Generating…"); |
406 | + | ||
407 | + // Resize left margin from locations' character length | ||
408 | + var max_character_length = 0; | ||
409 | + data.forEach(function(datum){ | ||
410 | + max_character_length = Math.max(max_character_length, datum[y_key].length); | ||
411 | + }); | ||
412 | + margin.left = Math.min(Math.round(0.618 * width), 42 + Math.round(5.13*max_character_length)); | ||
413 | + width = Math.max(800, $(vizid).parent().width()) - margin.left - margin.right; | ||
414 | + svg_tag | ||
415 | + .attr("width", width + margin.left + margin.right) | ||
416 | + .attr("height", height + margin.top + margin.bottom); | ||
417 | + svg.attr("transform", | ||
418 | + "translate(" + margin.left + "," + margin.top + ")"); | ||
419 | + | ||
407 | // Extrema | 420 | // Extrema |
408 | var data_x_max = d3.max(data, function (d) { | 421 | var data_x_max = d3.max(data, function (d) { |
409 | return parseFloat(d[x_key]); | 422 | return parseFloat(d[x_key]); |
requirements.txt
@@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
2 | Flask==1.1.1 | 2 | Flask==1.1.1 |
3 | 3 | ||
4 | # Flask Extensions | 4 | # Flask Extensions |
5 | -Flask-Admin==1.5.4 | 5 | +Flask-Admin==1.5.7 |
6 | Flask-Assets==0.12 | 6 | Flask-Assets==0.12 |
7 | Flask-BasicAuth==0.2.0 | 7 | Flask-BasicAuth==0.2.0 |
8 | Flask-Caching==1.7.2 | 8 | Flask-Caching==1.7.2 |
@@ -10,7 +10,7 @@ Flask-DebugToolbar==0.10.0 | @@ -10,7 +10,7 @@ Flask-DebugToolbar==0.10.0 | ||
10 | Flask-Login==0.4.0 | 10 | Flask-Login==0.4.0 |
11 | Flask-Mail==0.9.1 | 11 | Flask-Mail==0.9.1 |
12 | Flask-Script==2.0.5 | 12 | Flask-Script==2.0.5 |
13 | -Flask-SQLAlchemy==2.1 | 13 | +Flask-SQLAlchemy==2.5.1 |
14 | Flask-WTF==0.14 | 14 | Flask-WTF==0.14 |
15 | Flask-Sessionstore==0.4.5 | 15 | Flask-Sessionstore==0.4.5 |
16 | Flask-Session-Captcha==1.2.0 | 16 | Flask-Session-Captcha==1.2.0 |
@@ -21,9 +21,10 @@ cssmin==0.2.0 | @@ -21,9 +21,10 @@ cssmin==0.2.0 | ||
21 | jsmin==2.2.1 | 21 | jsmin==2.2.1 |
22 | pyyaml==5.1.2 | 22 | pyyaml==5.1.2 |
23 | Markdown==3.1.1 | 23 | Markdown==3.1.1 |
24 | -numpy==1.16.5 | 24 | +numpy==1.18.5 |
25 | enum34==1.1.6 | 25 | enum34==1.1.6 |
26 | -geopy==1.20.0 | 26 | +geopy==1.23.0 |
27 | +certifi==2019.11.28 | ||
27 | python-dotenv==0.10.3 | 28 | python-dotenv==0.10.3 |
28 | 29 | ||
29 | # Force stable werkzeug | 30 | # Force stable werkzeug |
@@ -33,7 +34,7 @@ Werkzeug==0.16.1 | @@ -33,7 +34,7 @@ Werkzeug==0.16.1 | ||
33 | 34 | ||
34 | # Spreadsheet reading | 35 | # Spreadsheet reading |
35 | # Note that 0.24 is the most recent version still supporting python 2.7 | 36 | # Note that 0.24 is the most recent version still supporting python 2.7 |
36 | -pandas==0.24.2 | 37 | +pandas==0.25.3 |
37 | # Excel support for wisdom-impaired Microsoft users | 38 | # Excel support for wisdom-impaired Microsoft users |
38 | xlrd==1.2.0 | 39 | xlrd==1.2.0 |
39 | # ODS reading support is only available natively in pandas 0.25 | 40 | # ODS reading support is only available natively in pandas 0.25 |