Blame view

flaskr/controllers/main_controller.py 20.4 KB
b935618e   Antoine Goutenoir   Count the number ...
1
2
from copy import deepcopy

314c65e2   Antoine Goutenoir   Implement Scenari...
3
4
import geopy
import sqlalchemy
51f564d3   Antoine Goutenoir   Add a big chunk o...
5

a4c03d8e   Antoine Goutenoir   Add the controlle...
6
from flask import Blueprint, render_template, flash, request, redirect, \
67f85bce   Antoine Goutenoir   Generate a CSV fi...
7
    url_for, abort, send_from_directory, Response
461850db   Antoine Goutenoir   Yet another joyfu...
8
from os.path import join
7b4d2926   Goutte   Rework the contro...
9

b9fc86c3   Antoine Goutenoir   Secure the admin ...
10
from flaskr.extensions import cache, basic_auth
7b4d2926   Goutte   Rework the contro...
11
from flaskr.forms import LoginForm, EstimateForm
a1f12452   Antoine Goutenoir   Add the new conte...
12
from flaskr.models import db, User, Estimation, StatusEnum, ScenarioEnum
24f55cde   Antoine Goutenoir   Geocode destinati...
13
from flaskr.geocoder import CachedGeocoder
7b4d2926   Goutte   Rework the contro...
14

77e86148   Antoine Goutenoir   Fake support for ...
15
from flaskr.core import generate_unique_id, get_emission_models
51f564d3   Antoine Goutenoir   Add a big chunk o...
16
from flaskr.content import content
7b4d2926   Goutte   Rework the contro...
17

f2fbbb72   Antoine Goutenoir   Display a nice er...
18
19
from wtforms import validators

70aa301f   Antoine Goutenoir   Implement another...
20
21
from yaml import safe_dump as yaml_dump

67f85bce   Antoine Goutenoir   Generate a CSV fi...
22
23
24
import csv
# from io import StringIO
from cStringIO import StringIO
24f55cde   Antoine Goutenoir   Geocode destinati...
25

9e44bb98   Antoine Goutenoir   Support file uplo...
26
27
28
import pandas
from pandas.compat import StringIO as PandasStringIO

7b4d2926   Goutte   Rework the contro...
29
30
31
main = Blueprint('main', __name__)


9621b8a5   Antoine Goutenoir   Fix the CSV gener...
32
33
34
OUT_ENCODING = 'utf-8'


67f85bce   Antoine Goutenoir   Generate a CSV fi...
35
# -----------------------------------------------------------------------------
67f85bce   Antoine Goutenoir   Generate a CSV fi...
36
37


461850db   Antoine Goutenoir   Yet another joyfu...
38
@main.route('/favicon.ico')
7f7c6b10   Antoine Goutenoir   Disable RFI in th...
39
@cache.cached(timeout=10000)
461850db   Antoine Goutenoir   Yet another joyfu...
40
41
42
43
44
45
46
def favicon():  # we want it served from the root, not from static/
    return send_from_directory(
        join(main.root_path, '..', 'static', 'img'),
        'favicon.ico', mimetype='image/vnd.microsoft.icon'
    )


7b4d2926   Goutte   Rework the contro...
47
@main.route('/')
b0ffb1ba   Antoine Goutenoir   Review actively.
48
49
@main.route('/home')
@main.route('/home.html')
7b4d2926   Goutte   Rework the contro...
50
51
@cache.cached(timeout=1000)
def home():
4c862b54   Antoine Goutenoir   Add a grouped bar...
52
    models = get_emission_models()
a3e9d0fc   Antoine Goutenoir   Fix home plot leg...
53
54
55
    models_dict = {}
    for model in models:
        models_dict[model.slug] = model.__dict__
4c862b54   Antoine Goutenoir   Add a grouped bar...
56
57
    return render_template(
        'home.html',
a3e9d0fc   Antoine Goutenoir   Fix home plot leg...
58
        models=models_dict,
4c862b54   Antoine Goutenoir   Add a grouped bar...
59
        colors=[model.color for model in models],
a3e9d0fc   Antoine Goutenoir   Fix home plot leg...
60
        labels=[model.name for model in models],
4c862b54   Antoine Goutenoir   Add a grouped bar...
61
    )
7b4d2926   Goutte   Rework the contro...
62
63


