Skip to content

Commit 71b6e4b

Browse files
committed
Seed project with initial files.
1 parent 8d7ffa5 commit 71b6e4b

File tree

14 files changed

+530
-51
lines changed

14 files changed

+530
-51
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: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,21 @@
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 Flask web app with PostgreSQL in Azure](TBD).
44

5-
## Features
5+
If you need an Azure account, you can [create on for free](https://azure.microsoft.com/en-us/free/).
66

7-
This project framework provides the following features:
7+
Temporary instructions for running:
88

9-
* Feature 1
10-
* Feature 2
11-
* ...
9+
* clone
10+
* specify .env variables based off of .env.example
11+
* py -m venv .venv
12+
* .venv\scripts\activate
13+
* pip install -r requirements.txt
14+
* flask db init and flask db migrate -m "first migration" (roughly equivalent to django "python manage.py migrate")
15+
* flask run (equivalent to django "python manage.py runserver")
1216

13-
## Getting Started
17+
To do:
1418

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-
- ...
19+
* investigate /admin functionality with Flask-Admin
20+
* handle 500 error in production
21+
* move app.py to root folder to avoid need for startup.txt (command) and one less step, [details](https://docs.microsoft.com/en-us/azure/developer/python/tutorial-deploy-app-service-on-linux-04#flask-startup-commands)

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+
)

models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from app import db
2+
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
3+
from sqlalchemy.orm import declarative_base, validates
4+
5+
# declarative base class
6+
Base = declarative_base()
7+
8+
class Restaurant(db.Model):
9+
__tablename__ = 'restaurant'
10+
id = Column(Integer, primary_key=True)
11+
name = Column(String(50))
12+
street_address = Column(String(50))
13+
description = Column(String(250))
14+
def __str__(self):
15+
return self.name
16+
17+
class Review(db.Model):
18+
__tablename__ = 'review'
19+
id = Column(Integer, primary_key=True)
20+
restaurant = Column(Integer, ForeignKey('restaurant.id', ondelete="CASCADE"))
21+
user_name = Column(String(30))
22+
rating = Column(Integer)
23+
review_text = Column(String(500))
24+
review_date = Column(DateTime)
25+
26+
@validates('rating')
27+
def validate_rating(self, key, value):
28+
assert value is None or (1 <= value <= 5)
29+
return value
30+
31+
def __str__(self):
32+
return self.restaurant.name + " (" + self.review_date.strftime("%x") +")"

requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Flask
2+
Flask-Migrate
3+
Flask-SQLAlchemy
4+
flask_wtf
5+
psycopg2
6+
python-dotenv
7+
SQLAlchemy

static/images/azure-icon.svg

Lines changed: 25 additions & 0 deletions
Loading

templates/base.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
{% block head %}
5+
<title>Flask web app with PostgreSQL in Azure - {% block title %}{% endblock %}</title>
6+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}">
7+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
8+
{% endblock %}
9+
</head>
10+
<body>
11+
<nav class="navbar navbar-expand-md navbar-light fixed-top bg-light">
12+
<div class="container">
13+
<a class="navbar-brand" href="/">
14+
<img src="{{ url_for('static', filename='images/azure-icon.svg') }}" alt="" width="30" height="24" class="d-inline-block align-text-top">
15+
Azure Restaurant Review
16+
</a>
17+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
18+
<span class="navbar-toggler-icon"></span>
19+
</button>
20+
<div class="collapse navbar-collapse" id="navbarCollapse">
21+
<ul class="navbar-nav mb-2 mb-md-0 ms-auto">
22+
<li class="nav-item dropdown">
23+
<a class="nav-link dropdown-toggle" href="#" id="dropdown07XL" data-bs-toggle="dropdown" aria-expanded="true">Azure Docs</a>
24+
<ul class="dropdown-menu" aria-labelledby="dropdown07XL" data-bs-popper="none">
25+
<li><a class="dropdown-item" target="_blank" href="https://docs.microsft.com/azure">Azure Docs Home</a></li>
26+
<li><a class="dropdown-item" target="_blank" href="#">Python Web App + Database Tutorial</a></li>
27+
<li><a class="dropdown-item" target="_blank" href="#">Configure Python on Azure</a></li>
28+
<li><a class="dropdown-item" target="_blank" href="https://docs.microsft.com/azure/developer/python">Python on Azure Developer Center</a></li>
29+
</ul>
30+
</li>
31+
</ul>
32+
</div>
33+
</div>
34+
</nav>
35+
36+
<main class="container">
37+
{% block content %}{% endblock %}
38+
</main>
39+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script>
40+
</body>
41+
</html>

0 commit comments

Comments
 (0)