| @@ -0,0 +1,14 @@ | |||
| venv/ | |||
| *.pyc | |||
| __pycache__/ | |||
| instance/ | |||
| .pytest_cache/ | |||
| .coverage | |||
| htmlcov/ | |||
| dist/ | |||
| build/ | |||
| *.egg-info/ | |||
| @@ -1,2 +1,4 @@ | |||
| # Default ignored files | |||
| /workspace.xml | |||
| /workspace.xml | |||
| # Datasource local storage ignored files | |||
| /dataSources.local.xml | |||
| @@ -0,0 +1,11 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <project version="4"> | |||
| <component name="DataSourceManagerImpl" format="xml" multifile-model="true"> | |||
| <data-source source="LOCAL" name="@localhost" uuid="1d925457-0ec6-4e05-8d64-06939788523b"> | |||
| <driver-ref>mysql.8</driver-ref> | |||
| <synchronize>true</synchronize> | |||
| <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver> | |||
| <jdbc-url>jdbc:mysql://localhost:3306</jdbc-url> | |||
| </data-source> | |||
| </component> | |||
| </project> | |||
| @@ -0,0 +1,8 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <project version="4"> | |||
| <component name="SqlDialectMappings"> | |||
| <file url="file://$PROJECT_DIR$/calender/auth.py" dialect="GenericSQL" /> | |||
| <file url="file://$PROJECT_DIR$/calender/calender.py" dialect="GenericSQL" /> | |||
| <file url="file://$PROJECT_DIR$/calender/schema.sql" dialect="SQLite" /> | |||
| </component> | |||
| </project> | |||
| @@ -0,0 +1,42 @@ | |||
| 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, 'calender.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 | |||
| # a simple page that says hello | |||
| @app.route('/hello') | |||
| def hello(): | |||
| return 'Hello, World!' | |||
| from . import db | |||
| db.init_app(app) | |||
| from . import auth | |||
| app.register_blueprint(auth.bp) | |||
| from . import calender | |||
| app.register_blueprint(calender.bp) | |||
| app.add_url_rule('/', endpoint='index') | |||
| return app | |||
| @@ -0,0 +1,95 @@ | |||
| import functools | |||
| from flask import ( | |||
| Blueprint, flash, redirect, render_template, request, session, url_for, | |||
| g) | |||
| from werkzeug.security import check_password_hash, generate_password_hash | |||
| from calender.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') | |||
| @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') | |||
| @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() | |||
| @bp.route('/logout') | |||
| def logout(): | |||
| session.clear() | |||
| return redirect(url_for('index')) | |||
| 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 | |||
| @@ -0,0 +1,99 @@ | |||
| from flask import ( | |||
| Blueprint, render_template, | |||
| flash, request, url_for, redirect, abort, g) | |||
| from calender.auth import login_required | |||
| from calender.db import get_db | |||
| bp = Blueprint('calender', __name__) | |||
| @bp.route('/') | |||
| def index(): | |||
| db = get_db() | |||
| posts = db.execute( | |||
| 'SELECT p.id, title, body, created, author_id, username' | |||
| ' FROM post p JOIN user u ON p.author_id = u.id' | |||
| ' ORDER BY created DESC' | |||
| ).fetchall() | |||
| return render_template('calender/index.html', posts=posts) | |||
| @bp.route('/create', methods=('GET', 'POST')) | |||
| @login_required | |||
| def create(): | |||
| if request.method == 'POST': | |||
| title = request.form['title'] | |||
| body = request.form['body'] | |||
| error = None | |||
| if not title: | |||
| error = 'Title is required.' | |||
| if error is not None: | |||
| flash(error) | |||
| else: | |||
| db = get_db() | |||
| db.execute( | |||
| 'INSERT INTO post (title, body, author_id)' | |||
| ' VALUES (?, ?, ?)', | |||
| (title, body, g.user['id']) | |||
| ) | |||
| db.commit() | |||
| return redirect(url_for('calender.index')) | |||
| return render_template('calender/create.html') | |||
| def get_post(id, check_author=True): | |||
| post = get_db().execute( | |||
| 'SELECT p.id, title, body, created, author_id, username' | |||
| ' FROM post p JOIN user u ON p.author_id = u.id' | |||
| ' WHERE p.id = ?', | |||
| (id,) | |||
| ).fetchone() | |||
| if post is None: | |||
| abort(404, "Post id {0} doesn't exist.".format(id)) | |||
| if check_author and post['author_id'] != g.user['id']: | |||
| abort(403) | |||
| return post | |||
| @bp.route('/<int:id>/update', methods=('GET', 'POST')) | |||
| @login_required | |||
| def update(id): | |||
| post = get_post(id) | |||
| if request.method == 'POST': | |||
| title = request.form['title'] | |||
| body = request.form['body'] | |||
| error = None | |||
| if not title: | |||
| error = 'Title is required.' | |||
| if error is not None: | |||
| flash(error) | |||
| else: | |||
| db = get_db() | |||
| db.execute( | |||
| 'UPDATE post SET title = ?, body = ?' | |||
| ' WHERE id = ?', | |||
| (title, body, id) | |||
| ) | |||
| db.commit() | |||
| return redirect(url_for('calender.index')) | |||
| return render_template('calender/update.html', post=post) | |||
| @bp.route('/<int:id>/delete', methods=('POST',)) | |||
| @login_required | |||
| def delete(id): | |||
| get_post(id) | |||
| db = get_db() | |||
| db.execute('DELETE FROM post WHERE id = ?', (id,)) | |||
| db.commit() | |||
| return redirect(url_for('calender.index')) | |||
| @@ -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) | |||
| @@ -0,0 +1,17 @@ | |||
| DROP TABLE IF EXISTS user; | |||
| DROP TABLE IF EXISTS post; | |||
| CREATE TABLE user ( | |||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| username TEXT UNIQUE NOT NULL, | |||
| password TEXT NOT NULL | |||
| ); | |||
| CREATE TABLE post ( | |||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| author_id INTEGER NOT NULL, | |||
| created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||
| title TEXT NOT NULL, | |||
| body TEXT NOT NULL, | |||
| FOREIGN KEY (author_id) REFERENCES user (id) | |||
| ); | |||
| @@ -0,0 +1,26 @@ | |||
| html { font-family: sans-serif; background: #eee; padding: 1rem; } | |||
| body { max-width: 960px; margin: 0 auto; background: white; } | |||
| h1 { 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; } | |||
| @@ -0,0 +1,15 @@ | |||
| {% extends 'base.html' %} | |||
| {% block header %} | |||
| <h1>{% block title %}Log In{% endblock %}</h1> | |||
| {% endblock %} | |||
| {% block content %} | |||
| <form method="post"> | |||
| <label for="username">Username</label> | |||
| <input name="username" id="username" required> | |||
| <label for="password">Password</label> | |||
| <input type="password" name="password" id="password" required> | |||
| <input type="submit" value="Log In"> | |||
| </form> | |||
| {% endblock %} | |||
| @@ -0,0 +1,15 @@ | |||
| {% extends 'base.html' %} | |||
| {% block header %} | |||
| <h1>{% block title %}Register{% endblock %}</h1> | |||
| {% endblock %} | |||
| {% block content %} | |||
| <form method="post"> | |||
| <label for="username">Username</label> | |||
| <input name="username" id="username" required> | |||
| <label for="password">Password</label> | |||
| <input type="password" name="password" id="password" required> | |||
| <input type="submit" value="Register"> | |||
| </form> | |||
| {% endblock %} | |||
| @@ -0,0 +1,24 @@ | |||
| <!doctype html> | |||
| <title>{% block title %}{% endblock %} - Flaskr</title> | |||
| <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> | |||
| <nav> | |||
| <h1>Flaskr</h1> | |||
| <ul> | |||
| {% if g.user %} | |||
| <li><span>{{ g.user['username'] }}</span> | |||
| <li><a href="{{ url_for('auth.logout') }}">Log Out</a> | |||
| {% else %} | |||
| <li><a href="{{ url_for('auth.register') }}">Register</a> | |||
| <li><a href="{{ url_for('auth.login') }}">Log In</a> | |||
| {% endif %} | |||
| </ul> | |||
| </nav> | |||
| <section class="content"> | |||
| <header> | |||
| {% block header %}{% endblock %} | |||
| </header> | |||
| {% for message in get_flashed_messages() %} | |||
| <div class="flash">{{ message }}</div> | |||
| {% endfor %} | |||
| {% block content %}{% endblock %} | |||
| </section> | |||
| @@ -0,0 +1,15 @@ | |||
| {% extends 'base.html' %} | |||
| {% block header %} | |||
| <h1>{% block title %}New Post{% endblock %}</h1> | |||
| {% endblock %} | |||
| {% block content %} | |||
| <form method="post"> | |||
| <label for="title">Title</label> | |||
| <input name="title" id="title" value="{{ request.form['title'] }}" required> | |||
| <label for="body">Body</label> | |||
| <textarea name="body" id="body">{{ request.form['body'] }}</textarea> | |||
| <input type="submit" value="Save"> | |||
| </form> | |||
| {% endblock %} | |||
| @@ -0,0 +1,28 @@ | |||
| {% extends 'base.html' %} | |||
| {% block header %} | |||
| <h1>{% block title %}Posts{% endblock %}</h1> | |||
| {% if g.user %} | |||
| <a class="action" href="{{ url_for('calender.create') }}">New</a> | |||
| {% endif %} | |||
| {% endblock %} | |||
| {% block content %} | |||
| {% for post in posts %} | |||
| <article class="post"> | |||
| <header> | |||
| <div> | |||
| <h1>{{ post['title'] }}</h1> | |||
| <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div> | |||
| </div> | |||
| {% if g.user['id'] == post['author_id'] %} | |||
| <a class="action" href="{{ url_for('calender.update', id=post['id']) }}">Edit</a> | |||
| {% endif %} | |||
| </header> | |||
| <p class="body">{{ post['body'] }}</p> | |||
| </article> | |||
| {% if not loop.last %} | |||
| <hr> | |||
| {% endif %} | |||
| {% endfor %} | |||
| {% endblock %} | |||
| @@ -0,0 +1,19 @@ | |||
| {% extends 'base.html' %} | |||
| {% block header %} | |||
| <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1> | |||
| {% endblock %} | |||
| {% block content %} | |||
| <form method="post"> | |||
| <label for="title">Title</label> | |||
| <input name="title" id="title" | |||
| value="{{ request.form['title'] or post['title'] }}" required> | |||
| <label for="body">Body</label> | |||
| <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea> | |||
| <input type="submit" value="Save"> | |||
| </form> | |||
| <hr> | |||
| <form action="{{ url_for('calender.delete', id=post['id']) }}" method="post"> | |||
| <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');"> | |||
| </form> | |||
| {% endblock %} | |||