Initial commit

This commit is contained in:
Raoul Snyman 2016-01-08 23:41:24 +02:00
commit c32c8cbee4
11 changed files with 448 additions and 0 deletions

5
stickynotes.cfg Normal file
View File

@ -0,0 +1,5 @@
[stickynotes]
database_url = sqlite:///stickynotes.sqlite
secret_key = yoursecretkeyhere
recaptcha_site_key =
recaptcha_secret_key =

40
stickynotes/__init__.py Normal file
View File

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

47
stickynotes/models.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,7 @@
<%inherit file="base.mako"/>
<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>

View File

@ -0,0 +1,71 @@
<!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

@ -0,0 +1,48 @@
<%inherit file="base.mako"/>
<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>
<div class="form-group">
<textarea name="source" id="source" class="form-control" rows="20"></textarea>
</div>
<div class="form-group">
<input type="text" name="title" id="title" class="form-control" placeholder="Title of your snippet">
</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>
</div>
<div class="form-group">
<label for="expiry">Expiry</label>
<select name="expiry" id="expiry" class="form-control selectpicker">
<option value="never" selected>Never</option>
<option value="10min">10 Minutes</option>
<option value="1h">1 Hour</option>
<option value="1d">1 Day</option>
<option value="1w">1 Week</option>
<option value="2w">2 Weeks</option>
<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>
% endif
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>

View File

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

View File

@ -0,0 +1,9 @@
<%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>

166
stickynotes/views.py Normal file
View File

@ -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('/<string:note_url>', 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/<string:note_url>', 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

10
wsgiapp.py Normal file
View File

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