├── app ├── static │ └── style.css ├── templates │ ├── mail │ │ └── feedback.html │ ├── index.html │ ├── admin.html │ ├── contact.html │ ├── login.html │ └── article.html ├── main │ ├── __init__.py │ ├── forms.py │ └── views.py ├── utils.py ├── __init__.py └── models.py ├── migrations ├── README ├── script.py.mako ├── alembic.ini ├── versions │ ├── 1fc9aa1fe77e_adding_employees_table.py │ └── b32daac53148_adding_users_table.py └── env.py ├── .gitignore ├── README.md ├── requirements.txt ├── runner.py ├── LICENSE └── config.py /app/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red 3 | } -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | .settings/ 4 | __pycache__/ 5 | env/ 6 | 7 | -------------------------------------------------------------------------------- /app/templates/mail/feedback.html: -------------------------------------------------------------------------------- 1 |
You have received a new feedback from {{ name }} <{{ email }}>
2 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | from . import views -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Name: {{ name }}
11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Exercise files for [Learn Flask](https://overiq.com/flask/0.12/intro-to-flask/) tutorial at https://overiq.com. 2 | 3 | Steps to install: 4 | 5 | 1. `git clone https://github.com/overiq/flask_app`. 6 | 1. `cd flask_app`. 7 | 1. `virtualenv env`. 8 | 1. `pip install -r requirements.txt`. 9 | 1. `python runner.py runserver` to launch the server. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.9.7 2 | blinker==1.4 3 | click==6.7 4 | Flask==0.12.2 5 | Flask-Login==0.4.1 6 | Flask-Mail==0.9.1 7 | Flask-Migrate==2.1.1 8 | Flask-Script==2.0.6 9 | Flask-SQLAlchemy==2.3.2 10 | Flask-WTF==0.14.2 11 | itsdangerous==0.24 12 | Jinja2==2.10 13 | Mako==1.0.7 14 | MarkupSafe==1.0 15 | PyMySQL==0.8.0 16 | python-dateutil==2.6.1 17 | python-editor==1.0.3 18 | six==1.11.0 19 | SQLAlchemy==1.2.1 20 | Werkzeug==0.14.1 21 | WTForms==2.1 22 | -------------------------------------------------------------------------------- /app/templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |{{ message }}
11 | {% endfor %} 12 | 13 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/main/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import Form, ValidationError 3 | from wtforms import StringField, SubmitField, TextAreaField, BooleanField 4 | from wtforms.validators import DataRequired, Email 5 | 6 | 7 | class ContactForm(FlaskForm): 8 | name = StringField("Name: ", validators=[DataRequired()]) 9 | email = StringField("Email: ", validators=[Email()]) 10 | message = TextAreaField("Message", validators=[DataRequired()]) 11 | submit = SubmitField() 12 | 13 | 14 | class LoginForm(FlaskForm): 15 | username = StringField("Username", validators=[DataRequired()]) 16 | password = StringField("Password", validators=[DataRequired()]) 17 | remember = BooleanField("Remember Me") 18 | submit = SubmitField() -------------------------------------------------------------------------------- /runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | from app import db, create_app 3 | from app.models import User, Post, Tag, Category, Employee, Feedback 4 | from flask_script import Manager, Shell 5 | from flask_migrate import MigrateCommand 6 | 7 | app = create_app(os.getenv('FLASK_ENV') or 'config.DevelopementConfig') 8 | manager = Manager(app) 9 | 10 | # these names will be available inside the shell without explicit import 11 | def make_shell_context(): 12 | return dict(app=app, db=db, User=User, Post=Post, Tag=Tag, Category=Category, 13 | Employee=Employee, Feedback=Feedback) 14 | 15 | manager.add_command('shell', Shell(make_context=make_shell_context)) 16 | manager.add_command('db', MigrateCommand) 17 | 18 | if __name__ == '__main__': 19 | manager.run() -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 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 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 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_migrate import Migrate, MigrateCommand 3 | from flask_mail import Mail, Message 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_script import Manager, Command, Shell 6 | from flask_login import LoginManager 7 | import os, config 8 | 9 | # initializes extensions 10 | db = SQLAlchemy() 11 | mail = Mail() 12 | migrate = Migrate() 13 | login_manager = LoginManager() 14 | login_manager.login_view = 'main.login' 15 | 16 | # application factory 17 | def create_app(config): 18 | 19 | # create application instance 20 | app = Flask(__name__) 21 | app.config.from_object(config) 22 | 23 | db.init_app(app) 24 | mail.init_app(app) 25 | migrate.init_app(app, db) 26 | login_manager.init_app(app) 27 | 28 | from .main import main as main_blueprint 29 | app.register_blueprint(main_blueprint) 30 | 31 | #from .admin import main as admin_blueprint 32 | #app.register_blueprint(admin_blueprint) 33 | 34 | return app 35 | 36 | -------------------------------------------------------------------------------- /migrations/versions/1fc9aa1fe77e_adding_employees_table.py: -------------------------------------------------------------------------------- 1 | """Adding employees table 2 | 3 | Revision ID: 1fc9aa1fe77e 4 | Revises: 5 | Create Date: 2018-01-23 17:34:19.953371 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1fc9aa1fe77e' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('employees', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=255), nullable=False), 24 | sa.Column('designation', sa.String(length=255), nullable=False), 25 | sa.Column('doj', sa.Date(), nullable=False), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('employees') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 OverIQ.com. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | app_dir = os.path.abspath(os.path.dirname(__file__)) 4 | 5 | class BaseConfig: 6 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'A SECRET KEY' 7 | SQLALCHEMY_TRACK_MODIFICATIONS = False 8 | 9 | ##### Flask-Mail configurations ##### 10 | MAIL_SERVER = 'smtp.googlemail.com' 11 | MAIL_PORT = 587 12 | MAIL_USE_TLS = True 13 | MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'infooveriq@gmail.com' 14 | MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'password' 15 | MAIL_DEFAULT_SENDER = MAIL_USERNAME 16 | 17 | 18 | class DevelopementConfig(BaseConfig): 19 | DEBUG = True 20 | SQLALCHEMY_DATABASE_URI = os.environ.get('DEVELOPMENT_DATABASE_URI') or \ 21 | 'mysql+pymysql://root:pass@localhost/flask_app_db' 22 | 23 | 24 | class TestingConfig(BaseConfig): 25 | DEBUG = True 26 | SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URI') or \ 27 | 'mysql+pymysql://root:pass@localhost/flask_app_db' 28 | 29 | class ProductionConfig(BaseConfig): 30 | DEBUG = False 31 | SQLALCHEMY_DATABASE_URI = os.environ.get('PRODUCTION_DATABASE_URI') or \ 32 | 'mysql+pymysql://root:pass@localhost/flask_app_db' -------------------------------------------------------------------------------- /migrations/versions/b32daac53148_adding_users_table.py: -------------------------------------------------------------------------------- 1 | """Adding users table 2 | 3 | Revision ID: b32daac53148 4 | Revises: 1fc9aa1fe77e 5 | Create Date: 2018-01-23 18:51:06.188821 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b32daac53148' 14 | down_revision = '1fc9aa1fe77e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('users', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=100), nullable=True), 24 | sa.Column('username', sa.String(length=50), nullable=False), 25 | sa.Column('email', sa.String(length=100), nullable=False), 26 | sa.Column('password_hash', sa.String(length=100), nullable=False), 27 | sa.Column('created_on', sa.DateTime(), nullable=True), 28 | sa.Column('updated_on', sa.DateTime(), nullable=True), 29 | sa.PrimaryKeyConstraint('id'), 30 | sa.UniqueConstraint('email'), 31 | sa.UniqueConstraint('username') 32 | ) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_table('users') 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /app/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |{{ message }}
11 | {% endif %} 12 | 13 | {% for category, message in get_flashed_messages(with_categories=true) %} 14 |Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam blanditiis debitis doloribus eos magni minus odit, provident tempora. Expedita fugiat harum in incidunt minus nam nesciunt voluptate. Facilis nesciunt, similique! 22 |
23 | 24 |Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias amet animi aperiam inventore molestiae quos, reiciendis voluptatem. Ab, cum cupiditate fugit illo incidunt ipsa neque quam, qui quidem vel voluptatum.
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger('alembic.env') 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | config.set_main_option('sqlalchemy.url', 22 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 23 | target_metadata = current_app.extensions['migrate'].db.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure(url=url) 45 | 46 | with context.begin_transaction(): 47 | context.run_migrations() 48 | 49 | 50 | def run_migrations_online(): 51 | """Run migrations in 'online' mode. 52 | 53 | In this scenario we need to create an Engine 54 | and associate a connection with the context. 55 | 56 | """ 57 | 58 | # this callback is used to prevent an auto-migration from being generated 59 | # when there are no changes to the schema 60 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if getattr(config.cmd_opts, 'autogenerate', False): 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | logger.info('No changes in schema detected.') 67 | 68 | engine = engine_from_config(config.get_section(config.config_ini_section), 69 | prefix='sqlalchemy.', 70 | poolclass=pool.NullPool) 71 | 72 | connection = engine.connect() 73 | context.configure(connection=connection, 74 | target_metadata=target_metadata, 75 | process_revision_directives=process_revision_directives, 76 | **current_app.extensions['migrate'].configure_args) 77 | 78 | try: 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | finally: 82 | connection.close() 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from app import db, login_manager 2 | from datetime import datetime 3 | from flask_login import (LoginManager, UserMixin, login_required, 4 | login_user, current_user, logout_user) 5 | from werkzeug.security import generate_password_hash, check_password_hash 6 | 7 | 8 | class Category(db.Model): 9 | __tablename__ = 'categories' 10 | id = db.Column(db.Integer(), primary_key=True) 11 | name = db.Column(db.String(255), nullable=False, unique=True) 12 | slug = db.Column(db.String(255), nullable=False, unique=True) 13 | created_on = db.Column(db.DateTime(), default=datetime.utcnow) 14 | posts = db.relationship('Post', backref='category', cascade='all,delete-orphan') 15 | 16 | def __repr__(self): 17 | return "<{}:{}>".format(self.id, self.name) 18 | 19 | 20 | post_tags = db.Table('post_tags', 21 | db.Column('post_id', db.Integer, db.ForeignKey('posts.id')), 22 | db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')) 23 | ) 24 | 25 | 26 | class Post(db.Model): 27 | __tablename__ = 'posts' 28 | id = db.Column(db.Integer(), primary_key=True) 29 | title = db.Column(db.String(255), nullable=False) 30 | slug = db.Column(db.String(255), nullable=False) 31 | content = db.Column(db.Text(), nullable=False) 32 | created_on = db.Column(db.DateTime(), default=datetime.utcnow) 33 | updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow) 34 | category_id = db.Column(db.Integer(), db.ForeignKey('categories.id')) 35 | 36 | def __repr__(self): 37 | return "<{}:{}>".format(self.id, self.title[:10]) 38 | 39 | 40 | class Tag(db.Model): 41 | __tablename__ = 'tags' 42 | id = db.Column(db.Integer(), primary_key=True) 43 | name = db.Column(db.String(255), nullable=False) 44 | slug = db.Column(db.String(255), nullable=False) 45 | created_on = db.Column(db.DateTime(), default=datetime.utcnow) 46 | posts = db.relationship('Post', secondary=post_tags, backref='tags') 47 | 48 | def __repr__(self): 49 | return "<{}:{}>".format(self.id, self.name) 50 | 51 | 52 | class Feedback(db.Model): 53 | __tablename__ = 'feedbacks' 54 | id = db.Column(db.Integer(), primary_key=True) 55 | name = db.Column(db.String(1000), nullable=False) 56 | email = db.Column(db.String(100), nullable=False) 57 | message = db.Column(db.Text(), nullable=False) 58 | created_on = db.Column(db.DateTime(), default=datetime.utcnow) 59 | 60 | def __repr__(self): 61 | return "<{}:{}>".format(self.id, self.name) 62 | 63 | 64 | class Employee(db.Model): 65 | __tablename__ = 'employees' 66 | id = db.Column(db.Integer(), primary_key=True) 67 | name = db.Column(db.String(255), nullable=False) 68 | designation = db.Column(db.String(255), nullable=False) 69 | doj = db.Column(db.Date(), nullable=False) 70 | 71 | 72 | @login_manager.user_loader 73 | def load_user(user_id): 74 | return db.session.query(User).get(user_id) 75 | 76 | 77 | class User(db.Model, UserMixin): 78 | __tablename__ = 'users' 79 | id = db.Column(db.Integer(), primary_key=True) 80 | name = db.Column(db.String(100)) 81 | username = db.Column(db.String(50), nullable=False, unique=True) 82 | email = db.Column(db.String(100), nullable=False, unique=True) 83 | password_hash = db.Column(db.String(100), nullable=False) 84 | created_on = db.Column(db.DateTime(), default=datetime.utcnow) 85 | updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow) 86 | 87 | def __repr__(self): 88 | return "<{}:{}>".format(self.id, self.username) 89 | 90 | def set_password(self, password): 91 | self.password_hash = generate_password_hash(password) 92 | 93 | def check_password(self, password): 94 | return check_password_hash(self.password_hash, password) 95 | 96 | -------------------------------------------------------------------------------- /app/main/views.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from . import main 3 | from flask import request, render_template, redirect, url_for, flash, make_response, session, current_app 4 | from flask_login import login_required, login_user, current_user, logout_user 5 | from app.models import User, Post, Category, Feedback, db 6 | from .forms import ContactForm, LoginForm 7 | from app.utils import send_mail 8 | 9 | 10 | @main.route('/') 11 | def index(): 12 | return render_template('index.html', name='Jerry') 13 | 14 | 15 | @main.route('/user/