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).
27 from base_handler
import BaseHandler
36 from google
.appengine
.api
import users
37 from google
.appengine
.ext
.deferred
import defer
38 from google
.appengine
.ext
import ndb
39 from google
.appengine
.api
import search
42 def reinitAll(sample_data
=True):
44 Deletes all product entities and documents, essentially resetting the app
45 state, then loads in static sample data if requested. Hardwired for the
46 expected product types in the sample data.
47 This function is intended to be run 'offline' (e.g., via a Task Queue task).
48 As an extension to this functionality, the channel ID could be used to notify
51 # delete all the product and review entities
52 review_keys
= models
.Review
.query().fetch(keys_only
=True)
53 ndb
.delete_multi(review_keys
)
54 prod_keys
= models
.Product
.query().fetch(keys_only
=True)
55 ndb
.delete_multi(prod_keys
)
56 # delete all the associated product documents in the doc and
58 docs
.Product
.deleteAllInProductIndex()
59 docs
.Store
.deleteAllInIndex()
60 # load in sample data if indicated
62 logging
.info('Loading product sample data')
63 # Load from csv sample files.
64 # The following are hardwired to the format of the sample data files
65 # for the two example product types ('books' and 'hd televisions')-- see
67 datafile
= os
.path
.join('data', config
.SAMPLE_DATA_BOOKS
)
69 reader
= csv
.DictReader(
71 ['pid', 'name', 'category', 'price',
72 'publisher', 'title', 'pages', 'author',
73 'description', 'isbn'])
75 datafile
= os
.path
.join('data', config
.SAMPLE_DATA_TVS
)
77 reader
= csv
.DictReader(
79 ['pid', 'name', 'category', 'price',
80 'size', 'brand', 'tv_type',
84 # next create docs from store location info
85 loadStoreLocationData()
87 logging
.info('Re-initialization complete.')
89 def loadStoreLocationData():
90 # create documents from store location info
91 # currently logs but otherwise swallows search errors.
94 logging
.info("s: %s", s
)
95 geopoint
= search
.GeoPoint(s
[3][0], s
[3][1])
96 fields
= [search
.TextField(name
='storename', value
=s
[1]),
97 search
.TextField(name
='address', value
=s
[2]),
98 search
.GeoField(name
='store_location', value
=geopoint
)
100 d
= search
.Document(doc_id
=s
[0], fields
=fields
)
102 add_result
= search
.Index(config
.STORE_INDEX_NAME
).add(d
)
104 logging
.exception("Error adding document:")
107 def importData(reader
):
108 """Import via the csv reader iterator using the specified batch size as set in
109 the config file. We want to ensure the batch is not too large-- we allow 100
110 rows/products max per batch."""
114 # ensure the batch size in the config file is not over the max or < 1.
115 batchsize
= utils
.intClamp(config
.IMPORT_BATCH_SIZE
, 1, MAX_BATCH_SIZE
)
116 logging
.debug('batchsize: %s', batchsize
)
118 if len(rows
) == batchsize
:
119 docs
.Product
.buildProductBatch(rows
)
124 docs
.Product
.buildProductBatch(rows
)
127 class AdminHandler(BaseHandler
):
128 """Displays the admin page."""
130 def buildAdminPage(self
, notification
=None):
131 # If necessary, build the app's product categories now. This is done only
132 # if there are no Category entities in the datastore.
133 models
.Category
.buildAllCategories()
135 'sampleb': config
.SAMPLE_DATA_BOOKS
,
136 'samplet': config
.SAMPLE_DATA_TVS
,
137 'update_sample': config
.DEMO_UPDATE_BOOKS_DATA
}
139 tdict
['notification'] = notification
140 self
.render_template('admin.html', tdict
)
142 @BaseHandler.logged_in
144 action
= self
.request
.get('action')
145 if action
== 'reinit':
146 # reinitialise the app data to the sample data
148 self
.buildAdminPage(notification
="Reinitialization performed.")
149 elif action
== 'demo_update':
150 # update the sample data, from (hardwired) book update
151 # data. Demonstrates updating some existing products, and adding some new
153 logging
.info('Loading product sample update data')
154 # The following is hardwired to the known format of the sample data file
155 datafile
= os
.path
.join('data', config
.DEMO_UPDATE_BOOKS_DATA
)
156 reader
= csv
.DictReader(
158 ['pid', 'name', 'category', 'price',
159 'publisher', 'title', 'pages', 'author',
160 'description', 'isbn'])
162 docs
.Product
.buildProduct(row
)
163 self
.buildAdminPage(notification
="Demo update performed.")
165 elif action
== 'update_ratings':
166 self
.update_ratings()
167 self
.buildAdminPage(notification
="Ratings update performed.")
169 self
.buildAdminPage()
171 def update_ratings(self
):
172 """Find the products that have had an average ratings change, and need their
173 associated documents updated (re-indexed) to reflect that change; and
174 re-index those docs in batch. There will only
175 be such products if config.BATCH_RATINGS_UPDATE is True; otherwise the
176 associated documents will be updated right away."""
177 # get the pids of the products that need review info updated in their
178 # associated documents.
179 pkeys
= models
.Product
.query(
180 models
.Product
.needs_review_reindex
== True).fetch(keys_only
=True)
181 # re-index these docs in batch
182 models
.Product
.updateProdDocsWithNewRating(pkeys
)
185 class DeleteProductHandler(BaseHandler
):
186 """Remove data for the product with the given pid, including that product's
187 reviews and its associated indexed document."""
189 @BaseHandler.logged_in
191 pid
= self
.request
.get('pid')
192 if not pid
: # this should not be reached
193 msg
= 'There was a problem: no product id given.'
196 linktext
= 'Go to product search page.'
197 self
.render_template(
199 {'title': 'Error', 'msg': msg
,
200 'goto_url': url
, 'linktext': linktext
})
203 # Delete the product entity within a transaction, and define transactional
204 # tasks for deleting the product's reviews and its associated document.
205 # These tasks will only be run if the transaction successfully commits.
207 prod
= models
.Product
.get_by_id(pid
)
210 defer(models
.Review
.deleteReviews
, prod
.key
.id(), _transactional
=True)
212 docs
.Product
.removeProductDocByPid
,
213 prod
.key
.id(), _transactional
=True)
218 'The product with product id %s has been ' +
219 'successfully removed.') % (pid
,)
221 linktext
= 'Go to product search page.'
222 self
.render_template(
224 {'title': 'Product Removed', 'msg': msg
,
225 'goto_url': url
, 'linktext': linktext
})
228 class CreateProductHandler(BaseHandler
):
229 """Handler to create a new product: this constitutes both a product entity
230 and its associated indexed document."""
232 def parseParams(self
):
233 """Filter the param set to the expected params."""
235 pid
= self
.request
.get('pid')
236 doc
= docs
.Product
.getDocFromPid(pid
)
238 if doc
: # populate default params from the doc
241 params
[f
.name
] = f
.value
243 # start with the 'core' fields
245 'pid': uuid
.uuid4().hex, # auto-generate default UID
250 pf
= categories
.product_dict
251 # add the fields specific to the categories
252 for _
, cdict
in pf
.iteritems():
254 for elt
in cdict
.keys():
258 for k
, v
in params
.iteritems():
259 # Process the request params. Possibly replace default values.
260 params
[k
] = self
.request
.get(k
, v
)
263 @BaseHandler.logged_in
265 params
= self
.parseParams()
266 self
.render_template('create_product.html', params
)
268 @BaseHandler.logged_in
270 self
.createProduct(self
.parseParams())
272 def createProduct(self
, params
):
273 """Create a product entity and associated document from the given params
277 product
= docs
.Product
.buildProduct(params
)
279 '/product?' + urllib
.urlencode(
280 {'pid': product
.pid
, 'pname': params
['name'],
281 'category': product
.category
283 except errors
.Error
as e
:
284 logging
.exception('Error:')
285 params
['error_message'] = e
.error_message
286 self
.render_template('create_product.html', params
)