Compare View

switch
from
...
to
 
Commits (25)
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"
@@ -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
1 -0.10.0  
2 \ No newline at end of file 1 \ No newline at end of file
  2 +1.3.6
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&amp;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

255 KB | W: | H:

255 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
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