From b5e00ee2e32339775f656255260785bbb1b0cf16 Mon Sep 17 00:00:00 2001 From: Jeff Mendoza Date: Wed, 17 Jun 2015 13:16:05 -0700 Subject: [PATCH] Move sample using appengine images to this repository, add tests. --- appengine/images/README.md | 68 ++++++++++++ appengine/images/__init__.py | 0 appengine/images/app.yaml | 17 +++ appengine/images/favicon.ico | Bin 0 -> 8348 bytes appengine/images/index.yaml | 17 +++ appengine/images/main.py | 132 +++++++++++++++++++++++ appengine/images/tests/__init__.py | 0 appengine/images/tests/test_guestbook.py | 65 +++++++++++ 8 files changed, 299 insertions(+) create mode 100644 appengine/images/README.md create mode 100644 appengine/images/__init__.py create mode 100644 appengine/images/app.yaml create mode 100644 appengine/images/favicon.ico create mode 100644 appengine/images/index.yaml create mode 100644 appengine/images/main.py create mode 100644 appengine/images/tests/__init__.py create mode 100644 appengine/images/tests/test_guestbook.py diff --git a/appengine/images/README.md b/appengine/images/README.md new file mode 100644 index 000000000000..dcc66d4ec19d --- /dev/null +++ b/appengine/images/README.md @@ -0,0 +1,68 @@ +## Images Guestbook Sample + +This is a sample app for Google App Engine that exercises the [images +Python +API](https://cloud.google.com/appengine/docs/python/images/usingimages). + +See our other [Google Cloud Platform github +repos](https://github.com/GoogleCloudPlatform) for sample applications +and scaffolding for other python frameworks and use cases. + +## Run Locally + +1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/), + including the [gcloud tool](https://cloud.google.com/sdk/gcloud/), + and [gcloud app + component](https://cloud.google.com/sdk/gcloud-app). + +1. Setup the gcloud tool. + ``` + gcloud components update app + gcloud auth login + gcloud config set project + ``` + You don't need a valid app-id to run locally, but will need a valid id + to deploy below. + +1. Clone this repo. + ``` + git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git + cd appengine/images/ + ``` + +1. Run this project locally from the command line. + ``` + gcloud preview app run ./app.yaml + ``` + +1. Visit the application at + [http://localhost:8080](http://localhost:8080). + +## Deploying + +1. Use the [Cloud Developer + Console](https://console.developer.google.com) to create a + project/app id. (App id and project id are identical) + +1. Configure gcloud with your app id. + ``` + gcloud config set project + ``` + +1. Use the [Admin Console](https://appengine.google.com) to view data, + queues, and other App Engine specific administration tasks. + +1. Use gcloud to deploy your app. + ``` + gcloud preview app deploy ./app.yaml + ``` + +1. Congratulations! Your application is now live at your-app-id.appspot.com + +## Contributing changes + +* See [CONTRIBUTING.md](/CONTRIBUTING.md) + +## Licensing + +* See [LICENSE](/LICENSE) diff --git a/appengine/images/__init__.py b/appengine/images/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/appengine/images/app.yaml b/appengine/images/app.yaml new file mode 100644 index 000000000000..6033ebaf3c3d --- /dev/null +++ b/appengine/images/app.yaml @@ -0,0 +1,17 @@ +# This file specifies your Python application's runtime configuration +# including URL routing, versions, static file uploads, etc. See +# https://developers.google.com/appengine/docs/python/config/appconfig +# for details. + +runtime: python27 +api_version: 1 +threadsafe: yes + +# Handlers define how to route requests to your application. +handlers: + +# This handler tells app engine how to route requests to a WSGI application. +# The script value is in the format . +# where is a WSGI application object. +- url: .* # This regex directs all routes to main.app + script: main.app diff --git a/appengine/images/favicon.ico b/appengine/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..23c553a2966ca4cecf146093f33d8114b4f1e368 GIT binary patch literal 8348 zcmeI0du&tJ8Ng3Vwv|ewX4Hu@my&|vCD%DN@CLnxyptBT_%HjatoYr7%N3zGPed#@ckvA><;HWecDU4&2}*2(h%gm&WgCftWj zkQfW;&mXr@0S&(csd+cjaImbXSy=NC<10}z(efGwgi=;B$dt=HkKYCvp-#3KX+qIu zxx$>zcwk+N=c%1^J9)sO44lmS0##~wzU(43f>wW-p_YXwj>s>3{gHI*^oo1lmJ!X*bw{3UBAKRut zUh5Uy4_94IIkdxC^v{_g%FjuEI+f&;9KQ3i+nK7pU_RivFjP_DTl-&gP_qr$nD=M{ z@z;C3#y5Jsh77svGQ4u$ZX)t}=e52{#Xvk;4qIxNh86kR$OhCAie%&e`U{r{ACQZ@ z-)1#sItef{2L za&U;nm+_lo@lbR7vat&^EcECsH)uSnhlht@etsV4LE5B`j#GW*iur~}xpYlTtc;FC{nl|LK^miZ0y3WU>MIM zmc}94VzEFx9?#&?4l+h;ggzvOeCI$ob=`tBp*&EtirH2xU z4a#KB)IMa{_YdYrnxq>-DrrQ>rfpYe8@|Nc-oHnG*L(HR$}d4Eo2&X@o1~9l@%@W) zU{%rv$`tBAOHJH+?zcv7`!T~x;=<#7@4R5T^5$-%Pz-NAYt+5{d;>R+9W24y&`;ZkZqFGQ6_)<35Z_$5V#ga#=N9985-M0KR*fl@h4M0BxWvbYQr6t zULtYBAMJ%iU>x|?U8z_Zyv1jg_VcZ^kO)pd_)jk`_~2MHZmybbloW?l)lnMrb~TAX zV&%#e+VLvM4!%js+%B6}N!=udFlN5Jv;yQm0sdW({6ny+{{$_b`%pI&q3%#p3R?q( z%3@zpafPo4-GA}ErIfU@j?gpfdgeyI);;S-otz(GefQbXvG3J6hbwD_0*YOuqb1V8 zpQm{(oT|g$!tcw;Ck6l>=mo$z0J@0f0^U0{x?+mD8}QB{9yV7qQ;&$5^%*fb@qUB& zX(O<{D-aZ493K;1xH%!}9+}vt8Sshs4os9)d132glTa=lIJv}MJyU_Y!ZFl62j9@l zggg2y{y~c&Vm0cMa@}r@bbn?HR4Sa|66p;nlMl_6D%_%EjCNRq)NBvx!R!t`@zUoW zKV#O#EhZy4>~?VU+rfg@_W_4Kt~zS<|3JjVMcZ#exy;pDUypq|xjpD&0#H{BHgrvM zI=z9nnTN~NCX?mf@DOU;!82-#gXGsw5CSSf22lJ>u8duE&igGuZq4lVZf*LH2%QqOqkv@O{w`Y}q~yWm0EP zeSP~Hau49ZBU@&hWwH5YG0dnGVN1^iztQFh>rIvj5$iQ;q^nyuSuXo`U~{E8DpuIS zA{kR5oC9o=_>jhfRX@QTs1KRm{@F31 zFKP1!w?51@R^I~kA%H*B0W^sKnyVIsv`_2;=zbUGR9pOTqUhV{{^UH=RQ2@S?`uaQ zEy`)Gsd}1IEedW&4lAe0Sg5h`nQXqaZ}RyEzX{FT?hmF3>0_QQT1V~jI$wc&1@aZh ZS0G=3d706zJ{{SaIO|Jj| literal 0 HcmV?d00001 diff --git a/appengine/images/index.yaml b/appengine/images/index.yaml new file mode 100644 index 000000000000..f933a1c8e6d2 --- /dev/null +++ b/appengine/images/index.yaml @@ -0,0 +1,17 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: Greeting + ancestor: yes + properties: + - name: date + direction: desc diff --git a/appengine/images/main.py b/appengine/images/main.py new file mode 100644 index 000000000000..408c388c8765 --- /dev/null +++ b/appengine/images/main.py @@ -0,0 +1,132 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START all] + +import cgi +import urllib + +# [START import_images] +from google.appengine.api import images +# [END import_images] +from google.appengine.api import users +from google.appengine.ext import ndb + +import webapp2 + + +# [START model] +class Greeting(ndb.Model): + """Models a Guestbook entry with an author, content, avatar, and date.""" + author = ndb.StringProperty() + content = ndb.TextProperty() + avatar = ndb.BlobProperty() + date = ndb.DateTimeProperty(auto_now_add=True) +# [END model] + + +def guestbook_key(guestbook_name=None): + """Constructs a Datastore key for a Guestbook entity with name.""" + return ndb.Key('Guestbook', guestbook_name or 'default_guestbook') + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.out.write('') + guestbook_name = self.request.get('guestbook_name') + + greetings = Greeting.query( + ancestor=guestbook_key(guestbook_name)) \ + .order(-Greeting.date) \ + .fetch(10) + + for greeting in greetings: + if greeting.author: + self.response.out.write( + '%s wrote:' % greeting.author) + else: + self.response.out.write('An anonymous person wrote:') + # [START display_image] + self.response.out.write('
' % + greeting.key.urlsafe()) + self.response.out.write('
%s
' % + cgi.escape(greeting.content)) + # [END display_image] + + # [START form] + self.response.out.write(""" +
+
+ +
+
+
+
+
+
+
Guestbook name: +
+ + """ % (urllib.urlencode({'guestbook_name': guestbook_name}), + cgi.escape(guestbook_name))) + # [END form] + + +# [START image_handler] +class Image(webapp2.RequestHandler): + def get(self): + greeting_key = ndb.Key(urlsafe=self.request.get('img_id')) + greeting = greeting_key.get() + if greeting.avatar: + self.response.headers['Content-Type'] = 'image/png' + self.response.out.write(greeting.avatar) + else: + self.response.out.write('No image') +# [END image_handler] + + +# [START sign_handler] +class Guestbook(webapp2.RequestHandler): + def post(self): + guestbook_name = self.request.get('guestbook_name') + greeting = Greeting(parent=guestbook_key(guestbook_name)) + + if users.get_current_user(): + greeting.author = users.get_current_user().nickname() + + greeting.content = self.request.get('content') + + # [START sign_handler_1] + avatar = self.request.get('img') + # [END sign_handler_1] + # [START transform] + avatar = images.resize(avatar, 32, 32) + # [END transform] + # [START sign_handler_2] + greeting.avatar = avatar + greeting.put() + # [END sign_handler_1] + + self.redirect('/?' + urllib.urlencode( + {'guestbook_name': guestbook_name})) +# [END sign_handler] + + +app = webapp2.WSGIApplication([('/', MainPage), + ('/img', Image), + ('/sign', Guestbook)], + debug=True) +# [END all] diff --git a/appengine/images/tests/__init__.py b/appengine/images/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/appengine/images/tests/test_guestbook.py b/appengine/images/tests/test_guestbook.py new file mode 100644 index 000000000000..4e9d7e8f19ef --- /dev/null +++ b/appengine/images/tests/test_guestbook.py @@ -0,0 +1,65 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# from the app main.py +from appengine.images import main +import mock +from tests import DatastoreTestbedCase + +import webapp2 + + +class TestHandlers(DatastoreTestbedCase): + def test_hello(self): + # Build a request object passing the URI path to be tested. + # You can also pass headers, query arguments etc. + request = webapp2.Request.blank('/') + # Get a response for that request. + response = request.get_response(main.app) + + # Let's check if the response is correct. + self.assertEqual(response.status_int, 200) + + @mock.patch('appengine.images.main.images') + def test_post(self, mock_images): + mock_images.resize.return_value = 'asdf' + request = webapp2.Request.blank( + '/sign', + POST={'content': 'asdf'}, + ) + response = request.get_response(main.app) + + # Correct response is a redirect + self.assertEqual(response.status_int, 302) + + def test_img(self): + # Bogus image id, should get error + request = webapp2.Request.blank('/img?img_id=123') + response = request.get_response(main.app) + + self.assertEqual(response.status_int, 500) + + @mock.patch('appengine.images.main.images') + def test_get(self, mock_images): + mock_images.resize.return_value = 'asdf' + request = webapp2.Request.blank( + '/sign', + POST={'content': 'asdf'}, + ) + response = request.get_response(main.app) + + request = webapp2.Request.blank('/') + response = request.get_response(main.app) + + self.assertEqual(response.status_int, 200)