add demonstration of geosearch
[gae-samples.git] / search / product_search_python / models.py
blob9287f2a30e88729d65acca2de4f50ce0aae9885d
1 #!/usr/bin/env python
3 # Copyright 2012 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """ Contains the Datastore model classes used by the app: Category, Product,
18 and Review.
19 Each Product entity will have a corresponding indexed "product" search.Document.
20 Product entities contain a subset of the fields in their corresponding document.
21 Product Review entities are not indexed (do not have corresponding Documents).
22 Reviews include a product id field, pointing to their 'parent' product, but
23 are not part of the same entity group, thus avoiding contention in
24 scenarios where a large number of product reviews might be edited/added at once.
25 """
27 import logging
29 import categories
30 import docs
32 from google.appengine.api import memcache
33 from google.appengine.ext import ndb
36 class Category(ndb.Model):
37 """The model class for product category information. Supports building a
38 category tree."""
40 _CATEGORY_INFO = None
41 _CATEGORY_DICT = None
42 _RCATEGORY_DICT = None
43 _ROOT = 'root' # the 'root' category of the category tree
45 parent_category = ndb.KeyProperty()
47 @property
48 def category_name(self):
49 return self.key.id()
51 @classmethod
52 def buildAllCategories(cls):
53 """ build the category instances from the provided static data, if category
54 entities do not already exist in the Datastore. (see categories.py)."""
56 # Don't build if there are any categories in the datastore already
57 if cls.query().get():
58 return
59 root_category = categories.ctree
60 cls.buildCategory(root_category, None)
62 @classmethod
63 def buildCategory(cls, category_data, parent_key):
64 """build a category and any children from the given data dict."""
66 if not category_data:
67 return
68 cname = category_data.get('name')
69 if not cname:
70 logging.warn('no category name for %s', category)
71 return
72 if parent_key:
73 cat = cls(id=cname, parent_category=parent_key)
74 else:
75 cat = cls(id=cname)
76 cat.put()
78 children = category_data.get('children')
79 # if there are any children, build them using their parent key
80 cls.buildChildCategories(children, cat.key)
82 @classmethod
83 def buildChildCategories(cls, children, parent_key):
84 """Given a list of category data structures and a parent key, build the
85 child categories, with the given key as their entity group parent."""
86 for cat in children:
87 cls.buildCategory(cat, parent_key)
89 @classmethod
90 def getCategoryInfo(cls):
91 """Build and cache a list of category id/name correspondences. This info is
92 used to populate html select menus."""
93 if not cls._CATEGORY_INFO:
94 cls.buildAllCategories() #first build categories from data file
95 # if required
96 cats = cls.query().fetch()
97 cls._CATEGORY_INFO = [(c.key.id(), c.key.id()) for c in cats
98 if c.key.id() != cls._ROOT]
99 return cls._CATEGORY_INFO
101 class Product(ndb.Model):
102 """Model for Product data. A Product entity will be built for each product,
103 and have an associated search.Document. The product entity does not include
104 all of the fields in its corresponding indexed product document, only 'core'
105 fields."""
107 doc_id = ndb.StringProperty() # the id of the associated document
108 price = ndb.FloatProperty()
109 category = ndb.StringProperty()
110 # average rating of the product over all its reviews
111 avg_rating = ndb.FloatProperty(default=0)
112 # the number of reviews of that product
113 num_reviews = ndb.IntegerProperty(default=0)
114 active = ndb.BooleanProperty(default=True)
115 # indicates whether the associated document needs to be re-indexed due to a
116 # change in the average review rating.
117 needs_review_reindex = ndb.BooleanProperty(default=False)
119 @property
120 def pid(self):
121 return self.key.id()
123 def reviews(self):
124 """Retrieve all the (active) associated reviews for this product, via the
125 reviews' product_key field."""
126 return Review.query(
127 Review.active == True,
128 Review.rating_added == True,
129 Review.product_key == self.key).fetch()
131 @classmethod
132 def updateProdDocsWithNewRating(cls, pkeys):
133 """Given a list of product entity keys, check each entity to see if it is
134 marked as needing a document re-index. This flag is set when a new review
135 is created for that product, and config.BATCH_RATINGS_UPDATE = True.
136 Generate the modified docs as needed and batch re-index them."""
138 doclist = []
140 def _tx(pid):
141 prod = cls.get_by_id(pid)
142 if prod and prod.needs_review_reindex:
144 # update the associated document with the new ratings info
145 # and reindex
146 modified_doc = docs.Product.updateRatingInDoc(
147 prod.doc_id, prod.avg_rating)
148 if modified_doc:
149 doclist.append(modified_doc)
150 prod.needs_review_reindex = False
151 prod.put()
152 for pkey in pkeys:
153 ndb.transaction(lambda: _tx(pkey.id()))
154 # reindex all modified docs in batch
155 docs.Product.add(doclist)
157 @classmethod
158 def create(cls, params, doc_id):
159 """Create a new product entity from a subset of the given params dict
160 values, and the given doc_id."""
161 prod = cls(
162 id=params['pid'], price=params['price'],
163 category=params['category'], doc_id=doc_id)
164 prod.put()
165 return prod
167 def update_core(self, params, doc_id):
168 """Update 'core' values from the given params dict and doc_id."""
169 self.populate(
170 price=params['price'], category=params['category'],
171 doc_id=doc_id)
173 @classmethod
174 def updateProdDocWithNewRating(cls, pid):
175 """Given the id of a product entity, see if it is marked as needing
176 a document re-index. This flag is set when a new review is created for
177 that product. If it needs a re-index, call the document method."""
179 def _tx():
180 prod = cls.get_by_id(pid)
181 if prod and prod.needs_review_reindex:
182 prod.needs_review_reindex = False
183 prod.put()
184 return (prod.doc_id, prod.avg_rating)
185 (doc_id, avg_rating) = ndb.transaction(_tx)
186 # update the associated document with the new ratings info
187 # and reindex
188 docs.Product.updateRatingsInfo(doc_id, avg_rating)
191 class Review(ndb.Model):
192 """Model for Review data. Associated with a product entity via the product
193 key."""
195 doc_id = ndb.StringProperty()
196 date_added = ndb.DateTimeProperty(auto_now_add=True)
197 product_key = ndb.KeyProperty(kind=Product)
198 username = ndb.StringProperty()
199 rating = ndb.IntegerProperty()
200 active = ndb.BooleanProperty(default=True)
201 comment = ndb.TextProperty()
202 rating_added = ndb.BooleanProperty(default=False)
204 @classmethod
205 def deleteReviews(cls, pid):
206 """Deletes the reviews associated with a product id."""
207 if not pid:
208 return
209 reviews = cls.query(
210 cls.product_key == ndb.Key(Product, pid)).fetch(keys_only=True)
211 return ndb.delete_multi(reviews)