├── .gitignore ├── README.md ├── intro_to_flask ├── __init__.py ├── forms.py ├── models.py ├── routes.py ├── static │ ├── css │ │ ├── .gitignore │ │ └── main.css │ ├── img │ │ ├── .gitignore │ │ └── branch-menu.png │ └── js │ │ └── .gitignore └── templates │ ├── .gitignore │ ├── about.html │ ├── contact.html │ ├── home.html │ ├── layout.html │ ├── profile.html │ ├── signin.html │ └── signup.html └── runserver.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intro to Flask 2 | 3 | This is a sample application for the Intro to Flask series on Nettuts+. 4 | 5 | This repo has the following checkpoints 6 | * [00\_project\_structure](https://github.com/lpolepeddi/intro-to-flask/tree/00_project_structure) 7 | * [01\_home\_page](https://github.com/lpolepeddi/intro-to-flask/tree/01_home_page) 8 | * [02\_app\_styling](https://github.com/lpolepeddi/intro-to-flask/tree/02_app_styling) 9 | * [03\_about\_page](https://github.com/lpolepeddi/intro-to-flask/tree/03_about_page) 10 | * [04\_nav\_styling](https://github.com/lpolepeddi/intro-to-flask/tree/04_nav_styling) 11 | * [05\_contact\_form](https://github.com/lpolepeddi/intro-to-flask/tree/05_contact_form) 12 | * [06_contact_styling](https://github.com/lpolepeddi/intro-to-flask/tree/06_contact_styling) 13 | * [07\_form\_validations](https://github.com/lpolepeddi/intro-to-flask/tree/07_form_validations) 14 | * [08\_error\_message\_flashing](https://github.com/lpolepeddi/intro-to-flask/tree/08_error_message_flashing) 15 | * [09\_specific\_message\_flashing](https://github.com/lpolepeddi/intro-to-flask/tree/09_specific_message_flashing) 16 | * [10\_send\_email](https://github.com/lpolepeddi/intro-to-flask/tree/10_send_email) 17 | * [11\_success\_message](https://github.com/lpolepeddi/intro-to-flask/tree/11_success_message) 18 | * [12\_contact\_nav\_link](https://github.com/lpolepeddi/intro-to-flask/tree/12_contact_nav_link) 19 | * [13\_packaged\_app](https://github.com/lpolepeddi/intro-to-flask/tree/13_packaged_app) 20 | * [14\_db_config](https://github.com/lpolepeddi/intro-to-flask/tree/14_db_config) 21 | * [15\_user\_model](https://github.com/lpolepeddi/intro-to-flask/tree/15_user_model) 22 | * [16\_signup\_form](https://github.com/lpolepeddi/intro-to-flask/tree/16_signup_form) 23 | * [17\_profile\_page](https://github.com/lpolepeddi/intro-to-flask/tree/17_profile_page) 24 | * [18\_signin\_form](https://github.com/lpolepeddi/intro-to-flask/tree/18_signin_form) 25 | * [19\_signout](https://github.com/lpolepeddi/intro-to-flask/tree/19_signout) 26 | * [20\_visibility\_control](https://github.com/lpolepeddi/intro-to-flask/tree/20_visibility_control) 27 | 28 | You can switch to each checkpoint to see what the code looks like up to that point by either cloning the repo or using the GitHub branch menu. 29 | 30 | ## Cloning the repo 31 | First clone this repo by running 32 | ```bash 33 | $ git clone https://github.com/lpolepeddi/intro-to-flask.git 34 | ``` 35 | 36 | Then fetch each checkpoint to see what the code looks like at that point by running `$ git checkout []`. For example, to view the code at checkpoint "05\_contact\_form", type 37 | ```bash 38 | $ git checkout 05_contact_form 39 | ``` 40 | 41 | ## Using the GitHub branch menu 42 | Alternatively, you can use GitHub's web interface to view each checkpoint. Click on the branch menu and select the checkpoint you want to view, shown below: 43 | 44 | ![GitHub Branch Menu](https://raw.github.com/lpolepeddi/intro-to-flask/master/intro_to_flask/static/img/branch-menu.png) 45 | -------------------------------------------------------------------------------- /intro_to_flask/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | app.secret_key = 'development key' 6 | 7 | app.config["MAIL_SERVER"] = "smtp.gmail.com" 8 | app.config["MAIL_PORT"] = 465 9 | app.config["MAIL_USE_SSL"] = True 10 | app.config["MAIL_USERNAME"] = 'contact@example.com' 11 | app.config["MAIL_PASSWORD"] = 'your-password' 12 | 13 | from routes import mail 14 | mail.init_app(app) 15 | 16 | app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://your-username:your-password@localhost/development' 17 | 18 | from models import db 19 | db.init_app(app) 20 | 21 | import intro_to_flask.routes 22 | -------------------------------------------------------------------------------- /intro_to_flask/forms.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form, TextField, TextAreaField, SubmitField, validators, ValidationError, PasswordField 2 | from models import db, User 3 | 4 | class ContactForm(Form): 5 | name = TextField("Name", [validators.Required("Please enter your name.")]) 6 | email = TextField("Email", [validators.Required("Please enter your email address."), validators.Email("Please enter your email address.")]) 7 | subject = TextField("Subject", [validators.Required("Please enter a subject.")]) 8 | message = TextAreaField("Message", [validators.Required("Please enter a message.")]) 9 | submit = SubmitField("Send") 10 | 11 | class SignupForm(Form): 12 | firstname = TextField("First name", [validators.Required("Please enter your first name.")]) 13 | lastname = TextField("Last name", [validators.Required("Please enter your last name.")]) 14 | email = TextField("Email", [validators.Required("Please enter your email address."), validators.Email("Please enter your email address.")]) 15 | password = PasswordField('Password', [validators.Required("Please enter a password.")]) 16 | submit = SubmitField("Create account") 17 | 18 | def __init__(self, *args, **kwargs): 19 | Form.__init__(self, *args, **kwargs) 20 | 21 | def validate(self): 22 | if not Form.validate(self): 23 | return False 24 | 25 | user = User.query.filter_by(email = self.email.data.lower()).first() 26 | if user: 27 | self.email.errors.append("That email is already taken") 28 | return False 29 | else: 30 | return True 31 | 32 | class SigninForm(Form): 33 | email = TextField("Email", [validators.Required("Please enter your email address."), validators.Email("Please enter your email address.")]) 34 | password = PasswordField('Password', [validators.Required("Please enter a password.")]) 35 | submit = SubmitField("Sign In") 36 | 37 | def __init__(self, *args, **kwargs): 38 | Form.__init__(self, *args, **kwargs) 39 | 40 | def validate(self): 41 | if not Form.validate(self): 42 | return False 43 | 44 | user = User.query.filter_by(email = self.email.data).first() 45 | if user and user.check_password(self.password.data): 46 | return True 47 | else: 48 | self.email.errors.append("Invalid e-mail or password") 49 | return False 50 | -------------------------------------------------------------------------------- /intro_to_flask/models.py: -------------------------------------------------------------------------------- 1 | from flask.ext.sqlalchemy import SQLAlchemy 2 | from werkzeug import generate_password_hash, check_password_hash 3 | 4 | db = SQLAlchemy() 5 | 6 | class User(db.Model): 7 | __tablename__ = 'users' 8 | uid = db.Column(db.Integer, primary_key = True) 9 | firstname = db.Column(db.String(100)) 10 | lastname = db.Column(db.String(100)) 11 | email = db.Column(db.String(120), unique=True) 12 | pwdhash = db.Column(db.String(54)) 13 | 14 | def __init__(self, firstname, lastname, email, password): 15 | self.firstname = firstname.title() 16 | self.lastname = lastname.title() 17 | self.email = email.lower() 18 | self.set_password(password) 19 | 20 | def set_password(self, password): 21 | self.pwdhash = generate_password_hash(password) 22 | 23 | def check_password(self, password): 24 | return check_password_hash(self.pwdhash, password) 25 | -------------------------------------------------------------------------------- /intro_to_flask/routes.py: -------------------------------------------------------------------------------- 1 | from intro_to_flask import app 2 | from flask import render_template, request, flash, session, url_for, redirect 3 | from forms import ContactForm, SignupForm, SigninForm 4 | from flask.ext.mail import Message, Mail 5 | from models import db, User 6 | 7 | mail = Mail() 8 | 9 | @app.route('/') 10 | def home(): 11 | return render_template('home.html') 12 | 13 | @app.route('/about') 14 | def about(): 15 | return render_template('about.html') 16 | 17 | @app.route('/contact', methods=['GET', 'POST']) 18 | def contact(): 19 | form = ContactForm() 20 | 21 | if request.method == 'POST': 22 | if form.validate() == False: 23 | flash('All fields are required.') 24 | return render_template('contact.html', form=form) 25 | else: 26 | msg = Message(form.subject.data, sender='contact@example.com', recipients=['your_email@example.com']) 27 | msg.body = """ 28 | From: %s <%s> 29 | %s 30 | """ % (form.name.data, form.email.data, form.message.data) 31 | mail.send(msg) 32 | 33 | return render_template('contact.html', success=True) 34 | 35 | elif request.method == 'GET': 36 | return render_template('contact.html', form=form) 37 | 38 | @app.route('/signup', methods=['GET', 'POST']) 39 | def signup(): 40 | form = SignupForm() 41 | 42 | if 'email' in session: 43 | return redirect(url_for('profile')) 44 | 45 | if request.method == 'POST': 46 | if form.validate() == False: 47 | return render_template('signup.html', form=form) 48 | else: 49 | newuser = User(form.firstname.data, form.lastname.data, form.email.data, form.password.data) 50 | db.session.add(newuser) 51 | db.session.commit() 52 | 53 | session['email'] = newuser.email 54 | return redirect(url_for('profile')) 55 | 56 | elif request.method == 'GET': 57 | return render_template('signup.html', form=form) 58 | 59 | @app.route('/profile') 60 | def profile(): 61 | 62 | if 'email' not in session: 63 | return redirect(url_for('signin')) 64 | 65 | user = User.query.filter_by(email = session['email']).first() 66 | 67 | if user is None: 68 | return redirect(url_for('signin')) 69 | else: 70 | return render_template('profile.html') 71 | 72 | @app.route('/signin', methods=['GET', 'POST']) 73 | def signin(): 74 | form = SigninForm() 75 | 76 | if 'email' in session: 77 | return redirect(url_for('profile')) 78 | 79 | if request.method == 'POST': 80 | if form.validate() == False: 81 | return render_template('signin.html', form=form) 82 | else: 83 | session['email'] = form.email.data 84 | return redirect(url_for('profile')) 85 | 86 | elif request.method == 'GET': 87 | return render_template('signin.html', form=form) 88 | 89 | @app.route('/signout') 90 | def signout(): 91 | 92 | if 'email' not in session: 93 | return redirect(url_for('signin')) 94 | 95 | session.pop('email', None) 96 | return redirect(url_for('home')) 97 | -------------------------------------------------------------------------------- /intro_to_flask/static/css/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lalithpolepeddi/intro-to-flask/4b041ca7fd40e1cc095e6ca4c3ef69027171b7fe/intro_to_flask/static/css/.gitignore -------------------------------------------------------------------------------- /intro_to_flask/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | color: #444; 6 | } 7 | 8 | /* Create dark grey header with a white logo */ 9 | 10 | header { 11 | background-color: #2B2B2B; 12 | height: 35px; 13 | width: 100%; 14 | opacity: .9; 15 | margin-bottom: 10px; 16 | } 17 | 18 | header h1.logo { 19 | margin: 0; 20 | font-size: 1.7em; 21 | color: #fff; 22 | text-transform: uppercase; 23 | float: left; 24 | } 25 | 26 | header h1.logo:hover { 27 | color: #fff; 28 | text-decoration: none; 29 | } 30 | 31 | /* Center the body content */ 32 | 33 | .container { 34 | width: 940px; 35 | margin: 0 auto; 36 | } 37 | 38 | div.jumbo { 39 | padding: 10px 0 30px 0; 40 | background-color: #eeeeee; 41 | -webkit-border-radius: 6px; 42 | -moz-border-radius: 6px; 43 | border-radius: 6px; 44 | } 45 | 46 | h2 { 47 | font-size: 3em; 48 | margin-top: 40px; 49 | text-align: center; 50 | letter-spacing: -2px; 51 | } 52 | 53 | h3 { 54 | font-size: 1.7em; 55 | font-weight: 100; 56 | margin-top: 30px; 57 | text-align: center; 58 | letter-spacing: -1px; 59 | color: #999; 60 | } 61 | 62 | /* Display navigation links inline */ 63 | 64 | .menu { 65 | float: right; 66 | margin-top: 8px; 67 | } 68 | 69 | .menu li { 70 | display: inline; 71 | } 72 | 73 | .menu li + li { 74 | margin-left: 35px; 75 | } 76 | 77 | .menu li a { 78 | color: #999; 79 | text-decoration: none; 80 | } 81 | 82 | /* Contact form */ 83 | form label { 84 | font-size: 1.2em; 85 | font-weight: bold; 86 | display: block; 87 | padding: 10px 0; 88 | } 89 | 90 | form input#name, 91 | form input#email, 92 | form input#subject { 93 | width: 400px; 94 | background-color: #fafafa; 95 | -webkit-border-radius: 3px; 96 | -moz-border-radius: 3px; 97 | border-radius: 3px; 98 | border: 1px solid #cccccc; 99 | padding: 5px; 100 | font-size: 1.1em; 101 | } 102 | 103 | form textarea#message { 104 | width: 500px; 105 | height: 100px; 106 | background-color: #fafafa; 107 | -webkit-border-radius: 3px; 108 | -moz-border-radius: 3px; 109 | border-radius: 3px; 110 | border: 1px solid #cccccc; 111 | margin-bottom: 10px; 112 | padding: 5px; 113 | font-size: 1.1em; 114 | } 115 | 116 | form input#submit { 117 | display: block; 118 | -webkit-border-radius: 3px; 119 | -moz-border-radius: 3px; 120 | border-radius: 3px; 121 | border:1px solid #d8d8d8; 122 | padding: 10px; 123 | font-weight:bold; 124 | text-align: center; 125 | color: #000000; 126 | background-color: #f4f4f4; 127 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f4f4f4), color-stop(100%, #e5e5e5)); 128 | background-image: -webkit-linear-gradient(top, #f4f4f4, #e5e5e5); 129 | background-image: -moz-linear-gradient(top, #f4f4f4, #e5e5e5); 130 | background-image: -ms-linear-gradient(top, #f4f4f4, #e5e5e5); 131 | background-image: -o-linear-gradient(top, #f4f4f4, #e5e5e5); 132 | background-image: linear-gradient(top, #f4f4f4, #e5e5e5);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=#f4f4f4, endColorstr=#e5e5e5); 133 | } 134 | 135 | form input#submit:hover{ 136 | cursor: pointer; 137 | border:1px solid #c1c1c1; 138 | background-color: #dbdbdb; 139 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dbdbdb), color-stop(100%, #cccccc)); 140 | background-image: -webkit-linear-gradient(top, #dbdbdb, #cccccc); 141 | background-image: -moz-linear-gradient(top, #dbdbdb, #cccccc); 142 | background-image: -ms-linear-gradient(top, #dbdbdb, #cccccc); 143 | background-image: -o-linear-gradient(top, #dbdbdb, #cccccc); 144 | background-image: linear-gradient(top, #dbdbdb, #cccccc);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=#dbdbdb, endColorstr=#cccccc); 145 | } 146 | 147 | /* Message flashing */ 148 | .flash { 149 | background-color: #FBB0B0; 150 | padding: 10px; 151 | width: 400px; 152 | } 153 | 154 | /* Signup form */ 155 | form input#firstname, 156 | form input#lastname, 157 | form input#password { 158 | width: 400px; 159 | background-color: #fafafa; 160 | -webkit-border-radius: 3px; 161 | -moz-border-radius: 3px; 162 | border-radius: 3px; 163 | border: 1px solid #cccccc; 164 | padding: 5px; 165 | font-size: 1.1em; 166 | } 167 | 168 | form input#password { 169 | margin-bottom: 10px; 170 | } 171 | -------------------------------------------------------------------------------- /intro_to_flask/static/img/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lalithpolepeddi/intro-to-flask/4b041ca7fd40e1cc095e6ca4c3ef69027171b7fe/intro_to_flask/static/img/.gitignore -------------------------------------------------------------------------------- /intro_to_flask/static/img/branch-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lalithpolepeddi/intro-to-flask/4b041ca7fd40e1cc095e6ca4c3ef69027171b7fe/intro_to_flask/static/img/branch-menu.png -------------------------------------------------------------------------------- /intro_to_flask/static/js/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lalithpolepeddi/intro-to-flask/4b041ca7fd40e1cc095e6ca4c3ef69027171b7fe/intro_to_flask/static/js/.gitignore -------------------------------------------------------------------------------- /intro_to_flask/templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lalithpolepeddi/intro-to-flask/4b041ca7fd40e1cc095e6ca4c3ef69027171b7fe/intro_to_flask/templates/.gitignore -------------------------------------------------------------------------------- /intro_to_flask/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |

About

5 |

This is an About page for the Intro to Flask article. Don't I look good? Oh stop, you're making me blush.

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /intro_to_flask/templates/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |

Contact

5 | 6 | {% if success %} 7 |

Thank you for your message. We'll get back to you shortly.

8 | 9 | {% else %} 10 | {% for message in form.name.errors %} 11 |
{{ message }}
12 | {% endfor %} 13 | 14 | {% for message in form.email.errors %} 15 |
{{ message }}
16 | {% endfor %} 17 | 18 | {% for message in form.subject.errors %} 19 |
{{ message }}
20 | {% endfor %} 21 | 22 | {% for message in form.message.errors %} 23 |
{{ message }}
24 | {% endfor %} 25 | 26 |
27 | {{ form.hidden_tag() }} 28 | 29 | {{ form.name.label }} 30 | {{ form.name }} 31 | 32 | {{ form.email.label }} 33 | {{ form.email }} 34 | 35 | {{ form.subject.label }} 36 | {{ form.subject }} 37 | 38 | {{ form.message.label }} 39 | {{ form.message }} 40 | 41 | {{ form.submit }} 42 |
43 | 44 | {% endif %} 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /intro_to_flask/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 |

Welcome to the Flask app

5 |

This is the home page for the Flask app

6 |

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /intro_to_flask/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flask App 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Flask App

12 | 26 |
27 |
28 | 29 |
30 | {% block content %} 31 | {% endblock %} 32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /intro_to_flask/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 |

Profile

5 |

This is {{ session['email'] }}'s profile page

6 |

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /intro_to_flask/templates/signin.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |

Sign In

5 | 6 | {% for message in form.email.errors %} 7 |
{{ message }}
8 | {% endfor %} 9 | 10 | {% for message in form.password.errors %} 11 |
{{ message }}
12 | {% endfor %} 13 | 14 |
15 | {{ form.hidden_tag() }} 16 | 17 | {{ form.email.label }} 18 | {{ form.email }} 19 | 20 | {{ form.password.label }} 21 | {{ form.password }} 22 | 23 | {{ form.submit }} 24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /intro_to_flask/templates/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |

Sign up

5 | 6 | {% for message in form.firstname.errors %} 7 |
{{ message }}
8 | {% endfor %} 9 | 10 | {% for message in form.lastname.errors %} 11 |
{{ message }}
12 | {% endfor %} 13 | 14 | {% for message in form.email.errors %} 15 |
{{ message }}
16 | {% endfor %} 17 | 18 | {% for message in form.password.errors %} 19 |
{{ message }}
20 | {% endfor %} 21 | 22 |
23 | {{ form.hidden_tag() }} 24 | 25 | {{ form.firstname.label }} 26 | {{ form.firstname }} 27 | 28 | {{ form.lastname.label }} 29 | {{ form.lastname }} 30 | 31 | {{ form.email.label }} 32 | {{ form.email }} 33 | 34 | {{ form.password.label }} 35 | {{ form.password }} 36 | 37 | {{ form.submit }} 38 |
39 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | from intro_to_flask import app 2 | 3 | app.run(debug=True) 4 | --------------------------------------------------------------------------------