From c32c8cbee4220d72d8bfe232e8c96e86b21d29e3 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 8 Jan 2016 23:41:24 +0200 Subject: [PATCH] Initial commit --- stickynotes.cfg | 5 + stickynotes/__init__.py | 40 ++++++++ stickynotes/models.py | 47 +++++++++ stickynotes/static/custom.css | 44 ++++++++ stickynotes/templates/about.mako | 7 ++ stickynotes/templates/base.mako | 71 +++++++++++++ stickynotes/templates/index.mako | 48 +++++++++ stickynotes/templates/raw.mako | 1 + stickynotes/templates/view.mako | 9 ++ stickynotes/views.py | 166 +++++++++++++++++++++++++++++++ wsgiapp.py | 10 ++ 11 files changed, 448 insertions(+) create mode 100644 stickynotes.cfg create mode 100644 stickynotes/__init__.py create mode 100644 stickynotes/models.py create mode 100644 stickynotes/static/custom.css create mode 100644 stickynotes/templates/about.mako create mode 100644 stickynotes/templates/base.mako create mode 100644 stickynotes/templates/index.mako create mode 100644 stickynotes/templates/raw.mako create mode 100644 stickynotes/templates/view.mako create mode 100644 stickynotes/views.py create mode 100644 wsgiapp.py diff --git a/stickynotes.cfg b/stickynotes.cfg new file mode 100644 index 0000000..2c7ebb6 --- /dev/null +++ b/stickynotes.cfg @@ -0,0 +1,5 @@ +[stickynotes] +database_url = sqlite:///stickynotes.sqlite +secret_key = yoursecretkeyhere +recaptcha_site_key = +recaptcha_secret_key = diff --git a/stickynotes/__init__.py b/stickynotes/__init__.py new file mode 100644 index 0000000..e79ee85 --- /dev/null +++ b/stickynotes/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +StickyNotes, yet another paste bin +""" +import os +from ConfigParser import SafeConfigParser + +from flask import Flask +from flask.ext.mako import MakoTemplates + +from models import init_db +from views import views + + +def read_config(): + """ + Read the configuration file and return the values in a dictionary + """ + config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'stickynotes.cfg')) + config_parser = SafeConfigParser() + config_parser.read(config_file) + config = {} + for option in config_parser.options(u'stickynotes'): + config[option.upper()] = config_parser.get(u'stickynotes', option) + print(config) + return config + + +def make_app(): + """ + Create the application object + """ + app = Flask(__name__) + # Load the config file + config = read_config() + app.config.update(config) + MakoTemplates(app) + init_db(config[u'DATABASE_URL']) + app.register_blueprint(views) + return app diff --git a/stickynotes/models.py b/stickynotes/models.py new file mode 100644 index 0000000..9b07bef --- /dev/null +++ b/stickynotes/models.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +The models in use +""" +from datetime import datetime + +from sqlalchemy import Column, Integer, String, Text, DateTime, create_engine, Boolean +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, scoped_session + +BaseModel = declarative_base() +db_session = None + + +class StickyNote(BaseModel): + """ + The main (only?) table in the system + """ + __tablename__ = u'sticky_notes' + + id = Column(Integer, autoincrement=True, primary_key=True) + title = Column(String(255)) + source = Column(Text) + lexer = Column(String(255), default=u'text') + created = Column(DateTime, default=datetime.now, index=True) + expiry = Column(DateTime, default=None, index=True) + url = Column(String(255), index=True) + private = Column(Boolean, default=False, index=True) + + +def init_db(database_url): + """ + Initialise the database connection + + :param database_url: The database connection URL + """ + global db_session + engine = create_engine(database_url, pool_recycle=3600) + db_session = scoped_session(sessionmaker(bind=engine))() + BaseModel.metadata.create_all(engine, checkfirst=True) + + +def get_session(): + """ + Get the current database session + """ + return db_session diff --git a/stickynotes/static/custom.css b/stickynotes/static/custom.css new file mode 100644 index 0000000..d4bbeaf --- /dev/null +++ b/stickynotes/static/custom.css @@ -0,0 +1,44 @@ +html { + position: relative; + min-height: 100%; +} + +body { + margin-bottom: 50px; +} + +body > .container { + padding: 70px 15px 0; +} + +.container .text-muted { + margin: 20px 0; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: 50px; +} + +.footer > .container { + padding-right: 15px; + padding-left: 15px; +} + +code { + font-size: 80%; +} + +.sourcetable { + width: 100%; +} + +.linenos { + text-align: right; +} + +.note-links { + margin-bottom: 10px; +} diff --git a/stickynotes/templates/about.mako b/stickynotes/templates/about.mako new file mode 100644 index 0000000..5ab1fd7 --- /dev/null +++ b/stickynotes/templates/about.mako @@ -0,0 +1,7 @@ +<%inherit file="base.mako"/> +
+
+

