Blame view

flaskr/controllers/main_controller.py 14.9 KB
314c65e2   Antoine Goutenoir   Implement Scenari...
1
2
import geopy
import sqlalchemy
51f564d3   Antoine Goutenoir   Add a big chunk o...
3

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

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

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

70aa301f   Antoine Goutenoir   Implement another...
16
17
from yaml import safe_dump as yaml_dump

67f85bce   Antoine Goutenoir   Generate a CSV fi...
18
19
20
import csv
# from io import StringIO
from cStringIO import StringIO
24f55cde   Antoine Goutenoir   Geocode destinati...
21

7b4d2926   Goutte   Rework the contro...
22
23
24
main = Blueprint('main', __name__)


9621b8a5   Antoine Goutenoir   Fix the CSV gener...
25
26
27
OUT_ENCODING = 'utf-8'


67f85bce   Antoine Goutenoir   Generate a CSV fi...
28
29
30
# -----------------------------------------------------------------------------
# refactor this outta here, like in core?

67f85bce   Antoine Goutenoir   Generate a CSV fi...
31
32
33
34

# -----------------------------------------------------------------------------


461850db   Antoine Goutenoir   Yet another joyfu...
35
@main.route('/favicon.ico')
7f7c6b10   Antoine Goutenoir   Disable RFI in th...
36
@cache.cached(timeout=10000)
461850db   Antoine Goutenoir   Yet another joyfu...
37
38
39
40
41
42
43
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...
44
45
46
@main.route('/')
@cache.cached(timeout=1000)
def home():
4c862b54   Antoine Goutenoir   Add a grouped bar...
47
48
49
50
51
52
    models = get_emission_models()
    return render_template(
        'home.html',
        models=models,
        colors=[model.color for model in models],
    )
7b4d2926   Goutte   Rework the contro...
53
54
55
56


@main.route("/estimate", methods=["GET", "POST"])
def estimate():
77e86148   Antoine Goutenoir   Fake support for ...
57
    models = get_emission_models()
7b4d2926   Goutte   Rework the contro...
58
59
60
61
    form = EstimateForm()

    if form.validate_on_submit():

7b4d2926   Goutte   Rework the contro...
62
63
        id = generate_unique_id()

7b4d2926   Goutte   Rework the contro...
64
        estimation = Estimation()
a4c03d8e   Antoine Goutenoir   Add the controlle...
65
        # estimation.email = form.email.data
7b4d2926   Goutte   Rework the contro...
66
67
        estimation.first_name = form.first_name.data
        estimation.last_name = form.last_name.data
