/
models.py
397 lines (322 loc) · 12.1 KB
/
models.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
import logging
import datetime
import StringIO
import pickle
from google.appengine.ext import db
from google.appengine.ext import blobstore
from google.appengine.api import images
from google.appengine.api import urlfetch
from lib import EXIF
from django.utils import simplejson
from lib.geo.geomodel import GeoModel
class SpecializedJSONEncoder(simplejson.JSONEncoder):
""" Serializes additional objects beyond the default simplejson encoder """
def default(self, obj):
obj_type = type(obj)
if obj_type is datetime.datetime:
return obj.isoformat()
elif obj_type is db.GeoPt:
return ','.join(map(str, (obj.lat, obj.lon)))
elif obj_type is db.Key:
return obj.id_or_name()
elif isinstance(obj, JSONableModel):
return obj.serialize()
return simplejson.JSONEncoder.default(self, obj)
class JSONableModel(db.Model):
""" Generic model that is JSON serializable """
serializable = ()
def serialize(self):
properties = self.properties()
keys = properties.keys()
allowed_keys = filter(lambda x: x in self.serializable, keys)
obj = {}
for x in allowed_keys:
obj[x] = properties[x].get_value_for_datastore(self)
return obj
def to_json(self):
return simplejson.dumps(self.serialize(), cls=SpecializedJSONEncoder)
class User(JSONableModel):
""" Service User """
email = db.EmailProperty()
display_name = db.TextProperty()
serializable = ('email', 'display_name', )
def serialize(self):
obj = super(User, self).serialize()
obj['key'] = str(self.key())
return obj
def _read_EXIF(fobj):
return EXIF.process_file(fobj)
def ref_to_sign(r):
""" Convert EXIF.ASCII to a sign """
return 1 if r.values in ('N', 'E', 'T') else -1
def ratio_to_frac(r):
""" Converts EXIF.Ratio to a float """
return float(r.num) / float(r.den) if r.den is not 0 else float('nan')
def decimalify(*xs):
""" Turns an array of degrees minutes seconds into a decimal
Arg:
xs - a bunch of numbers representing DMS
Returns:
decimal representation
"""
s = 0
for x in reversed(xs):
s = x + s / 60.0
return s
def _iOS_coord_to_decimal(coord, ref=None):
""" Convert iOS photo EXIF to decimal
Args:
coord - EXIF GPS coordinate in DMS
ref - EXIF GPS coordinate ref
Returns:
decimal representation
"""
coord = map(ratio_to_frac, coord.values)
sign = 1 if not ref else ref_to_sign(ref)
return sign * decimalify(*coord)
class Photo(JSONableModel, GeoModel):
""" Photo
A Photo's information is available as a JSON object at:
GET /photos/<key>
Overview of information:
key - the string key that uniquely identifies this photo in the
service
taken_at - time the photo EXIF reports it was taken
caption - a caption specified by the user
location - the GPS coordinates the photo EXIF reported
location_geocells - http://geohash.org/
geoname - reverse geocode result from ws.geonames.org
user - a string key that uniquely identifies the photo's user
In order to get a photo's image data:
GET /photos/<key>/<OS>/<size>
where OS is the mobile operating system and size is one of the
following:
t - tiny
s - small
m - medium
f - full
The default size is full. Valid operating systems are:
iOS
iOS_retina
"""
user = db.ReferenceProperty(User, required=True)
caption = db.StringProperty()
taken_at = db.DateTimeProperty()
created_at = db.DateTimeProperty(auto_now_add=True)
updated_at = db.DateTimeProperty(auto_now=True)
exif = db.BlobProperty()
geoname = db.BlobProperty()
img_orig = blobstore.BlobReferenceProperty(required=True)
img_ios_t = db.BlobProperty()
img_ios_s = db.BlobProperty()
img_ios_m = db.BlobProperty()
img_ios_f = db.BlobProperty()
img_ios_r_t = db.BlobProperty()
img_ios_r_s = db.BlobProperty()
img_ios_r_m = db.BlobProperty()
img_ios_r_f = db.BlobProperty()
serializable = ('taken_at', 'caption', 'location', )
def put(self, **kwargs):
""" Postprocess img_orig """
saved = True
try:
self.key()
except db.NotSavedError:
saved = False
# Image, thumbnails, location are not mutable
if not saved:
self._postprocess()
return super(Photo, self).put(**kwargs)
def _img_orig_fobj(self):
if type(self.img_orig) is blobstore.BlobInfo:
return blobstore.BlobReader(self.img_orig, buffer_size=2**10)
else:
return StringIO.StringIO(self.img_orig)
def _get_geo_name(self):
""" Attempts to get the results of a reverse geocode """
try:
result = urlfetch.fetch(
'http://ws.geonames.org/findNearestAddressJSON?lat=%f&lng=%f' %
(self.location.lat, self.location.lon)).content
# TODO handle cases when server is overloaded.
except urlfetch.DownloadError:
result = 'Unknown'
except urlfetch.Error:
result = 'Unknown'
return simplejson.loads(result)
def _set_location(self, geopt):
""" Sets the location of the photo to geopt and updates the location
hash and reverse geocode results.
"""
self.location = geopt
self.geoname = pickle.dumps(self._get_geo_name())
# Sync geocell indexing
self.update_location()
def _postprocess(self):
""" Gather image data and make thumbnails.
1. Read and store EXIF
2. Update image information based on EXIF
2. Generate smaller sizes
"""
logging.info("Processing original image")
fobj = self._img_orig_fobj()
exif, byterange = _read_EXIF(fobj)
logging.info(byterange)
self.exif = pickle.dumps(exif)
logging.info("Got EXIF: \n%s" % exif)
lat = 0
try:
lat = _iOS_coord_to_decimal(
exif['GPS GPSLatitude'], exif['GPS GPSLatitudeRef'])
except KeyError:
pass
lon = 0
try:
lon = _iOS_coord_to_decimal(
exif['GPS GPSLongitude'], exif['GPS GPSLongitudeRef'])
except KeyError:
pass
try:
dir = _iOS_coord_to_decimal(
exif['GPS GPSImgDirection'], exif['GPS GPSImgDirectionRef'])
except KeyError:
pass
try:
alt = _iOS_coord_to_decimal(
exif['GPS GPSAltitude'], exif['GPS GPSAltitudeRef'])
except KeyError:
pass
try:
gpstime = _iOS_coord_to_decimal(
exif['GPS GPSTimeStamp'])
except KeyError:
pass
image_time = datetime.datetime.now()
try:
image_time = datetime.datetime.strptime(exif['Image DateTime'].values,
'%Y:%m:%d %H:%M:%S')
except KeyError:
pass
try:
info = exif['Image GPSInfo'].values
except KeyError:
pass
try:
horiz = True if exif['Image Orientation'].values else False
except KeyError:
pass
self.taken_at = image_time
self._set_location(db.GeoPt(lat, lon))
self._generate_thumbnails()
def _generate_thumbnail(self, width, height):
""" Generates a thumbnail of size width x height. Unfortunately the
only allowed output encodings are JPEG and PNG. Since this
operates on photos, JPEG has been selected.
"""
logging.info("Generating thumbnail %dx%d" % (width, height))
if type(self.img_orig) is blobstore.BlobInfo:
img = images.Image(blob_key=str(self.img_orig.key()))
else:
img = images.Image(image_data=self.img_orig)
img.resize(width, height)
# TODO rotate the image if necessary
# I'm feeling luckyify?
result = None
try:
result = img.execute_transforms(output_encoding=images.JPEG, quality=85)
except Exception, e:
logging.error("Transformations failed: %s" % e)
return result
def _generate_thumbnails(self):
self.img_ios_t = self._generate_thumbnail(50, 50)
self.img_ios_s = self._generate_thumbnail(61, 61)
self.img_ios_m = self._generate_thumbnail(125, 130)
self.img_ios_f = self._generate_thumbnail(290, 360)
self.img_ios_r_t = self.img_ios_t
self.img_ios_r_s = self._generate_thumbnail(122, 122)
self.img_ios_r_m = self._generate_thumbnail(250, 260)
self.img_ios_r_f = self._generate_thumbnail(520, 580)
def get_os_img(self, os='iOS', size='t'):
""" Returns a bytestring valid image of the corresponding OS and size.
Args:
os - the OS type
size - the size (one of t, s, m, l, o). The sizes are tiny,
small, medium, large, and original
"""
if os == 'iOS':
return self.get_iOS_img(size)
elif os == 'iOS_retina':
return self.get_iOS_retina_img(size)
def get_iOS_img(self, size='t'):
if size == 't':
return self.img_ios_t
elif size == 's':
return self.img_ios_s
elif size == 'm':
return self.img_ios_m
elif size == 'f':
return self.img_ios_f
else:
return self.img_ios_f
def get_iOS_retina_img(self, size='t'):
if size == 't':
return self.img_ios_r_t
elif size == 's':
return self.img_ios_r_s
elif size == 'm':
return self.img_ios_r_m
elif size == 'f':
return self.img_ios_r_f
else:
return self.img_ios_r_f
def thumbs(self):
""" Get summary of number of thumbs up """
return self.thumb_set.filter('up', True).count() - \
self.thumb_set.filter('up', False).count()
def serialize(self):
obj = super(Photo, self).serialize()
obj['key'] = str(self.key())
obj['user'] = str(self.user.key())
obj['thumbs'] = self.thumbs()
obj['comments'] = [x for x in self.comment_set]
obj['tags'] = [x for x in self.tag_set]
obj['geoname'] = pickle.loads(self.geoname)
return obj
class Tag(JSONableModel):
""" A photo can be tagged by a user with a phrase.
TODO perhaps expand this to also allow tagging an area with a phrase?
"""
tag = db.StringProperty(required=True, multiline=True)
photo = db.ReferenceProperty(Photo, required=True)
user = db.ReferenceProperty(User, required=True)
created_at = db.DateTimeProperty(auto_now_add=True)
serializable = ('tag', 'created_at', )
def serialize(self):
obj = super(Tag, self).serialize()
obj['photo'] = str(self.photo.key())
obj['user'] = str(self.user.key())
return obj
class Thumb(JSONableModel):
""" A thumb is a representation of a user's opinion on a photo. A user can
only change a thumb up or down once they have given an opinion.
"""
up = db.BooleanProperty(required=True)
user = db.ReferenceProperty(User, required=True)
photo = db.ReferenceProperty(Photo, required=True)
serializable = ('up', )
def serialize(self):
obj = super(Thumb, self).serialize()
obj['photo'] = str(self.photo.key())
obj['user'] = str(self.user.key())
return obj
class Comment(JSONableModel):
comment = db.StringProperty(required=True, multiline=True)
photo = db.ReferenceProperty(Photo, required=True)
user = db.ReferenceProperty(User, required=True)
created_at = db.DateTimeProperty(auto_now_add=True)
serializable = ('comment', 'created_at', )
def serialize(self):
obj = super(Comment, self).serialize()
obj['photo'] = str(self.photo.key())
obj['user'] = str(self.user.key())
return obj