Skip to content
Snippets Groups Projects
Commit b6a67fcd authored by Patrick Jentsch's avatar Patrick Jentsch
Browse files

Merge base templates. Add database support. Add blueprint for main.

parent f6b2dd32
No related branches found
No related tags found
No related merge requests found
Showing
with 414 additions and 73 deletions
from config import config
from flask import Flask, render_template
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
def create_app(config_name):
......@@ -7,11 +15,13 @@ def create_app(config_name):
app.config.from_object(config[config_name])
config[config_name].init_app(app)
@app.route('/')
def index():
return render_template('base.html.j2')
db.init_app(app)
login_manager.init_app(app)
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')
from flask import render_template
from flask import flash, redirect, render_template, request, url_for
from flask_login import login_required, login_user, logout_user
from . import auth
from .forms import LoginForm
from ..models import User
@auth.route('/login', methods=['GET', 'POST'])
def login():
return render_template('auth/login.html.j2', title='Log in')
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
next = request.args.get('next')
if next is None or not next.startswith('/'):
next = url_for('main.index')
return redirect(next)
flash('Invalid username or password.')
return render_template('auth/login.html.j2', form=form, title='Log in')
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.')
return redirect(url_for('main.index'))
@auth.route('/register', methods=['GET', 'POST'])
......
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views
from flask import render_template
from . import main
@main.route('/')
def index():
return render_template('main/index.html.j2')
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from . import db
from . import login_manager
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return '<Role %r>' % self.name
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
......@@ -16,6 +16,7 @@
</div>
</div>
</div>
<div class="col s12 m6">
<div class="card medium">
<div class="card-content">
......
......@@ -14,7 +14,35 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
{% include 'header.html.j2' %}
<header>
<div id="nav-notifications-dropdown" class="dropdown-content">
<p>Notifications</p>
</div>
<div id="nav-settings-dropdown" class="dropdown-content">
<p>Settings</p>
</div>
<nav>
<div class="nav-wrapper">
<a href="#!" class="brand-logo">
{% if title %}{{ title }}{% else %}Opaque{% endif %}
</a>
<a href="#" data-target="slide-out" class="sidenav-trigger"><i class="material-icons">menu</i></a>
<ul class="right hide-on-med-and-down">
<li><a id="nav-notifications" class="dropdown-trigger" href="#!" data-target="nav-notifications-dropdown"><i class="material-icons">notifications</i></a></li>
<li><a id="nav-settings" class="dropdown-trigger" href="#!" data-target="nav-settings-dropdown"><i class="material-icons">settings</i></a></li>
</ul>
</div>
</nav>
<ul id="slide-out" class="sidenav sidenav-fixed">
<li><a href="{{ url_for('main.index') }}">Opaque</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Log in</a></li>
{% endif %}
</ul>
</header>
<main class="grey lighten-5">
<div class="container">
......@@ -25,7 +53,45 @@
</div>
</main>
{% include 'footer.html.j2' %}
<footer class="page-footer">
<div class="container">
<div class="row">
<div class="col s12 l3">
<img src="{{ url_for('static', filename='images/logo_sfb_1288.png') }}" class="responsive-img" style="max-height: 140px;">
</div>
<div class="col s12 l3">
<h5 class="white-text">About</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="#!">Link 1</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 2</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 3</a></li>
</ul>
</div>
<div class="col s12 l3">
<h5 class="white-text">Connect</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="#!">Link 1</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 2</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 3</a></li>
</ul>
</div>
<div class="col s12 l3">
<h5 class="white-text">Contact</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="#!">Link 1</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 2</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 3</a></li>
</ul>
</div>
</div>
</div>
<div class="footer-copyright">
<div class="container">
© 2019 Bielefeld University
<a class="grey-text text-lighten-4 right" href="#!">Impress</a>
</div>
</div>
</footer>
<!--JavaScript at end of body for optimized loading-->
<script type="text/javascript" src="{{ url_for('static', filename='js/materialize.min.js') }}"></script>
......
<footer class="page-footer">
<div class="container">
<div class="row">
<div class="col s12 l3">
<img src="{{ url_for('static', filename='images/logo_sfb_1288.png') }}" class="responsive-img" style="max-height: 140px;">
</div>
<div class="col s12 l3">
<h5 class="white-text">About</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="#!">Link 1</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 2</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 3</a></li>
</ul>
</div>
<div class="col s12 l3">
<h5 class="white-text">Connect</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="#!">Link 1</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 2</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 3</a></li>
</ul>
</div>
<div class="col s12 l3">
<h5 class="white-text">Contact</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="#!">Link 1</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 2</a></li>
<li><a class="grey-text text-lighten-3" href="#!">Link 3</a></li>
</ul>
</div>
</div>
</div>
<div class="footer-copyright">
<div class="container">
© 2019 Bielefeld University
<a class="grey-text text-lighten-4 right" href="#!">Impress</a>
</div>
</div>
</footer>
<header>
<div id="nav-notifications-dropdown" class="dropdown-content">
<p>Notifications</p>
</div>
<div id="nav-settings-dropdown" class="dropdown-content">
<p>Settings</p>
</div>
<nav>
<div class="nav-wrapper">
<a href="#!" class="brand-logo">
{% if title %}{{ title }}{% else %}Opaque{% endif %}
</a>
<a href="#" data-target="slide-out" class="sidenav-trigger"><i class="material-icons">menu</i></a>
<ul class="right hide-on-med-and-down">
<li><a id="nav-notifications" class="dropdown-trigger" href="#!" data-target="nav-notifications-dropdown"><i class="material-icons">notifications</i></a></li>
<li><a id="nav-settings" class="dropdown-trigger" href="#!" data-target="nav-settings-dropdown"><i class="material-icons">settings</i></a></li>
</ul>
</div>
</nav>
<ul id="slide-out" class="sidenav sidenav-fixed">
<li><a href="{{ url_for('index') }}">Opaque</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
</ul>
</header>
......@@ -2,6 +2,19 @@
{% block page_content %}
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title">
Hello,
{% if current_user.is_authenticated %}
{{ current_user.username }}
{% else %}
Stranger
{% endif %}!
</span>
</div>
</div>
<h2>Services</h2>
<div class="row">
......
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
SQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod
def init_app(app):
......@@ -11,6 +15,7 @@ class Config:
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data_dev.sqlite')
# class TestingConfig(Config):
......
File added
Generic single-database configuration.
\ No newline at end of file
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
'sqlalchemy.url', current_app.config.get(
'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
"""initial migration
Revision ID: 0d4e0dde8ae4
Revises:
Create Date: 2019-07-05 14:43:36.246455
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0d4e0dde8ae4'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=64), nullable=True),
sa.Column('username', sa.String(length=64), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
op.drop_table('roles')
# ### end Alembic commands ###
from app import create_app
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate
import os
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
Flask==1.0.3
Flask-Login==0.4.1
Flask-Migrate==2.5.2
Flask-SQLAlchemy==2.4.0
Flask-WTF==0.14.2
python-dotenv==0.10.3
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment