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
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
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&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
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 | ... | ... |