16f69d07   Antoine Goutenoir   Add an (unsecured...
68
        estimation.institution = form.institution.data
24f55cde   Antoine Goutenoir   Geocode destinati...
69
70
71
        estimation.status = StatusEnum.pending
        estimation.origin_addresses = form.origin_addresses.data
        estimation.destination_addresses = form.destination_addresses.data
b9fc86c3   Antoine Goutenoir   Secure the admin ...
72
        # estimation.compute_optimal_destination = form.compute_optimal_destination.data
77e86148   Antoine Goutenoir   Fake support for ...
73
74
75
76
77
        models_slugs = []
        for model in models:
            if getattr(form, 'use_model_' % model.slug).data:
                models_slugs.append(model.slug)
        estimation.models_slugs = "\n".join(models_slugs)
7b4d2926   Goutte   Rework the contro...
78
79
80
81
82

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

        flash("Estimation request submitted successfully.", "success")
a4c03d8e   Antoine Goutenoir   Add the controlle...
83
84
85
        return redirect(url_for(
            endpoint=".consult_estimation",
            public_id=estimation.public_id,
91751451   Antoine Goutenoir   Shift the API to ...
86
            extension='html'
a4c03d8e   Antoine Goutenoir   Add the controlle...
87
        ))
7b4d2926   Goutte   Rework the contro...
88
89
        # return render_template("estimate-debrief.html", form=form)

77e86148   Antoine Goutenoir   Fake support for ...
90
    return render_template("estimate.html", form=form, models=models)
7b4d2926   Goutte   Rework the contro...
91

4392f295   Goutte   Update the Estima...
92

15e57dca   Antoine Goutenoir   Naming things and...
93
94
95
96
97
98
99
100
101
102
103
104
105
106
@main.route("/invalidate")
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...
107
@main.route("/compute")
51f564d3   Antoine Goutenoir   Add a big chunk o...
108
def compute():  # process the queue of estimation requests
24f55cde   Antoine Goutenoir   Geocode destinati...
109
110
111
112
113

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

    def _handle_failure(_estimation, _failure_message):
a4c03d8e   Antoine Goutenoir   Add the controlle...
114
        _estimation.status = StatusEnum.failure
70aa301f   Antoine Goutenoir   Implement another...
115
        _estimation.errors = _failure_message
24f55cde   Antoine Goutenoir   Geocode destinati...
116
117
        db.session.commit()

72460978   Antoine Goutenoir   Use warnings inst...
118
119
120
121
    def _handle_warning(_estimation, _warning_message):
        _estimation.warnings = _warning_message
        db.session.commit()

4392f295   Goutte   Update the Estima...
122
123
    response = ""

03c194bf   Antoine Goutenoir   Actually implemen...
124
125
126
127
128
129
130
    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...
131
132
133
    try:
        estimation = Estimation.query \
            .filter_by(status=StatusEnum.pending) \
1d48272e   Antoine Goutenoir   Compute the mean ...
134
            .order_by(Estimation.id.asc()) \
51f564d3   Antoine Goutenoir   Add a big chunk o...
135
            .first()
24f55cde   Antoine Goutenoir   Geocode destinati...
136
137
    except sqlalchemy.orm.exc.NoResultFound:
        return _respond("No estimation in the queue.")
51f564d3   Antoine Goutenoir   Add a big chunk o...
138
139
140
    except Exception as e:
        return _respond("Database error: %s" % (e,))

a6f63a6d   Antoine Goutenoir   Zzz…
141
142
    if not estimation:
        return _respond("No estimation in the queue.")
24f55cde   Antoine Goutenoir   Geocode destinati...
143

03c194bf   Antoine Goutenoir   Actually implemen...
144
145
146
147
148
149
    estimation.status = StatusEnum.working
    db.session.commit()

    response += u"Processing estimation `%s`...\n" % (
        estimation.public_id
    )
24f55cde   Antoine Goutenoir   Geocode destinati...
150
151
152

    geocoder = CachedGeocoder()

51f564d3   Antoine Goutenoir   Add a big chunk o...
153
154
    # GEOCODE ORIGINS #########################################################

461850db   Antoine Goutenoir   Yet another joyfu...
155
    origins_addresses = estimation.origin_addresses.strip().split("\n")
51f564d3   Antoine Goutenoir   Add a big chunk o...
156
157
158
159
    origins = []

    for i in range(len(origins_addresses)):

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

        try:
118da78e   Antoine Goutenoir   Wrapping up some ...
163
            origin = geocoder.geocode(origin_address.encode('utf-8'))
51f564d3   Antoine Goutenoir   Add a big chunk o...
164
        except geopy.exc.GeopyError as e:
72460978   Antoine Goutenoir   Use warnings inst...
165
            response += u"Failed to geocode origin `%s`.\n%s" % (
51f564d3   Antoine Goutenoir   Add a big chunk o...
166
167
                origin_address, e,
            )
72460978   Antoine Goutenoir   Use warnings inst...
168
169
            _handle_warning(estimation, response)
            continue
51f564d3   Antoine Goutenoir   Add a big chunk o...
170
171

        if origin is None:
72460978   Antoine Goutenoir   Use warnings inst...
172
            response += u"Failed to geocode origin `%s`." % (
51f564d3   Antoine Goutenoir   Add a big chunk o...
173
174
                origin_address,
            )
72460978   Antoine Goutenoir   Use warnings inst...
175
176
            _handle_warning(estimation, response)
            continue
51f564d3   Antoine Goutenoir   Add a big chunk o...
177
178
179
180
181
182
183
184
185
186

        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...
187
    destinations_addresses = estimation.destination_addresses.strip().split("\n")
24f55cde   Antoine Goutenoir   Geocode destinati...
188
189
190
191
    destinations = []

    for i in range(len(destinations_addresses)):

118da78e   Antoine Goutenoir   Wrapping up some ...
192
        destination_address = destinations_addresses[i].strip()
24f55cde   Antoine Goutenoir   Geocode destinati...
193
194

        try:
118da78e   Antoine Goutenoir   Wrapping up some ...
195
            destination = geocoder.geocode(destination_address.encode('utf-8'))
24f55cde   Antoine Goutenoir   Geocode destinati...
196
        except geopy.exc.GeopyError as e:
1d48272e   Antoine Goutenoir   Compute the mean ...
197
            response += u"Failed to geocode destination `%s`.\n%s" % (
24f55cde   Antoine Goutenoir   Geocode destinati...
198
199
                destination_address, e,
            )
72460978   Antoine Goutenoir   Use warnings inst...
200
201
            _handle_warning(estimation, response)
            continue
24f55cde   Antoine Goutenoir   Geocode destinati...
202
203

        if destination is None:
1d48272e   Antoine Goutenoir   Compute the mean ...
204
            response += u"Failed to geocode destination `%s`." % (
24f55cde   Antoine Goutenoir   Geocode destinati...
205
206
                destination_address,
            )
72460978   Antoine Goutenoir   Use warnings inst...
207
208
            _handle_warning(estimation, response)
            continue
24f55cde   Antoine Goutenoir   Geocode destinati...
209

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

24f55cde   Antoine Goutenoir   Geocode destinati...
212
213
214
215
216
217
218
        destinations.append(destination)

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

70aa301f   Antoine Goutenoir   Implement another...
219
    # GTFO IF NO ORIGINS OR NO DESTINATIONS ###################################
51f564d3   Antoine Goutenoir   Add a big chunk o...
220
221

    if 0 == len(origins):
70aa301f   Antoine Goutenoir   Implement another...
222
        response += u"Failed to geocode all the origin(s)."
51f564d3   Antoine Goutenoir   Add a big chunk o...
223
224
225
        _handle_failure(estimation, response)
        return _respond(response)
    if 0 == len(destinations):
70aa301f   Antoine Goutenoir   Implement another...
226
        response += u"Failed to geocode all the destination(s)."
51f564d3   Antoine Goutenoir   Add a big chunk o...
227
228
229
        _handle_failure(estimation, response)
        return _respond(response)

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

67f85bce   Antoine Goutenoir   Generate a CSV fi...
232
    emission_models = get_emission_models()
51f564d3   Antoine Goutenoir   Add a big chunk o...
233
234
235
236
237
238
239

    # print(emission_models)

    # PREPARE RESULT DICTIONARY THAT WILL BE STORED ###########################

    results = {}

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

314c65e2   Antoine Goutenoir   Implement Scenari...
242
    def get_city_key(_location):
461850db   Antoine Goutenoir   Yet another joyfu...
243
244
245
246
247
248
249
250
251
252
253

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

70aa301f   Antoine Goutenoir   Implement another...
255
256
257
258
259
260
    def compute_one_to_many(
            _origin,
            _destinations,
            use_train_below=0.0
    ):
        _results = {}
51f564d3   Antoine Goutenoir   Add a big chunk o...
261
262
        footprints = {}

584b13cc   Antoine Goutenoir   Order the results...
263
264
        destinations_by_city_key = {}

1d48272e   Antoine Goutenoir   Compute the mean ...
265
        cities_sum = {}
51f564d3   Antoine Goutenoir   Add a big chunk o...
266
        for model in emission_models:
584b13cc   Antoine Goutenoir   Order the results...
267
            cities_dict = {}
70aa301f   Antoine Goutenoir   Implement another...
268
            for _destination in _destinations:
51f564d3   Antoine Goutenoir   Add a big chunk o...
269
                footprint = model.compute_travel_footprint(
70aa301f   Antoine Goutenoir   Implement another...
270
271
272
273
                    origin_latitude=_origin.latitude,
                    origin_longitude=_origin.longitude,
                    destination_latitude=_destination.latitude,
                    destination_longitude=_destination.longitude,
51f564d3   Antoine Goutenoir   Add a big chunk o...
274
                )
70aa301f   Antoine Goutenoir   Implement another...
275

314c65e2   Antoine Goutenoir   Implement Scenari...
276
                city_key = get_city_key(_destination)
70aa301f   Antoine Goutenoir   Implement another...
277

584b13cc   Antoine Goutenoir   Order the results...
278
279
280
281
282
283
284
285
286
                destinations_by_city_key[city_key] = _destination

                if city_key not in cities_dict:
                    cities_dict[city_key] = {
                        'city': city_key,
                        'address': _destination.address,
                        'footprint': 0.0,
                    }
                cities_dict[city_key]['footprint'] += footprint
70aa301f   Antoine Goutenoir   Implement another...
287
288
289
290
                if city_key not in cities_sum:
                    cities_sum[city_key] = 0.0
                cities_sum[city_key] += footprint

584b13cc   Antoine Goutenoir   Order the results...
291
292
293
            cities = [cities_dict[k] for k in cities_dict.keys()]
            cities = sorted(cities, key=lambda c: c['footprint'])

70aa301f   Antoine Goutenoir   Implement another...
294
            footprints[model.slug] = {
51f564d3   Antoine Goutenoir   Add a big chunk o...
295
296
297
                'cities': cities,
            }

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

314c65e2   Antoine Goutenoir   Implement Scenari...
300
301
        total = 0.0

584b13cc   Antoine Goutenoir   Order the results...
302
        cities_mean_dict = {}
1d48272e   Antoine Goutenoir   Compute the mean ...
303
        for city in cities_sum.keys():
314c65e2   Antoine Goutenoir   Implement Scenari...
304
            city_mean = 1.0 * cities_sum[city] / len(emission_models)
584b13cc   Antoine Goutenoir   Order the results...
305
306
307
308
309
            cities_mean_dict[city] = {
                'address': destinations_by_city_key[city].address,
                'city': city,
                'footprint': city_mean,
            }
314c65e2   Antoine Goutenoir   Implement Scenari...
310
            total += city_mean
1d48272e   Antoine Goutenoir   Compute the mean ...
311

584b13cc   Antoine Goutenoir   Order the results...
312
313
314
        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 ...
315
        _results['mean_footprint'] = {  # DEPRECATED?
1d48272e   Antoine Goutenoir   Compute the mean ...
316
317
            'cities': cities_mean
        }
91751451   Antoine Goutenoir   Shift the API to ...
318
        _results['cities'] = cities_mean
1d48272e   Antoine Goutenoir   Compute the mean ...
319

94ae2730   Antoine Goutenoir   Ignore duplicates...
320
321
        _results['total'] = total  # DEPRECATED
        _results['footprint'] = total
314c65e2   Antoine Goutenoir   Implement Scenari...
322

70aa301f   Antoine Goutenoir   Implement another...
323
324
325
326
327
328
329
330
331
332
333
334
335
336
        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):
        results = compute_one_to_many(
            _origin=origins[0],
            _destinations=destinations,
            use_train_below=0,
        )

