diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..53f8e65 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,28 @@ +from flask import Flask +from flask_bootstrap import Bootstrap +from flask_mail import Mail +from flask_moment import Moment +from flask_sqlalchemy import SQLAlchemy +from config import config + + +bootstrap = Bootstrap() +mail = Mail() +moment = Moment() +db = SQLAlchemy() + + +def create_app(config_name): + app = Flask(__name__) + app.config.from_object(config[config_name]) + config[config_name].init_app(app) + + bootstrap.init_app(app) + mail.init_app(app) + moment.init_app(app) + db.init_app(app) + + from .main import main as main_blueprint + app.register_blueprint(main_blueprint) + + return app diff --git a/app/email.py b/app/email.py new file mode 100644 index 0000000..0f6ac52 --- /dev/null +++ b/app/email.py @@ -0,0 +1,20 @@ +from threading import Thread +from flask import current_app, render_template +from flask_mail import Message +from . import mail + + +def send_async_email(app, msg): + with app.app_context(): + mail.send(msg) + + +def send_email(to, subject, template, **kwargs): + app = current_app._get_current_object() + msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject, + sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to]) + msg.body = render_template(template + '.txt', **kwargs) + msg.html = render_template(template + '.html', **kwargs) + thr = Thread(target=send_async_email, args=[app, msg]) + thr.start() + return thr diff --git a/app/main/__init__.py b/app/main/__init__.py new file mode 100644 index 0000000..153e0f2 --- /dev/null +++ b/app/main/__init__.py @@ -0,0 +1,7 @@ +from flask import Blueprint + + +main = Blueprint('main', __name__) + + +from . import views, errors diff --git a/app/main/errors.py b/app/main/errors.py new file mode 100644 index 0000000..7c76c77 --- /dev/null +++ b/app/main/errors.py @@ -0,0 +1,12 @@ +from flask import render_template +from . import main + + +@main.app_errorhandler(404) +def page_not_found(e): + return render_template('404.html'), 404 + + +@main.app_errorhandler(500) +def internal_server_error(e): + return render_template('500.html'), 500 diff --git a/app/main/forms.py b/app/main/forms.py new file mode 100644 index 0000000..2ca9277 --- /dev/null +++ b/app/main/forms.py @@ -0,0 +1,8 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField +from wtforms.validators import DataRequired + + +class NameForm(FlaskForm): + name = StringField('What is your name?', validators=[DataRequired()]) + submit = SubmitField('Submit') diff --git a/app/main/views.py b/app/main/views.py new file mode 100644 index 0000000..b8a380e --- /dev/null +++ b/app/main/views.py @@ -0,0 +1,29 @@ +from flask import render_template, session, redirect, url_for, current_app +from .. import db +from ..models import User +from ..email import send_email +from . import main +from .forms import NameForm + + +@main.route('/', methods=['GET', 'POST']) +def index(): + form = NameForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=form.name.data).first() + if user is None: + user = User(username=form.name.data) + db.session.add(user) + db.session.commit() + session['known'] = False + if current_app.config['FLASKY_ADMIN']: + send_email(current_app.config['FLASKY_ADMIN'], 'New user', + 'mail/new_user', user=user) + else: + session['known'] = True + session['name'] = form.name.data + form.name.data = '' + return redirect(url_for('.index')) + return render_template('index.html', + form=form, name=session.get('name'), + known=session.get('known', False)) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..5c885d6 --- /dev/null +++ b/app/models.py @@ -0,0 +1,21 @@ +from . import db + + +class Role(db.Model): + __tablename__ = 'roles' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), unique=True) + users = db.relationship('User', backref='role', lazy='dynamic') + + def __repr__(self): + return '' % self.name + + +class User(db.Model): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True, index=True) + role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) + + def __repr__(self): + return '' % self.username diff --git a/static/favicon.ico b/app/static/favicon.ico similarity index 100% rename from static/favicon.ico rename to app/static/favicon.ico diff --git a/templates/404.html b/app/templates/404.html similarity index 100% rename from templates/404.html rename to app/templates/404.html diff --git a/templates/500.html b/app/templates/500.html similarity index 100% rename from templates/500.html rename to app/templates/500.html diff --git a/templates/base.html b/app/templates/base.html similarity index 89% rename from templates/base.html rename to app/templates/base.html index 3a8525d..1119f07 100644 --- a/templates/base.html +++ b/app/templates/base.html @@ -19,11 +19,11 @@ - Flasky + Flasky diff --git a/templates/index.html b/app/templates/index.html similarity index 100% rename from templates/index.html rename to app/templates/index.html diff --git a/templates/mail/new_user.html b/app/templates/mail/new_user.html similarity index 100% rename from templates/mail/new_user.html rename to app/templates/mail/new_user.html diff --git a/templates/mail/new_user.txt b/app/templates/mail/new_user.txt similarity index 100% rename from templates/mail/new_user.txt rename to app/templates/mail/new_user.txt diff --git a/config.py b/config.py new file mode 100644 index 0000000..0824dc5 --- /dev/null +++ b/config.py @@ -0,0 +1,46 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' + MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.strato.de') + MAIL_PORT = int(os.environ.get('MAIL_PORT', '587')) + MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \ + ['true', 'on', '1'] + MAIL_USERNAME = os.environ.get('MAIL_USERNAME') + MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') + FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]' + FLASKY_MAIL_SENDER = 'Flasky Admin ' + FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN') + SQLALCHEMY_TRACK_MODIFICATIONS = False + + @staticmethod + def init_app(app): + pass + + +class DevelopmentConfig(Config): + DEBUG = True + SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') + + +class TestingConfig(Config): + TESTING = True + SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \ + 'sqlite://' + + +class ProductionConfig(Config): + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'data.sqlite') + + +config = { + 'development': DevelopmentConfig, + 'testing': TestingConfig, + 'production': ProductionConfig, + + 'default': DevelopmentConfig +} diff --git a/flasky.py b/flasky.py new file mode 100644 index 0000000..ac7af0e --- /dev/null +++ b/flasky.py @@ -0,0 +1,21 @@ +import os +from flask_migrate import Migrate +from app import create_app, db +from app.models import User, Role + + +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) + + +@app.cli.command() +def test(): + """Run the unit tests.""" + import unittest + tests = unittest.TestLoader().discover('tests') + unittest.TextTestRunner(verbosity=2).run(tests) diff --git a/hello.py b/hello.py deleted file mode 100644 index 3dc6f3d..0000000 --- a/hello.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from threading import Thread -from flask import Flask, render_template, session, redirect, url_for -from flask_bootstrap import Bootstrap -from flask_moment import Moment -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField -from wtforms.validators import DataRequired -from flask_sqlalchemy import SQLAlchemy -from flask_migrate import Migrate -from flask_mail import Mail, Message - - -basedir = os.path.abspath(os.path.dirname(__file__)) - -app = Flask(__name__) -app.config['SECRET_KEY'] = 'hard to guess string' -app.config['SQLALCHEMY_DATABASE_URI'] = \ - 'sqlite:///' + os.path.join(basedir, 'data.sqlite') -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config['MAIL_SERVER'] = 'smtp.strato.com' -app.config['MAIL_PORT'] = 587 -app.config['MAIL_USE_TLS'] = True -app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME') -app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD') -app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]' -app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin ' -app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN') - -bootstrap = Bootstrap(app) -moment = Moment(app) -db = SQLAlchemy(app) -migrate = Migrate(app, db) -mail = Mail(app) - - -class Role(db.Model): - __tablename__ = 'roles' - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(64), unique=True) - users = db.relationship('User', backref='role', lazy='dynamic') - - def __repr__(self): - return '' % self.name - - -class User(db.Model): - __tablename__ = 'users' - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(64), unique=True, index=True) - role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) - - def __repr__(self): - return '' % self.username - - -def send_async_email(app, msg): - with app.app_context(): - mail.send(msg) - - -def send_email(to, subject, template, **kwargs): - msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject, - sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to]) - msg.body = render_template(template + '.txt', **kwargs) - msg.html = render_template(template + '.html', **kwargs) - thr = Thread(target=send_async_email, args=[app, msg]) - thr.start() - return thr - - -class NameForm(FlaskForm): - name = StringField('What is your name?', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -@app.shell_context_processor -def make_shell_context(): - return dict(db=db, User=User, Role=Role) - - -@app.errorhandler(404) -def page_not_found(e): - return render_template('404.html'), 404 - - -@app.errorhandler(500) -def internal_server_error(e): - return render_template('500.html'), 500 - - -@app.route('/', methods=['GET', 'POST']) -def index(): - form = NameForm() - if form.validate_on_submit(): - user = User.query.filter_by(username=form.name.data).first() - if user is None: - user = User(username=form.name.data) - db.session.add(user) - db.session.commit() - session['known'] = False - if app.config['FLASKY_ADMIN']: - send_email(app.config['FLASKY_ADMIN'], ' New user', - 'mail/new_user', user=user) - else: - session['known'] = True - session['name'] = form.name.data - form.name.data = '' - return redirect(url_for('index')) - return render_template('index.html', form=form, name=session.get('name'), - known=session.get('known', False)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..27efa21 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +alembic==1.0.1 +blinker==1.4 +Click==7.0 +dominate==2.3.4 +Flask==1.0.2 +Flask-Bootstrap==3.3.7.1 +Flask-Mail==0.9.1 +Flask-Migrate==2.2.1 +Flask-Moment==0.6.0 +Flask-SQLAlchemy==2.3.2 +Flask-WTF==0.14.2 +itsdangerous==0.24 +Jinja2==2.10 +Mako==1.0.7 +MarkupSafe==1.0 +python-dateutil==2.7.3 +python-editor==1.0.3 +six==1.11.0 +SQLAlchemy==1.2.12 +visitor==0.1.3 +Werkzeug==0.14.1 +WTForms==2.2.1 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_basics.py b/tests/test_basics.py new file mode 100644 index 0000000..5f8e353 --- /dev/null +++ b/tests/test_basics.py @@ -0,0 +1,22 @@ +import unittest +from flask import current_app +from app import create_app, db + + +class BasicTestCase(unittest.TestCase): + def setUp(self): + self.app = create_app('testing') + self.app_context = self.app.app_context() + self.app_context.push() + db.create_all() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_app_exists(self): + self.assertFalse(current_app is None) + + def test_app_is_testing(self): + self.assertTrue(current_app.config['TESTING'])