-
Notifications
You must be signed in to change notification settings - Fork 6.5k
Move sample using appengine images to this repository, add tests. #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <your-app-id> | ||
``` | ||
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 <your-app-id> | ||
``` | ||
|
||
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <path.to.module>.<wsgi_application> | ||
# where <wsgi_application> is a WSGI application object. | ||
- url: .* # This regex directs all routes to main.app | ||
script: main.app |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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('<html><body>') | ||
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( | ||
'<b>%s</b> wrote:' % greeting.author) | ||
else: | ||
self.response.out.write('An anonymous person wrote:') | ||
# [START display_image] | ||
self.response.out.write('<div><img src="/img?img_id=%s"></img>' % | ||
greeting.key.urlsafe()) | ||
self.response.out.write('<blockquote>%s</blockquote></div>' % | ||
cgi.escape(greeting.content)) | ||
# [END display_image] | ||
|
||
# [START form] | ||
self.response.out.write(""" | ||
<form action="/sign?%s" | ||
enctype="multipart/form-data" | ||
method="post"> | ||
<div> | ||
<textarea name="content" rows="3" cols="60"></textarea> | ||
</div> | ||
<div><label>Avatar:</label></div> | ||
<div><input type="file" name="img"/></div> | ||
<div><input type="submit" value="Sign Guestbook"></div> | ||
</form> | ||
<hr> | ||
<form>Guestbook name: <input value="%s" name="guestbook_name"> | ||
<input type="submit" value="switch"></form> | ||
</body> | ||
</html>""" % (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] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also test the success case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how to generate a webapp2(webob) Request post object with multipart form data to include the image. Do you know? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you can just add an entity in datastore and call /img?img_id=123 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll try it out. |
||
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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this test for Image.get()? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying to test a get on '/' after a '/sign' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then test_post_and_get or something better name? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds good. |
||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check whether the resize() is called once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do