Merge pull request 'Migrate to Quart' (#1) from migrate-to-quart into master
Reviewed-on: raoul/stickynotes#1
This commit is contained in:
commit
73f2e54a74
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@ __pycache__
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
*.sqlite
|
*.sqlite
|
||||||
stickynotes.cfg
|
stickynotes.cfg
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
62
pyproject.toml
Normal file
62
pyproject.toml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling", "hatch-vcs"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "StickyNotes"
|
||||||
|
dynamic = ["version"]
|
||||||
|
description = "A simple pastebin"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
authors = [
|
||||||
|
{ name = "Raoul Snyman", email = "raoul@snyman.info" },
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Web Environment",
|
||||||
|
# "Framework :: Quart",
|
||||||
|
"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 :: Only",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"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",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"Quart",
|
||||||
|
"Quart-Flask-Patch",
|
||||||
|
"Flask-SQLAlchemy",
|
||||||
|
"nord-pygments",
|
||||||
|
"psycopg2_binary",
|
||||||
|
"Pygments",
|
||||||
|
"requests",
|
||||||
|
"short_url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://bin.snyman.info"
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
source = "vcs"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = [
|
||||||
|
"/stickynotes",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.hatch.envs.default.scripts]
|
||||||
|
server = "quart -A stickynotes.app run"
|
@ -5,7 +5,8 @@ StickyNotes, yet another paste bin
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import Flask
|
import quart_flask_patch # noqa: F401
|
||||||
|
from quart import Quart
|
||||||
|
|
||||||
from stickynotes.db import db
|
from stickynotes.db import db
|
||||||
from stickynotes.views import views
|
from stickynotes.views import views
|
||||||
@ -19,6 +20,8 @@ def read_config(config_path=None):
|
|||||||
config_file = config_path / 'stickynotes.cfg'
|
config_file = config_path / 'stickynotes.cfg'
|
||||||
else:
|
else:
|
||||||
config_file = Path(__file__).parent / '..' / 'stickynotes.cfg'
|
config_file = Path(__file__).parent / '..' / 'stickynotes.cfg'
|
||||||
|
if not config_file.exists():
|
||||||
|
return {}
|
||||||
config_parser = ConfigParser()
|
config_parser = ConfigParser()
|
||||||
config_parser.read(config_file)
|
config_parser.read(config_file)
|
||||||
config = {}
|
config = {}
|
||||||
@ -31,13 +34,16 @@ def make_app(config_path=None):
|
|||||||
"""
|
"""
|
||||||
Create the application object
|
Create the application object
|
||||||
"""
|
"""
|
||||||
app = Flask(__name__)
|
app = Quart(__name__)
|
||||||
# Load the config file
|
# Load the config file
|
||||||
config = read_config(config_path)
|
config = read_config(config_path)
|
||||||
app.config.update(config)
|
app.config.update(config)
|
||||||
app.config.update({'SQLALCHEMY_TRACK_MODIFICATIONS': False})
|
app.config.update({'SQLALCHEMY_TRACK_MODIFICATIONS': False})
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
with app.app_context():
|
|
||||||
db.create_all()
|
|
||||||
app.register_blueprint(views)
|
app.register_blueprint(views)
|
||||||
|
|
||||||
|
@app.before_first_request
|
||||||
|
async def setup_db():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
3
stickynotes/__main__.py
Normal file
3
stickynotes/__main__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from stickynotes.app import application
|
||||||
|
|
||||||
|
application.run()
|
@ -5,7 +5,7 @@ This is the entry point for the WSGI server
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from stickynotes import make_app
|
from stickynotes import make_app
|
||||||
|
|
||||||
application = make_app(Path(__file__).parent)
|
application = make_app(Path(__file__).parent.parent)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
application.run(debug=True)
|
application.run(debug=True)
|
@ -65,3 +65,9 @@ td.linenos pre {
|
|||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.computer {
|
||||||
|
display: none;
|
||||||
|
left: -9000000000px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
||||||
<h2>About StickyNotes</h2>
|
<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>
|
<p>StickyNotes is a quick code paste application written in Python with Quartz, SQLAlchemy, Mako, Pygments and a few other Python libraries.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -29,17 +29,10 @@
|
|||||||
<option value="1m">1 Month</option>
|
<option value="1m">1 Month</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group computer">
|
||||||
<div class="custom-control custom-checkbox">
|
<label for="computer">Computer? (just type "no")</label>
|
||||||
<input type="checkbox" name="private" class="custom-control-input" id="private">
|
<input type="text" name="computer" class="form-control" id="computer" autocomplete="off" placeholder="Computers type yes">
|
||||||
<label for="private" class="custom-control-label">Unlisted (doesn't appear in the list on the notes page)</label>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
The views
|
The views
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
import requests
|
from quart import Blueprint, redirect, request, flash, make_response, current_app, render_template
|
||||||
import short_url
|
|
||||||
from flask import Blueprint, redirect, request, flash, make_response, current_app, render_template
|
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
from pygments.formatters.html import HtmlFormatter
|
from pygments.formatters.html import HtmlFormatter
|
||||||
from pygments.lexers import get_lexer_by_name, get_all_lexers
|
from pygments.lexers import get_lexer_by_name, get_all_lexers
|
||||||
@ -30,34 +30,28 @@ EXPIRY_DELTAS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _is_recaptcha_valid(secret, response, remote_ip=None):
|
def _generate_short_url():
|
||||||
"""
|
"""
|
||||||
POST to the recaptcha service to check if the recaptcha is valid
|
Encode the URL
|
||||||
"""
|
"""
|
||||||
data = {'secret': secret, 'response': response}
|
alphabet = string.ascii_lowercase + string.digits
|
||||||
if remote_ip:
|
short_url = ''.join(secrets.choice(alphabet) for _ in range(8))
|
||||||
data['remoteip'] = remote_ip
|
return short_url
|
||||||
response = requests.post('https://www.google.com/recaptcha/api/siteverify', data=data)
|
|
||||||
try:
|
|
||||||
json_response = response.json()
|
|
||||||
return json_response['success']
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@views.route('/', methods=['GET'])
|
@views.route('/', methods=['GET'])
|
||||||
def index():
|
async def index():
|
||||||
"""
|
"""
|
||||||
Add a new sticky note
|
Add a new sticky note
|
||||||
"""
|
"""
|
||||||
all_lexers = [(lexer[1][0], lexer[0]) for lexer in get_all_lexers() if len(lexer) > 1 and len(lexer[1]) > 0]
|
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())
|
all_lexers.sort(key=lambda x: x[1].lower())
|
||||||
recaptcha_site_key = current_app.config.get('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)
|
return await render_template('index.html', lexers=all_lexers, recaptcha_site_key=recaptcha_site_key)
|
||||||
|
|
||||||
|
|
||||||
@views.route('/notes', methods=['GET'])
|
@views.route('/notes', methods=['GET'])
|
||||||
def notes():
|
async def notes():
|
||||||
"""
|
"""
|
||||||
Show a list of recent notes
|
Show a list of recent notes
|
||||||
"""
|
"""
|
||||||
@ -66,54 +60,41 @@ def notes():
|
|||||||
.filter(~StickyNote.private)\
|
.filter(~StickyNote.private)\
|
||||||
.order_by(StickyNote.created.desc())\
|
.order_by(StickyNote.created.desc())\
|
||||||
.limit(10) # noqa
|
.limit(10) # noqa
|
||||||
return render_template('notes.html', notes=notes)
|
return await render_template('notes.html', notes=notes)
|
||||||
|
|
||||||
|
|
||||||
@views.route('/about', methods=['GET'])
|
@views.route('/about', methods=['GET'])
|
||||||
def about():
|
async def about():
|
||||||
"""
|
"""
|
||||||
Show the about page
|
Show the about page
|
||||||
"""
|
"""
|
||||||
return render_template('about.html')
|
return await render_template('about.html')
|
||||||
|
|
||||||
|
|
||||||
@views.route('/', methods=['POST'])
|
@views.route('/', methods=['POST'])
|
||||||
def save():
|
async def save():
|
||||||
"""
|
"""
|
||||||
Save a sticky note
|
Save a sticky note
|
||||||
"""
|
"""
|
||||||
# Check if the recaptcha is valid
|
form = await request.form
|
||||||
recaptcha_secret_key = current_app.config.get('RECAPTCHA_SECRET_KEY')
|
if form.get('computer') and form['computer'].lower() != 'no':
|
||||||
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('/')
|
return redirect('/')
|
||||||
# Save the note
|
# Save the note
|
||||||
try:
|
try:
|
||||||
created = datetime.utcnow()
|
created = datetime.utcnow()
|
||||||
expiry = EXPIRY_DELTAS.get(request.form['expiry'])
|
expiry = created + EXPIRY_DELTAS.get(form['expiry'], EXPIRY_DELTAS['1d'])
|
||||||
if expiry:
|
# Generate a short url, and check if it exists in the db
|
||||||
expiry = created + expiry
|
url = _generate_short_url()
|
||||||
source = request.form['source']
|
while StickyNote.query.filter(StickyNote.url == url).first():
|
||||||
lexer = request.form['language']
|
url = _generate_short_url()
|
||||||
title = request.form.get('title', '')
|
# Create a new note
|
||||||
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(
|
note = StickyNote(
|
||||||
title=title,
|
title=form.get('title', ''),
|
||||||
source=source,
|
source=form['source'],
|
||||||
lexer=lexer,
|
lexer=form['language'],
|
||||||
created=created,
|
created=created,
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
private=private,
|
private=True,
|
||||||
url=url
|
url=url
|
||||||
)
|
)
|
||||||
session.add(note)
|
session.add(note)
|
||||||
@ -126,7 +107,7 @@ def save():
|
|||||||
|
|
||||||
|
|
||||||
@views.route('/<string:note_url>', methods=['GET'])
|
@views.route('/<string:note_url>', methods=['GET'])
|
||||||
def view(note_url):
|
async def view(note_url):
|
||||||
"""
|
"""
|
||||||
Show a sticky note
|
Show a sticky note
|
||||||
|
|
||||||
@ -140,11 +121,11 @@ def view(note_url):
|
|||||||
lexer = get_lexer_by_name(note.lexer)
|
lexer = get_lexer_by_name(note.lexer)
|
||||||
formatter = HtmlFormatter(linenos=True, cssclass='source')
|
formatter = HtmlFormatter(linenos=True, cssclass='source')
|
||||||
result = highlight(note.source, lexer, formatter)
|
result = highlight(note.source, lexer, formatter)
|
||||||
return render_template('view.html', note=note, source=result)
|
return await render_template('view.html', note=note, source=result)
|
||||||
|
|
||||||
|
|
||||||
@views.route('/raw/<string:note_url>', methods=['GET'])
|
@views.route('/raw/<string:note_url>', methods=['GET'])
|
||||||
def raw(note_url):
|
async def raw(note_url):
|
||||||
"""
|
"""
|
||||||
Show the raw version of a sticky note
|
Show the raw version of a sticky note
|
||||||
|
|
||||||
@ -154,14 +135,14 @@ def raw(note_url):
|
|||||||
if not note:
|
if not note:
|
||||||
flash('That note does not exist', 'danger')
|
flash('That note does not exist', 'danger')
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
return render_template('raw.html', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
return await render_template('raw.html', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
|
||||||
@views.route('/pygments.css', methods=['GET'])
|
@views.route('/pygments.css', methods=['GET'])
|
||||||
def pygments_css():
|
async def pygments_css():
|
||||||
"""
|
"""
|
||||||
Return the Pygments CSS to the browser
|
Return the Pygments CSS to the browser
|
||||||
"""
|
"""
|
||||||
response = make_response(HtmlFormatter(style='nord').get_style_defs())
|
response = await make_response(HtmlFormatter(style='nord').get_style_defs())
|
||||||
response.headers['Content-Type'] = 'text/css'
|
response.headers['Content-Type'] = 'text/css'
|
||||||
return response
|
return response
|
||||||
|
Loading…
Reference in New Issue
Block a user