-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
445 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
FLASK_DEBUG=1 | ||
# only enable this when testing | ||
# DATABASE_URL='sqlite:///:memory:' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import os | ||
import sys | ||
from datetime import datetime | ||
|
||
import click | ||
from flask import Flask, redirect, url_for, render_template, flash | ||
from flask_sqlalchemy import SQLAlchemy | ||
from sqlalchemy import select, Column, Integer, String, Text, DateTime | ||
from flask_wtf import FlaskForm | ||
from wtforms import SubmitField, StringField, TextAreaField | ||
from wtforms.validators import DataRequired, Length | ||
from faker import Faker | ||
|
||
|
||
# SQLite URI compatible | ||
prefix = 'sqlite:///' if sys.platform.startswith('win') else 'sqlite:////' | ||
|
||
|
||
app = Flask(__name__) | ||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'secret string') | ||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv( | ||
'DATABASE_URL', prefix + os.path.join(app.root_path, 'data.db') | ||
) | ||
db = SQLAlchemy(app) | ||
|
||
|
||
# Models | ||
class Note(db.Model): | ||
id = Column(Integer, primary_key=True) | ||
title = Column(String(50)) | ||
body = Column(Text) | ||
created_at = Column(DateTime, default=datetime.now) | ||
updated_at = Column(DateTime, onupdate=datetime.now) | ||
|
||
def __repr__(self): | ||
return f'<Note {self.id}: {self.title}>' # pragma: no cover | ||
|
||
|
||
# Callbacks | ||
@app.shell_context_processor | ||
def make_shell_context(): | ||
return dict(db=db, Note=Note) # pragma: no cover | ||
|
||
|
||
# Commands | ||
@app.cli.command() | ||
@click.option('--drop-table', is_flag=True, help='Re-create the tables.') | ||
def init(drop_table): | ||
"""Initialize the application.""" | ||
if drop_table: | ||
click.confirm( | ||
'This operation will delete the tables, do you want to continue?', | ||
abort=True | ||
) | ||
db.drop_all() | ||
click.echo('Dropped tables.') | ||
db.create_all() | ||
click.echo('Initialized.') | ||
|
||
|
||
@app.cli.command() | ||
@click.option('--count', default=20, help='Quantity of notes, default is 20.') | ||
def fake(count): | ||
"""Generate fake data.""" | ||
db.drop_all() | ||
db.create_all() | ||
|
||
fake = Faker() | ||
|
||
for _ in range(count): | ||
note = Note( | ||
title=fake.sentence(5), | ||
body=fake.text(200), | ||
created_at=fake.date_time_this_year(), | ||
) | ||
db.session.add(note) | ||
|
||
db.session.commit() | ||
click.echo(f'Created {count} notes.') | ||
|
||
|
||
# Forms | ||
class NoteForm(FlaskForm): | ||
title = StringField('Title', validators=[DataRequired(), Length(1, 50)]) | ||
body = TextAreaField('Body', validators=[DataRequired()]) | ||
submit = SubmitField() | ||
|
||
|
||
# Views | ||
@app.route('/') | ||
def index(): | ||
notes = db.session.execute(select(Note)).scalars().all() | ||
return render_template('index.html', notes=notes) | ||
|
||
|
||
@app.route('/new', methods=['GET', 'POST']) | ||
def new_note(): | ||
form = NoteForm() | ||
if form.validate_on_submit(): | ||
title = form.title.data | ||
body = form.body.data | ||
note = Note(title=title, body=body) | ||
db.session.add(note) | ||
db.session.commit() | ||
flash('Note saved.') | ||
return redirect(url_for('index')) | ||
return render_template('new_note.html', form=form) | ||
|
||
|
||
@app.route('/edit/<int:note_id>', methods=['GET', 'POST']) | ||
def edit_note(note_id): | ||
form = NoteForm() | ||
note = db.session.get(Note, note_id) | ||
if form.validate_on_submit(): | ||
note.title = form.title.data | ||
note.body = form.body.data | ||
db.session.commit() | ||
flash('Note updated.') | ||
return redirect(url_for('index')) | ||
# Pre-fill form | ||
form.title.data = note.title | ||
form.body.data = note.body | ||
return render_template('edit_note.html', form=form) | ||
|
||
|
||
@app.route('/delete/<int:note_id>', methods=['POST']) | ||
def delete_note(note_id): | ||
note = db.session.get(Note, note_id) | ||
db.session.delete(note) | ||
db.session.commit() | ||
flash('Note deleted.') | ||
return redirect(url_for('index')) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
body { | ||
margin: auto; | ||
width: 750px; | ||
} | ||
|
||
nav ul { | ||
list-style-type: none; | ||
margin: 0; | ||
padding: 0; | ||
overflow: hidden; | ||
background-color: #333; | ||
} | ||
|
||
nav li { | ||
float: left; | ||
} | ||
|
||
nav li a { | ||
display: block; | ||
color: white; | ||
text-align: center; | ||
padding: 14px 16px; | ||
text-decoration: none; | ||
} | ||
|
||
nav li a:hover { | ||
background-color: #111; | ||
} | ||
|
||
main { | ||
padding: 10px 20px; | ||
} | ||
|
||
.alert { | ||
position: relative; | ||
padding: 0.75rem 1.25rem; | ||
margin-bottom: 1rem; | ||
border: 1px solid transparent; | ||
border-radius: 0.25rem; | ||
color: #004085; | ||
background-color: #cce5ff; | ||
border-color: #b8daff; | ||
} | ||
|
||
.error { | ||
color: #721c24; | ||
background-color: #f8d7da; | ||
border-color: #f5c6cb; | ||
} | ||
|
||
.note { | ||
padding: 5px 10px 10px; | ||
border-left: solid 2px #bbb; | ||
margin-bottom: 20px; | ||
} | ||
|
||
.note form { | ||
display: inline; | ||
} | ||
|
||
.info { | ||
color: grey; | ||
} | ||
|
||
.btn { | ||
font-family: Arial; | ||
font-size: 12px; | ||
padding: 5px 8px; | ||
text-decoration: none; | ||
cursor: pointer; | ||
background-color: white; | ||
color: black; | ||
border: 1px solid #999999; | ||
} | ||
|
||
.btn:hover { | ||
text-decoration: none; | ||
border: 1px solid black; | ||
} | ||
|
||
footer { | ||
font-size: 13px; | ||
color: #888; | ||
border-top: 1px solid #eee; | ||
margin-top: 25px; | ||
text-align: center; | ||
padding: 20px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{% macro form_field(field) %} | ||
{{ field.label }}<br> | ||
{% if field.flags.required %} | ||
{{ field(required='required', **kwargs) }}<br> | ||
{% else %} | ||
{{ field(**kwargs) }}<br> | ||
{% endif %} | ||
{% if field.errors %} | ||
{% for error in field.errors %} | ||
<small class="error">{{ error }}</small><br> | ||
{% endfor %} | ||
{% endif %} | ||
{% endmacro %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
{% block head %} | ||
<meta charset="utf-8"> | ||
<title>{% block title %}Notebook{% endblock %}</title> | ||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> | ||
{% block styles %} | ||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css' ) }}"> | ||
{% endblock %} | ||
{% endblock %} | ||
</head> | ||
<body> | ||
<nav> | ||
<ul> | ||
<li><a href="{{ url_for('index') }}">Notebook</a></li> | ||
</ul> | ||
</nav> | ||
<main> | ||
{% for message in get_flashed_messages() %} | ||
<div class="alert">{{ message }}</div> | ||
{% endfor %} | ||
{% block content %}{% endblock %} | ||
</main> | ||
<footer> | ||
{% block footer %} | ||
<small> © 2023 <a href="https://greyli.com">Grey Li</a> / | ||
<a href="https://github.com/greyli/helloflask">GitHub</a> / | ||
<a href="https://helloflask.com">HelloFlask</a> | ||
</small> | ||
{% endblock %} | ||
</footer> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{% extends 'base.html' %} | ||
{% from '_macros.html' import form_field %} | ||
|
||
{% block title %}Edit Note{% endblock %} | ||
|
||
{% block content %} | ||
<h3>Edit Note</h3> | ||
|
||
<form method="post"> | ||
{{ form.csrf_token }} | ||
{{ form_field(form.title) }} | ||
{{ form_field(form.body, rows=5, cols=50) }} | ||
{{ form.submit }}<br> | ||
</form> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{% extends 'base.html' %} | ||
|
||
{% block content %} | ||
<h3>{{ notes|length }} Notes (<a href="{{ url_for('new_note') }}">new</a>):</h3> | ||
{% for note in notes %} | ||
<div class="note"> | ||
<strong>{{ note.title }}</strong> | ||
<p>{{ note.body }}</p> | ||
<small class="info">Created at: {{ note.created_at.strftime('%Y/%m/%d %H:%M') }}</small> | ||
{% if note.updated_at %} | ||
<small class="info">Updated at: {{ note.updated_at.strftime('%Y/%m/%d %H:%M') }}</small> | ||
{% endif %} | ||
<a class='btn' href="{{ url_for('edit_note', note_id=note.id) }}">Edit</a> | ||
<form method="post" action="{{ url_for('delete_note', note_id=note.id) }}"> | ||
<input class="btn" type="submit" value="Delete" onclick="return confirm('Are you sure?')"> | ||
</form> | ||
</div> | ||
{% endfor %} | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{% extends 'base.html' %} | ||
{% from '_macros.html' import form_field %} | ||
|
||
{% block title %}New Note{% endblock %} | ||
|
||
{% block content %} | ||
<h3>New Note</h3> | ||
|
||
<form method="post"> | ||
{{ form.csrf_token }} | ||
{{ form_field(form.title) }} | ||
{{ form_field(form.body, rows=5, cols=50) }} | ||
{{ form.submit }}<br> | ||
</form> | ||
{% endblock %} |
Oops, something went wrong.