Compare View

switch
from
...
to
 
Commits (25)
Dockerfile
1   -#FROM python:2.7
2   -FROM tiangolo/uwsgi-nginx-flask:python2.7
  1 +FROM tiangolo/uwsgi-nginx-flask:python3.8
3 2 ADD . /app
4 3 WORKDIR /app
  4 +RUN apt-get update && apt-get install -y cython3
5 5 RUN pip install -r requirements.txt
6 6 ENV FLASK_APP "flaskr"
7 7 ENV FLASK_ENV "production"
... ...
README.md
... ... @@ -2,7 +2,7 @@
2 2 # Travel Carbon Footprint Calculator
3 3  
4 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 8 ## Overview
... ... @@ -16,20 +16,25 @@
16 16  
17 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 23 ### Create a virtual environment
22 24  
23 25 You don't _have to_. But it's useful for development.
24 26  
25   - virtualenv venv
  27 + virtualenv .venv
26 28  
27 29 Then, source it to enable it.
28 30  
29   - source venv/bin/activate
  31 + source .venv/bin/activate
30 32  
31 33 ### Install the python dependencies
32 34  
  35 +You will need Cython.
  36 +
  37 + sudo apt install cython
33 38 pip install -r requirements.txt
34 39  
35 40 ### Create an empty database
... ... @@ -44,18 +49,16 @@ Then, source it to enable it.
44 49 ### Configure permissions
45 50  
46 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 56 flask assets build
52 57  
53 58  
54   -
55   -
56 59 ## Development
57 60  
58   - source venv/bin/activate
  61 + source .venv/bin/activate
59 62 source .env.flaskrun
60 63 flask run
61 64  
... ...
VERSION
1   -0.10.0
2 1 \ No newline at end of file
  2 +1.3.6
... ...
docker-compose.yml
... ... @@ -4,8 +4,8 @@
4 4  
5 5 version: '2.0'
6 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 9 restart: always
10 10 build: .
11 11 volumes:
... ... @@ -22,13 +22,12 @@ services:
22 22 LETSENCRYPT_HOST: travel-footprint-calculator.apps.goutenoir.com
23 23 LETSENCRYPT_EMAIL: antoine@goutenoir.com
24 24 VIRTUAL_HOST: travel-footprint-calculator.apps.goutenoir.com
25   - # 80 is the default port
26 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 31 image: jsonfry/curl-cron
33 32 restart: always
34 33 environment:
... ...
flaskr/__init__.py
1 1 #! ../venv/bin/python
2 2 import os
3   -from markdown import markdown
4 3  
5 4 from dotenv import load_dotenv, find_dotenv, dotenv_values
  5 +from markdown import markdown
  6 +
6 7 # Load config from .env ; do this before importing local libs (and flask)
7 8 # 1. Write into OS environment -- if this fails you forgot to create .env file
8 9 load_dotenv(find_dotenv(raise_error_if_not_found=True), override=True)
... ... @@ -63,7 +64,7 @@ def create_app(object_name):
63 64 app.config['BASIC_AUTH_USERNAME'] = os.getenv('ADMIN_USERNAME')
64 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 68 app.config['CAPTCHA_LENGTH'] = 5
68 69 app.config['CAPTCHA_WIDTH'] = 256
69 70 app.config['CAPTCHA_HEIGHT'] = 158
... ...
flaskr/content.py
1 1 from collections import namedtuple
2   -from yaml import safe_load as yaml_safe_load
3 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 9 PROJECT_DIRECTORY = abspath(dirname(dirname(__file__)))
7 10  
... ... @@ -21,7 +24,7 @@ class Struct(object):
21 24 def __new__(cls, data):
22 25 if isinstance(data, dict):
23 26 return namedtuple(
24   - 'Struct', data.iterkeys()
  27 + 'Struct', data.keys()
25 28 )(
26 29 *(Struct(val) for val in data.values())
27 30 )
... ... @@ -33,9 +36,6 @@ class Struct(object):
33 36  
34 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 40 # For Python3?
41 41 # def dict2obj(d):
... ...
flaskr/controllers/main_controller.py
1 1 import csv
2 2 import re
3   -# from io import StringIO
4   -from cStringIO import StringIO
  3 +# from cStringIO import StringIO
5 4 from copy import deepcopy
6   -from os import unlink
  5 +from io import StringIO
  6 +from os import unlink, getenv
