Note-taking app
import os | |
from flask import Flask | |
from micawber import bootstrap_basic | |
from peewee import SqliteDatabase | |
APP_ROOT = os.path.dirname(os.path.realpath(__file__)) | |
DATABASE = os.path.join(APP_ROOT, 'notes.db') | |
DEBUG = False | |
app = Flask(__name__) | |
app.config.from_object(__name__) | |
db = SqliteDatabase(app.config['DATABASE'], threadlocals=True) | |
oembed = bootstrap_basic() |
import datetime | |
from flask import Markup | |
from markdown import markdown | |
from micawber import parse_html | |
from peewee import * | |
from app import db, oembed | |
class Note(Model): | |
content = TextField() | |
timestamp = DateTimeField(default=datetime.datetime.now) | |
archived = BooleanField(default=False) | |
class Meta: | |
database = db | |
def html(self): | |
html = parse_html( | |
markdown(self.content), | |
oembed, | |
maxwidth=300, | |
urlize_all=True) | |
return Markup(html) | |
@classmethod | |
def public(cls): | |
return (Note | |
.select() | |
.where(Note.archived == False) | |
.order_by(Note.timestamp.desc())) |
Notes = window.Notes || {}; | |
(function(exports, $) { | |
function Editor() { | |
this.form = $('form#note-form'); | |
this.editor = $('textarea#content'); | |
this.container = $('ul.notes'); | |
this.initialize(); | |
} | |
Editor.prototype.initialize = function() { | |
this.setupMasonry(); | |
this.setupNotes(); | |
this.setupForm(); | |
this.editor.focus(); | |
} | |
Editor.prototype.setupMasonry = function() { | |
var self = this; | |
imagesLoaded(this.container, function() { | |
self.container.masonry({'itemSelector': '.note'}); | |
}); | |
} | |
Editor.prototype.setupNotes = function() { | |
var self = this; | |
$('a.archive-note').on('click', this.archiveNote); | |
} | |
Editor.prototype.archiveNote = function(e) { | |
e.preventDefault(); | |
var elem = $(this); | |
var panel = elem.parents('.panel'); | |
var self = this; | |
$.post(elem.attr('href'), function(data) { | |
if (data.success) { | |
panel.remove(); | |
$('ul.notes').masonry(); | |
} | |
}); | |
} | |
Editor.prototype.setupForm = function() { | |
var self = this; | |
this.editor.on('keydown', function(e) { | |
if (e.ctrlKey && e.keyCode == 13) { | |
self.form.submit(); | |
} | |
}); | |
this.form.submit(function(e) { | |
e.preventDefault(); | |
self.addNote(); | |
}); | |
this.addMarkdownHelpers(); | |
} | |
Editor.prototype.addNote = function() { | |
var self = this; | |
this.editor.css('color', '#464545'); | |
$.post(this.form.attr('target'), this.form.serialize(), function(data) { | |
if (data.success) { | |
self.editor.val('').focus(); | |
listElem = $(data.note); | |
listElem.find('a.archive-note').on('click', self.archiveNote); | |
self.container.prepend(listElem); | |
self.container.masonry('prepended', listElem); | |
} else { | |
self.editor.css('color', '#dd1111'); | |
} | |
}); | |
} | |
Editor.prototype.addMarkdownHelpers = function() { | |
var self = this; | |
this.addHelper('indent-left', function(line) { | |
return ' ' + line; | |
}); | |
this.addHelper('indent-right', function(line) { | |
return line.substring(4); | |
}); | |
this.addHelper('list', function(line) { | |
return '* ' + line; | |
}); | |
this.addHelper('bold', function(line) { | |
return '**' + line + '**'; | |
}); | |
this.addHelper('italic', function(line) { | |
return '*' + line + '*'; | |
}); | |
this.addHelper('font', null, function() { | |
self.focus().select(); | |
}); | |
} | |
Editor.prototype.addHelper = function(iconClass, lineHandler, callBack) { | |
var link = $('<a>', {'class': 'btn btn-xs'}), | |
icon = $('<span>', {'class': 'glyphicon glyphicon-' + iconClass}), | |
self = this; | |
if (!callBack) { | |
callBack = function() { | |
self.modifySelection(lineHandler); | |
} | |
} | |
link.on('click', function(e) { | |
e.preventDefault(); | |
callBack(); | |
}); | |
link.append(icon); | |
this.editor.before(link); | |
} | |
Editor.prototype.modifySelection = function(lineHandler) { | |
var selection = this.getSelectedText(); | |
if (!selection) return; | |
var lines = selection.split('\n'), | |
result = []; | |
for (var i = 0; i < lines.length; i++) { | |
result.push(lineHandler(lines[i])); | |
} | |
this.editor.val( | |
this.editor.val().split(selection).join(result.join('\n')) | |
); | |
} | |
Editor.prototype.getSelectedText = function() { | |
var textAreaDOM = this.editor[0]; | |
return this.editor.val().substring( | |
textAreaDOM.selectionStart, | |
textAreaDOM.selectionEnd); | |
} | |
exports.Editor = Editor; | |
})(Notes, jQuery); |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>Notes</title> | |
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet"> | |
<link href="{{ url_for('static', filename='css/site.css') }}" rel="stylesheet"> | |
<script src="{{ url_for('static', filename='js/jquery-1.11.0.min.js') }}"></script> | |
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script> | |
<script src="{{ url_for('static', filename='js/imagesloaded.pkgd.min.js') }}"></script> | |
<script src="{{ url_for('static', filename='js/masonry.pkgd.min.js') }}"></script> | |
<script src="{{ url_for('static', filename='js/notes.js') }}"></script> | |
<script type="text/javascript"> | |
$(function() { | |
new Notes.Editor(); | |
}); | |
</script> | |
</head> | |
<body> | |
<div class="container content"> | |
<div class="page-header"> | |
<h1>Notes</h1> | |
</div> | |
<form action="." class="form" id="note-form" method="post"> | |
<button class="btn btn-primary btn-xs" type="submit"> | |
<span class="glyphicon glyphicon-plus"></span> Add Note | |
</button> | |
<textarea class="form-control" id="content" name="content"></textarea> | |
</form> | |
<ul class="list-unstyled notes"> | |
{% for note in notes %} | |
{% include "note.html" %} | |
{% endfor %} | |
</ul> | |
<div style="clear:both;"></div> | |
</div> | |
</body> | |
</html> |
<li class="note col-xs-12 col-sm-6 col-lg-4"> | |
<div class="panel panel-primary"> | |
<div class="panel-heading"> | |
{{ note.timestamp.strftime('%b %d, %Y %I:%M%p').lower() }} | |
<a | |
class="btn btn-danger btn-xs archive-note pull-right" | |
data-note="{{ note.id }}" | |
href="{{ url_for('archive_note', pk=note.id) }}">×</a> | |
</div> | |
<div class="panel-body"> | |
{{ note.html() }} | |
</div> | |
</div> | |
</li> |
from flask import abort, jsonify, render_template, request | |
from app import app | |
from models import Note | |
def get_page(): | |
page = request.args.get('page') | |
if not page or not page.isdigit(): | |
return 1 | |
return min(int(page), 1) | |
@app.route('/', methods=['GET', 'POST']) | |
def homepage(): | |
if request.method == 'POST': | |
if request.form.get('content'): | |
note = Note.create(content=request.form['content']) | |
rendered = render_template('note.html', note=note) | |
return jsonify({'note': rendered, 'success': True}) | |
return jsonify({'success': False}) | |
notes = Note.public().paginate(get_page(), 50) | |
return render_template('homepage.html', notes=notes) | |
@app.route('/archive/<int:pk>/', methods=['POST']) | |
def archive_note(pk): | |
try: | |
note = Note.get(Note.id == pk) | |
except Note.DoesNotExist: | |
abort(404) | |
note.archived = True | |
note.save() | |
return jsonify({'success': True}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment