diff --git a/appengine/images/README.md b/appengine/images/README.md new file mode 100644 index 00000000000..dcc66d4ec19 --- /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 00000000000..e69de29bb2d diff --git a/appengine/images/app.yaml b/appengine/images/app.yaml new file mode 100644 index 00000000000..6033ebaf3c3 --- /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 00000000000..23c553a2966 Binary files /dev/null and b/appengine/images/favicon.ico differ diff --git a/appengine/images/index.yaml b/appengine/images/index.yaml new file mode 100644 index 00000000000..f933a1c8e6d --- /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 00000000000..408c388c876 --- /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 00000000000..e69de29bb2d diff --git a/appengine/images/tests/test_guestbook.py b/appengine/images/tests/test_guestbook.py new file mode 100644 index 00000000000..5e249e0e5bb --- /dev/null +++ b/appengine/images/tests/test_guestbook.py @@ -0,0 +1,82 @@ +# 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_get(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) + mock_images.resize.assert_called_once() + + # Correct response is a redirect + self.assertEqual(response.status_int, 302) + + def test_img(self): + greeting = main.Greeting( + parent=main.guestbook_key('default_guestbook'), + id=123, + ) + greeting.author = 'asdf' + greeting.content = 'asdf' + greeting.put() + + request = webapp2.Request.blank( + '/img?img_id=%s' % greeting.key.urlsafe() + ) + response = request.get_response(main.app) + + self.assertEqual(response.status_int, 200) + + def test_img_missing(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_post_and_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)