From b1b53679b3a7f2dd82f317f1f7196bb054ccef0e Mon Sep 17 00:00:00 2001 From: Guilherme Capanema Date: Thu, 27 Jun 2019 11:30:26 -0300 Subject: [PATCH] Commit inicial. --- .gitignore | 14 ++ MANIFEST.in | 4 + interruptor/__init__.py | 41 ++++++ interruptor/auth.py | 90 ++++++++++++ interruptor/db.py | 41 ++++++ interruptor/interruptor.py | 45 ++++++ interruptor/schema.sql | 18 +++ interruptor/static/style.css | 134 ++++++++++++++++++ interruptor/templates/auth/login.html.j2 | 15 ++ interruptor/templates/auth/register.html.j2 | 15 ++ interruptor/templates/base.html.j2 | 24 ++++ .../templates/interruptor/index.html.j2 | 23 +++ setup.py | 13 ++ 13 files changed, 477 insertions(+) create mode 100644 .gitignore create mode 100644 MANIFEST.in create mode 100644 interruptor/__init__.py create mode 100644 interruptor/auth.py create mode 100644 interruptor/db.py create mode 100644 interruptor/interruptor.py create mode 100644 interruptor/schema.sql create mode 100644 interruptor/static/style.css create mode 100644 interruptor/templates/auth/login.html.j2 create mode 100644 interruptor/templates/auth/register.html.j2 create mode 100644 interruptor/templates/base.html.j2 create mode 100644 interruptor/templates/interruptor/index.html.j2 create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e72499f --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +venv/ + +*.pyc +__pycache__/ + +instance/ + +.pytest_cache/ +.coverage +htmlcov/ + +dist/ +build/ +*.egg-info/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ec02f21 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include interruptor/schema.sql +graft interruptor/static +graft interruptor/templates +global-exclude *.pyc diff --git a/interruptor/__init__.py b/interruptor/__init__.py new file mode 100644 index 0000000..70c2662 --- /dev/null +++ b/interruptor/__init__.py @@ -0,0 +1,41 @@ +import os + +from flask import Flask + +def create_app(test_config=None): + # create and configure the app + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY='dev', + DATABASE=os.path.join(app.instance_path, 'interruptor.sqlite'), + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile('config.py', silent=True) + else: + # load the test config if passed in + app.config.from_mapping(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # setup GPIO + import RPi.GPIO as GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(17, GPIO.OUT) + + from . import db + db.init_app(app) + + from . import auth + app.register_blueprint(auth.bp) + + from . import interruptor + app.register_blueprint(interruptor.bp) + app.add_url_rule('/', endpoint='index') + + return app diff --git a/interruptor/auth.py b/interruptor/auth.py new file mode 100644 index 0000000..07a6104 --- /dev/null +++ b/interruptor/auth.py @@ -0,0 +1,90 @@ +import functools + +from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for +) +from werkzeug.security import check_password_hash, generate_password_hash + +from interruptor.db import get_db + +bp = Blueprint('auth', __name__, url_prefix='/auth') + +@bp.route('/register', methods=('GET', 'POST')) +def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + + if not username: + error = 'Username is required.' + elif not password: + error = 'Password is required.' + elif db.execute( + 'SELECT id FROM user WHERE username = ?', (username,) + ).fetchone() is not None: + error = 'User {} is already registered.'.format(username) + + if error is None: + db.execute( + 'INSERT INTO user (username, password) VALUES (?, ?)', + (username, generate_password_hash(password)) + ) + db.commit() + return redirect(url_for('auth.login')) + + flash(error) + + return render_template('auth/register.html.j2') + +@bp.route('/login', methods=('GET', 'POST')) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + user = db.execute( + 'SELECT * FROM user WHERE username = ?', (username,) + ).fetchone() + + if user is None: + error = 'Incorrect username.' + elif not check_password_hash(user['password'], password): + error = 'Incorrect password.' + + if error is None: + session.clear() + session['user_id'] = user['id'] + return redirect(url_for('index')) + + flash(error) + + return render_template('auth/login.html.j2') + +@bp.route('/logout') +def logout(): + session.clear() + return redirect(url_for('index')) + +@bp.before_app_request +def load_logged_in_user(): + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + g.user = get_db().execute( + 'SELECT * FROM user WHERE id = ?', (user_id,) + ).fetchone() + +def login_required(view): + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('auth.login')) + + return view(**kwargs) + + return wrapped_view diff --git a/interruptor/db.py b/interruptor/db.py new file mode 100644 index 0000000..55cc1a4 --- /dev/null +++ b/interruptor/db.py @@ -0,0 +1,41 @@ +import sqlite3 + +import click +from flask import current_app, g +from flask.cli import with_appcontext + + +def get_db(): + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + + +def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +def init_db(): + db = get_db() + + with current_app.open_resource('schema.sql') as f: + db.executescript(f.read().decode('utf8')) + + +@click.command('init-db') +@with_appcontext +def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + +def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) diff --git a/interruptor/interruptor.py b/interruptor/interruptor.py new file mode 100644 index 0000000..751cd88 --- /dev/null +++ b/interruptor/interruptor.py @@ -0,0 +1,45 @@ +from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for +) +from werkzeug.exceptions import abort + +from interruptor.auth import login_required +from interruptor.db import get_db + +import RPi.GPIO as GPIO + +bp = Blueprint('interruptor', __name__) + +@bp.route('/') +def index(): + db = get_db() + status = db.execute( + 'SELECT user_id, status, created' + ' FROM status JOIN user ON status.user_id = user.id' + ' ORDER BY created DESC LIMIT 1' + ).fetchone() + return render_template('interruptor/index.html.j2', status=status) + +@bp.route('/toggle') +@login_required +def toggle(): + db = get_db() + status = db.execute( + 'SELECT status' + ' FROM status JOIN user ON status.user_id = user.id' + ' ORDER BY created DESC LIMIT 1' + ).fetchone() + + if status['status']: + GPIO.output(17, GPIO.LOW) + else: + GPIO.output(17, GPIO.HIGH) + + db.execute( + 'INSERT INTO status (user_id, status)' + ' VALUES (?, ?)', + (g.user['id'], not status['status']) + ) + db.commit() + + return redirect(url_for('interruptor.index')) diff --git a/interruptor/schema.sql b/interruptor/schema.sql new file mode 100644 index 0000000..97c0d83 --- /dev/null +++ b/interruptor/schema.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS status; + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL +); + +CREATE TABLE status ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + status BOOLEAN NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +INSERT INTO STATUS (user_id, status) VALUES (1, 1); diff --git a/interruptor/static/style.css b/interruptor/static/style.css new file mode 100644 index 0000000..ece24a7 --- /dev/null +++ b/interruptor/static/style.css @@ -0,0 +1,134 @@ +html { + font-family: sans-serif; + background: #eee; + padding: 1rem; +} + +body { + max-width: 960px; + margin: 0 auto; + background: white; +} + +h1, h2, h3, h4, h5, h6 { + font-family: serif; + color: #377ba8; + margin: 1rem 0; +} + +a { + color: #377ba8; +} + +hr { + border: none; + border-top: 1px solid lightgray; +} + +nav { + background: lightgray; + display: flex; + align-items: center; + padding: 0 0.5rem; +} + +nav h1 { + flex: auto; + margin: 0; +} + +nav h1 a { + text-decoration: none; + padding: 0.25rem 0.5rem; +} + +nav ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; +} + +nav ul li a, nav ul li span, header .action { + display: block; + padding: 0.5rem; +} + +.content { + padding: 0 1rem 1rem; +} + +.content > header { + border-bottom: 1px solid lightgray; + display: flex; + align-items: flex-end; +} + +.content > header h1 { + flex: auto; + margin: 1rem 0 0.25rem 0; +} + +.flash { + margin: 1em 0; + padding: 1em; + background: #cae6f6; + border: 1px solid #377ba8; +} + +.post > header { + display: flex; + align-items: flex-end; + font-size: 0.85em; +} + +.post > header > div:first-of-type { + flex: auto; +} + +.post > header h1 { + font-size: 1.5em; + margin-bottom: 0; +} + +.post .about { + color: slategray; + font-style: italic; +} + +.post .body { + white-space: pre-line; +} + +.content:last-child { + margin-bottom: 0; +} + +.content form { + margin: 1em 0; + display: flex; + flex-direction: column; +} + +.content label { + font-weight: bold; + margin-bottom: 0.5em; +} + +.content input, .content textarea { + margin-bottom: 1em; +} + +.content textarea { + min-height: 12em; + resize: vertical; +} + +input.danger { + color: #cc2f2e; +} + +input[type=submit] { + align-self: start; + min-width: 10em; +} diff --git a/interruptor/templates/auth/login.html.j2 b/interruptor/templates/auth/login.html.j2 new file mode 100644 index 0000000..347cc5f --- /dev/null +++ b/interruptor/templates/auth/login.html.j2 @@ -0,0 +1,15 @@ +{% extends 'base.html.j2' %} + +{% block header %} +