9e44bb98   Antoine Goutenoir   Support file uplo...
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def gather_addresses(from_list, from_file):
    addresses = []
    if from_file:
        file_mimetype = from_file.mimetype
        file_contents = from_file.read()

        rows_dicts = None

        if 'text/csv' == file_mimetype:

            rows_dicts = pandas \
                .read_csv(PandasStringIO(file_contents)) \
                .rename(str.lower, axis='columns') \
                .to_dict(orient="row")

        # Here are just *some* of the mimetypes that Microsoft's
        # garbage spreadsheet files may have.
        # application/vnd.ms-excel (official)
        # application/msexcel
        # application/x-msexcel
        # application/x-ms-excel
        # application/x-excel
        # application/x-dos_ms_excel
        # application/xls
        # application/x-xls
        # application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
        # ... Let's check extension instead.

        elif from_file.filename.endswith('xls') \
                or from_file.filename.endswith('xlsx'):

            rows_dicts = pandas \
                .read_excel(PandasStringIO(file_contents)) \
                .rename(str.lower, axis='columns') \
                .to_dict(orient="row")

        # Python 3.7 only
        # elif from_file.filename.endswith('ods'):
        #
        #     rows_dicts = read_ods(PandasStringIO(file_contents), 1) \
        #         .rename(str.lower, axis='columns') \
        #         .to_dict(orient="row")

        if rows_dicts is not None:
            for row_dict in rows_dicts:
                if 'address' in row_dict:
                    addresses.append(row_dict['address'])
                    continue
                address = None
                if 'city' in row_dict:
                    address = row_dict['city']
                if 'country' in row_dict:
                    if address is None:
                        address = row_dict['country']
                    else:
                        address += "," + row_dict['country']
                if address is not None:
                    addresses.append(address)
                else:
f2fbbb72   Antoine Goutenoir   Display a nice er...
123
124
125
                    raise validators.ValidationError(
                        "We could not find Address data in the spreadsheet."
                    )
9e44bb98   Antoine Goutenoir   Support file uplo...
126
        else:
f2fbbb72   Antoine Goutenoir   Display a nice er...
127
128
129
            raise validators.ValidationError(
                "We could not find any data in the spreadsheet."
            )
9e44bb98   Antoine Goutenoir   Support file uplo...
130
131
132
133

    else:
        addresses = from_list.replace("\r", '').split("\n")

68da7cd4   Antoine Goutenoir   Improve robustnes...
134
135
136
137
138
139
140
141
142
143
144
145
    clean_addresses = []
    for address in addresses:
        if not address:
            continue
        elif type(address).__name__ == 'str':
            clean_addresses.append(unicode(address, 'utf-8'))
        else:
            clean_addresses.append(address)
    addresses = clean_addresses

    # Remove empty lines (if any) and white characters
    addresses = [a.strip() for a in addresses if a]
8ec0ce68   Antoine Goutenoir   Improve robustnes...
146

9e44bb98   Antoine Goutenoir   Support file uplo...
147
148
149
    return "\n".join(addresses)


7b4d2926   Goutte   Rework the contro...
150
@main.route("/estimate", methods=["GET", "POST"])
b0ffb1ba   Antoine Goutenoir   Review actively.
151
@main.route("/estimate.html", methods=["GET", "POST"])
7b4d2926   Goutte   Rework the contro...
152
def estimate():
77e86148   Antoine Goutenoir   Fake support for ...
153
    models = get_emission_models()
7b4d2926   Goutte   Rework the contro...
154
155
    form = EstimateForm()

f2fbbb72   Antoine Goutenoir   Display a nice er...
156
157
158
    def show_form():
        return render_template("estimate.html", form=form, models=models)

7b4d2926   Goutte   Rework the contro...
159
160
    if form.validate_on_submit():

7b4d2926   Goutte   Rework the contro...
161
162
        id = generate_unique_id()

7b4d2926   Goutte   Rework the contro...
163
        estimation = Estimation()
a4c03d8e   Antoine Goutenoir   Add the controlle...
164
        # estimation.email = form.email.data
7b4d2926   Goutte   Rework the contro...
165
166
        estimation.first_name = form.first_name.data
        estimation.last_name = form.last_name.data
16f69d07   Antoine Goutenoir   Add an (unsecured...
167
        estimation.institution = form.institution.data
24f55cde   Antoine Goutenoir   Geocode destinati...
168
        estimation.status = StatusEnum.pending
f2fbbb72   Antoine Goutenoir   Display a nice er...
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187

        try:
            estimation.origin_addresses = gather_addresses(
                form.origin_addresses.data,
                form.origin_addresses_file.data
            )
        except validators.ValidationError as e:
            form.origin_addresses_file.errors.append(e.message)
            return show_form()

        try:
            estimation.destination_addresses = gather_addresses(
                form.destination_addresses.data,
                form.destination_addresses_file.data
            )
        except validators.ValidationError as e:
            form.destination_addresses_file.errors.append(e.message)
            return show_form()

4276f1aa   Antoine Goutenoir   Support train emi...
188
        estimation.use_train_below_km = form.use_train_below_km.data
f2fbbb72   Antoine Goutenoir   Display a nice er...
189

77e86148   Antoine Goutenoir   Fake support for ...
190
191
        models_slugs = []
        for model in models:
35fbac1f   Antoine Goutenoir   Fix a blooper.
192
            if getattr(form, 'use_model_%s' % model.slug).data:
77e86148   Antoine Goutenoir   Fake support for ...
193
194
                models_slugs.append(model.slug)
        estimation.models_slugs = "\n".join(models_slugs)
7b4d2926   Goutte   Rework the contro...
195
196
197
198
199

        db.session.add(estimation)
        db.session.commit()

        flash("Estimation request submitted successfully.", "success")
a4c03d8e   Antoine Goutenoir   Add the controlle...
200
201
202
        return redirect(url_for(
            endpoint=".consult_estimation",
            public_id=estimation.public_id,
91751451   Antoine Goutenoir   Shift the API to ...
203
            extension='html'
a4c03d8e   Antoine Goutenoir   Add the controlle...
204
        ))
7b4d2926   Goutte   Rework the contro...
205
206
        # return render_template("estimate-debrief.html", form=form)

f2fbbb72   Antoine Goutenoir   Display a nice er...
207
    return show_form()
7b4d2926   Goutte   Rework the contro...
208

4392f295   Goutte   Update the Estima...
209

15e57dca   Antoine Goutenoir   Naming things and...
210
@main.route("/invalidate")
4276f1aa   Antoine Goutenoir   Support train emi...
211
@main.route("/invalidate.html")
15e57dca   Antoine Goutenoir   Naming things and...
212
213
214
215
216
217
218
219
220
221
222
223
224
def invalidate():
    stuck_estimations = Estimation.query \
        .filter_by(status=StatusEnum.working) \
        .all()

    for estimation in stuck_estimations:
        estimation.status = StatusEnum.failure
        estimation.errors = "Invalidated. Try again."
        db.session.commit()

    return ""


4392f295   Goutte   Update the Estima...
225
@main.route("/compute")
51f564d3   Antoine Goutenoir   Add a big chunk o...
226
def compute():  # process the queue of estimation requests
24f55cde   Antoine Goutenoir   Geocode destinati...
227
228
229
230
231

    def _respond(_msg):
        return "<pre>%s</pre>" % _msg

    def _handle_failure(_estimation, _failure_message):
a4c03d8e   Antoine Goutenoir   Add the controlle...
232
        _estimation.status = StatusEnum.failure
70aa301f   Antoine Goutenoir   Implement another...
233
        _estimation.errors = _failure_message
24f55cde   Antoine Goutenoir   Geocode destinati...
234
235
        db.session.commit()

72460978   Antoine Goutenoir   Use warnings inst...
236
237
238
239
    def _handle_warning(_estimation, _warning_message):
        _estimation.warnings = _warning_message
        db.session.commit()

4392f295   Goutte   Update the Estima...
240
241
    response = ""

03c194bf   Antoine Goutenoir   Actually implemen...
242
243
244
245
246
247
248
    count_working = Estimation.query \
        .filter_by(status=StatusEnum.working) \
        .count()

    if 0 < count_working:
        return _respond("Already working on estimation.")

24f55cde   Antoine Goutenoir   Geocode destinati...
249
250
251
    try:
        estimation = Estimation.query \
            .filter_by(status=StatusEnum.pending) \
1d48272e   Antoine Goutenoir   Compute the mean ...
252
            .order_by(Estimation.id.asc()) \
51f564d3   Antoine Goutenoir   Add a big chunk o...
253
            .first()
24f55cde   Antoine Goutenoir   Geocode destinati...
254
255
    except sqlalchemy.orm.exc.NoResultFound:
        return _respond("No estimation in the queue.")
51f564d3   Antoine Goutenoir   Add a big chunk o...
256
257
258
    except Exception as e:
        return _respond("Database error: %s" % (e,))

a6f63a6d   Antoine Goutenoir   Zzz…
259
260
    if not estimation:
        return _respond("No estimation in the queue.")
24f55cde   Antoine Goutenoir   Geocode destinati...
261

03c194bf   Antoine Goutenoir   Actually implemen...
262
263
264
265
266
267
    estimation.status = StatusEnum.working
    db.session.commit()

    response += u"Processing estimation `%s`...\n" % (
        estimation.public_id
    )
24f55cde   Antoine Goutenoir   Geocode destinati...
268
269
270

    geocoder = CachedGeocoder()

51f564d3   Antoine Goutenoir   Add a big chunk o...
271
272
    # GEOCODE ORIGINS #########################################################

461850db   Antoine Goutenoir   Yet another joyfu...
273
    origins_addresses = estimation.origin_addresses.strip().split("\n")
51f564d3   Antoine Goutenoir   Add a big chunk o...
274
275
276
277
    origins = []

    for i in range(len(origins_addresses)):

118da78e   Antoine Goutenoir   Wrapping up some ...
278
        origin_address = origins_addresses[i].strip()
51f564d3   Antoine Goutenoir   Add a big chunk o...
279
280

        try:
118da78e   Antoine Goutenoir   Wrapping up some ...
281
            origin = geocoder.geocode(origin_address.encode('utf-8'))
51f564d3   Antoine Goutenoir   Add a big chunk o...
282
        except geopy.exc.GeopyError as e:
42abd933   Antoine Goutenoir   Improve legibilit...
283
            response += u"Failed to geocode origin `%s`.\n%s\n" % (
51f564d3   Antoine Goutenoir   Add a big chunk o...
284
285
                origin_address, e,
            )
72460978   Antoine Goutenoir   Use warnings inst...
286
287
            _handle_warning(estimation, response)
            continue
51f564d3   Antoine Goutenoir   Add a big chunk o...
288
289

        if origin is None:
42abd933   Antoine Goutenoir   Improve legibilit...
290
            response += u"Failed to geocode origin `%s`.\n" % (
51f564d3   Antoine Goutenoir   Add a big chunk o...
291
292
                origin_address,
            )
72460978   Antoine Goutenoir   Use warnings inst...
293
294
            _handle_warning(estimation, response)
            continue
51f564d3   Antoine Goutenoir   Add a big chunk o...
295
296
297
298
299
300
301
302
303
304

        origins.append(origin)

        response += u"Origin: %s == %s (%f, %f)\n" % (
            origin_address, origin.address,
            origin.latitude, origin.longitude,
        )

    # GEOCODE DESTINATIONS ####################################################

461850db   Antoine Goutenoir   Yet another joyfu...
305
    destinations_addresses = estimation.destination_addresses.strip().split("\n")
24f55cde   Antoine Goutenoir   Geocode destinati...
306
307
308
309
    destinations = []

    for i in range(len(destinations_addresses)):

118da78e   Antoine Goutenoir   Wrapping up some ...
310
        destination_address = destinations_addresses[i].strip()
24f55cde   Antoine Goutenoir   Geocode destinati...
311
312

        try:
118da78e   Antoine Goutenoir   Wrapping up some ...
313
            destination = geocoder.geocode(destination_address.encode('utf-8'))
24f55cde   Antoine Goutenoir   Geocode destinati...
314
        except geopy.exc.GeopyError as e:
42abd933   Antoine Goutenoir   Improve legibilit...
315
            response += u"Failed to geocode destination `%s`.\n%s\n" % (
24f55cde   Antoine Goutenoir   Geocode destinati...
316
317
                destination_address, e,
            )
72460978   Antoine Goutenoir   Use warnings inst...
318
319
            _handle_warning(estimation, response)
            continue
24f55cde   Antoine Goutenoir   Geocode destinati...
320
321

        if destination is None:
42abd933   Antoine Goutenoir   Improve legibilit...
322
            response += u"Failed to geocode destination `%s`.\n" % (
24f55cde   Antoine Goutenoir   Geocode destinati...
323
324
                destination_address,
            )
72460978   Antoine Goutenoir   Use warnings inst...
325
326
            _handle_warning(estimation, response)
            continue
24f55cde   Antoine Goutenoir   Geocode destinati...
327

118da78e   Antoine Goutenoir   Wrapping up some ...
328
        # print(repr(destination.raw))
314c65e2   Antoine Goutenoir   Implement Scenari...
329

24f55cde   Antoine Goutenoir   Geocode destinati...
330
331
332
333
334
335
336
        destinations.append(destination)

        response += u"Destination: %s == %s (%f, %f)\n" % (
            destination_address, destination.address,
            destination.latitude, destination.longitude,
        )

70aa301f   Antoine Goutenoir   Implement another...
337
    # GTFO IF NO ORIGINS OR NO DESTINATIONS ###################################
51f564d3   Antoine Goutenoir   Add a big chunk o...
338
339

    if 0 == len(origins):
42abd933   Antoine Goutenoir   Improve legibilit...
340
        response += u"Failed to geocode all the origin(s).\n"
51f564d3   Antoine Goutenoir   Add a big chunk o...
341
342
343
        _handle_failure(estimation, response)
        return _respond(response)
    if 0 == len(destinations):
42abd933   Antoine Goutenoir   Improve legibilit...
344
        response += u"Failed to geocode all the destination(s).\n"
51f564d3   Antoine Goutenoir   Add a big chunk o...
345
346
347
        _handle_failure(estimation, response)
        return _respond(response)

70aa301f   Antoine Goutenoir   Implement another...
348
    # GRAB AND CONFIGURE THE EMISSION MODELS ##################################
51f564d3   Antoine Goutenoir   Add a big chunk o...
349

a3e9d0fc   Antoine Goutenoir   Fix home plot leg...
350
    emission_models = estimation.get_models()
51f564d3   Antoine Goutenoir   Add a big chunk o...
351
352
    # print(emission_models)

b935618e   Antoine Goutenoir   Count the number ...
353
    extra_config = {
4276f1aa   Antoine Goutenoir   Support train emi...
354
355
        'use_train_below_distance': estimation.use_train_below_km,
        # 'use_train_below_distance': 300,
b935618e   Antoine Goutenoir   Count the number ...
356
    }
8a693e06   Antoine Goutenoir   Glue the extra co...
357

51f564d3   Antoine Goutenoir   Add a big chunk o...
358
359
360
361
    # PREPARE RESULT DICTIONARY THAT WILL BE STORED ###########################

    results = {}

70aa301f   Antoine Goutenoir   Implement another...
362
    # UTILITY PRIVATE FUNCTION(S) #############################################
51f564d3   Antoine Goutenoir   Add a big chunk o...
363

314c65e2   Antoine Goutenoir   Implement Scenari...
364
    def get_city_key(_location):
18ba3d7a   Antoine Goutenoir   Boost the models'...
365
        # Will this hack hold?  Suspense...
461850db   Antoine Goutenoir   Yet another joyfu...
366
367
368
369
370
371
372
373
374
375
        return _location.address.split(',')[0]

        # _city_key = _location.address
        # # if 'address100' in _location.raw['address']:
        # #     _city_key = _location.raw['address']['address100']
        # if 'city' in _location.raw['address']:
        #     _city_key = _location.raw['address']['city']
        # elif 'state' in _location.raw['address']:
        #     _city_key = _location.raw['address']['state']
        # return _city_key
314c65e2   Antoine Goutenoir   Implement Scenari...
376

70aa301f   Antoine Goutenoir   Implement another...
377
378
379
    def compute_one_to_many(
            _origin,
            _destinations,
18ba3d7a   Antoine Goutenoir   Boost the models'...
380
            _extra_config=None
70aa301f   Antoine Goutenoir   Implement another...
381
382
    ):
        _results = {}
51f564d3   Antoine Goutenoir   Add a big chunk o...
383
384
        footprints = {}

584b13cc   Antoine Goutenoir   Order the results...
385
386
        destinations_by_city_key = {}

5634c975   Antoine Goutenoir   Expose travel dis...
387
388
        cities_sum_foot = {}
        cities_sum_dist = {}
b935618e   Antoine Goutenoir   Count the number ...
389
        cities_dict_first_model = None
51f564d3   Antoine Goutenoir   Add a big chunk o...
390
        for model in emission_models:
584b13cc   Antoine Goutenoir   Order the results...
391
            cities_dict = {}
70aa301f   Antoine Goutenoir   Implement another...
392
            for _destination in _destinations:
51f564d3   Antoine Goutenoir   Add a big chunk o...
393
                footprint = model.compute_travel_footprint(
70aa301f   Antoine Goutenoir   Implement another...
394
395
396
397
                    origin_latitude=_origin.latitude,
                    origin_longitude=_origin.longitude,
                    destination_latitude=_destination.latitude,
                    destination_longitude=_destination.longitude,
18ba3d7a   Antoine Goutenoir   Boost the models'...
398
                    extra_config=_extra_config,
51f564d3   Antoine Goutenoir   Add a big chunk o...
399
                )
70aa301f   Antoine Goutenoir   Implement another...
400

b935618e   Antoine Goutenoir   Count the number ...
401
                _key = get_city_key(_destination)
70aa301f   Antoine Goutenoir   Implement another...
402

b935618e   Antoine Goutenoir   Count the number ...
403
                destinations_by_city_key[_key] = _destination
584b13cc   Antoine Goutenoir   Order the results...
404

b935618e   Antoine Goutenoir   Count the number ...
405
406
407
                if _key not in cities_dict:
                    cities_dict[_key] = {
                        'city': _key,
584b13cc   Antoine Goutenoir   Order the results...
408
409
                        'address': _destination.address,
                        'footprint': 0.0,
5634c975   Antoine Goutenoir   Expose travel dis...
410
                        'distance': 0.0,
b935618e   Antoine Goutenoir   Count the number ...
411
412
                        'train_trips': 0,
                        'plane_trips': 0,
584b13cc   Antoine Goutenoir   Order the results...
413
                    }
b935618e   Antoine Goutenoir   Count the number ...
414
415
416
417
418
419
420
421
422
423
424
425
                cities_dict[_key]['footprint'] += footprint['co2eq_kg']
                cities_dict[_key]['distance'] += footprint['distance_km']
                cities_dict[_key]['train_trips'] += footprint['train_trips']
                cities_dict[_key]['plane_trips'] += footprint['plane_trips']
                if _key not in cities_sum_foot:
                    cities_sum_foot[_key] = 0.0
                cities_sum_foot[_key] += footprint['co2eq_kg']
                if _key not in cities_sum_dist:
                    cities_sum_dist[_key] = 0.0
                cities_sum_dist[_key] += footprint['distance_km']

            cities = sorted(cities_dict.values(), key=lambda c: c['footprint'])
584b13cc   Antoine Goutenoir   Order the results...
426

70aa301f   Antoine Goutenoir   Implement another...
427
            footprints[model.slug] = {
51f564d3   Antoine Goutenoir   Add a big chunk o...
428
429
430
                'cities': cities,
            }

b935618e   Antoine Goutenoir   Count the number ...
431
432
433
            if cities_dict_first_model is None:
                cities_dict_first_model = deepcopy(cities_dict)

70aa301f   Antoine Goutenoir   Implement another...
434
        _results['footprints'] = footprints
51f564d3   Antoine Goutenoir   Add a big chunk o...
435

5634c975   Antoine Goutenoir   Expose travel dis...
436
437
        total_foot = 0.0
        total_dist = 0.0
b935618e   Antoine Goutenoir   Count the number ...
438
439
        total_train_trips = 0
        total_plane_trips = 0
314c65e2   Antoine Goutenoir   Implement Scenari...
440

584b13cc   Antoine Goutenoir   Order the results...
441
        cities_mean_dict = {}
5634c975   Antoine Goutenoir   Expose travel dis...
442
443
444
        for city in cities_sum_foot.keys():
            city_mean_foot = 1.0 * cities_sum_foot[city] / len(emission_models)
            city_mean_dist = 1.0 * cities_sum_dist[city] / len(emission_models)
b935618e   Antoine Goutenoir   Count the number ...
445
446
            city_train_trips = cities_dict_first_model[city]['train_trips']
            city_plane_trips = cities_dict_first_model[city]['plane_trips']
584b13cc   Antoine Goutenoir   Order the results...
447
448
449
            cities_mean_dict[city] = {
                'address': destinations_by_city_key[city].address,
                'city': city,
5634c975   Antoine Goutenoir   Expose travel dis...
450
451
                'footprint': city_mean_foot,
                'distance': city_mean_dist,
b935618e   Antoine Goutenoir   Count the number ...
452
453
                'train_trips': city_train_trips,
                'plane_trips': city_plane_trips,
584b13cc   Antoine Goutenoir   Order the results...
454
            }
5634c975   Antoine Goutenoir   Expose travel dis...
455
456
            total_foot += city_mean_foot
            total_dist += city_mean_dist
b935618e   Antoine Goutenoir   Count the number ...
457
458
            total_train_trips += city_train_trips
            total_plane_trips += city_plane_trips
1d48272e   Antoine Goutenoir   Compute the mean ...
459

584b13cc   Antoine Goutenoir   Order the results...
460
461
462
        cities_mean = [cities_mean_dict[k] for k in cities_mean_dict.keys()]
        cities_mean = sorted(cities_mean, key=lambda c: c['footprint'])

91751451   Antoine Goutenoir   Shift the API to ...
463
        _results['mean_footprint'] = {  # DEPRECATED?
1d48272e   Antoine Goutenoir   Compute the mean ...
464
465
            'cities': cities_mean
        }
91751451   Antoine Goutenoir   Shift the API to ...
466
        _results['cities'] = cities_mean
1d48272e   Antoine Goutenoir   Compute the mean ...
467

5634c975   Antoine Goutenoir   Expose travel dis...
468
469
470
471
        _results['total'] = total_foot  # DEPRECATED
        _results['footprint'] = total_foot

        _results['distance'] = total_dist
b935618e   Antoine Goutenoir   Count the number ...
472
473
        _results['train_trips'] = total_train_trips
        _results['plane_trips'] = total_plane_trips
314c65e2   Antoine Goutenoir   Implement Scenari...
474

70aa301f   Antoine Goutenoir   Implement another...
475
476
477
478
479
480
481
482
        return _results

    # SCENARIO A : One Origin, At Least One Destination #######################
    #
    # In this scenario, we compute the sum of each of the travels' footprint,
    # for each of the Emission Models, and present a mean of all Models.
    #
    if 1 == len(origins):
a1f12452   Antoine Goutenoir   Add the new conte...
483
        estimation.scenario = ScenarioEnum.one_to_many
70aa301f   Antoine Goutenoir   Implement another...
484
485
486
        results = compute_one_to_many(
            _origin=origins[0],
            _destinations=destinations,
8a693e06   Antoine Goutenoir   Glue the extra co...
487
            _extra_config=extra_config,
70aa301f   Antoine Goutenoir   Implement another...
488
489
        )

51f564d3   Antoine Goutenoir   Add a big chunk o...
490
491
492
493
    # SCENARIO B : At Least One Origin, One Destination #######################
    #
    # Same as A for now.
    #
1d48272e   Antoine Goutenoir   Compute the mean ...
494
    elif 1 == len(destinations):
a1f12452   Antoine Goutenoir   Add the new conte...
495
        estimation.scenario = ScenarioEnum.many_to_one
70aa301f   Antoine Goutenoir   Implement another...
496
497
498
        results = compute_one_to_many(
            _origin=destinations[0],
            _destinations=origins,
8a693e06   Antoine Goutenoir   Glue the extra co...
499
            _extra_config=extra_config,
70aa301f   Antoine Goutenoir   Implement another...
500
        )
24f55cde   Antoine Goutenoir   Geocode destinati...
501

51f564d3   Antoine Goutenoir   Add a big chunk o...
502
503
504
505
    # SCENARIO C : At Least One Origin, At Least One Destination ##############
    #
    # Run Scenario A for each Destination, and expose optimum Destination.
    #
1d48272e   Antoine Goutenoir   Compute the mean ...
506
    else:
a1f12452   Antoine Goutenoir   Add the new conte...
507
        estimation.scenario = ScenarioEnum.many_to_many
94ae2730   Antoine Goutenoir   Ignore duplicates...
508
        unique_city_keys = []
584b13cc   Antoine Goutenoir   Order the results...
509
        result_cities = []
314c65e2   Antoine Goutenoir   Implement Scenari...
510
511
512
        for destination in destinations:
            city_key = get_city_key(destination)

94ae2730   Antoine Goutenoir   Ignore duplicates...
513
514
515
516
517
            if city_key in unique_city_keys:
                continue
            else:
                unique_city_keys.append(city_key)

314c65e2   Antoine Goutenoir   Implement Scenari...
518
            city_results = compute_one_to_many(
584b13cc   Antoine Goutenoir   Order the results...
519
                _origin=destination,
314c65e2   Antoine Goutenoir   Implement Scenari...
520
                _destinations=origins,
8a693e06   Antoine Goutenoir   Glue the extra co...
521
                _extra_config=extra_config,
314c65e2   Antoine Goutenoir   Implement Scenari...
522
523
            )
            city_results['city'] = city_key
15e57dca   Antoine Goutenoir   Naming things and...
524
            city_results['address'] = destination.address
584b13cc   Antoine Goutenoir   Order the results...
525
            result_cities.append(city_results)
314c65e2   Antoine Goutenoir   Implement Scenari...
526

b935618e   Antoine Goutenoir   Count the number ...
527
        result_cities = sorted(result_cities, key=lambda c: int(c['footprint']))
584b13cc   Antoine Goutenoir   Order the results...
528
529
530
        results = {
            'cities': result_cities,
        }
1d48272e   Antoine Goutenoir   Compute the mean ...
531

a4c03d8e   Antoine Goutenoir   Add the controlle...
532
533
534
535
536
537
538
539
    # WRITE RESULTS INTO THE DATABASE #########################################

    estimation.status = StatusEnum.success
    estimation.output_yaml = yaml_dump(results)
    db.session.commit()

    # FINALLY, RESPOND ########################################################

70aa301f   Antoine Goutenoir   Implement another...
540
    response += yaml_dump(results) + "\n"
4392f295   Goutte   Update the Estima...
541

24f55cde   Antoine Goutenoir   Geocode destinati...
542
    return _respond(response)
a4c03d8e   Antoine Goutenoir   Add the controlle...
543
544


b9fc86c3   Antoine Goutenoir   Secure the admin ...
545
546
@main.route("/estimation/<public_id>.<extension>")
def consult_estimation(public_id, extension):
a4c03d8e   Antoine Goutenoir   Add the controlle...
547
548
549
550
551
552
553
554
555
556
557
558
559
560
    try:
        estimation = Estimation.query \
            .filter_by(public_id=public_id) \
            .one()
    except sqlalchemy.orm.exc.NoResultFound:
        return abort(404)
    except Exception as e:
        # TODO: log
        return abort(500)

    # allowed_formats = ['html']
    # if format not in allowed_formats:
    #     abort(404)

e721cb31   Antoine Goutenoir   Provide a YAML fi...
561
    unavailable_statuses = [StatusEnum.pending, StatusEnum.working]
314c65e2   Antoine Goutenoir   Implement Scenari...
562

b9fc86c3   Antoine Goutenoir   Secure the admin ...
563
    if extension in ['xhtml', 'html', 'htm']:
e721cb31   Antoine Goutenoir   Provide a YAML fi...
564
565

        if estimation.status in unavailable_statuses:
a4c03d8e   Antoine Goutenoir   Add the controlle...
566
567
568
569
570
            return render_template(
                "estimation-queue-wait.html",
                estimation=estimation
            )
        else:
40382971   Antoine Goutenoir   Add the sum of es...
571
572
573
574
575
            estimation_output = estimation.get_output_dict()
            estimation_sum = 0
            for city in estimation_output['cities']:
                estimation_sum += city['footprint']

a4c03d8e   Antoine Goutenoir   Add the controlle...
576
577
            return render_template(
                "estimation.html",
91751451   Antoine Goutenoir   Shift the API to ...
578
                estimation=estimation,
40382971   Antoine Goutenoir   Add the sum of es...
579
580
                estimation_output=estimation_output,
                estimation_sum=estimation_sum,
a4c03d8e   Antoine Goutenoir   Add the controlle...
581
582
            )

b9fc86c3   Antoine Goutenoir   Secure the admin ...
583
    elif extension in ['yaml', 'yml']:
e721cb31   Antoine Goutenoir   Provide a YAML fi...
584
585
586
587
588
589

        if estimation.status in unavailable_statuses:
            abort(404)

        return estimation.output_yaml

b9fc86c3   Antoine Goutenoir   Secure the admin ...
590
    elif 'csv' == extension:
a4c03d8e   Antoine Goutenoir   Add the controlle...
591

e721cb31   Antoine Goutenoir   Provide a YAML fi...
592
593
594
        if estimation.status in unavailable_statuses:
            abort(404)

a4c03d8e   Antoine Goutenoir   Add the controlle...
595
596
        si = StringIO()
        cw = csv.writer(si, quoting=csv.QUOTE_ALL)
b935618e   Antoine Goutenoir   Count the number ...
597
598
599
600
601
        cw.writerow([
            u"city", u"address",
            u"co2 (kg)", u"distance (km)",
            u"plane trips", u'train trips',
        ])
a4c03d8e   Antoine Goutenoir   Add the controlle...
602
603

        results = estimation.get_output_dict()
5634c975   Antoine Goutenoir   Expose travel dis...
604
605
606
607
608
609
        for city in results['cities']:
            cw.writerow([
                city['city'].encode(OUT_ENCODING),
                city['address'].encode(OUT_ENCODING),
                round(city['footprint'], 3),
                round(city['distance'], 3),
b935618e   Antoine Goutenoir   Count the number ...
610
611
                city['plane_trips'],
                city['train_trips'],
5634c975   Antoine Goutenoir   Expose travel dis...
612
613
            ])

67f85bce   Antoine Goutenoir   Generate a CSV fi...
614
615
616
617
618
619
620
621
        # return si.getvalue().strip('\r\n')
        return Response(
            response=si.getvalue().strip('\r\n'),
            headers={
                'Content-type': 'text/csv',
                'Content-disposition': "attachment; filename=%s.csv"%public_id,
            },
        )
a4c03d8e   Antoine Goutenoir   Add the controlle...
622
623
624

    else:
        abort(404)
b9fc86c3   Antoine Goutenoir   Secure the admin ...
625
626


67f85bce   Antoine Goutenoir   Generate a CSV fi...
627
628
@main.route("/scaling_laws.csv")
def get_scaling_laws_csv():
a728e600   Antoine Goutenoir   Allow configurati...
629
    distances = content.laws_plot.distances
67f85bce   Antoine Goutenoir   Generate a CSV fi...
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
    models = get_emission_models()

    si = StringIO()
    cw = csv.writer(si, quoting=csv.QUOTE_ALL)

    header = ['distance'] + [model.slug for model in models]
    cw.writerow(header)

    for distance in distances:
        row = [distance]
        for model in models:
            row.append(model.compute_airplane_distance_footprint(distance))
        cw.writerow(row)

    return Response(
        response=si.getvalue().strip('\r\n'),
        headers={
            'Content-type': 'text/csv',
            'Content-disposition': 'attachment; filename=scaling_laws.csv',
        },
    )


b9fc86c3   Antoine Goutenoir   Secure the admin ...
653
654
655
656
657
658
@main.route("/test")
@basic_auth.required
def dev_test():
    import os

    return os.getenv('ADMIN_USERNAME')