Skip to content

Commit 7737b5e

Browse files
authored
Merge pull request #1 from vmagelo/main
Seed project with initial files from working repo.
2 parents 8d7ffa5 + a73c678 commit 7737b5e

File tree

18 files changed

+684
-55
lines changed

18 files changed

+684
-55
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FLASK_ENV=development
2+
DBNAME=<database name>
3+
DBHOST=<database-hostname>
4+
DBUSER=<db-user-name>
5+
DBPASS=<db-password>

README.md

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,5 @@
1-
# Project Name
1+
# Deploy a Flask web app with PostgreSQL in Azure
22

3-
(short, 1-3 sentenced, description of the project)
3+
This is a Python web app using the Flask framework and the Azure Database for PostgreSQL relational database service. The Flask app is hosted in a fully managed Azure App Service. This app is designed to be be run locally and then deployed to Azure. For more information on how to use this web app, see the tutorial [Deploy a Django/Flask web app with PostgreSQL in Azure](TBD).
44

5-
## Features
6-
7-
This project framework provides the following features:
8-
9-
* Feature 1
10-
* Feature 2
11-
* ...
12-
13-
## Getting Started
14-
15-
### Prerequisites
16-
17-
(ideally very short, if any)
18-
19-
- OS
20-
- Library version
21-
- ...
22-
23-
### Installation
24-
25-
(ideally very short)
26-
27-
- npm install [package name]
28-
- mvn install
29-
- ...
30-
31-
### Quickstart
32-
(Add steps to get up and running quickly)
33-
34-
1. git clone [repository clone url]
35-
2. cd [respository name]
36-
3. ...
37-
38-
39-
## Demo
40-
41-
A demo app is included to show how to use the project.
42-
43-
To run the demo, follow these steps:
44-
45-
(Add steps to start up the demo)
46-
47-
1.
48-
2.
49-
3.
50-
51-
## Resources
52-
53-
(Any additional resources or related projects)
54-
55-
- Link to supporting information
56-
- Link to similar sample
57-
- ...
5+
If you need an Azure account, you can [create on for free](https://azure.microsoft.com/en-us/free/).

app.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from flask import Flask, render_template, request, redirect, url_for
2+
from flask_sqlalchemy import SQLAlchemy
3+
from flask_migrate import Migrate
4+
from flask_wtf.csrf import CSRFProtect
5+
from datetime import datetime
6+
import os
7+
8+
app = Flask(__name__, static_folder='static')
9+
csrf = CSRFProtect(app)
10+
11+
# WEBSITE_HOSTNAME exists only in production environment
12+
if not 'WEBSITE_HOSTNAME' in os.environ:
13+
# local development, where we'll use environment variables
14+
print("Loading config.development and environment variables from .env file.")
15+
app.config.from_object('azureproject.development')
16+
else:
17+
# production
18+
print("Loading config.production.")
19+
app.config.from_object('azureproject.production')
20+
21+
app.config.update(
22+
SQLALCHEMY_DATABASE_URI=app.config.get('DATABASE_URI'),
23+
SQLALCHEMY_TRACK_MODIFICATIONS=False,
24+
)
25+
26+
# Initialize the database connection
27+
db = SQLAlchemy(app)
28+
29+
# Enable Flask-Migrate commands "flask db init/migrate/upgrade" to work
30+
migrate = Migrate(app, db)
31+
32+
# Create databases, if databases exists doesn't issue create
33+
# For schema changes, run "flask db migrate"
34+
from models import Restaurant, Review
35+
db.create_all()
36+
db.session.commit()
37+
38+
@app.route('/', methods=['GET'])
39+
def index():
40+
from models import Restaurant
41+
print('Request for index page received')
42+
restaurants = Restaurant.query.all()
43+
return render_template('index.html', restaurants=restaurants)
44+
45+
@app.route('/<int:id>', methods=['GET'])
46+
def details(id):
47+
from models import Restaurant, Review
48+
restaurant = Restaurant.query.where(Restaurant.id == id).first()
49+
reviews = Review.query.where(Review.restaurant==id)
50+
return render_template('details.html', restaurant=restaurant, reviews=reviews)
51+
52+
@app.route('/create', methods=['GET'])
53+
def create_restaurant():
54+
print('Request for add restaurant page received')
55+
return render_template('create_restaurant.html')
56+
57+
@app.route('/add', methods=['POST'])
58+
@csrf.exempt
59+
def add_restaurant():
60+
from models import Restaurant
61+
try:
62+
name = request.values.get('restaurant_name')
63+
street_address = request.values.get('street_address')
64+
description = request.values.get('description')
65+
except (KeyError):
66+
# Redisplay the question voting form.
67+
return render_template('add_restaurant.html', {
68+
'error_message': "You must include a restaurant name, address, and description",
69+
})
70+
else:
71+
restaurant = Restaurant()
72+
restaurant.name = name
73+
restaurant.street_address = street_address
74+
restaurant.description = description
75+
db.session.add(restaurant)
76+
db.session.commit()
77+
78+
return redirect(url_for('details', id=restaurant.id))
79+
80+
@app.route('/review/<int:id>', methods=['POST'])
81+
@csrf.exempt
82+
def add_review(id):
83+
from models import Review
84+
try:
85+
user_name = request.values.get('user_name')
86+
rating = request.values.get('rating')
87+
review_text = request.values.get('review_text')
88+
except (KeyError):
89+
#Redisplay the question voting form.
90+
return render_template('add_review.html', {
91+
'error_message': "Error adding review",
92+
})
93+
else:
94+
review = Review()
95+
review.restaurant = id
96+
review.review_date = datetime.now()
97+
review.user_name = user_name
98+
review.rating = int(rating)
99+
review.review_text = review_text
100+
db.session.add(review)
101+
db.session.commit()
102+
103+
return redirect(url_for('details', id=id))
104+
105+
@app.context_processor
106+
def utility_processor():
107+
def star_rating(id):
108+
from models import Review
109+
reviews = Review.query.where(Review.restaurant==id)
110+
111+
ratings = []
112+
review_count = 0;
113+
for review in reviews:
114+
ratings += [review.rating]
115+
review_count += 1
116+
117+
avg_rating = sum(ratings)/len(ratings) if ratings else 0
118+
stars_percent = round((avg_rating / 5.0) * 100) if review_count > 0 else 0
119+
return {'avg_rating': avg_rating, 'review_count': review_count, 'stars_percent': stars_percent}
120+
121+
return dict(star_rating=star_rating)
122+
123+
if __name__ == '__main__':
124+
app.run()

azureproject/__init__.py

Whitespace-only changes.

azureproject/development.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pathlib import Path
2+
import os
3+
4+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
5+
BASE_DIR = Path(__file__).resolve().parent.parent
6+
7+
DEBUG = True
8+
9+
DATABASE_URI = 'postgresql+psycopg2://{dbuser}:{dbpass}@{dbhost}/{dbname}'.format(
10+
dbuser=os.environ['DBUSER'],
11+
dbpass=os.environ['DBPASS'],
12+
dbhost=os.environ['DBHOST'],
13+
dbname=os.environ['DBNAME']
14+
)
15+
16+
TIME_ZONE = 'UTC'
17+
18+
STATICFILES_DIRS = (str(BASE_DIR.joinpath('static')),)
19+
STATIC_URL = 'static/'
20+

azureproject/production.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
3+
# SECURITY WARNING: keep the secret key used in production secret!
4+
SECRET_KEY = 'django-insecure-7ppocbnx@w71dcuinn*t^_mzal(t@o01v3fee27g%rg18fc5d@'
5+
6+
DEBUG = False
7+
ALLOWED_HOSTS = [os.environ['WEBSITE_HOSTNAME']] if 'WEBSITE_HOSTNAME' in os.environ else []
8+
CSRF_TRUSTED_ORIGINS = ['https://'+ os.environ['WEBSITE_HOSTNAME']] if 'WEBSITE_HOSTNAME' in os.environ else []
9+
10+
# Configure Postgres database; the full username for PostgreSQL flexible server is
11+
# username (not @sever-name).
12+
DATABASE_URI = 'postgresql+psycopg2://{dbuser}:{dbpass}@{dbhost}/{dbname}'.format(
13+
dbuser=os.environ['DBUSER'],
14+
dbpass=os.environ['DBPASS'],
15+
dbhost=os.environ['DBHOST'] + ".postgres.database.azure.com",
16+
dbname=os.environ['DBNAME']
17+
)

migrations/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Single-database configuration for Flask.

migrations/alembic.ini

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# template used to generate migration files
5+
# file_template = %%(rev)s_%%(slug)s
6+
7+
# set to 'true' to run the environment during
8+
# the 'revision' command, regardless of autogenerate
9+
# revision_environment = false
10+
11+
12+
# Logging configuration
13+
[loggers]
14+
keys = root,sqlalchemy,alembic,flask_migrate
15+
16+
[handlers]
17+
keys = console
18+
19+
[formatters]
20+
keys = generic
21+
22+
[logger_root]
23+
level = WARN
24+
handlers = console
25+
qualname =
26+
27+
[logger_sqlalchemy]
28+
level = WARN
29+
handlers =
30+
qualname = sqlalchemy.engine
31+
32+
[logger_alembic]
33+
level = INFO
34+
handlers =
35+
qualname = alembic
36+
37+
[logger_flask_migrate]
38+
level = INFO
39+
handlers =
40+
qualname = flask_migrate
41+
42+
[handler_console]
43+
class = StreamHandler
44+
args = (sys.stderr,)
45+
level = NOTSET
46+
formatter = generic
47+
48+
[formatter_generic]
49+
format = %(levelname)-5.5s [%(name)s] %(message)s
50+
datefmt = %H:%M:%S

migrations/env.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from __future__ import with_statement
2+
3+
import logging
4+
from logging.config import fileConfig
5+
6+
from flask import current_app
7+
8+
from alembic import context
9+
10+
# this is the Alembic Config object, which provides
11+
# access to the values within the .ini file in use.
12+
config = context.config
13+
14+
# Interpret the config file for Python logging.
15+
# This line sets up loggers basically.
16+
fileConfig(config.config_file_name)
17+
logger = logging.getLogger('alembic.env')
18+
19+
# add your model's MetaData object here
20+
# for 'autogenerate' support
21+
# from myapp import mymodel
22+
# target_metadata = mymodel.Base.metadata
23+
config.set_main_option(
24+
'sqlalchemy.url',
25+
str(current_app.extensions['migrate'].db.get_engine().url).replace(
26+
'%', '%%'))
27+
target_metadata = current_app.extensions['migrate'].db.metadata
28+
29+
# other values from the config, defined by the needs of env.py,
30+
# can be acquired:
31+
# my_important_option = config.get_main_option("my_important_option")
32+
# ... etc.
33+
34+
35+
def run_migrations_offline():
36+
"""Run migrations in 'offline' mode.
37+
38+
This configures the context with just a URL
39+
and not an Engine, though an Engine is acceptable
40+
here as well. By skipping the Engine creation
41+
we don't even need a DBAPI to be available.
42+
43+
Calls to context.execute() here emit the given string to the
44+
script output.
45+
46+
"""
47+
url = config.get_main_option("sqlalchemy.url")
48+
context.configure(
49+
url=url, target_metadata=target_metadata, literal_binds=True
50+
)
51+
52+
with context.begin_transaction():
53+
context.run_migrations()
54+
55+
56+
def run_migrations_online():
57+
"""Run migrations in 'online' mode.
58+
59+
In this scenario we need to create an Engine
60+
and associate a connection with the context.
61+
62+
"""
63+
64+
# this callback is used to prevent an auto-migration from being generated
65+
# when there are no changes to the schema
66+
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
67+
def process_revision_directives(context, revision, directives):
68+
if getattr(config.cmd_opts, 'autogenerate', False):
69+
script = directives[0]
70+
if script.upgrade_ops.is_empty():
71+
directives[:] = []
72+
logger.info('No changes in schema detected.')
73+
74+
connectable = current_app.extensions['migrate'].db.get_engine()
75+
76+
with connectable.connect() as connection:
77+
context.configure(
78+
connection=connection,
79+
target_metadata=target_metadata,
80+
process_revision_directives=process_revision_directives,
81+
**current_app.extensions['migrate'].configure_args
82+
)
83+
84+
with context.begin_transaction():
85+
context.run_migrations()
86+
87+
88+
if context.is_offline_mode():
89+
run_migrations_offline()
90+
else:
91+
run_migrations_online()

0 commit comments

Comments
 (0)