Skip to content

Instantly share code, notes, and snippets.

@coleifer

coleifer/app.py Secret

Last active February 14, 2022 07:54
  • Star 47 You must be signed in to star a gist
  • Fork 23 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save coleifer/632d3c9aa6b2ea519384 to your computer and use it in GitHub Desktop.
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()
from app import app
from models import Note
import views
if __name__ == '__main__':
Note.create_table(True)
app.run(debug=True)
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) }}">&times;</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