Drag this up into the current technology

pull/1/head
Raoul Snyman 2021-01-26 22:43:59 -07:00
parent c32c8cbee4
commit 27e7624198
Signed by: raoul
GPG Key ID: F55BCED79626AE9C
17 changed files with 273 additions and 214 deletions

3
.gitignore vendored 100644
View File

@ -0,0 +1,3 @@
__pycache__
*.egg-info
*.sqlite

49
setup.py 100644
View File

@ -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',
],
)

View File

@ -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 =

View File

@ -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

15
stickynotes/db.py 100644
View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -1,7 +1,9 @@
<%inherit file="base.mako"/>
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h2>About StickyNotes</h2>
<p>StickyNotes is a quick code paste application written in Python with Flask, SQLAlchemy, Mako, Pygments and a few other Python libraries.</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>StickyNotes</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@4.6.0/dist/darkly/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.14/dist/css/bootstrap-select.min.css" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" type="text/css">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=PT+Mono&display=swap" rel="stylesheet">
<link href="/pygments.css" rel="stylesheet" type="text/css">
<link href="/static/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary navbar-fixed-top">
<div class="container">
<a href="/" class="navbar-brand">StickyNotes</a>
<button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbar" class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item{% if request.path == '/' %} active{% endif %}"><a class="nav-link" href="/">New</a></li>
{# <li class="nav-item{% if request.path == '/notes' %} active{% endif %}"><a class="nav-link" href="/notes">Notes</a></li> #}
<li class="nav-item{% if request.path == '/about' %} active{% endif %}"><a class="nav-link" href="/about">About</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
{% for category, message in get_flashed_messages(True) %}
<div class="alert alert-{{category}}" role="alert">
{{message}}
</div>
{% endfor %}
{% block content %}
{% endblock %}
</div>
<footer class="footer">
<div class="container">
<p class="text-muted">Copyright &copy; 2015 Raoul Snyman.</p>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.14/dist/js/bootstrap-select.min.js"></script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</body>
</html>

View File

@ -1,71 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>StickyNotes</title>
<link href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.9.3/css/bootstrap-select.min.css" rel="stylesheet" type="text/css">
<link href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.6/lumen/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet">
<link href="/static/custom.css" rel="stylesheet" type="text/css">
<link href="/pygments.css" rel="stylesheet" type="text/css">
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand">StickyNotes</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
% if self.name == u'self:index.mako':
<li class="active"><a href="/">New</a></li>
%else:
<li><a href="/">New</a></li>
% endif
<%doc>
% if self.name == u'self:list.mako':
<li class="active"><a href="/notes">Notes</a></li>
%else:
<li><a href="/notes">Notes</a></li>
% endif
</%doc>
% if self.name == u'self:about.mako':
<li class="active"><a href="/about">About</a></li>
%else:
<li><a href="/about">About</a></li>
% endif
</ul>
</div>
</div>
</nav>
<div class="container">
<% messages = get_flashed_messages(True) %>
% if messages:
% for category, message in messages:
<div class="alert alert-${category}" role="alert">
${message}
</div>
% endfor
% endif
${self.body()}
</div>
<footer class="footer">
<div class="container">
<p class="text-muted">Copyright &copy; 2015 Raoul Snyman.</p>
</div>
</footer>
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js" type="application/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.9.3/js/bootstrap-select.min.js"></script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</body>
</html>

View File

@ -1,7 +1,8 @@
<%inherit file="base.mako"/>
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<form role="form" action="/save" method="post" form>
<form role="form" action="/" method="post">
<div class="form-group">
<textarea name="source" id="source" class="form-control" rows="20"></textarea>
</div>
@ -10,19 +11,15 @@
</div>
<div class="form-group">
<label for="language">Language</label>
<select name="language" id="language" class="form-control selectpicker" data-live-search="true" data-size="15">
% for language in lexers:
% if language[0] == 'text':
<option value="${language[0]}" selected>${language[1]}</option>
% else:
<option value="${language[0]}">${language[1]}</option>
% endif
% endfor
<select name="language" id="language" class="form-control selectpicker" data-live-search="true" data-size="15" data-style="btn-default">
{% for language in lexers %}
<option value="{{language[0]}}"{% if language[0] == 'text' %} selected{% endif %}>{{language[1]}}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="expiry">Expiry</label>
<select name="expiry" id="expiry" class="form-control selectpicker">
<select name="expiry" id="expiry" class="form-control selectpicker" data-style="btn-default">
<option value="never" selected>Never</option>
<option value="10min">10 Minutes</option>
<option value="1h">1 Hour</option>
@ -32,17 +29,19 @@
<option value="1m">1 Month</option>
</select>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="private"> Unlisted (doesn't appear in the list on the notes page)
</label>
</div>
% if recaptcha_site_key:
<div class="form-group">
<div class="g-recaptcha" data-sitekey="${recaptcha_site_key}"></div>
<div class="custom-control custom-checkbox">
<input type="checkbox" name="private" class="custom-control-input" id="private">
<label for="private" class="custom-control-label">Unlisted (doesn't appear in the list on the notes page)</label>
</div>
</div>
% endif
{% if recaptcha_site_key %}
<div class="form-group">
<div class="g-recaptcha" data-sitekey="{{recaptcha_site_key}}"></div>
</div>
{% endif %}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1 @@
{{source}}

View File

@ -1 +0,0 @@
${source}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-10 col-sm-12">
<h4>{{note.title}}</h4>
</div>
<div class="col-md-2 col-sm-12 text-right">
<a href="/raw/{{note.url}}" class="btn btn-secondary btn-sm"><i class="fa fa-fw fa-file-text"></i> Raw</a>
</div>
</div>
<div class="row">
<div class="col-12">
{{source | safe}}
</div>
</div>
{% endblock %}

View File

@ -1,9 +0,0 @@
<%inherit file="base.mako"/>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 text-right note-links">
<a href="/raw/${note.url}" class="btn btn-default btn-sm"><i class="fa fa-fw fa-file-text"></i> Raw</a>
</div>
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
${source}
</div>
</div>

View File

@ -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('/<string:note_url>', methods=[u'GET'])
@views.route('/<string:note_url>', 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/<string:note_url>', methods=[u'GET'])
@views.route('/raw/<string:note_url>', 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

View File

@ -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)