51f564d3   Antoine Goutenoir   Add a big chunk o...
337
338
339
340
    # SCENARIO B : At Least One Origin, One Destination #######################
    #
    # Same as A for now.
    #
1d48272e   Antoine Goutenoir   Compute the mean ...
341
    elif 1 == len(destinations):
70aa301f   Antoine Goutenoir   Implement another...
342
343
344
345
346
        results = compute_one_to_many(
            _origin=destinations[0],
            _destinations=origins,
            use_train_below=0,
        )
24f55cde   Antoine Goutenoir   Geocode destinati...
347

51f564d3   Antoine Goutenoir   Add a big chunk o...
348
349
350
351
    # 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 ...
352
    else:
94ae2730   Antoine Goutenoir   Ignore duplicates...
353
        unique_city_keys = []
584b13cc   Antoine Goutenoir   Order the results...
354
        result_cities = []
314c65e2   Antoine Goutenoir   Implement Scenari...
355
356
357
        for destination in destinations:
            city_key = get_city_key(destination)

94ae2730   Antoine Goutenoir   Ignore duplicates...
358
359
360
361
362
            if city_key in unique_city_keys:
                continue
            else:
                unique_city_keys.append(city_key)

314c65e2   Antoine Goutenoir   Implement Scenari...
363
            city_results = compute_one_to_many(
584b13cc   Antoine Goutenoir   Order the results...
364
                _origin=destination,
314c65e2   Antoine Goutenoir   Implement Scenari...
365
366
367
368
                _destinations=origins,
                use_train_below=0,
            )
            city_results['city'] = city_key
