Blame view

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

a4c03d8e   Antoine Goutenoir   Add the controlle...
5
6
from flask import Blueprint, render_template, flash, request, redirect, \
    url_for, abort
7b4d2926   Goutte   Rework the contro...
7
8
9

from flaskr.extensions import cache
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
13

from flaskr.core import generate_unique_id
51f564d3   Antoine Goutenoir   Add a big chunk o...
14
from flaskr.content import content
7b4d2926   Goutte   Rework the contro...
15

a4c03d8e   Antoine Goutenoir   Add the controlle...
16
# from io import StringIO
70aa301f   Antoine Goutenoir   Implement another...
17
18
from yaml import safe_dump as yaml_dump

24f55cde   Antoine Goutenoir   Geocode destinati...
19

7b4d2926   Goutte   Rework the contro...
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
main = Blueprint('main', __name__)


@main.route('/')
@cache.cached(timeout=1000)
def home():
    return render_template('index.html')


@main.route("/estimate", methods=["GET", "POST"])
def estimate():
    form = EstimateForm()

    if form.validate_on_submit():

7b4d2926   Goutte   Rework the contro...
35
36
        id = generate_unique_id()

7b4d2926   Goutte   Rework the contro...
37
        estimation = Estimation()
a4c03d8e   Antoine Goutenoir   Add the controlle...
38
        # estimation.email = form.email.data
7b4d2926   Goutte   Rework the contro...
39
40
        estimation.first_name = form.first_name.data
        estimation.last_name = form.last_name.data
24f55cde   Antoine Goutenoir   Geocode destinati...
41
42
43
44
        estimation.status = StatusEnum.pending
        estimation.origin_addresses = form.origin_addresses.data
        estimation.destination_addresses = form.destination_addresses.data
        estimation.compute_optimal_destination = form.compute_optimal_destination.data
7b4d2926   Goutte   Rework the contro...
45
46
47
48
49

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

        flash("Estimation request submitted successfully.", "success")
a4c03d8e   Antoine Goutenoir   Add the controlle...
50
51
52
53
54
        return redirect(url_for(
            endpoint=".consult_estimation",
            public_id=estimation.public_id,
            format='html'
        ))
7b4d2926   Goutte   Rework the contro...
55
56
57
58
        # return render_template("estimate-debrief.html", form=form)

    return render_template("estimate.html", form=form)

4392f295   Goutte   Update the Estima...
59

15e57dca   Antoine Goutenoir   Naming things and...
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@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...
74
@main.route("/compute")
51f564d3   Antoine Goutenoir   Add a big chunk o...
75
def compute():  # process the queue of estimation requests
24f55cde   Antoine Goutenoir   Geocode destinati...
76
77
78
79
80

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

    def _handle_failure(_estimation, _failure_message):
a4c03d8e   Antoine Goutenoir   Add the controlle...
81
        _estimation.status = StatusEnum.failure
70aa301f   Antoine Goutenoir   Implement another...
82
        _estimation.errors = _failure_message
24f55cde   Antoine Goutenoir   Geocode destinati...
83
84
        db.session.commit()

4392f295   Goutte   Update the Estima...
85
86
    response = ""

03c194bf   Antoine Goutenoir   Actually implemen...
87
88
89
90
91
92
93
    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...
94
95
96
    try:
        estimation = Estimation.query \
            .filter_by(status=StatusEnum.pending) \
1d48272e   Antoine Goutenoir   Compute the mean ...
97
            .order_by(Estimation.id.asc()) \
51f564d3   Antoine Goutenoir   Add a big chunk o...
98
            .first()
24f55cde   Antoine Goutenoir   Geocode destinati...
99
100
    except sqlalchemy.orm.exc.NoResultFound:
        return _respond("No estimation in the queue.")
51f564d3   Antoine Goutenoir   Add a big chunk o...
101
102
103
    except Exception as e:
        return _respond("Database error: %s" % (e,))

a6f63a6d   Antoine Goutenoir   Zzz…
104
105
    if not estimation:
        return _respond("No estimation in the queue.")
24f55cde   Antoine Goutenoir   Geocode destinati...
106

03c194bf   Antoine Goutenoir   Actually implemen...
107
108
109
110
111
112
    estimation.status = StatusEnum.working
    db.session.commit()

    response += u"Processing estimation `%s`...\n" % (
        estimation.public_id
    )
24f55cde   Antoine Goutenoir   Geocode destinati...
113
114
115

    geocoder = CachedGeocoder()

51f564d3   Antoine Goutenoir   Add a big chunk o...
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
    # GEOCODE ORIGINS #########################################################

    origins_addresses = estimation.origin_addresses.split("\n")
    origins = []

    for i in range(len(origins_addresses)):

        origin_address = str(origins_addresses[i]).strip()

        try:
            origin = geocoder.geocode(origin_address)
        except geopy.exc.GeopyError as e:
            response += u"Failed to geolocalize origin `%s`.\n%s" % (
                origin_address, e,
            )
            _handle_failure(estimation, response)
            return _respond(response)

        if origin is None:
            response += u"Failed to geolocalize origin `%s`." % (
                origin_address,
            )
            _handle_failure(estimation, response)
            return _respond(response)

        origins.append(origin)

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

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

24f55cde   Antoine Goutenoir   Geocode destinati...
150
151
152
153
154
    destinations_addresses = estimation.destination_addresses.split("\n")
    destinations = []

    for i in range(len(destinations_addresses)):

51f564d3   Antoine Goutenoir   Add a big chunk o...
155
        destination_address = str(destinations_addresses[i]).strip()
24f55cde   Antoine Goutenoir   Geocode destinati...
156
157
158
159

        try:
            destination = geocoder.geocode(destination_address)
        except geopy.exc.GeopyError as e:
1d48272e   Antoine Goutenoir   Compute the mean ...
160
            response += u"Failed to geocode destination `%s`.\n%s" % (
24f55cde   Antoine Goutenoir   Geocode destinati...
161
162
163
164
165
166
                destination_address, e,
            )
            _handle_failure(estimation, response)
            return _respond(response)

        if destination is None:
1d48272e   Antoine Goutenoir   Compute the mean ...
167
            response += u"Failed to geocode destination `%s`." % (
24f55cde   Antoine Goutenoir   Geocode destinati...
168
169
170
171
172
                destination_address,
            )
            _handle_failure(estimation, response)
            return _respond(response)

314c65e2   Antoine Goutenoir   Implement Scenari...
173
174
        print(repr(destination.raw))

24f55cde   Antoine Goutenoir   Geocode destinati...
175
176
177
178
179
180
181
        destinations.append(destination)

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

70aa301f   Antoine Goutenoir   Implement another...
182
    # GTFO IF NO ORIGINS OR NO DESTINATIONS ###################################
51f564d3   Antoine Goutenoir   Add a big chunk o...
183
184

    if 0 == len(origins):
70aa301f   Antoine Goutenoir   Implement another...
185
        response += u"Failed to geocode all the origin(s)."
51f564d3   Antoine Goutenoir   Add a big chunk o...
186
187
188
        _handle_failure(estimation, response)
        return _respond(response)
    if 0 == len(destinations):
70aa301f   Antoine Goutenoir   Implement another...
189
        response += u"Failed to geocode all the destination(s)."
51f564d3   Antoine Goutenoir   Add a big chunk o...
190
191
192
        _handle_failure(estimation, response)
        return _respond(response)

70aa301f   Antoine Goutenoir   Implement another...
193
    # GRAB AND CONFIGURE THE EMISSION MODELS ##################################
51f564d3   Antoine Goutenoir   Add a big chunk o...
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212

    emission_models_confs = content.models
    emission_models = []

    for model_conf in emission_models_confs:
        model_file = model_conf.file
        the_module = importlib.import_module("flaskr.laws.%s" % model_file)

        model = the_module.EmissionModel(model_conf)
        # model.configure(extra_model_conf)

        emission_models.append(model)

    # print(emission_models)

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

    results = {}

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

314c65e2   Antoine Goutenoir   Implement Scenari...
215
216
217
218
219
220
221
222
223
224
    def get_city_key(_location):
        _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

70aa301f   Antoine Goutenoir   Implement another...
225
226
227
228
229
230
    def compute_one_to_many(
            _origin,
            _destinations,
            use_train_below=0.0
    ):
        _results = {}
51f564d3   Antoine Goutenoir   Add a big chunk o...
231
232
        footprints = {}

1d48272e   Antoine Goutenoir   Compute the mean ...
233
        cities_sum = {}
51f564d3   Antoine Goutenoir   Add a big chunk o...
234
235
        for model in emission_models:
            cities = {}
70aa301f   Antoine Goutenoir   Implement another...
236
            for _destination in _destinations:
51f564d3   Antoine Goutenoir   Add a big chunk o...
237
                footprint = model.compute_travel_footprint(
70aa301f   Antoine Goutenoir   Implement another...
238
239
240
241
                    origin_latitude=_origin.latitude,
                    origin_longitude=_origin.longitude,
                    destination_latitude=_destination.latitude,
                    destination_longitude=_destination.longitude,
51f564d3   Antoine Goutenoir   Add a big chunk o...
242
                )
