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