Added importer and ability to register accounts

This commit is contained in:
Yusur 2023-01-05 11:46:54 +01:00
parent 506fefc1f0
commit 83e2c892b3
9 changed files with 204 additions and 78 deletions

124
app.py
View file

@ -15,7 +15,7 @@ Application is kept compact, with all its core in a single file.
from flask import (
Flask, Markup, abort, flash, g, jsonify, make_response, redirect, request,
render_template, send_from_directory)
from flask_login import LoginManager, login_user, logout_user, current_user
from flask_login import LoginManager, login_user, logout_user, current_user, login_required
from flask_wtf import CSRFProtect
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.routing import BaseConverter
@ -39,6 +39,8 @@ FK = ForeignKeyField
SLUG_RE = r'[a-z0-9]+(?:-[a-z0-9]+)*'
ILINK_RE = r'\]\(/(p/\d+|' + SLUG_RE + ')/?\)'
USERNAME_RE = r'[a-z0-9_-]{3,30}'
PING_RE = r'(?<!\w)@(' + USERNAME_RE + r')'
#### GENERAL CONFIG ####
@ -115,6 +117,11 @@ class SpoilerExtension(markdown.extensions.Extension):
def extendMarkdown(self, md, md_globals):
md.inlinePatterns.register(markdown.inlinepatterns.SimpleTagInlineProcessor(r'()>!(.*?)!<', 'span class="spoiler"'), 'spoiler', 14)
#class PingExtension(markdown.extensions.Extension):
# def extendMarkdown(self, md):
# pass
#### DATABASE SCHEMA ####
database_url = _getconf('database', 'url')
@ -142,6 +149,17 @@ class User(BaseModel):
privileges = BitField()
is_admin = privileges.flag(1)
# helpers for flask_login
@property
def is_anonymous(self):
return False
@property
def is_active(self):
return True
@property
def is_authenticated(self):
return True
class Page(BaseModel):
url = CharField(64, unique=True, null=True)
@ -208,7 +226,7 @@ class PageText(BaseModel):
else:
return c.decode('latin-1')
@classmethod
def create_content(cls, text, treshold=600, search_dup=True):
def create_content(cls, text, *, treshold=600, search_dup=True):
c = text.encode('utf-8')
use_gzip = len(c) > treshold
if use_gzip and gzip:
@ -387,6 +405,9 @@ def remove_tags(text, convert=True, headings=True):
text = md(text, toc=False, math=False)
return re.sub(r'<.*?>', '', text)
def is_username(s):
return re.match('^' + USERNAME_RE + '$', s)
#### I18N ####
i18n.load_path.append(os.path.join(APP_BASE_DIR, 'i18n'))
@ -421,6 +442,8 @@ app.url_map.converters['slug'] = SlugConverter
csrf = CSRFProtect(app)
login_manager = LoginManager(app)
login_manager.login_view = 'accounts_login'
#### ROUTES ####
@ -801,6 +824,36 @@ def accounts_login():
return redirect(request.args.get('next', '/'))
return render_template('login.html')
@app.route('/accounts/register/', methods=['GET','POST'])
def accounts_register():
if current_user.is_authenticated:
return redirect(request.args.get('next', '/'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if not is_username(username):
flash('Invalid username: usernames can contain only letters, numbers, underscores and hyphens.')
return render_template('register.html')
if request.form['password'] != request.form['confirm_password']:
flash('Passwords do not match.')
return render_template('register.html')
if not request.form['legal']:
flash('You must accept Terms in order to register.')
try:
with database.atomic():
u = User.create(
username = username,
email = request.form.get('email'),
password = generate_password_hash(password),
join_date = datetime.datetime.now()
)
login_user(u)
return redirect(request.args.get('next', '/'))
except IntegrityError:
flash('Username taken')
return render_template('register.html')
@app.route('/accounts/logout/')
def accounts_logout():
logout_user()
@ -862,6 +915,10 @@ class Exporter(object):
pobj['title'] = p.title
pobj['url'] = p.url
pobj['tags'] = [tag.name for tag in p.tags]
pobj['calendar'] = p.calendar
pobj['flags'] = p.flags
if include_users:
pobj['owner'] = p.owner_id
hist = []
for rev in (p.revisions if include_history else [p.latest]):
revobj = {}
@ -884,6 +941,58 @@ class Exporter(object):
def export(self):
return json.dumps(self.root)
class Importer(object):
def __init__(self, dump, *, overwrite_urls = True):
self.root = json.loads(dump)
self.owner = None
self.overwrite_urls = overwrite_urls
def claim(self, owner):
self.owner = owner
def execute(self):
no_pages = 0
no_revs = 0
for pobj in self.root['pages']:
purl = pobj.get("url")
try:
if purl:
try:
p2 = Page.get(Page.url == purl)
p2.url = None
p2.save()
except Page.DoesNotExist:
pass
p = Page.create(
url = purl if self.overwrite_urls else None,
title = pobj['title'],
calendar = pobj.get('calendar'),
owner = self.owner.id,
flags = pobj.get('flags'),
touched = datetime.datetime.now()
)
p.change_tags(pobj.get('tags'))
no_pages += 1
for revobj in pobj['history']:
textref = PageText.create_content(
revobj['text']
)
rev = PageRevision.create(
page = p,
user = self.owner.id,
textref = textref,
comment = revobj.get('comment'),
pub_date = datetime.datetime.fromtimestamp(revobj['timestamp']),
length = revobj['length']
)
no_revs += 1
except Exception as e:
sys.excepthook(*sys.exc_info())
continue
return no_pages, no_revs
@app.route('/manage/export/', methods=['GET', 'POST'])
def exportpages():
if request.method == 'POST':
@ -914,7 +1023,18 @@ def exportpages():
return render_template('exportpages.html')
@app.route('/manage/import/', methods=['GET', 'POST'])
@login_required
def importpages():
if request.method == 'POST':
if current_user.is_admin:
f = request.files['import']
overwrite_urls = request.form.get('ovwurls')
im = Importer(f.read(), overwrite_urls=overwrite_urls)
im.claim(current_user)
res = im.execute()
flash('Imported successfully {} pages and {} revisions'.format(*res))
else:
flash('Pages can be imported by Administrators only!')
return render_template('importpages.html')
#### EXTENSIONS ####