70aa301f   Antoine Goutenoir   Implement another...
243

314c65e2   Antoine Goutenoir   Implement Scenari...
244
                city_key = get_city_key(_destination)
70aa301f   Antoine Goutenoir   Implement another...
245
246
247
248
249
250
251
252
253

                if city_key not in cities:
                    cities[city_key] = 0.0
                cities[city_key] += footprint
                if city_key not in cities_sum:
                    cities_sum[city_key] = 0.0
                cities_sum[city_key] += footprint

            footprints[model.slug] = {
51f564d3   Antoine Goutenoir   Add a big chunk o...
254
255
256
                'cities': cities,
            }

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

314c65e2   Antoine Goutenoir   Implement Scenari...
259
260
        total = 0.0

1d48272e   Antoine Goutenoir   Compute the mean ...
261
262
        cities_mean = {}
        for city in cities_sum.keys():
314c65e2   Antoine Goutenoir   Implement Scenari...
263
264
265
            city_mean = 1.0 * cities_sum[city] / len(emission_models)
            cities_mean[city] = city_mean
            total += city_mean
1d48272e   Antoine Goutenoir   Compute the mean ...
266

70aa301f   Antoine Goutenoir   Implement another...
267
        _results['mean_footprint'] = {
1d48272e   Antoine Goutenoir   Compute the mean ...
268
269
270
            'cities': cities_mean
        }

314c65e2   Antoine Goutenoir   Implement Scenari...
271
272
        _results['total'] = total

70aa301f   Antoine Goutenoir   Implement another...
273
274
275
276
277
278
279
280
281
282
283
284
285
286
        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...
287
288
289
290
    # SCENARIO B : At Least One Origin, One Destination #######################
    #
    # Same as A for now.
    #
1d48272e   Antoine Goutenoir   Compute the mean ...
291
    elif 1 == len(destinations):
70aa301f   Antoine Goutenoir   Implement another...
292
293
294
295
296
        results = compute_one_to_many(
            _origin=destinations[0],
            _destinations=origins,
            use_train_below=0,
        )
24f55cde   Antoine Goutenoir   Geocode destinati...
297

51f564d3   Antoine Goutenoir   Add a big chunk o...
298
299
300
301
    # 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 ...
302
    else:
314c65e2   Antoine Goutenoir   Implement Scenari...
303
304
305
306
307
308
309
310
311
312
313
314
        results = {
            'cities': [],
        }
        for destination in destinations:
            city_key = get_city_key(destination)

            city_results = compute_one_to_many(
                _origin=destinations[0],
                _destinations=origins,
                use_train_below=0,
            )
            city_results['city'] = city_key
15e57dca   Antoine Goutenoir   Naming things and...
315
            city_results['address'] = destination.address
314c65e2   Antoine Goutenoir   Implement Scenari...
316
317
318
            results['cities'].append(city_results)

            # Todo: sort cities, and perhaps extract optimum
1d48272e   Antoine Goutenoir   Compute the mean ...
319

a4c03d8e   Antoine Goutenoir   Add the controlle...
320
321
322
323
324
325
326
327
    # 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...
328
    response += yaml_dump(results) + "\n"
4392f295   Goutte   Update the Estima...
329

24f55cde   Antoine Goutenoir   Geocode destinati...
330
    return _respond(response)
a4c03d8e   Antoine Goutenoir   Add the controlle...
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349


@main.route("/estimation/<public_id>.<format>")
def consult_estimation(public_id, format):
    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)

    if 'html' == format:
314c65e2   Antoine Goutenoir   Implement Scenari...
350

4561eddb   Antoine Goutenoir   Continue waiting ...
351
        if estimation.status in [StatusEnum.pending, StatusEnum.working]:
a4c03d8e   Antoine Goutenoir   Add the controlle...
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
            return render_template(
                "estimation-queue-wait.html",
                estimation=estimation
            )
        else:
            return render_template(
                "estimation.html",
                estimation=estimation
            )

    elif 'csv' == format:

        import csv
        from cStringIO import StringIO

        si = StringIO()
        cw = csv.writer(si, quoting=csv.QUOTE_ALL)
        cw.writerow([u"city", u"co2 (g)"])

        results = estimation.get_output_dict()
        for city_name in results['mean_footprint']['cities'].keys():
            cw.writerow([city_name, results['mean_footprint']['cities'][city_name]])

        # Where are the headers?
        return si.getvalue().strip('\r\n')

    else:
        abort(404)