mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-12-24 10:34:17 +00:00
Merge base templates. Add database support. Add blueprint for main.
This commit is contained in:
parent
f6b2dd3282
commit
b6a67fcd4d
@ -1,5 +1,13 @@
|
||||
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
|
||||
|
@ -0,0 +1,11 @@
|
||||
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')
|
@ -1,10 +1,31 @@
|
||||
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'])
|
||||
|
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
from . import views
|
@ -0,0 +1,7 @@
|
||||
from flask import render_template
|
||||
from . import main
|
||||
|
||||
|
||||
@main.route('/')
|
||||
def index():
|
||||
return render_template('main/index.html.j2')
|
43
app/models.py
Normal file
43
app/models.py
Normal file
@ -0,0 +1,43 @@
|
||||
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>
|
||||
|
@ -1,39 +0,0 @@
|
||||
<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>
|
@ -1,25 +0,0 @@
|
||||
<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">
|
@ -1,8 +1,12 @@
|
||||
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):
|
||||
|
BIN
data_dev.sqlite
Normal file
BIN
data_dev.sqlite
Normal file
Binary file not shown.
1
migrations/README
Normal file
1
migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
migrations/alembic.ini
Normal file
45
migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
||||
# 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
|
96
migrations/env.py
Normal file
96
migrations/env.py
Normal file
@ -0,0 +1,96 @@
|
||||
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()
|
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
||||
"""${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"}
|
47
migrations/versions/0d4e0dde8ae4_initial_migration.py
Normal file
47
migrations/versions/0d4e0dde8ae4_initial_migration.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""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 ###
|
10
opaque.py
10
opaque.py
@ -1,5 +1,13 @@
|
||||
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)
|
||||
|
@ -1,4 +1,6 @@
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user