7 7 from os.path import join
8 8  
9 9 import geopy
... ... @@ -13,13 +13,14 @@ from flask import (
13 13 Blueprint,
14 14 Response,
15 15 render_template,
  16 + request,
16 17 flash,
17 18 redirect,
18 19 url_for,
19 20 abort,
20 21 send_from_directory,
21 22 )
22   -from pandas.compat import StringIO as PandasStringIO
  23 +# from pandas.compat import StringIO as PandasStringIO
23 24 from wtforms import validators
24 25 from yaml import safe_dump as yaml_dump
25 26  
... ... @@ -28,7 +29,7 @@ from flaskr.core import (
28 29 get_emission_models,
29 30 increment_hit_counter,
30 31 )
31   -from flaskr.extensions import cache, send_email
  32 +from flaskr.extensions import send_email
32 33 from flaskr.forms import EstimateForm
33 34 from flaskr.geocoder import CachedGeocoder
34 35 from flaskr.models import db, Estimation, StatusEnum, ScenarioEnum
... ... @@ -36,7 +37,7 @@ from flaskr.models import db, Estimation, StatusEnum, ScenarioEnum
36 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 49  
49 50  
50 51 @main.route('/favicon.ico')
51   -@cache.cached(timeout=10000)
  52 +# @cache.cached(timeout=10000)
52 53 def favicon(): # we want it served from the root, not from static/
53 54 return send_from_directory(
54 55 join(main.root_path, '..', 'static', 'img'),
... ... @@ -82,14 +83,16 @@ def gather_addresses(from_list, from_file):
82 83 addresses = []
83 84 if from_file:
84 85 file_mimetype = from_file.mimetype
85   - file_contents = from_file.read()
  86 + file_contents = from_file.read().decode()
86 87  
87 88 rows_dicts = None
88 89  
89 90 if 'text/csv' == file_mimetype:
90   -
  91 + delimiter = ','
  92 + if ';' in file_contents:
  93 + delimiter = ';'
91 94 rows_dicts = pandas \
92   - .read_csv(PandasStringIO(file_contents)) \
  95 + .read_csv(StringIO(file_contents), delimiter=delimiter) \
93 96 .rename(str.lower, axis='columns') \
94 97 .to_dict(orient="row")
95 98  
... ... @@ -110,7 +113,7 @@ def gather_addresses(from_list, from_file):
110 113 or from_file.filename.endswith('xlsx'):
111 114  
112 115 rows_dicts = pandas \
113   - .read_excel(PandasStringIO(file_contents)) \
  116 + .read_excel(StringIO(file_contents)) \
114 117 .rename(str.lower, axis='columns') \
115 118 .to_dict(orient="row")
116 119  
... ... @@ -153,8 +156,9 @@ def gather_addresses(from_list, from_file):
153 156 for address in addresses:
154 157 if not address:
155 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 162 if to_ignore.match(address) is not None:
159 163 continue
160 164 clean_addresses.append(address)
... ... @@ -192,7 +196,7 @@ def estimate(): # register new estimation request, more accurately
192 196 form.origin_addresses_file.data
193 197 )
194 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 200 return show_form()
197 201  
198 202 try:
... ... @@ -201,7 +205,7 @@ def estimate(): # register new estimation request, more accurately
201 205 form.destination_addresses_file.data
202 206 )
203 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 209 return show_form()
206 210  
207 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 376 continue
373 377  
374 378 try:
375   - origin = geocoder.geocode(origin_address.encode('utf-8'))
  379 + origin = geocoder.geocode(origin_address)
376 380 except geopy.exc.GeopyError as e:
377 381 warning = u"Ignoring origin `%s` " \
378 382 u"since we failed to geocode it.\n%s\n" % (
... ... @@ -428,9 +432,7 @@ def compute(): # process the queue of estimation requests
428 432 continue
429 433  
430 434 try:
431   - destination = geocoder.geocode(
432   - destination_address.encode('utf-8')
433   - )
  435 + destination = geocoder.geocode(destination_address)
434 436 except geopy.exc.GeopyError as e:
435 437 warning = u"Ignoring destination `%s` " \
436 438 u"since we failed to geocode it.\n%s\n" % (
... ... @@ -489,6 +491,13 @@ def compute(): # process the queue of estimation requests
489 491  
490 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 501 def _get_city_key(_location):
493 502 return _location.address.split(',')[0]
494 503  
... ... @@ -512,7 +521,7 @@ def compute(): # process the queue of estimation requests
512 521 _results = {}
513 522 footprints = {}
514 523  
515   - destinations_by_city_key = {}
  524 + destinations_by_key = {}
516 525  
517 526 cities_sum_foot = {}
518 527 cities_sum_dist = {}
... ... @@ -528,13 +537,13 @@ def compute(): # process the queue of estimation requests
528 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 543 if _key not in cities_dict:
536 544 cities_dict[_key] = {
537   - 'city': _key,
  545 + 'location': _get_location_key(_destination),
  546 + 'city': _get_city_key(_destination),
538 547 'country': _get_country_key(_destination),
539 548 'address': _destination.address,
540 549 'latitude': _destination.latitude,
... ... @@ -578,11 +587,12 @@ def compute(): # process the queue of estimation requests
578 587 city_train_trips = cities_dict_first_model[city]['train_trips']
579 588 city_plane_trips = cities_dict_first_model[city]['plane_trips']
580 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 596 'footprint': city_mean_foot,
587 597 'distance': city_mean_dist,
588 598 'train_trips': city_train_trips,
... ... @@ -638,19 +648,21 @@ def compute(): # process the queue of estimation requests
638 648 # SCENARIO C : At Least One Origin, At Least One Destination ##########
639 649 #
640 650 # Run Scenario A for each Destination, and expose optimum Destination.
  651 + # Skip destinations already visited. (collapse duplicate destinations)
641 652 #
642 653 else:
643 654 estimation.scenario = ScenarioEnum.many_to_many
644   - unique_city_keys = []
  655 + unique_location_keys = []
645 656 result_cities = []
646 657 for destination in destinations:
  658 + location_key = _get_location_key(destination)
647 659 city_key = _get_city_key(destination)
648 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 663 continue
652 664 else:
653   - unique_city_keys.append(city_key)
  665 + unique_location_keys.append(location_key)
654 666  
655 667 city_results = compute_one_to_many(
656 668 _origin=destination,
... ... @@ -659,6 +671,7 @@ def compute(): # process the queue of estimation requests
659 671 )
660 672 city_results['city'] = city_key
661 673 city_results['country'] = country_key
  674 + city_results['location'] = location_key
662 675 city_results['address'] = destination.address
663 676 city_results['latitude'] = destination.latitude
664 677 city_results['longitude'] = destination.longitude
... ... @@ -699,7 +712,9 @@ def compute(): # process the queue of estimation requests
699 712  
700 713 except Exception as e:
701 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 718 if estimation:
704 719 _handle_failure(estimation, errmsg)
705 720 return _respond(errmsg)
... ... @@ -717,7 +732,7 @@ def consult_estimation(public_id, extension):
717 732 except sqlalchemy.orm.exc.NoResultFound:
718 733 return abort(404)
719 734 except Exception as e:
720   - # TODO: log?
  735 + # log? (or not)
721 736 return abort(500)
722 737  
723 738 # allowed_formats = ['html']
... ... @@ -761,6 +776,7 @@ def consult_estimation(public_id, extension):
761 776 si = StringIO()
762 777 cw = csv.writer(si, quoting=csv.QUOTE_ALL)
763 778 cw.writerow([
  779 + u"location",
764 780 u"city", u"country", u"address",
765 781 u"latitude", u"longitude",
766 782 u"co2_kg",
... ... @@ -772,9 +788,11 @@ def consult_estimation(public_id, extension):
772 788 results = estimation.get_output_dict()
773 789 for city in results['cities']:
774 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 796 city.get('latitude', 0.0),
779 797 city.get('longitude', 0.0),
780 798 round(city['footprint'], 3),
... ... @@ -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 1012 @main.route("/test")
952 1013 # @basic_auth.required
953 1014 def dev_test():
... ...
flaskr/forms.py
... ... @@ -11,7 +11,6 @@ from wtforms import validators
11 11  
12 12 from .content import content_dict as content
13 13 from .core import models
14   -from .extensions import captcha
15 14 from .models import User
16 15  
17 16 form_content = content['estimate']['form']
... ... @@ -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 145 upload_set = ['csv', 'xls', 'xlsx']
147 146  
... ... @@ -169,12 +168,12 @@ class EstimateForm(FlaskForm):
169 168 if not check_validate:
170 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 178 # Origins must be set either by field or file
180 179 if (
... ...
flaskr/geocoder.py
1   -import geopy
2 1 import shelve
  2 +import ssl
3 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 15 class CachedGeocoder:
9 16  
10 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 19 self.cache = shelve.open(get_path(geocache), writeback=True)
13 20 # self.timestamp = time.time() + 1.5
14 21  
... ... @@ -25,5 +32,8 @@ class CachedGeocoder:
25 32 )
26 33 return self.cache[address]
27 34  
  35 + def __del__(self):
  36 + self.close()
  37 +
28 38 def close(self):
29 39 self.cache.close()
... ...
flaskr/models.py
... ... @@ -8,7 +8,7 @@ from flask_sqlalchemy import SQLAlchemy
8 8 from werkzeug.security import generate_password_hash, check_password_hash
9 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 12 from flaskr.core import generate_unique_id, models
13 13  
14 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 70 {% endif %}
71 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 110 {% block body %}
74 111 {% endblock %}
75 112 </div>
... ...
flaskr/templates/estimation-request.html
... ... @@ -176,30 +176,30 @@
176 176 <small class="form-text text-muted">{{ content.estimate.help.run_name | safe }}</small>
177 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 204 <button type="submit" class="btn btn-primary">
205 205 Submit a Request
... ...
flaskr/templates/estimation.html
... ... @@ -122,7 +122,7 @@
122 122 {% endif %}
123 123 {#############################################################################}
124 124  
125   -{# SORTED EMISSIONS INEQUALITY ##########################################}
  125 +{# SORTED EMISSIONS INEQUALITY ###############################################}
126 126 {# That plot makes no sense with our many to many data. #}
127 127 {% if not estimation.is_many_to_many() %}
128 128 <div class="row">
... ... @@ -378,13 +378,12 @@ jQuery(document).ready(function($){
378 378 console.info("[Footprint Lollipop] Starting…");
379 379 var vizid = "#cities_footprints_d3viz_lollipop";
380 380 var csvUrl = "/estimation/{{ estimation.public_id }}.csv";
381   - var y_key = 'city';
  381 + var y_key = 'address';
382 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 388 var svg_tag = d3.select(vizid)
390 389 .append("svg")
... ... @@ -404,6 +403,20 @@ jQuery(document).ready(function($){
404 403  
405 404 d3.csv(csvUrl).then(function (data) {
406 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 420 // Extrema
408 421 var data_x_max = d3.max(data, function (d) {
409 422 return parseFloat(d[x_key]);
... ...
requirements.txt
... ... @@ -2,7 +2,7 @@
2 2 Flask==1.1.1
3 3  
4 4 # Flask Extensions
5   -Flask-Admin==1.5.4
  5 +Flask-Admin==1.5.7
6 6 Flask-Assets==0.12
7 7 Flask-BasicAuth==0.2.0
8 8 Flask-Caching==1.7.2
... ... @@ -10,7 +10,7 @@ Flask-DebugToolbar==0.10.0
10 10 Flask-Login==0.4.0
11 11 Flask-Mail==0.9.1
12 12 Flask-Script==2.0.5
13   -Flask-SQLAlchemy==2.1
  13 +Flask-SQLAlchemy==2.5.1
14 14 Flask-WTF==0.14
15 15 Flask-Sessionstore==0.4.5
16 16 Flask-Session-Captcha==1.2.0
... ... @@ -21,9 +21,10 @@ cssmin==0.2.0
21 21 jsmin==2.2.1
22 22 pyyaml==5.1.2
23 23 Markdown==3.1.1
24   -numpy==1.16.5
  24 +numpy==1.18.5
25 25 enum34==1.1.6
26   -geopy==1.20.0
  26 +geopy==1.23.0
  27 +certifi==2019.11.28
27 28 python-dotenv==0.10.3
28 29  
29 30 # Force stable werkzeug
... ... @@ -33,7 +34,7 @@ Werkzeug==0.16.1
33 34  
34 35 # Spreadsheet reading
35 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 38 # Excel support for wisdom-impaired Microsoft users
38 39 xlrd==1.2.0
39 40 # ODS reading support is only available natively in pandas 0.25
... ...