About StickyNotes

+

StickyNotes is a quick code paste application written in Python with Flask, SQLAlchemy, Mako, Pygments and a few other Python libraries.

+
+
diff --git a/stickynotes/templates/base.mako b/stickynotes/templates/base.mako new file mode 100644 index 0000000..c2bfdad --- /dev/null +++ b/stickynotes/templates/base.mako @@ -0,0 +1,71 @@ + + + + + + + StickyNotes + + + + + + + + +
+<% messages = get_flashed_messages(True) %> +% if messages: + % for category, message in messages: + + % endfor +% endif + ${self.body()} +
+ + + + + + + + diff --git a/stickynotes/templates/index.mako b/stickynotes/templates/index.mako new file mode 100644 index 0000000..a240eea --- /dev/null +++ b/stickynotes/templates/index.mako @@ -0,0 +1,48 @@ +<%inherit file="base.mako"/> +
+
+
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+% if recaptcha_site_key: +
+
+
+% endif + +
+
+
diff --git a/stickynotes/templates/raw.mako b/stickynotes/templates/raw.mako new file mode 100644 index 0000000..fc64b76 --- /dev/null +++ b/stickynotes/templates/raw.mako @@ -0,0 +1 @@ +${source} diff --git a/stickynotes/templates/view.mako b/stickynotes/templates/view.mako new file mode 100644 index 0000000..d773c6f --- /dev/null +++ b/stickynotes/templates/view.mako @@ -0,0 +1,9 @@ +<%inherit file="base.mako"/> +
+ +
+ ${source} +
+
diff --git a/stickynotes/views.py b/stickynotes/views.py new file mode 100644 index 0000000..bb0689f --- /dev/null +++ b/stickynotes/views.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +""" +The views +""" +from datetime import timedelta, datetime +import logging + +from flask import Blueprint, redirect, request, flash, make_response, current_app +from flask.ext.mako import render_template +from pygments import highlight +from pygments.formatters.html import HtmlFormatter +from pygments.lexers import get_lexer_by_name, get_all_lexers +from sqlalchemy import or_ +import requests +import short_url + +from models import StickyNote, get_session + +log = logging.getLogger(__name__) +views = Blueprint(u'views', __name__) + +EXPIRY_DELTAS = { + u'10min': timedelta(minutes=10), + u'1d': timedelta(days=1), + u'1w': timedelta(days=7), + u'2w': timedelta(days=14), + u'1m': timedelta(days=30) +} + + +def _is_recaptcha_valid(secret, response, remote_ip=None): + """ + POST to the recaptcha service to check if the recaptcha is valid + """ + data = {u'secret': secret, u'response': response} + if remote_ip: + data[u'remoteip'] = remote_ip + response = requests.post(u'https://www.google.com/recaptcha/api/siteverify', data=data) + try: + json_response = response.json() + print(json_response) + return json_response[u'success'] + except ValueError: + print response + return False + + +@views.route('/', methods=[u'GET']) +def index(): + """ + Add a new sticky note + """ + all_lexers = [(lexer[1][0], lexer[0]) for lexer in get_all_lexers()] + all_lexers.sort(key=lambda x: x[1].lower()) + now = datetime.utcnow() + recaptcha_site_key = current_app.config.get(u'RECAPTCHA_SITE_KEY') + return render_template(u'index.mako', lexers=all_lexers, recaptcha_site_key=recaptcha_site_key) + + +@views.route('/notes', methods=[u'GET']) +def notes(): + """ + Show a list of recent notes + """ + recent_notes = get_session().query(StickyNote)\ + .filter(or_(StickyNote.expiry == None, StickyNote.expiry < now))\ + .filter(~StickyNote.private)\ + .order_by(StickyNote.created.desc())\ + .limit(10) + return render_template(u'notes.mako', recent=recent_notes) + + +@views.route('/about', methods=[u'GET']) +def about(): + """ + Show the about page + """ + return render_template(u'about.mako') + + +@views.route('/save', methods=[u'POST']) +def save(): + """ + Save a sticky note + """ + # Check if the recaptcha is valid + recaptcha_secret_key = current_app.config.get(u'RECAPTCHA_SECRET_KEY') + is_recaptcha_valid = False + try: + is_recaptcha_valid = _is_recaptcha_valid(recaptcha_secret_key, request.form[u'g-recaptcha-response']) + except KeyError: + flash(u'Unable to verify you, don\'t forget to complete the captcha.', u'danger') + print(u'No form variable') + if not is_recaptcha_valid: + return redirect(u'/') + # Save the note + db_session = get_session() + try: + created = datetime.utcnow() + expiry = EXPIRY_DELTAS.get(request.form[u'expiry']) + if expiry: + expiry = created + expiry + source = request.form[u'source'] + lexer = request.form[u'language'] + title = request.form.get(u'title', u'') + private = True if request.form.get(u'private') else False + string_id = u''.join([source, lexer, title, created.isoformat()]) + url = short_url.encode_url(sum([ord(char) for char in string_id]), min_length=8) + note = StickyNote( + title=title, + source=source, + lexer=lexer, + created=created, + expiry=expiry, + private=private, + url=url + ) + db_session.add(note) + db_session.commit() + return redirect(u'/' + note.url) + except Exception as e: + flash(str(e), 'danger') + db_session.rollback() + return redirect(u'/') + + +@views.route('/', methods=[u'GET']) +def view(note_url): + """ + Show a sticky note + + :param note_url: The note to show + """ + note = get_session().query(StickyNote).filter(StickyNote.url == note_url).scalar() + if not note: + flash(u'That note does not exist', 'danger') + return redirect(u'/') + + lexer = get_lexer_by_name(note.lexer) + formatter = HtmlFormatter(linenos=True, cssclass=u'source') + result = highlight(note.source, lexer, formatter) + return render_template(u'view.mako', note=note, source=result) + + +@views.route('/raw/', methods=[u'GET']) +def raw(note_url): + """ + Show the raw version of a sticky note + + :param note_url: The note to show + """ + note = get_session().query(StickyNote).filter(StickyNote.url == note_url).scalar() + if not note: + flash(u'That note does not exist', 'danger') + return redirect(u'/') + return render_template(u'raw.mako', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'} + + +@views.route(u'/pygments.css', methods=[u'GET']) +def pygments_css(): + """ + Return the Pygments CSS to the browser + """ + response = make_response(HtmlFormatter().get_style_defs()) + response.headers['Content-Type'] = 'text/css' + return response diff --git a/wsgiapp.py b/wsgiapp.py new file mode 100644 index 0000000..87a0f37 --- /dev/null +++ b/wsgiapp.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +This is the entry point for the WSGI server +""" +from stickynotes import make_app + +application = make_app() + +if __name__ == '__main__': + application.run(debug=True)