-
Notifications
You must be signed in to change notification settings - Fork 575
Python Getting Started "Bookshelf" app #210
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
bebc312
9e68c00
5e69569
7520799
a1a696c
0251dc3
b9210b4
d45290e
ed07a18
c5f8763
c52a22e
a32a156
f27712e
fa50c40
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 |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ htmlcov/ | |
| .cache | ||
| nosetests.xml | ||
| coverage.xml | ||
| *_log.xml | ||
| *,cover | ||
| sponge_log.xml | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| runtime: python37 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Copyright 2019 Google LLC | ||
| # | ||
| # 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 bookshelf_firestore_client_import] | ||
| from google.cloud import firestore | ||
| # [END bookshelf_firestore_client_import] | ||
|
|
||
|
|
||
| def document_to_dict(doc): | ||
| if not doc.exists: | ||
| return None | ||
| doc_dict = doc.to_dict() | ||
| doc_dict['id'] = doc.id | ||
| return doc_dict | ||
|
|
||
|
|
||
| def next_page(limit=10, start_after=None): | ||
| db = firestore.Client() | ||
|
|
||
| query = db.collection(u'Book').limit(limit).order_by(u'title') | ||
|
|
||
| if start_after: | ||
| # Construct a new query starting at this document. | ||
| query = query.start_after({u'title': start_after}) | ||
|
|
||
| docs = query.stream() | ||
| docs = list(map(document_to_dict, docs)) | ||
|
|
||
| last_title = None | ||
| if limit == len(docs): | ||
| # Get the last document from the results and set as the last title. | ||
| last_title = docs[-1][u'title'] | ||
| return docs, last_title | ||
|
|
||
|
|
||
| def read(book_id): | ||
| # [START bookshelf_firestore_client] | ||
| db = firestore.Client() | ||
| book_ref = db.collection(u'Book').document(book_id) | ||
| snapshot = book_ref.get() | ||
| # [END bookshelf_firestore_client] | ||
| return document_to_dict(snapshot) | ||
|
|
||
|
|
||
| def update(data, book_id=None): | ||
| db = firestore.Client() | ||
| book_ref = db.collection(u'Book').document(book_id) | ||
| book_ref.set(data) | ||
| return document_to_dict(book_ref.get()) | ||
|
|
||
|
|
||
| create = update | ||
|
|
||
|
|
||
| def delete(id): | ||
| db = firestore.Client() | ||
| book_ref = db.collection(u'Book').document(id) | ||
| book_ref.delete() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| # Copyright 2019 Google LLC | ||
| # | ||
| # 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. | ||
|
|
||
| import logging | ||
|
|
||
| import firestore | ||
| from flask import current_app, flash, Flask, Markup, redirect, render_template | ||
| from flask import request, url_for | ||
| from google.cloud import error_reporting | ||
| import google.cloud.logging | ||
| import storage | ||
|
|
||
|
|
||
| # [START upload_image_file] | ||
| def upload_image_file(img): | ||
| """ | ||
| Upload the user-uploaded file to Google Cloud Storage and retrieve its | ||
| publicly-accessible URL. | ||
| """ | ||
| if not img: | ||
| return None | ||
|
|
||
| public_url = storage.upload_file( | ||
| img.read(), | ||
| img.filename, | ||
| img.content_type | ||
| ) | ||
|
|
||
| current_app.logger.info( | ||
| 'Uploaded file %s as %s.', img.filename, public_url) | ||
|
|
||
| return public_url | ||
| # [END upload_image_file] | ||
|
|
||
|
|
||
| app = Flask(__name__) | ||
| app.config.update( | ||
| SECRET_KEY='secret', | ||
| MAX_CONTENT_LENGTH=8 * 1024 * 1024, | ||
| ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif']) | ||
| ) | ||
|
|
||
| app.debug = False | ||
| app.testing = False | ||
|
|
||
| # Configure logging | ||
| if not app.testing: | ||
| logging.basicConfig(level=logging.INFO) | ||
| client = google.cloud.logging.Client() | ||
| # Attaches a Google Stackdriver logging handler to the root logger | ||
| client.setup_logging(logging.INFO) | ||
|
|
||
|
|
||
| @app.route('/') | ||
| def list(): | ||
| start_after = request.args.get('start_after', None) | ||
| books, last_title = firestore.next_page(start_after=start_after) | ||
|
|
||
| return render_template('list.html', books=books, last_title=last_title) | ||
|
|
||
|
|
||
| @app.route('/books/<book_id>') | ||
| def view(book_id): | ||
| book = firestore.read(book_id) | ||
| return render_template('view.html', book=book) | ||
|
|
||
|
|
||
| @app.route('/books/add', methods=['GET', 'POST']) | ||
| def add(): | ||
| if request.method == 'POST': | ||
| data = request.form.to_dict(flat=True) | ||
|
|
||
| # If an image was uploaded, update the data to point to the new image. | ||
| image_url = upload_image_file(request.files.get('image')) | ||
|
|
||
| if image_url: | ||
| data['imageUrl'] = image_url | ||
|
|
||
| book = firestore.create(data) | ||
|
|
||
| return redirect(url_for('.view', book_id=book['id'])) | ||
|
|
||
| return render_template('form.html', action='Add', book={}) | ||
|
Contributor
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. Nit: should this be at the top, like so? if request.method != 'POST':
return render_template(...)
...
Contributor
Author
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 think it's more idiomatic for python as is, mostly because this is how the previous bookshelf was set up. So I'd prefer to leave it as-is |
||
|
|
||
|
|
||
| @app.route('/books/<book_id>/edit', methods=['GET', 'POST']) | ||
| def edit(book_id): | ||
| book = firestore.read(book_id) | ||
|
|
||
| if request.method == 'POST': | ||
| data = request.form.to_dict(flat=True) | ||
|
|
||
| # If an image was uploaded, update the data to point to the new image. | ||
| image_url = upload_image_file(request.files.get('image')) | ||
|
|
||
| if image_url: | ||
| data['imageUrl'] = image_url | ||
|
|
||
| book = firestore.update(data, book_id) | ||
|
|
||
| return redirect(url_for('.view', book_id=book['id'])) | ||
|
|
||
| return render_template('form.html', action='Edit', book=book) | ||
|
Contributor
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. Same comment as above - should this be at the top? |
||
|
|
||
|
|
||
| @app.route('/books/<book_id>/delete') | ||
| def delete(book_id): | ||
| firestore.delete(book_id) | ||
| return redirect(url_for('.list')) | ||
|
|
||
|
|
||
| @app.route('/logs') | ||
| def logs(): | ||
| logging.info('Hey, you triggered a custom log entry. Good job!') | ||
| flash(Markup('''You triggered a custom log entry. You can view it in the | ||
| <a href="https://console.cloud.google.com/logs">Cloud Console</a>''')) | ||
| return redirect(url_for('.list')) | ||
|
|
||
|
|
||
| @app.route('/errors') | ||
| def errors(): | ||
| raise Exception('This is an intentional exception.') | ||
|
|
||
|
|
||
| # Add an error handler that reports exceptions to Stackdriver Error | ||
| # Reporting. Note that this error handler is only used when debug | ||
| # is False | ||
| @app.errorhandler(500) | ||
| def server_error(e): | ||
| client = error_reporting.Client() | ||
| client.report_exception( | ||
| http_context=error_reporting.build_flask_context(request)) | ||
| return """ | ||
| An internal error occurred: <pre>{}</pre> | ||
| See logs for full stacktrace. | ||
| """.format(e), 500 | ||
|
|
||
|
|
||
| # This is only used when running locally. When running live, gunicorn runs | ||
| # the application. | ||
| if __name__ == '__main__': | ||
| app.run(host='127.0.0.1', port=8080, debug=True) | ||
Uh oh!
There was an error while loading. Please reload this page.