Add samples for search API.
[gae-samples.git] / search / product_search_python / admin_handlers.py
bloba0aa37f5b77162f68fbcd627049ecd56334ca865
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 admin request handlers for the app (those that require
18 administrative access).
19 """
21 import csv
22 import logging
23 import os
24 import urllib
25 import uuid
27 from base_handler import BaseHandler
28 import categories
29 import config
30 import docs
31 import errors
32 import models
33 import utils
35 from google.appengine.api import users
36 from google.appengine.ext.deferred import defer
37 from google.appengine.ext import ndb
40 def reinitAll(sample_data=True):
41 """
42 Deletes all product entities and documents, essentially resetting the app
43 state, then loads in static sample data if requested. Hardwired for the
44 expected product types in the sample data.
45 This function is intended to be run 'offline' (e.g., via a Task Queue task).
46 As an extension to this functionality, the channel ID could be used to notify
47 when done."""
49 # delete all the product and review entities
50 review_keys = models.Review.query().fetch(keys_only=True)
51 ndb.delete_multi(review_keys)
52 prod_keys = models.Product.query().fetch(keys_only=True)
53 ndb.delete_multi(prod_keys)
54 # delete all the associated product documents in the doc index
55 docs.Product.deleteAllInProductIndex()
56 # load in sample data if indicated
57 if sample_data:
58 logging.info('Loading product sample data')
59 # Load from csv sample files.
60 # The following are hardwired to the format of the sample data files
61 # for the two example product types ('books' and 'televisions')-- see
62 # categories.py
63 datafile = os.path.join('data', config.SAMPLE_DATA_BOOKS)
64 # books
65 reader = csv.DictReader(
66 open(datafile, 'r'),
67 ['pid', 'name', 'category', 'price',
68 'publisher', 'title', 'pages', 'author',
69 'description', 'isbn'])
70 importData(reader)
71 datafile = os.path.join('data', config.SAMPLE_DATA_TVS)
72 # tvs
73 reader = csv.DictReader(
74 open(datafile, 'r'),
75 ['pid', 'name', 'category', 'price',
76 'size', 'brand', 'tv_type',
77 'description'])
78 importData(reader)
79 logging.info('Re-initialization complete.')
82 def importData(reader):
83 """Import via the csv reader iterator using the specified batch size as set in
84 the config file. We want to ensure the batch is not too large-- we allow 100
85 rows/products max per batch."""
86 MAX_BATCH_SIZE = 100
87 rows = []
88 # index in batches
89 # ensure the batch size in the config file is not over the max or < 1.
90 batchsize = utils.intClamp(config.IMPORT_BATCH_SIZE, 1, MAX_BATCH_SIZE)
91 logging.debug('batchsize: %s', batchsize)
92 for row in reader:
93 if len(rows) == batchsize:
94 docs.Product.buildProductBatch(rows)
95 rows = [row]
96 else:
97 rows.append(row)
98 if rows:
99 docs.Product.buildProductBatch(rows)
102 class AdminHandler(BaseHandler):
103 """Displays the admin page."""
105 def buildAdminPage(self, notification=None):
106 # If necessary, build the app's product categories now. This is done only
107 # if there are no Category entities in the datastore.
108 models.Category.buildAllCategories()
109 tdict = {
110 'sampleb': config.SAMPLE_DATA_BOOKS,
111 'samplet': config.SAMPLE_DATA_TVS,
112 'update_sample': config.DEMO_UPDATE_BOOKS_DATA}
113 if notification:
114 tdict['notification'] = notification
115 self.render_template('admin.html', tdict)
117 @BaseHandler.admin
118 def get(self):
119 action = self.request.get('action')
120 if action == 'reinit':
121 # reinitialise the app data to the sample data
122 defer(reinitAll)
123 self.buildAdminPage(notification="Reinitialization performed.")
124 elif action == 'demo_update':
125 # update the sample data, from (hardwired) book update
126 # data. Demonstrates updating some existing products, and adding some new
127 # ones.
128 logging.info('Loading product sample update data')
129 # The following is hardwired to the known format of the sample data file
130 datafile = os.path.join('data', config.DEMO_UPDATE_BOOKS_DATA)
131 reader = csv.DictReader(
132 open(datafile, 'r'),
133 ['pid', 'name', 'category', 'price',
134 'publisher', 'title', 'pages', 'author',
135 'description', 'isbn'])
136 for row in reader:
137 docs.Product.buildProduct(row)
138 self.buildAdminPage(notification="Demo update performed.")
140 elif action == 'update_ratings':
141 self.update_ratings()
142 self.buildAdminPage(notification="Ratings update performed.")
143 else:
144 self.buildAdminPage()
146 def update_ratings(self):
147 """Find the products that have had an average ratings change, and need their
148 associated documents updated (re-indexed) to reflect that change; and
149 re-index those docs in batch. There will only
150 be such products if config.BATCH_RATINGS_UPDATE is True; otherwise the
151 associated documents will be updated right away."""
152 # get the pids of the products that need review info updated in their
153 # associated documents.
154 pkeys = models.Product.query(
155 models.Product.needs_review_reindex == True).fetch(keys_only=True)
156 # re-index these docs in batch
157 models.Product.updateProdDocsWithNewRating(pkeys)
160 class DeleteProductHandler(BaseHandler):
161 """Remove data for the product with the given pid, including that product's
162 reviews and its associated indexed document."""
164 @BaseHandler.admin
165 def post(self):
166 pid = self.request.get('pid')
167 if not pid: # this should not be reached
168 msg = 'There was a problem: no product id given.'
169 logging.error(msg)
170 url = '/'
171 linktext = 'Go to product search page.'
172 self.render_template(
173 'notification.html',
174 {'title': 'Error', 'msg': msg,
175 'goto_url': url, 'linktext': linktext})
176 return
178 # Delete the product entity within a transaction, and define transactional
179 # tasks for deleting the product's reviews and its associated document.
180 # These tasks will only be run if the transaction successfully commits.
181 def _tx():
182 prod = models.Product.get_by_id(pid)
183 if prod:
184 prod.key.delete()
185 defer(models.Review.deleteReviews, prod.key.id(), _transactional=True)
186 defer(
187 docs.Product.removeProductDocByPid,
188 prod.key.id(), _transactional=True)
190 ndb.transaction(_tx)
191 # indicate success
192 msg = (
193 'The product with product id %s has been ' +
194 'successfully removed.') % (pid,)
195 url = '/'
196 linktext = 'Go to product search page.'
197 self.render_template(
198 'notification.html',
199 {'title': 'Product Removed', 'msg': msg,
200 'goto_url': url, 'linktext': linktext})
203 class CreateProductHandler(BaseHandler):
204 """Handler to create a new product: this constitutes both a product entity
205 and its associated indexed document."""
207 def parseParams(self):
208 """Filter the param set to the expected params."""
210 pid = self.request.get('pid')
211 doc = docs.Product.getDocFromPid(pid)
212 params = {}
213 if doc: # populate default params from the doc
214 fields = doc.fields
215 for f in fields:
216 if f.name == 'catname':
217 params['category'] = f.value
218 else:
219 params[f.name] = f.value
220 else:
221 # start with the 'core' fields
222 params = {
223 'pid': uuid.uuid4().hex, # auto-generate default UID
224 'name': '',
225 'description': '',
226 'category': '',
227 'price': ''}
228 pf = categories.product_dict
229 # add the fields specific to the categories
230 for _, cdict in pf.iteritems():
231 temp = {}
232 for elt in cdict.keys():
233 temp[elt] = ''
234 params.update(temp)
236 for k, v in params.iteritems():
237 # Process the request params. Possibly replace default values.
238 params[k] = self.request.get(k, v)
239 return params
241 @BaseHandler.admin
242 def get(self):
243 params = self.parseParams()
244 self.render_template('create_product.html', params)
246 @BaseHandler.admin
247 def post(self):
248 self.createProduct(self.parseParams())
250 def createProduct(self, params):
251 """Create a product entity and associated document from the given params
252 dict."""
254 try:
255 product = docs.Product.buildProduct(params)
256 self.redirect(
257 '/product?' + urllib.urlencode(
258 {'pid': product.pid, 'pname': params['name'],
259 'category': product.category
261 except errors.Error as e:
262 logging.exception('Error:')
263 params['error_message'] = e.error_message
264 self.render_template('create_product.html', params)