From 27e7624198161abf631d2f6e10ec741fe8b3831a Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Jan 2021 22:43:59 -0700 Subject: [PATCH] Drag this up into the current technology --- .gitignore | 3 + setup.py | 49 +++++++ stickynotes.cfg => stickynotes.example.cfg | 2 +- stickynotes/__init__.py | 33 +++-- stickynotes/db.py | 15 ++ stickynotes/models.py | 32 +---- stickynotes/static/custom.css | 31 ++++- .../templates/{about.mako => about.html} | 4 +- stickynotes/templates/base.html | 51 +++++++ stickynotes/templates/base.mako | 71 ---------- .../templates/{index.mako => index.html} | 37 +++-- stickynotes/templates/raw.html | 1 + stickynotes/templates/raw.mako | 1 - stickynotes/templates/view.html | 16 +++ stickynotes/templates/view.mako | 9 -- stickynotes/views.py | 129 +++++++++--------- wsgiapp.py => wsgi.py | 3 +- 17 files changed, 273 insertions(+), 214 deletions(-) create mode 100644 .gitignore create mode 100644 setup.py rename stickynotes.cfg => stickynotes.example.cfg (62%) create mode 100644 stickynotes/db.py rename stickynotes/templates/{about.mako => about.html} (81%) create mode 100644 stickynotes/templates/base.html delete mode 100644 stickynotes/templates/base.mako rename stickynotes/templates/{index.mako => index.html} (62%) create mode 100644 stickynotes/templates/raw.html delete mode 100644 stickynotes/templates/raw.mako create mode 100644 stickynotes/templates/view.html delete mode 100644 stickynotes/templates/view.mako rename wsgiapp.py => wsgi.py (70%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf1b9d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.egg-info +*.sqlite diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..506840a --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +from setuptools import setup + + +setup( + name='StickyNotes', + version='0.2', + author='Raoul Snyman', + description='A simple pastebin', + url='https://bin.snyman.info', + license='GPLv3+', + packages=['stickynotes'], + include_package_data=True, + platforms='any', + python_requires='>=3.5', + install_requires=[ + 'Flask', + 'Flask-SQLAlchemy', + 'Pygments', + 'requests', + 'short_url' + ], + extras_require={ + 'dev': [ + 'pytest>=3', + 'pytest-cov', + ], + }, + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Environment :: Web Environment', + 'Framework :: Flask', + 'Intended Audience :: Other Audience', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], +) diff --git a/stickynotes.cfg b/stickynotes.example.cfg similarity index 62% rename from stickynotes.cfg rename to stickynotes.example.cfg index 2c7ebb6..89a739f 100644 --- a/stickynotes.cfg +++ b/stickynotes.example.cfg @@ -1,5 +1,5 @@ [stickynotes] -database_url = sqlite:///stickynotes.sqlite +sqlalchemy_database_uri = sqlite:///stickynotes.sqlite secret_key = yoursecretkeyhere recaptcha_site_key = recaptcha_secret_key = diff --git a/stickynotes/__init__.py b/stickynotes/__init__.py index e79ee85..2c3124e 100644 --- a/stickynotes/__init__.py +++ b/stickynotes/__init__.py @@ -2,39 +2,42 @@ """ StickyNotes, yet another paste bin """ -import os -from ConfigParser import SafeConfigParser +from configparser import ConfigParser +from pathlib import Path from flask import Flask -from flask.ext.mako import MakoTemplates -from models import init_db -from views import views +from stickynotes.db import db +from stickynotes.views import views -def read_config(): +def read_config(config_path=None): """ 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() + if config_path: + config_file = config_path / 'stickynotes.cfg' + else: + config_file = Path(__file__).parent / '..' / 'stickynotes.cfg' + config_parser = ConfigParser() 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) + for option in config_parser.options('stickynotes'): + config[option.upper()] = config_parser.get('stickynotes', option) return config -def make_app(): +def make_app(config_path=None): """ Create the application object """ app = Flask(__name__) # Load the config file - config = read_config() + config = read_config(config_path) app.config.update(config) - MakoTemplates(app) - init_db(config[u'DATABASE_URL']) + app.config.update({'SQLALCHEMY_TRACK_MODIFICATIONS': False}) + db.init_app(app) + with app.app_context(): + db.create_all() app.register_blueprint(views) return app diff --git a/stickynotes/db.py b/stickynotes/db.py new file mode 100644 index 0000000..dc6f272 --- /dev/null +++ b/stickynotes/db.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +""" +The basics of the database +""" +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() +session = db.session +Model = db.Model +Column = db.Column +Integer = db.Integer +String = db.String +Text = db.String +DateTime = db.DateTime +Boolean = db.Boolean diff --git a/stickynotes/models.py b/stickynotes/models.py index 9b07bef..4e55d8c 100644 --- a/stickynotes/models.py +++ b/stickynotes/models.py @@ -4,44 +4,20 @@ 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 +from stickynotes.db import Model, Column, Integer, String, Text, DateTime, Boolean -class StickyNote(BaseModel): +class StickyNote(Model): """ The main (only?) table in the system """ - __tablename__ = u'sticky_notes' + __tablename__ = 'sticky_notes' id = Column(Integer, autoincrement=True, primary_key=True) title = Column(String(255)) source = Column(Text) - lexer = Column(String(255), default=u'text') + lexer = Column(String(255), default='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 index d4bbeaf..b370cca 100644 --- a/stickynotes/static/custom.css +++ b/stickynotes/static/custom.css @@ -8,7 +8,7 @@ body { } body > .container { - padding: 70px 15px 0; + padding: 1rem 0; } .container .text-muted { @@ -27,18 +27,41 @@ body > .container { padding-left: 15px; } -code { - font-size: 80%; +code, kbd, pre, samp { + font-family: 'PT Mono', 'Hack', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; } .sourcetable { - width: 100%; + width: 100%; } .linenos { text-align: right; } +td.linenos pre { + color: var(--light); + background-color: var(--secondary); +} + +.source { + color: var(--light); + background-color: var(--dark); +} + .note-links { margin-bottom: 10px; } + +.form-control, +.form-control:focus { + background-color: var(--dark); +} + +.form-control { + color: var(--light); +} + +.form-control:focus { + color: var(--white); +} diff --git a/stickynotes/templates/about.mako b/stickynotes/templates/about.html similarity index 81% rename from stickynotes/templates/about.mako rename to stickynotes/templates/about.html index 5ab1fd7..badad55 100644 --- a/stickynotes/templates/about.mako +++ b/stickynotes/templates/about.html @@ -1,7 +1,9 @@ -<%inherit file="base.mako"/> +{% extends "base.html" %} + {% block content %}

About StickyNotes

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

+ {% endblock %} diff --git a/stickynotes/templates/base.html b/stickynotes/templates/base.html new file mode 100644 index 0000000..3d02ef9 --- /dev/null +++ b/stickynotes/templates/base.html @@ -0,0 +1,51 @@ + + + + + + + StickyNotes + + + + + + + + + + +
+ {% for category, message in get_flashed_messages(True) %} + + {% endfor %} + {% block content %} + {% endblock %} +
+ + + + + + + diff --git a/stickynotes/templates/base.mako b/stickynotes/templates/base.mako deleted file mode 100644 index c2bfdad..0000000 --- a/stickynotes/templates/base.mako +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - 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.html similarity index 62% rename from stickynotes/templates/index.mako rename to stickynotes/templates/index.html index a240eea..1a1a9ac 100644 --- a/stickynotes/templates/index.mako +++ b/stickynotes/templates/index.html @@ -1,7 +1,8 @@ -<%inherit file="base.mako"/> +{% extends "base.html" %} + {% block content %}
-
+
@@ -10,19 +11,15 @@
- + {% for language in lexers %} + + {% endfor %}
- @@ -32,17 +29,19 @@
-
- -
-% if recaptcha_site_key:
-
+
+ + +
-% endif + {% if recaptcha_site_key %} +
+
+
+ {% endif %}
+ {% endblock %} diff --git a/stickynotes/templates/raw.html b/stickynotes/templates/raw.html new file mode 100644 index 0000000..3a73287 --- /dev/null +++ b/stickynotes/templates/raw.html @@ -0,0 +1 @@ +{{source}} diff --git a/stickynotes/templates/raw.mako b/stickynotes/templates/raw.mako deleted file mode 100644 index fc64b76..0000000 --- a/stickynotes/templates/raw.mako +++ /dev/null @@ -1 +0,0 @@ -${source} diff --git a/stickynotes/templates/view.html b/stickynotes/templates/view.html new file mode 100644 index 0000000..39d6183 --- /dev/null +++ b/stickynotes/templates/view.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + {% block content %} +
+
+

{{note.title}}

+
+
+ Raw +
+
+
+
+ {{source | safe}} +
+
+ {% endblock %} diff --git a/stickynotes/templates/view.mako b/stickynotes/templates/view.mako deleted file mode 100644 index d773c6f..0000000 --- a/stickynotes/templates/view.mako +++ /dev/null @@ -1,9 +0,0 @@ -<%inherit file="base.mako"/> -
- -
- ${source} -
-
diff --git a/stickynotes/views.py b/stickynotes/views.py index bb0689f..f5a41b3 100644 --- a/stickynotes/views.py +++ b/stickynotes/views.py @@ -2,29 +2,31 @@ """ The views """ -from datetime import timedelta, datetime import logging +from datetime import timedelta, datetime -from flask import Blueprint, redirect, request, flash, make_response, current_app -from flask.ext.mako import render_template +import requests +import short_url +from flask import Blueprint, redirect, request, flash, make_response, current_app, 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 +from stickynotes.db import session +from stickynotes.models import StickyNote + log = logging.getLogger(__name__) -views = Blueprint(u'views', __name__) +views = Blueprint('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) + '10min': timedelta(minutes=10), + '1d': timedelta(days=1), + '1w': timedelta(days=7), + '2w': timedelta(days=14), + '1m': timedelta(days=30) } @@ -32,79 +34,78 @@ 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} + data = {'secret': secret, 'response': response} if remote_ip: - data[u'remoteip'] = remote_ip - response = requests.post(u'https://www.google.com/recaptcha/api/siteverify', data=data) + data['remoteip'] = remote_ip + response = requests.post('https://www.google.com/recaptcha/api/siteverify', data=data) try: json_response = response.json() - print(json_response) - return json_response[u'success'] + return json_response['success'] except ValueError: - print response return False -@views.route('/', methods=[u'GET']) +@views.route('/', methods=['GET']) def index(): """ Add a new sticky note """ - all_lexers = [(lexer[1][0], lexer[0]) for lexer in get_all_lexers()] + all_lexers = [(lexer[1][0], lexer[0]) for lexer in get_all_lexers() if len(lexer) > 1 and len(lexer[1]) > 0] 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) + recaptcha_site_key = current_app.config.get('RECAPTCHA_SITE_KEY') + return render_template('index.html', lexers=all_lexers, recaptcha_site_key=recaptcha_site_key) -@views.route('/notes', methods=[u'GET']) +@views.route('/notes', methods=['GET']) def notes(): """ Show a list of recent notes """ - recent_notes = get_session().query(StickyNote)\ - .filter(or_(StickyNote.expiry == None, StickyNote.expiry < now))\ + recent_notes = StickyNote.query\ + .filter(or_(StickyNote.expiry == None, StickyNote.expiry < datetime.utcnow()))\ .filter(~StickyNote.private)\ .order_by(StickyNote.created.desc())\ - .limit(10) - return render_template(u'notes.mako', recent=recent_notes) + .limit(10) # noqa + return render_template('notes.html', recent=recent_notes) -@views.route('/about', methods=[u'GET']) +@views.route('/about', methods=['GET']) def about(): """ Show the about page """ - return render_template(u'about.mako') + return render_template('about.html') -@views.route('/save', methods=[u'POST']) +@views.route('/', methods=['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') + recaptcha_secret_key = current_app.config.get('RECAPTCHA_SECRET_KEY') + if recaptcha_secret_key: + is_recaptcha_valid = False + try: + is_recaptcha_valid = _is_recaptcha_valid(recaptcha_secret_key, request.form['g-recaptcha-response']) + except KeyError: + flash('Unable to verify you, don\'t forget to complete the captcha.', 'danger') + print('No form variable') + else: + is_recaptcha_valid = True if not is_recaptcha_valid: - return redirect(u'/') + return redirect('/') # Save the note - db_session = get_session() try: created = datetime.utcnow() - expiry = EXPIRY_DELTAS.get(request.form[u'expiry']) + expiry = EXPIRY_DELTAS.get(request.form['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()]) + source = request.form['source'] + lexer = request.form['language'] + title = request.form.get('title', '') + private = True if request.form.get('private') else False + string_id = ''.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, @@ -115,52 +116,52 @@ def save(): private=private, url=url ) - db_session.add(note) - db_session.commit() - return redirect(u'/' + note.url) + session.add(note) + session.commit() + return redirect('/' + note.url) except Exception as e: flash(str(e), 'danger') - db_session.rollback() - return redirect(u'/') + session.rollback() + return redirect('/') -@views.route('/', methods=[u'GET']) +@views.route('/', methods=['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() + note = StickyNote.query.filter(StickyNote.url == note_url).scalar() if not note: - flash(u'That note does not exist', 'danger') - return redirect(u'/') + flash('That note does not exist', 'danger') + return redirect('/') lexer = get_lexer_by_name(note.lexer) - formatter = HtmlFormatter(linenos=True, cssclass=u'source') + formatter = HtmlFormatter(linenos=True, cssclass='source') result = highlight(note.source, lexer, formatter) - return render_template(u'view.mako', note=note, source=result) + return render_template('view.html', note=note, source=result) -@views.route('/raw/', methods=[u'GET']) +@views.route('/raw/', methods=['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() + note = StickyNote.query.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'} + flash('That note does not exist', 'danger') + return redirect('/') + return render_template('raw.html', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'} -@views.route(u'/pygments.css', methods=[u'GET']) +@views.route('/pygments.css', methods=['GET']) def pygments_css(): """ Return the Pygments CSS to the browser """ - response = make_response(HtmlFormatter().get_style_defs()) + response = make_response(HtmlFormatter(style='nord').get_style_defs()) response.headers['Content-Type'] = 'text/css' return response diff --git a/wsgiapp.py b/wsgi.py similarity index 70% rename from wsgiapp.py rename to wsgi.py index 87a0f37..0e3bac0 100644 --- a/wsgiapp.py +++ b/wsgi.py @@ -2,9 +2,10 @@ """ This is the entry point for the WSGI server """ +from pathlib import Path from stickynotes import make_app -application = make_app() +application = make_app(Path(__file__).parent) if __name__ == '__main__': application.run(debug=True)