Skip to content

Commit 804f4a9

Browse files
author
Jeff Mendoza
committed
Merge pull request #40 from GoogleCloudPlatform/jlm/appengine-images
Move sample using appengine images to this repository, add tests.
2 parents bde9556 + 0cd2032 commit 804f4a9

File tree

8 files changed

+316
-0
lines changed

8 files changed

+316
-0
lines changed

appengine/images/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
## Images Guestbook Sample
2+
3+
This is a sample app for Google App Engine that exercises the [images
4+
Python
5+
API](https://cloud.google.com/appengine/docs/python/images/usingimages).
6+
7+
See our other [Google Cloud Platform github
8+
repos](https://github.com/GoogleCloudPlatform) for sample applications
9+
and scaffolding for other python frameworks and use cases.
10+
11+
## Run Locally
12+
13+
1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/),
14+
including the [gcloud tool](https://cloud.google.com/sdk/gcloud/),
15+
and [gcloud app
16+
component](https://cloud.google.com/sdk/gcloud-app).
17+
18+
1. Setup the gcloud tool.
19+
```
20+
gcloud components update app
21+
gcloud auth login
22+
gcloud config set project <your-app-id>
23+
```
24+
You don't need a valid app-id to run locally, but will need a valid id
25+
to deploy below.
26+
27+
1. Clone this repo.
28+
```
29+
git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
30+
cd appengine/images/
31+
```
32+
33+
1. Run this project locally from the command line.
34+
```
35+
gcloud preview app run ./app.yaml
36+
```
37+
38+
1. Visit the application at
39+
[http://localhost:8080](http://localhost:8080).
40+
41+
## Deploying
42+
43+
1. Use the [Cloud Developer
44+
Console](https://console.developer.google.com) to create a
45+
project/app id. (App id and project id are identical)
46+
47+
1. Configure gcloud with your app id.
48+
```
49+
gcloud config set project <your-app-id>
50+
```
51+
52+
1. Use the [Admin Console](https://appengine.google.com) to view data,
53+
queues, and other App Engine specific administration tasks.
54+
55+
1. Use gcloud to deploy your app.
56+
```
57+
gcloud preview app deploy ./app.yaml
58+
```
59+
60+
1. Congratulations! Your application is now live at your-app-id.appspot.com
61+
62+
## Contributing changes
63+
64+
* See [CONTRIBUTING.md](/CONTRIBUTING.md)
65+
66+
## Licensing
67+
68+
* See [LICENSE](/LICENSE)

appengine/images/__init__.py

Whitespace-only changes.

appengine/images/app.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# This file specifies your Python application's runtime configuration
2+
# including URL routing, versions, static file uploads, etc. See
3+
# https://developers.google.com/appengine/docs/python/config/appconfig
4+
# for details.
5+
6+
runtime: python27
7+
api_version: 1
8+
threadsafe: yes
9+
10+
# Handlers define how to route requests to your application.
11+
handlers:
12+
13+
# This handler tells app engine how to route requests to a WSGI application.
14+
# The script value is in the format <path.to.module>.<wsgi_application>
15+
# where <wsgi_application> is a WSGI application object.
16+
- url: .* # This regex directs all routes to main.app
17+
script: main.app

appengine/images/favicon.ico

8.15 KB
Binary file not shown.

appengine/images/index.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
indexes:
2+
3+
# AUTOGENERATED
4+
5+
# This index.yaml is automatically updated whenever the dev_appserver
6+
# detects that a new type of query is run. If you want to manage the
7+
# index.yaml file manually, remove the above marker line (the line
8+
# saying "# AUTOGENERATED"). If you want to manage some indexes
9+
# manually, move them above the marker line. The index.yaml file is
10+
# automatically uploaded to the admin console when you next deploy
11+
# your application using appcfg.py.
12+
13+
- kind: Greeting
14+
ancestor: yes
15+
properties:
16+
- name: date
17+
direction: desc

appengine/images/main.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Copyright 2015 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START all]
16+
17+
import cgi
18+
import urllib
19+
20+
# [START import_images]
21+
from google.appengine.api import images
22+
# [END import_images]
23+
from google.appengine.api import users
24+
from google.appengine.ext import ndb
25+
26+
import webapp2
27+
28+
29+
# [START model]
30+
class Greeting(ndb.Model):
31+
"""Models a Guestbook entry with an author, content, avatar, and date."""
32+
author = ndb.StringProperty()
33+
content = ndb.TextProperty()
34+
avatar = ndb.BlobProperty()
35+
date = ndb.DateTimeProperty(auto_now_add=True)
36+
# [END model]
37+
38+
39+
def guestbook_key(guestbook_name=None):
40+
"""Constructs a Datastore key for a Guestbook entity with name."""
41+
return ndb.Key('Guestbook', guestbook_name or 'default_guestbook')
42+
43+
44+
class MainPage(webapp2.RequestHandler):
45+
def get(self):
46+
self.response.out.write('<html><body>')
47+
guestbook_name = self.request.get('guestbook_name')
48+
49+
greetings = Greeting.query(
50+
ancestor=guestbook_key(guestbook_name)) \
51+
.order(-Greeting.date) \
52+
.fetch(10)
53+
54+
for greeting in greetings:
55+
if greeting.author:
56+
self.response.out.write(
57+
'<b>%s</b> wrote:' % greeting.author)
58+
else:
59+
self.response.out.write('An anonymous person wrote:')
60+
# [START display_image]
61+
self.response.out.write('<div><img src="/img?img_id=%s"></img>' %
62+
greeting.key.urlsafe())
63+
self.response.out.write('<blockquote>%s</blockquote></div>' %
64+
cgi.escape(greeting.content))
65+
# [END display_image]
66+
67+
# [START form]
68+
self.response.out.write("""
69+
<form action="/sign?%s"
70+
enctype="multipart/form-data"
71+
method="post">
72+
<div>
73+
<textarea name="content" rows="3" cols="60"></textarea>
74+
</div>
75+
<div><label>Avatar:</label></div>
76+
<div><input type="file" name="img"/></div>
77+
<div><input type="submit" value="Sign Guestbook"></div>
78+
</form>
79+
<hr>
80+
<form>Guestbook name: <input value="%s" name="guestbook_name">
81+
<input type="submit" value="switch"></form>
82+
</body>
83+
</html>""" % (urllib.urlencode({'guestbook_name': guestbook_name}),
84+
cgi.escape(guestbook_name)))
85+
# [END form]
86+
87+
88+
# [START image_handler]
89+
class Image(webapp2.RequestHandler):
90+
def get(self):
91+
greeting_key = ndb.Key(urlsafe=self.request.get('img_id'))
92+
greeting = greeting_key.get()
93+
if greeting.avatar:
94+
self.response.headers['Content-Type'] = 'image/png'
95+
self.response.out.write(greeting.avatar)
96+
else:
97+
self.response.out.write('No image')
98+
# [END image_handler]
99+
100+
101+
# [START sign_handler]
102+
class Guestbook(webapp2.RequestHandler):
103+
def post(self):
104+
guestbook_name = self.request.get('guestbook_name')
105+
greeting = Greeting(parent=guestbook_key(guestbook_name))
106+
107+
if users.get_current_user():
108+
greeting.author = users.get_current_user().nickname()
109+
110+
greeting.content = self.request.get('content')
111+
112+
# [START sign_handler_1]
113+
avatar = self.request.get('img')
114+
# [END sign_handler_1]
115+
# [START transform]
116+
avatar = images.resize(avatar, 32, 32)
117+
# [END transform]
118+
# [START sign_handler_2]
119+
greeting.avatar = avatar
120+
greeting.put()
121+
# [END sign_handler_1]
122+
123+
self.redirect('/?' + urllib.urlencode(
124+
{'guestbook_name': guestbook_name}))
125+
# [END sign_handler]
126+
127+
128+
app = webapp2.WSGIApplication([('/', MainPage),
129+
('/img', Image),
130+
('/sign', Guestbook)],
131+
debug=True)
132+
# [END all]

appengine/images/tests/__init__.py

Whitespace-only changes.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright 2015 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# from the app main.py
16+
from appengine.images import main
17+
import mock
18+
from tests import DatastoreTestbedCase
19+
20+
import webapp2
21+
22+
23+
class TestHandlers(DatastoreTestbedCase):
24+
def test_get(self):
25+
# Build a request object passing the URI path to be tested.
26+
# You can also pass headers, query arguments etc.
27+
request = webapp2.Request.blank('/')
28+
# Get a response for that request.
29+
response = request.get_response(main.app)
30+
31+
# Let's check if the response is correct.
32+
self.assertEqual(response.status_int, 200)
33+
34+
@mock.patch('appengine.images.main.images')
35+
def test_post(self, mock_images):
36+
mock_images.resize.return_value = 'asdf'
37+
request = webapp2.Request.blank(
38+
'/sign',
39+
POST={'content': 'asdf'},
40+
)
41+
response = request.get_response(main.app)
42+
mock_images.resize.assert_called_once()
43+
44+
# Correct response is a redirect
45+
self.assertEqual(response.status_int, 302)
46+
47+
def test_img(self):
48+
greeting = main.Greeting(
49+
parent=main.guestbook_key('default_guestbook'),
50+
id=123,
51+
)
52+
greeting.author = 'asdf'
53+
greeting.content = 'asdf'
54+
greeting.put()
55+
56+
request = webapp2.Request.blank(
57+
'/img?img_id=%s' % greeting.key.urlsafe()
58+
)
59+
response = request.get_response(main.app)
60+
61+
self.assertEqual(response.status_int, 200)
62+
63+
def test_img_missing(self):
64+
# Bogus image id, should get error
65+
request = webapp2.Request.blank('/img?img_id=123')
66+
response = request.get_response(main.app)
67+
68+
self.assertEqual(response.status_int, 500)
69+
70+
@mock.patch('appengine.images.main.images')
71+
def test_post_and_get(self, mock_images):
72+
mock_images.resize.return_value = 'asdf'
73+
request = webapp2.Request.blank(
74+
'/sign',
75+
POST={'content': 'asdf'},
76+
)
77+
response = request.get_response(main.app)
78+
79+
request = webapp2.Request.blank('/')
80+
response = request.get_response(main.app)
81+
82+
self.assertEqual(response.status_int, 200)

0 commit comments

Comments
 (0)