| venv/ | |||||
| *.pyc | |||||
| __pycache__/ | |||||
| instance/ | |||||
| .pytest_cache/ | |||||
| .coverage | |||||
| htmlcov/ | |||||
| dist/ | |||||
| build/ | |||||
| *.egg-info/ |
| # Default ignored files | # Default ignored files | ||||
| /workspace.xml | |||||
| /workspace.xml | |||||
| # Datasource local storage ignored files | |||||
| /dataSources.local.xml |
| <?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> |
| <?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> |
| 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 |
| 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 |
| 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')) |
| 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) |
| 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) | |||||
| ); |
| 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; } |
| {% 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 %} |
| {% 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 %} |
| <!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> |
| {% 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 %} |
| {% 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 %} |
| {% 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 %} |