-
Notifications
You must be signed in to change notification settings - Fork 0
/
server.py
457 lines (321 loc) · 14.2 KB
/
server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
"""Homespector Gadget"""
from collections import OrderedDict
from datetime import datetime
import os
# import string
from flask import Flask, render_template, request, flash, redirect, session, jsonify
from flask_debugtoolbar import DebugToolbarExtension
from jinja2 import StrictUndefined
from model import connect_to_db, db, User, Property, UserProperty
from utils import create_address_url, RGB_TUPLES, HEX_COLOR_STRINGS, make_marker_text, get_zoom_level
######################################################################################
app = Flask(__name__)
# Required to use Flask sessions and the debug toolbar
app.secret_key = "ABC"
# Normally, if you use an undefined variable in Jinja2, it fails silently.
# This is horrible. Fix this so that, instead, it raises an error.
app.jinja_env.undefined = StrictUndefined
mapbox_api_key = os.environ["MAPBOX_KEY"]
foursq_clientid = os.environ["FOURSQ_CLIENTID"]
foursq_clientsecret = os.environ["FOURSQ_CLIENTSECRET"]
###################################
# General registration and login #
###################################
@app.route('/', methods=['GET','POST'])
def index():
"""Base.html"""
return render_template("base.html")
@app.route('/homepage')
def homepage():
"""Homepage"""
return render_template("homepage.html")
@app.route('/register', methods=['GET'])
def register_form():
"""Show form for user signup."""
return render_template('registration-form.html')
@app.route('/register', methods=['POST'])
def register_process():
"""Process registration."""
user_email = request.form['email']
user = User.query.filter(User.email == user_email).first()
if user is None:
# Get form variables
fname = request.form["fname"]
lname = request.form["lname"]
email = request.form["email"]
password = request.form["password"]
zipcode = request.form["zipcode"]
new_user = User(fname=fname, lname=lname, email=email, password=password, zipcode=zipcode)
db.session.add(new_user)
db.session.commit()
return render_template("registration-confirm.html", error_message = "New user %s added." % email)
else:
return render_template("error-dialog.html", error_message = "This email is already Registered.\n Please login or register with a different email.")
@app.route('/login', methods=['GET'])
def login_form():
"""Show login form."""
return render_template("login-form.html")
@app.route('/login', methods=['POST'])
def login_process():
"""Process login."""
# Get form variables
email = request.form["email"]
password = request.form["password"]
user = User.query.filter_by(email=email).first()
if not user:
flash("No such user. Please register an account.")
return "register"
# if user.password != password:
if not user.check_password(password):
flash("Incorrect password")
return "incorrectlogin"
session["user_id"] = user.user_id
liked = UserProperty.query.filter_by(user_id=user.user_id).all()
liked = [x.zpid for x in liked]
session['properties'] = liked
session['comp_table'] = []
session['used_color_map']={}
for zpid in liked:
if str(zpid) not in session['used_color_map']:
rgb_tuple = RGB_TUPLES.pop()
r,g,b = rgb_tuple
hex_color_string = HEX_COLOR_STRINGS.pop()
color_map = {'r': r, 'g': g, 'b': b, 'hex': hex_color_string}
session['used_color_map'][str(zpid)] = color_map
flash("Hello, %s!" % user.fname)
return render_template("session-login.html", session=session)
@app.route('/logout')
def logout():
"""Log out."""
del session["user_id"]
if session['properties']:
del session['properties']
if session['comp_table']:
del session['comp_table']
flash("Logged Out.")
return redirect("/")
##########################################################
# Searching, populating session, side column and table #
##########################################################
@app.route('/search', methods=['GET'])
def parse_address_search():
"""Parses the address for API call"""
if request.args:
raw_address_text = request.args.get("address-search")
address_url_encode, citystatezip_url_encode, address_for_walkscore = create_address_url(raw_address_text)
property_from_url, error_code = Property.generate_from_address(address=address_url_encode,
citystatezip=citystatezip_url_encode)
#instantiate a session
if 'properties' not in session.keys():
session['properties'] = []
if error_code == Property.ERROR_OK:
if property_from_url.zpid not in session['properties']:
session['properties'].append(property_from_url.zpid)
this_property = Property.query.filter(Property.zpid == property_from_url.zpid).first()
if this_property is None:
db.session.add(property_from_url)
db.session.commit()
this_property = property_from_url
return render_template("address-confirmation.html", house=this_property)
elif error_code == Property.ERROR_MANY:
message = "Ambiguous Result. Please check your address and specify a unique property."
return error_message_utility(message)
else:
message = "No property found. Please search again."
return error_message_utility(message)
@app.route("/delete-property", methods=['POST'])
def delete_property():
"""Delete the property from session
if wrong address was returned."""
zpid = request.form['Delete-Property']
props_in_list = session['properties']
index = props_in_list.index(zpid)
if index >=0 :
props_in_list.pop(index)
return "Deleted"
@app.route("/show-error", methods=['GET'])
def error_message_utility(message = None):
if message is None:
message = request.args.get('message', "")
return render_template("error-dialog.html", error_message = message)
@app.route("/property-list", methods=['GET'])
def get_propeties_list():
"""Have user confirm the search results.
Upon confirmation, add property to session.
Show list of properties stored in the session."""
#Get the properties stored in session or create an empty session
#Turn the list of properties into a set to get rid of repeats
props_in_set = set(session.get('properties',[]))
# Our output cart will be a dictionary (so we can easily see if we
# already have the property in there
properties = []
used_color_map = session.get('used_color_map', {})
# Loop over the ZPIDs in the session cart and add each one to
# the output cart
for zpid in props_in_set:
house_data = Property.query.get(zpid)
if house_data is not None:
properties.append(house_data)
if str(zpid) not in used_color_map:
rgb_tuple = RGB_TUPLES.pop()
r,g,b = rgb_tuple
hex_color_string = HEX_COLOR_STRINGS.pop()
color_map = {'r': r, 'g': g, 'b': b, 'hex': hex_color_string}
used_color_map[str(zpid)] = color_map
session['used_color_map'] = used_color_map
user_id = session.get('user_id')
liked = None
if user_id:
results = db.session.query(UserProperty.zpid).filter_by(user_id=user_id).all()
liked = [zpid for (zpid, ) in results]
props_in_table = [int(x) for x in session.get('comp_table',[])]
page_state = request.args.get('view-state', 0)
return render_template("left-column.html", properties=properties, liked=liked, props_in_table=props_in_table, used_color_map=used_color_map, page_state=int(page_state))
@app.route("/add-favorites", methods=['POST'])
def add_to_favorites():
"""Add a property to user's favorites list
Done with Ajax to add an record in the UserProperty table
linking the user_id to the zpid"""
user_id = session.get('user_id')
if user_id:
zpid = request.form.get('property')
liked = UserProperty.query.filter_by(user_id=user_id, zpid=zpid).first()
else:
raise Exception("No user logged in.")
if liked:
db.session.delete(liked)
else:
new_like = UserProperty(user_id=user_id,
zpid=zpid,
time_saved=datetime.utcnow())
db.session.add(new_like)
db.session.commit()
return "Victory"
@app.route("/delete-from-session", methods=['POST'])
def delete_from_session():
"""Delete a property from the session.
Also deletes a property from comparison table
when it's deleted from session.
Done with Ajax to take out the property from the table"""
global RGB_TUPLES
global HEX_COLOR_STRINGS
#delete from session
props_in_list = session.get('properties',[])
used_color_map = session.get('used_color_map',{})
zpid = request.form.get('property')
if zpid in props_in_list:
zpid_index = props_in_list.index(zpid)
props_in_list.pop(zpid_index)
if str(zpid) in used_color_map:
color_map = used_color_map[str(zpid)]
rgb_tuple = (color_map['r'], color_map['g'], color_map['b'])
hex_color_string = color_map['hex']
RGB_TUPLES.append(rgb_tuple)
HEX_COLOR_STRINGS.append(hex_color_string)
del used_color_map[str(zpid)]
zpids_in_table = session.get('comp_table',[])
if zpid in zpids_in_table:
zpid_index2 = zpids_in_table.index(zpid)
zpids_in_table.pop(zpid_index2)
return "Victory"
@app.route("/comparison-table", methods=['GET', 'POST'])
def generate_comparison_table():
"""Populate and change comparison table."""
zpids_in_table = set(session.get('comp_table',[]))
props_in_table = []
for zpid in zpids_in_table:
house = Property.query.get(zpid)
props_in_table.append(house)
return render_template("comparison-table.html", props_in_table=props_in_table)
@app.route('/clear-comparison-table', methods=['GET', 'POST'])
def clear_comp_table():
if 'comp_table' in session:
del session['comp_table']
return "Cleared, please go back to previous page"
return "Not cleared, but go back anyway"
@app.route("/update-comparison-table", methods=['GET','POST'])
def update_comp_table():
"""Adds or remove a property in comparison table"""
zpid = request.form.get('zpid')
is_in_table = request.form.get('is_in_table')
zpids_in_table = set(session.get('comp_table',[]))
result = 1
if is_in_table == "true":
if zpid in zpids_in_table:
zpids_in_table.remove(zpid)
else:
if zpid not in zpids_in_table:
if len(zpids_in_table) < 4:
zpids_in_table.add(zpid)
else:
flash("Too many!")
result = 0
zpids_in_table = list(zpids_in_table)
session['comp_table'] = zpids_in_table
return str(result)
#########
# Maps #
#########
def get_session_lonlats():
"""Helper function that creates a list of lon, lat tuples
for all the properties in the session."""
lonlat_list = []
props_in_list = session.get('properties',[])
for zpid in props_in_list:
this_house = Property.query.filter(Property.zpid == int(zpid)).first()
longitude = this_house.longitude
latitude = this_house.latitude
lonlat_list.append((zpid, longitude,latitude))
return lonlat_list
# DEFAULT MAP
# generate marker text for an API call of the form: {name}-{label}+{color}({lon},{lat})
# example : pin-l-park+482(-73.975,40.767)
# NAME MUST BE pin-l, pin-m or pin-s
# Text string for API call in util.py
@app.route("/default-map", methods=['GET'])
def show_default_map():
"""Get the longitude and latitudes from the get_session_lonlats.
Generate marker text from make_marker_text for each house.
Show the properties stored in session on a map
then allow to zoom in on properties to show pindrops
or heat maps"""
if len(session['properties']) > 0:
lonlat_tuples = get_session_lonlats()
marker_string_list = make_marker_text(lonlat_tuples)
marker_api_string = ",".join(marker_string_list)
imgwidth = 1024
imgheight = 650
#calculate map centers
lon_center = sum([float(lon) for zpid, lon, lat in lonlat_tuples])/len(lonlat_tuples)
lat_center = sum([float(lat) for zpid, lon, lat in lonlat_tuples])/len(lonlat_tuples)
#calculate bounds with max and mins
lon_max = max([float(lon) for zpid, lon, lat in lonlat_tuples])
lon_min = min([float(lon) for zpid, lon, lat in lonlat_tuples])
lat_max = max([float(lat) for zpid, lon, lat in lonlat_tuples])
lat_min = min([float(lat) for zpid, lon, lat in lonlat_tuples])
zoom_level = get_zoom_level(lat_max, lat_min, lon_max, lon_min, imgheight, imgwidth) if len(lonlat_tuples) > 1 else 16
lon_lat_zoom = str(lon_center) + ',' + str(lat_center) + ',' + str(zoom_level)
imgsize = str(imgwidth) + 'x' + str(imgheight)
new_src = 'https://api.mapbox.com/v4/mapbox.streets/' + marker_api_string + '/' + lon_lat_zoom + '/' + imgsize + '.png?access_token=' + mapbox_api_key
return render_template("map.html", imgwidth=imgwidth, imgheight=imgheight, src=new_src)
else:
return("<div id='map-error'><br><b>Please add at least one property to your list to map.</b><div>")
@app.route("/detailed-map", methods=['POST'])
def generate_detailed_map():
zpid = request.form.get('property')
query = request.form.get('query')
map_color_dict = session.get('used_color_map',{})
color = map_color_dict[zpid]['hex']
this_property = Property.query.filter(Property.zpid == zpid).first()
return render_template("detailed-map.html", house=this_property, color=color, query=query, placeholder_text=query if query is not None else "Search for places, e.g. coffee", MapboxKey=mapbox_api_key, FourSqID=foursq_clientid, FourSqSecret=foursq_clientsecret)
################################
if __name__ == "__main__":
# We have to set debug=True here, since it has to be True at the point
# that we invoke the DebugToolbarExtension
app.debug = True
connect_to_db(app)
# Use the DebugToolbar
DebugToolbarExtension(app)
app.run()
else:
connect_to_db(app)