15e57dca   Antoine Goutenoir   Naming things and...
369
            city_results['address'] = destination.address
584b13cc   Antoine Goutenoir   Order the results...
370
            result_cities.append(city_results)
314c65e2   Antoine Goutenoir   Implement Scenari...
371

584b13cc   Antoine Goutenoir   Order the results...
372
373
374
375
        result_cities = sorted(result_cities, key=lambda c: int(c['total']))
        results = {
            'cities': result_cities,
        }
1d48272e   Antoine Goutenoir   Compute the mean ...
376

a4c03d8e   Antoine Goutenoir   Add the controlle...
377
378
379
380
381
382
383
384
    # 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...
385
    response += yaml_dump(results) + "\n"
4392f295   Goutte   Update the Estima...
386

24f55cde   Antoine Goutenoir   Geocode destinati...
387
    return _respond(response)
a4c03d8e   Antoine Goutenoir   Add the controlle...
388
389


b9fc86c3   Antoine Goutenoir   Secure the admin ...
390
391
@main.route("/estimation/<public_id>.<extension>")
def consult_estimation(public_id, extension):
a4c03d8e   Antoine Goutenoir   Add the controlle...
392
393
394
395
396
397
398
399
400
401
402
403
404
405
    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...
406
    unavailable_statuses = [StatusEnum.pending, StatusEnum.working]
