From 83e2c892b370a3239a115bfb661bacb02c38defa Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Thu, 5 Jan 2023 11:46:54 +0100 Subject: [PATCH] Added importer and ability to register accounts --- app.py | 124 ++++++++++++++++++++++++++++++++++++- extensions/importexport.py | 70 --------------------- i18n/salvi.en.json | 11 +++- i18n/salvi.it.json | 10 ++- migrations/0_6to0_7.py | 4 +- templates/base.html | 3 +- templates/importpages.html | 22 +++++++ templates/login.html | 2 +- templates/register.html | 36 +++++++++++ 9 files changed, 204 insertions(+), 78 deletions(-) delete mode 100644 extensions/importexport.py create mode 100644 templates/register.html diff --git a/app.py b/app.py index 1f064a1..abf0e80 100644 --- a/app.py +++ b/app.py @@ -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'(?!(.*?)!<', '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 #### diff --git a/extensions/importexport.py b/extensions/importexport.py deleted file mode 100644 index 87dba27..0000000 --- a/extensions/importexport.py +++ /dev/null @@ -1,70 +0,0 @@ -from flask import Blueprint, render_template, request -import json, datetime - -from app import Page, PageTag - -bp = Blueprint('importexport', __name__) - -class Exporter(object): - def __init__(self): - self.root = {'pages': [], 'users': {}} - def add_page(self, p, include_history=True, include_users=False): - pobj = {} - pobj['title'] = p.title - pobj['url'] = p.url - pobj['tags'] = [tag.name for tag in p.tags] - hist = [] - for rev in (p.revisions if include_history else [p.latest]): - revobj = {} - revobj['text'] = rev.text - revobj['timestamp'] = rev.pub_date.timestamp() - if include_users: - revobj['user'] = rev.user_id - if rev.user_id not in self.root['users']: - self.root['users'][rev.user_id] = rev.user_info() - else: - revobj['user'] = None - revobj['comment'] = rev.comment - revobj['length'] = rev.length - hist.append(revobj) - pobj['history'] = hist - self.root['pages'].append(pobj) - def add_page_list(self, pl, include_history=True, include_users=False): - for p in pl: - self.add_page(p, include_history=include_history, include_users=include_users) - def export(self): - return json.dumps(self.root) - -@bp.route('/manage/export/', methods=['GET', 'POST']) -def exportpages(): - if request.method == 'POST': - raw_list = request.form['export-list'] - q_list = [] - for item in raw_list.split('\n'): - item = item.strip() - if len(item) < 2: - continue - if item.startswith('+'): - q_list.append(Page.select().where(Page.id == item[1:])) - elif item.startswith('#'): - q_list.append(Page.select().join(PageTag, on=PageTag.page).where(PageTag.name == item[1:])) - elif item.startswith('/'): - q_list.append(Page.select().where(Page.url == item[1:].rstrip('/'))) - else: - q_list.append(Page.select().where(Page.title == item)) - if not q_list: - flash('Failed to export pages: The list is empty!') - return render_template('exportpages.html') - query = q_list.pop(0) - while q_list: - query |= q_list.pop(0) - e = Exporter() - e.add_page_list(query) - return e.export(), {'Content-Type': 'application/json', 'Content-Disposition': 'attachment; ' + - 'filename=export-{}.json'.format(datetime.datetime.now().strftime('%Y%m%d-%H%M%S'))} - return render_template('exportpages.html') - -@bp.route('/manage/import/', methods=['GET', 'POST']) -def importpages(): - return render_template('importpages.html') - diff --git a/i18n/salvi.en.json b/i18n/salvi.en.json index b367465..c33dab1 100644 --- a/i18n/salvi.en.json +++ b/i18n/salvi.en.json @@ -45,6 +45,15 @@ "notes-count-with-url": "Number of pages with URL set", "revision-count": "Number of revisions", "revision-count-per-page": "Average revisions per page", - "remember-me-for": "Remember me for" + "remember-me-for": "Remember me for", + "confirm-password": "Confirm password", + "email": "E-mail", + "optional": "optional", + "have-read-terms": "I have read {0} and {1}", + "terms-of-service": "Terms of Service", + "privacy-policy": "Privacy Policy", + "already-have-account": "Already have an account?", + "logged-in-as": "Logged in as", + "not-logged-in": "Not logged in" } } \ No newline at end of file diff --git a/i18n/salvi.it.json b/i18n/salvi.it.json index bd20613..a7b061a 100644 --- a/i18n/salvi.it.json +++ b/i18n/salvi.it.json @@ -35,8 +35,16 @@ "login": "Entra", "username": "Nome utente", "password": "Password", + "no-account-sign-up": "Non hai un account?", + "sign-up": "Registrati", "not-found": "Non trovato", "not-found-text-1": "La pagina con url", - "not-found-text-2": "non esiste" + "not-found-text-2": "non esiste", + "users-count": "Numero di utenti", + "notes-count": "Numero di note", + "notes-count-with-url": "Numero di note con URL impostato", + "revision-count": "Numero di revisioni", + "revision-count-per-page": "Media di revisioni per pagina", + "remember-me-for": "Ricordami per" } } \ No newline at end of file diff --git a/migrations/0_6to0_7.py b/migrations/0_6to0_7.py index ef1d488..5d11341 100644 --- a/migrations/0_6to0_7.py +++ b/migrations/0_6to0_7.py @@ -1,6 +1,6 @@ from playhouse.migrate import migrate, SqliteMigrator, MySQLMigrator from peewee import MySQLDatabase, SqliteDatabase, \ - IntegerField, DateTimeField, ForeignKeyField + IntegerField, DateTimeField, ForeignKeyField, DeferredForeignKey from app import database, User if type(database) == MySQLDatabase: @@ -15,6 +15,6 @@ with database.atomic(): database.create_tables([User]) migrate( migrator.add_column('page', 'calendar', DateTimeField(index=True, null=True)), - migrator.add_column('page', 'owner_id', IntegerField(null=True)) + migrator.add_column('page', 'owner', DeferredForeignKey('User', null=True)) ) diff --git a/templates/base.html b/templates/base.html index f72a183..05b2850 100644 --- a/templates/base.html +++ b/templates/base.html @@ -44,7 +44,8 @@
  • login
  • diff --git a/templates/importpages.html b/templates/importpages.html index 711e9e5..67b3a64 100644 --- a/templates/importpages.html +++ b/templates/importpages.html @@ -5,5 +5,27 @@ {% block content %}

    Import pages

    +{% if current_user.is_admin %} +

    + You can import files produced by the exporter tool, in JSON format. + Importing pages can be done by users with Admin permissions only. +

    + + +
    + +
    + +
    +
    + + +
    +
    + +
    +
    +{% else %}

    Importing pages can be done by users with Admin permissions only.

    +{% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index bb3eb5b..384a652 100644 --- a/templates/login.html +++ b/templates/login.html @@ -29,5 +29,5 @@ -

    {{ T('no-account-sign-up') }} {{ T("sign-up") }}

    +

    {{ T('no-account-sign-up') }} {{ T("sign-up") }}

    {% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..4e34b07 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}{{ T('sign-up') }} – {{ app_name }}{% endblock %} + +{% block content %} +

    {{ T('sign-up') }}

    + +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + +

    {{ T('already-have-account') }} {{ T("login") }}

    +{% endblock %} \ No newline at end of file