{% block title %}Log In{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/interruptor/templates/auth/register.html.j2 b/interruptor/templates/auth/register.html.j2 new file mode 100644 index 0000000..cb38435 --- /dev/null +++ b/interruptor/templates/auth/register.html.j2 @@ -0,0 +1,15 @@ +{% extends 'base.html.j2' %} + +{% block header %} +

{% block title %}Register{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/interruptor/templates/base.html.j2 b/interruptor/templates/base.html.j2 new file mode 100644 index 0000000..8ca27c9 --- /dev/null +++ b/interruptor/templates/base.html.j2 @@ -0,0 +1,24 @@ + +{% block title %}{% endblock %} - Interruptor + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
diff --git a/interruptor/templates/interruptor/index.html.j2 b/interruptor/templates/interruptor/index.html.j2 new file mode 100644 index 0000000..04443a6 --- /dev/null +++ b/interruptor/templates/interruptor/index.html.j2 @@ -0,0 +1,23 @@ +{% extends 'base.html.j2' %} + +{% block header %} +

{% block title %}Status{% endblock %}

+ {% if g.user %} + + {% if status.status %} + Desligar + {% else %} + Ligar + {% endif %} + + {% endif %} +{% endblock %} + +{% block content %} + + {% if status.status %} + Ligado + {% else %} + Desligado + {% endif %} +{% endblock %} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4adcfa0 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from setuptools import find_packages, setup + +setup( + name='interruptor', + version='1.0.0', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=[ + 'flask', + 'RPi.GPIO', + ], +)