314c65e2   Antoine Goutenoir   Implement Scenari...
407

b9fc86c3   Antoine Goutenoir   Secure the admin ...
408
    if extension in ['xhtml', 'html', 'htm']:
e721cb31   Antoine Goutenoir   Provide a YAML fi...
409
410

        if estimation.status in unavailable_statuses:
a4c03d8e   Antoine Goutenoir   Add the controlle...
411
412
413
414
415
416
417
            return render_template(
                "estimation-queue-wait.html",
                estimation=estimation
            )
        else:
            return render_template(
                "estimation.html",
91751451   Antoine Goutenoir   Shift the API to ...
418
419
                estimation=estimation,
                estimation_output=estimation.get_output_dict(),
a4c03d8e   Antoine Goutenoir   Add the controlle...
420
421
            )

b9fc86c3   Antoine Goutenoir   Secure the admin ...
422
    elif extension in ['yaml', 'yml']:
e721cb31   Antoine Goutenoir   Provide a YAML fi...
423
424
425
426
427
428

        if estimation.status in unavailable_statuses:
            abort(404)

        return estimation.output_yaml

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

e721cb31   Antoine Goutenoir   Provide a YAML fi...
431
432
433
        if estimation.status in unavailable_statuses:
            abort(404)

a4c03d8e   Antoine Goutenoir   Add the controlle...
434
435
        si = StringIO()
        cw = csv.writer(si, quoting=csv.QUOTE_ALL)
9621b8a5   Antoine Goutenoir   Fix the CSV gener...
436
        cw.writerow([u"city", u"address", u"co2 (g)"])
a4c03d8e   Antoine Goutenoir   Add the controlle...
437
438

        results = estimation.get_output_dict()
118da78e   Antoine Goutenoir   Wrapping up some ...
439
440
        if 'mean_footprint' in results:
            for city in results['mean_footprint']['cities']:
9621b8a5   Antoine Goutenoir   Fix the CSV gener...
441
442
443
444
445
                cw.writerow([
                    city['city'].encode(OUT_ENCODING),
                    city['address'].encode(OUT_ENCODING),
                    city['footprint']
                ])
118da78e   Antoine Goutenoir   Wrapping up some ...
446
447
        elif 'cities' in results:
            for city in results['cities']:
9621b8a5   Antoine Goutenoir   Fix the CSV gener...
448
449
450
451
452
                cw.writerow([
                    city['city'].encode(OUT_ENCODING),
                    city['address'].encode(OUT_ENCODING),
                    city['total']
                ])
118da78e   Antoine Goutenoir   Wrapping up some ...
453
454

        # HTTP headers?
67f85bce   Antoine Goutenoir   Generate a CSV fi...
455
456
457
458
459
460
461
462
        # 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...
463
464
465

    else:
        abort(404)
b9fc86c3   Antoine Goutenoir   Secure the admin ...
466
467


67f85bce   Antoine Goutenoir   Generate a CSV fi...
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
@main.route("/scaling_laws.csv")
def get_scaling_laws_csv():
    distances = [
        500., 1000., 1500., 2500., 3000., 4500.,
        5000., 8000., 10000., 12000.,
    ]
    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 ...
497
498
499
500
501
502
@main.route("/test")
@basic_auth.required
def dev_test():
    import os

    return os.getenv('ADMIN_USERNAME')