class WineTasting(BaseModel): consumption_type = ndb.StringProperty() consumption_location = ndb.StringProperty() consumption_location_name = ndb.StringProperty() date = ndb.DateProperty() rating = ndb.IntegerProperty() flawed = ndb.BooleanProperty(default=False) note = ndb.StringProperty() def config(self, data): self.apply([ "consumption_type", "consumption_location", "consumption_location_name", "date", "rating", "flawed", "note" ], data) def create(self, data): self.config(data) key = self.put() return key def modify(self, data): self.config(data) key = self.put() return key def delete(self): self.key.delete()
class Event(BaseModel): """ User <X> has created model <Key> at time <Z> """ user = ndb.StringProperty(required=True, indexed=False) action = ndb.StringProperty(required=True, choices=actions) model = ndb.KeyProperty(required=True) model_type = ndb.StringProperty(required=True) time = ndb.DateTimeProperty(auto_now_add=True) @staticmethod def add_event(event, user, model_type, model): e = Event() e.action = event e.user = user e.model = model e.model_type = model_type return e.put() @staticmethod def create(user, model_type, model): Event.add_event("CREATE", user, model_type, model) @staticmethod def update(user, model_type, model): Event.add_event("UPDATE", user, model_type, model) @staticmethod def delete(user, model_type, model): Event.add_event("DELETE", user, model_type, model)
class UserWine(BaseModel): user = ndb.KeyProperty() drink_after = ndb.DateProperty() drink_before = ndb.DateProperty() tags = ndb.StringProperty(repeated=True) # list of tags def config(self, data): self.apply(["drink_before", "drink_after"], data) if 'user' in data: self.user = data['user'].key del data['user'] if 'tags' in data: tags = json.loads(data['tags']) if type(tags) != list: tags = [tags] self.tags = list(set(self.tags + tags)) del data['tags'] def create(self, data): self.config(data) key = self.put() return key def modify(self, data): self.config(data) key = self.put() return key def delete(self): self.key.delete()
class WineBottle(BaseModel): # Parent: wine wine = ndb.KeyProperty() bottle_size = ndb.StringProperty() purchase_date = ndb.DateProperty() purchase_location = ndb.StringProperty() storage_location1 = ndb.StringProperty() storage_location2 = ndb.StringProperty() cost = ndb.IntegerProperty() # Stored as number of cents consumed = ndb.BooleanProperty(default=False) consumed_date = ndb.DateProperty() json = ndb.JsonProperty(indexed=False) def config(self, data): self.apply(['bottle_size', 'purchase_date', 'purchase_location', 'storage_location1', 'storage_location2', 'consumed', 'consumed_date'], data) if 'cost' in data and data['cost'] != '': self.cost = int(float(data['cost']) * 100) del data['cost'] json = BaseModel.tidy_up_the_post_object(data) self.json = json key = self.put() return key def create(self, data): self.config(data) key = self.put() return key def modify(self, data): self.config(data) key = self.put() return key def delete(self): self.key.delete()
class WineCellar(BaseModel): name = ndb.StringProperty() def create(self, data): self.apply(["name"], data) key = self.put() return key def modify(self, data): self.apply(["name"], data) key = self.put() return key def delete(self): self.key.delete()
class User(BaseModel): name = ndb.StringProperty() guser = ndb.UserProperty() email = ndb.StringProperty() cellar = ndb.KeyProperty() def create(self, data): logging.info("adding: " + data['email']) self.apply(['name', 'email', 'guser'], data) key = self.put() return key def delete(self): self.key.delete() @staticmethod def get_current_user(): guser = users.get_current_user() if guser is None: raise Exception("Not Authenticated") qry = User.query(User.guser == guser) results = qry.fetch(1) if len(results) <= 0: # create the user user = User() user.create({ 'guser': guser, 'name': guser.nickname(), 'email': guser.email() }) return user else: return results[0] @staticmethod def has_access(cellar_key): return User.get_current_user().cellar == cellar_key
class Wine(BaseModel): # Parent: Winery year = ndb.IntegerProperty() name = ndb.StringProperty() winetype = ndb.StringProperty(required=True, choices=wine_types.types) varietal = ndb.StringProperty() upc = ndb.StringProperty() @property def has_year(self): return 'year' in self @property def has_name(self): return 'name' in self @property def has_winetype(self): return 'winetype' in self @property def has_varietal(self): return 'varietal' in self @property def has_upc(self): return 'upc' in self verified = ndb.BooleanProperty(default=False) verified_by = ndb.StringProperty() @property def has_verified(self): return 'verified' in self @property def has_verified_by(self): return 'verified_by' in self json = ndb.JsonProperty(indexed=False) # JSON properties encompass the bits that might want to be tracked # but are too fiddly to keep track of regularly, like # bud_break, veraison, oak_regime, barrel_age, harvest_date, # bottling_date, alcohol_content, winemaker_notes, vineyard_notes... def has_json(self): return 'json' in self def create(self, post, winery): """ >>> v = Winery() >>> v_key = v.create({'name':'Winery'}) >>> w = Wine(parent=v_key) >>> post = {} >>> post['name'] = 'Red Character' >>> post['varietal'] = 'Blend' >>> post['winetype'] = 'Red' >>> post['upc'] = '1234567890' >>> post['blend'] = ['Merlot', 'Cabernet Franc'] >>> post['token'] = "stub-uuid" >>> w_key = w.create(post, v) >>> w.name 'Red Character' >>> w.varietal 'Blend' >>> w.winetype 'Red' >>> w.to_dict()['json']['blend'] ['Merlot', 'Cabernet Franc'] >>> w.verified True >>> w.verified_by 'Winery' """ name = None if 'name' in post: name = post['name'] self.name = name del post['name'] year = None if 'year' in post: year = int(post['year']) self.year = year del post['year'] winetype = None if 'winetype' in post: winetype = post['winetype'] if not winetype in wine_types.types: raise ValueError("Invalid Wine Type: " + str(winetype)) else: self.winetype = winetype del post['winetype'] varietal = None if 'varietal' in post: varietal = post['varietal'] self.varietal = varietal del post['varietal'] upc = None if 'upc' in post: upc = post['upc'] del post['upc'] if not winetype: raise ValueError("You must provide a winetype") if not name and not year and not varietal and not upc: raise ValueError("You must provide a name, year, " + " varietal, or upc.") token = None if 'token' in post: token = post['token'] self.verify(token, winery) del post['token'] json = BaseModel.tidy_up_the_post_object(post) self.json = json key = self.put() return key def modify(self, post, winery): """ >>> v = Winery() >>> v_key = v.create({"name":"Winery"}) >>> w = Wine(parent=v_key) >>> w_key = w.create({"winetype":"Red", "year":"2010"}, v) The base case. >>> w.modify({"name":"Red Character", "terroir":"Good"}, v) >>> w.to_dict()["name"] 'Red Character' >>> w.to_dict()["json"]["terroir"] 'Good' >>> w2 = Wine(parent=v_key) >>> w2.modify({"winetype":"White", "year":"2011"}, v) >>> w2.to_dict()["winetype"] 'White' >>> w2.to_dict()["year"] 2011 >>> w.modify({"varietal":"Blend"}, v) >>> w.to_dict()["varietal"] 'Blend' >>> w.modify({"upc":"123456789"}, v) >>> w.to_dict()["upc"] '123456789' You can't replace a field. >>> w.modify({"winetype":"White"}, v) Traceback (most recent call last): ... YouNeedATokenForThat... >>> w.modify({"terroir": "at danger lake!"}, v) Traceback (most recent call last): ... YouNeedATokenForThat... Should be fine if you update a thing with itself. >>> w.modify({"winetype":"Red"}, v) >>> w.modify({"terroir":"Good"}, v) You can update whatever you like if you have a token. >>> w.modify({"winetype":"White", "token":"stub-uuid"}, v) >>> w.to_dict()["winetype"] 'White' """ def field_edit_error(fieldname): return YouNeedATokenForThat(("You can't edit fields that already" + " exist: %s" % fieldname)) can_edit_fields = False if 'token' in post: token = post['token'] can_edit_fields = self.verify(token, winery) del post['token'] def can_edit_field(field_name): if field_name in post: if not field_name in self: return True if post[field_name] == str(self.to_dict()[field_name]): return True if can_edit_fields: return True raise field_edit_error(field_name) else: return False name = None if can_edit_field('name'): name = post['name'] self.name = name del post['name'] year = None if can_edit_field('year'): year = int(post['year']) self.year = year del post['year'] winetype = None if can_edit_field('winetype'): winetype = post['winetype'] if not winetype in wine_types.types: raise ValueError("Invalid Wine Type: " + str(winetype)) else: self.winetype = winetype del post['winetype'] varietal = None if can_edit_field('varietal'): varietal = post['varietal'] self.varietal = varietal del post['varietal'] upc = None if can_edit_field('upc'): upc = post['upc'] self.upc = upc del post['upc'] json = BaseModel.tidy_up_the_post_object(post) if 'json' in self.to_dict(): new_json = self.to_dict()['json'] for key, value in json.iteritems(): if key in new_json and can_edit_fields: new_json[key] = value if not (key in new_json): new_json[key] = value if key in new_json and new_json[key] == value: pass else: raise field_edit_error(key) self.json = new_json else: self.json = json key = self.put() return None def calculate_rank(self): rank = 0 if self.has_verified: rank += 24 if self.has_year: rank += 2 if self.has_name: rank += 2 if self.has_winetype: rank += 2 if self.has_varietal: rank += 4 if self.has_upc: rank += 8 if self.has_json: for key in self.to_dict()['json']: rank += 1 return rank def verify(self, token=None, winery=None): """ Sets self.verified and self.verified_by, True if the token is valid False if not """ if not token: return False if winery and token == winery.private_token: self.verified = True self.verified_by = "Winery" return True return False def update(self, winery): # get all wines for this winery wines = Wine.winery_query(winery) # remove the wine we just edited wines = [wine for wine in wines if self.key.id() != wine.key.id()] # add the wine we just edited wines.append(self) # recalculate the winery rank winery.update(wines) winery.put() # recreate search indexes for every wine under the winery # with the new winery rank as their search index. for wine in wines: wine.create_search_index(winery) def create_search_index(self, winery): """ create a search index for this wine """ if not self.key: raise Exception("Can't update without a key.") index = search.Index(name="wines") searchkey = str(self.key.id()) # search rank is winery rank + individual rank rank = winery.to_dict()['rank'] + self.calculate_rank() fields = [] if self.has_year: year = self.to_dict()['year'] fields.append(search.NumberField(name='year', value=year)) if self.has_name: name = self.to_dict()['name'] partial_name = BaseModel.partial_search_string(name) fields.append(search.TextField(name='name', value=name)) fields.append( search.TextField(name='partial_name', value=partial_name)) winery_name = winery.to_dict()['name'] partial_winery_name = BaseModel.partial_search_string(winery_name) fields.append(search.TextField(name='winery', value=winery_name)) fields.append( search.TextField(name='partial_winery', value=partial_winery_name)) if self.has_winetype: winetype = self.to_dict()['winetype'] fields.append(search.AtomField(name='winetype', value=winetype)) if self.has_varietal: varietal = self.to_dict()['varietal'] partial_varietal = BaseModel.partial_search_string(varietal) fields.append(search.TextField(name='varietal', value=varietal)) fields.append( search.TextField(name='partial_varietal', value=partial_varietal)) if self.has_upc: upc = self.to_dict()['upc'] fields.append(search.TextField(name='upc', value=upc)) fields.append( search.AtomField(name='verified', value=str(self.has_verified))) fields.append(search.TextField(name='id', value=str(self.key.id()))) fields.append( search.TextField(name='winery_id', value=str(winery.key.id()))) fields.append(search.NumberField(name='rank', value=rank)) searchdoc = search.Document(doc_id=searchkey, fields=fields, rank=rank) index.put(searchdoc) return None @staticmethod def winery_query(winery): qry = Wine.query(ancestor=winery.key) results = qry.fetch(MAX_RESULTS) return [x for x in results]
class Winery(BaseModel): """ Represents a single wine producer. """ name = ndb.StringProperty(required=True) location = ndb.StringProperty(choices=regions.location_list) location_fuzzy = ndb.StringProperty() @property def has_location(self): return 'location' in self @property def has_location_fuzzy(self): return 'location_fuzzy' in self # these fields are calculated from location # and can't be set from the web interface country = ndb.StringProperty(choices=regions.countries) region = ndb.StringProperty(choices=regions.regions) subregion = ndb.StringProperty(choices=regions.subregions) @property def has_country(self): return 'country' in self @property def has_region(self): return 'region' in self @property def has_subregion(self): return 'subregion' in self #security stuff verified = ndb.BooleanProperty(default=False) verified_by = ndb.StringProperty() private_token = ndb.StringProperty(indexed=False) rank = ndb.IntegerProperty(required=True, default=0) @property def has_verified(self): return 'verified' in self @property def has_verified_by(self): return 'verified_by' in self json = ndb.JsonProperty(indexed=False) @property def has_json(self): return 'json' in self # website, phone_number def create(self, post): """ Given a dict 'post' object containing winery-y fields, populate and put this Winery object, return the key. Throws a ValueError if no 'name' is included. >>> v_for_winery = Winery() >>> v_for_winery.create({}) Traceback (most recent call last): ... ValueError: 'name' field is mandatory Basic use-case test. >>> location = 'Canada - British Columbia: Okanagan Valley' >>> post = {'name':'Super Winery', 'location':location } >>> v_for_winery = Winery() >>> database_key = v_for_winery.create(post) >>> v_for_winery.country u'Canada' >>> v_for_winery.region u'British Columbia' >>> v_for_winery.subregion u'Okanagan Valley' >>> database_key.id() 'stub-key' If the location isn't in our list, it goes into location_fuzzy >>> location = 'Other' >>> post = {'name':'Super Winery 2', 'location':location} >>> v_for_winery = Winery() >>> database_key = v_for_winery.create(post) >>> v_for_winery.to_dict() {...'location_fuzzy': 'Other', ...} >>> v_for_winery.to_dict()['location_fuzzy'] 'Other' Winerys get a private token, which doesn't appear in to_dict() >>> v_for_winery.private_token 'stub-uuid' >>> v_for_winery.to_dict()['private_token'] Traceback (most recent call last): ... KeyError: 'private_token' Fields that we don't have database rows for go into the JSON. >>> post = {'name':'Super Winery 3', 'other_field':'glerg'} >>> v_for_winery = Winery() >>> database_key = v_for_winery.create(post) >>> v_for_winery.json {'other_field': 'glerg'} """ if not 'name' in post: raise ValueError("'name' field is mandatory") name = post['name'] self.name = name del post['name'] if 'location' in post: location = post['location'] self.set_location(location) del post['location'] self.private_token = uuid.uuid4().hex # special: winerys + verifieds are magic if 'token' in post: self.verify(post['token']) del post['token'] # having taken care of 'name' and 'location', # we wedge everything else into the JSON json = BaseModel.tidy_up_the_post_object(post) self.json = json self.rank = self.calculate_rank() key = self.put() return key def modify(self, post): """ Given a dict 'post' object containing winery-y fields, update this object. Empty fields can always be updated. Fields cannot be overwritten without a valid winery or verifier token. If this object has been verified, no changes may occur without a valid winery or verifier token. Throws a YouNeedATokenForThat error if the user tries something he doesn't have access to without a token Let's create a Winery to update... >>> location = 'Canada - British Columbia: Okanagan Valley' >>> post = {'name':'Super Winery' } >>> post['location_fuzzy'] = 'Somewhere' >>> post['json_thing'] = 'blerg' >>> v = Winery() >>> v.location <truth.stubs.StringProperty ...> >>> database_key = v.create(post) And update the location: >>> new_post = {'location':location} >>> new_post['json_other_thing'] = 'blorg' >>> v.modify(new_post) >>> v.country u'Canada' >>> v.to_dict()['json']['json_other_thing'] 'blorg' Updates of existing fields should fail. >>> other_location = 'Canada - British Columbia: Similkameen Valley' >>> v.modify({'location':other_location}) Traceback (most recent call last): ... YouNeedATokenForThat... Updates of json fields that already exist should fail. >>> v.modify({'json_other_thing':'beep boop'}) Traceback (most recent call last): ... YouNeedATokenForThat... >>> v.to_dict()['json']['json_other_thing'] 'blorg' Updating a thing with itself should be fine. >>> v.modify({'name':'Super Winery'}) >>> v.modify({'json_other_thing':'blorg'}) >>> v.modify({'location':location}) You can update whatever you want if you have a stub. >>> v.modify({'location':other_location, 'token':'stub-uuid'}) >>> v.subregion u'Similkameen Valley' Once a token has touched something it can still be changed. >>> v.modify({'completely_new_field':'hurfdorf'}) """ def field_edit_error(fieldname): return YouNeedATokenForThat(("You can't edit fields that already" + " exist: %s" % fieldname)) can_edit_fields = False if 'token' in post: can_edit_fields = self.verify(post['token']) del post['token'] if 'name' in post and post['name'] != self.to_dict()['name']: if not can_edit_fields: raise field_edit_error('name') else: self.name = post['name'] del post['name'] # if new location == old location, fugeddaboutit if ('location' in post and self.has_location and post['location'] == self.to_dict()['location']): del post['location'] if 'location' in post: location = post['location'] # a proper location can trump a location_fuzzy if (not self.has_location and self.has_location_fuzzy and location in regions.location_list): self.set_location(location) self.location_fuzzy = None # a more specific location can trump a less specific location # something trumps nothing elif (not self.has_location and not self.has_location_fuzzy): self.set_location(location) elif can_edit_fields: self.set_location(location) else: raise field_edit_error('location') del post['location'] json = BaseModel.tidy_up_the_post_object(post) if self.has_json: new_json = self.to_dict()['json'] for key, value in json.iteritems(): if key in new_json and can_edit_fields: new_json[key] = value if not (key in new_json): new_json[key] = value if key in new_json and new_json[key] == value: pass else: raise field_edit_error(key) self.json = new_json else: self.json = json self.put() def verify(self, token=None): """ If the token doesn't exist, return False If the token exists and belongs to the winery, return "True", and set self.verified and self.verified_by If the token exists and belongs to a verifier, return "True", and set self.verified and self.verified_by If the token exists but doesn't belong, return False >>> v = Winery() >>> v.create({'name':'Winery'}) <truth.stubs.Key object...> >>> v.private_token 'stub-uuid' >>> v.verify('butts') False >>> v.has_verified False >>> v.verify('stub-uuid') True >>> v.verified_by 'Winery' """ if not token: return False if token == self.private_token: self.verified = True self.verified_by = "Winery" return True return False def update(self, wines=[]): """ Recalculate this object's rank, and create a search index for it. """ if not self.key: raise Exception("Can't update without a key.") index = search.Index(name="wineries") searchkey = str(self.key.id()) self.rank = self.calculate_rank(wines) location = "" if self.has_location: location = self.to_dict()['location'] elif self.has_location_fuzzy: location = self.to_dict()['location_fuzzy'] partial_location = BaseModel.partial_search_string(location) fields = [] name = self.to_dict()['name'] partial_name = BaseModel.partial_search_string(name) fields.append(search.TextField(name='name', value=name)) fields.append(search.TextField(name='partial_name', value=partial_name)) fields.append(search.TextField(name='location', value=location)) fields.append( search.TextField(name='partial_location', value=partial_location)) if self.has_country: country = self.to_dict()['country'] fields.append(search.AtomField(name='country', value=country)) if self.has_region: region = self.to_dict()['region'] fields.append(search.AtomField(name='region', value=region)) if self.has_subregion: subregion = self.to_dict()['subregion'] fields.append(search.AtomField(name='subregion', value=subregion)) fields.append( search.AtomField(name='verified', value=str(self.has_verified))) fields.append(search.NumberField(name='rank', value=self.rank)) fields.append(search.TextField(name='id', value=str(self.key.id()))) searchdoc = search.Document(doc_id=searchkey, fields=fields, rank=self.rank) index.put(searchdoc) return None def calculate_rank(self, wines=[]): """ A potentially expensive operation, to try to quantify how much data this Winery object contains. More is better. """ rank = 0 if self.has_verified and self.has_verified_by: rank += 10000 if self.has_country: rank += 50 if self.has_region: rank += 100 if self.has_subregion: rank += 200 if not self.has_location and self.has_location_fuzzy: rank += 50 if self.has_json: for key in self.to_dict()['json']: rank += 10 for wine in wines: rank += wine.calculate_rank() return rank def to_dict(self): """ Get the contents of this model as a dictionary. Never includes 'private_token' Tries to include 'key' """ dict_ = copy.deepcopy(super(Winery, self).to_dict()) if 'private_token' in dict_: del dict_['private_token'] try: if self.key: dict_['key'] = self.key.id() except AttributeError: pass return dict_ def set_location(self, location): """ Given a location from the list of locations provided by regions.py (i.e. "Canada - British Columbia: Okanagan Valley") parse it into country, region, and subregion, and save those to the model. >>> v = Winery() >>> v.set_location("Canada - British Columbia: Okanagan Valley") >>> v.country u'Canada' >>> v.region u'British Columbia' >>> v.subregion u'Okanagan Valley' >>> v = Winery() >>> v.set_location("What is this I don't even") >>> v.country <truth.stubs.StringProperty instance at ...> >>> v.location_fuzzy "What is this I don't even" """ if location not in regions.location_list: self.location_fuzzy = location else: country, region, subregion = regions.location_map[location] self.location = location if country: self.country = country if region: self.region = region if subregion: self.subregion = subregion @staticmethod def all_query(): qry = Winery.query() results = qry.fetch( MAX_RESULTS, projection=[Winery.name, Winery.verified, Winery.location]) return [x for x in results] @staticmethod def country_query(country): qry = Winery.query(Winery.country == country) results = qry.fetch( MAX_RESULTS, projection=[Winery.name, Winery.verified, Winery.location]) return [x for x in results] @staticmethod def region_query(region): qry = Winery.query(Winery.region == region) results = qry.fetch( MAX_RESULTS, projection=[Winery.name, Winery.verified, Winery.location]) return [x for x in results] @staticmethod def subregion_query(subregion): qry = Winery.query(Winery.subregion == subregion) results = qry.fetch( MAX_RESULTS, projection=[Winery.name, Winery.verified, Winery.location]) return [x for x in results] @staticmethod def name_query(name): qry = Winery.query(Winery.name == name) results = qry.fetch(MAX_RESULTS, projection=[Winery.verified, Winery.location]) return [x for x in results] @staticmethod def verified_query(verified): qry = Winery.query(Winery.verified == verified) results = qry.fetch(MAX_RESULTS, projection=[Winery.name, Winery.location]) return [x for x in results] @staticmethod def verified_by_query(verified_by): qry = Winery.query(Winery.verified_by == verified_by) results = qry.fetch(MAX_RESULTS, projection=[Winery.name, Winery.location]) return [x for x in results] @staticmethod def location_query(location): qry = Winery.query(Winery.location == location) results = qry.fetch(MAX_RESULTS, projection=[Winery.name, Winery.verified]) return [x for x in results] @staticmethod def location_fuzzy_query(location): qry = Winery.query(Winery.location_fuzzy == location) results = qry.fetch(MAX_RESULTS, projection=[Winery.name, Winery.verified]) return [x for x in results] @staticmethod def all_fuzzy_locations(): qry = Winery.query(Winery.location_fuzzy is not None) results = qry.fetch(MAX_RESULTS) return [x.location_fuzzy for x in results] @staticmethod def search(query): query = "partial_name = " + query winery_index = search.Index(name="wineries") winery_query = search.Query(query_string=query) winery_results = winery_index.search(winery_query) return [ndb.Key(Winery, int(x.doc_id)).get() for x in winery_results]