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.cfg
index 2c7ebb6..89a739f 100644
--- a/stickynotes.cfg
+++ b/stickynotes.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) %}
+
+ {{message}}
+
+ {% 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:
-
- ${message}
-
- % 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 %}
-
-
+
@@ -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 %}
+
+
+
+ {{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"/>
-
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)