diasporadiaries

a platform for writing stories with personal accounts and messages
git clone git://parazyd.org/diasporadiaries.git
Log | Files | Refs | Submodules | README | LICENSE

commit c278f9558efcc15bb5ef459323163425c73bc5bd
parent e345d743e09c7b5cf76a42c414fa9510eb400e5a
Author: parazyd <parazyd@dyne.org>
Date:   Wed, 16 Jan 2019 20:35:10 +0100

Refactor into separate files.

Diffstat:
A.gitignore | 1+
Adb.py | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdiaspora.py | 397++++++++++++++++++-------------------------------------------------------------
Rtemplates/deletefail.html -> templates/fail_delete.html | 0
Atemplates/fail_edit.html | 17+++++++++++++++++
Mtemplates/footer.html | 6+++---
Mtemplates/nav.html | 6+-----
Rtemplates/approved.html -> templates/success_approve.html | 0
Rtemplates/deleted.html -> templates/success_delete.html | 0
Rtemplates/redacted.html -> templates/success_edit.html | 0
Rtemplates/submitted.html -> templates/success_submit.html | 0
Autils.py | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 378 insertions(+), 316 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/db.py b/db.py @@ -0,0 +1,131 @@ +# Copyright (c) 2019 Ivan Jelincic <parazyd@dyne.org> +# +# This file is part of diasporadiaries +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Module for sqlite database operations. +""" +from sqlite3 import connect + + +def initdb(dbpath): + """ + Initializes the sqlite3 database and returns the db context and cursor. + + There are two tables, one which holds users, and the other holds stories. + """ + _dbctx = connect(dbpath, check_same_thread=False) + _db = _dbctx.cursor() + + userdb_init = ''' + CREATE TABLE IF NOT EXISTS users ( + id integer PRIMARY KEY, + email text UNIQUE NOT NULL, + name text NOT NULL, + password text NOT NULL, + cap integer NOT NULL, + first_seen integer NOT NULL, + last_seen integer NOT NULL, + is_active integer NOT NULL, + is_authed integer NOT NULL + ); + ''' + + storydb_init = ''' + CREATE TABLE IF NOT EXISTS stories ( + id integer PRIMARY KEY, + name text NOT NULL, + embark text NOT NULL, + disembark text NOT NULL, + email text, + contact text, + story text NOT NULL, + timestamp integer NOT NULL, + visible integer NOT NULL, + deletekey text NOT NULL + ); + ''' + + _db.execute(userdb_init) + _db.execute(storydb_init) + + return _dbctx, _db + + +dbctx, db = initdb('./stories.db') + + +def sql_select_col_where(col0, col1, val, table='stories'): + """ + Queries col0 where col1 = val. + """ + db.execute("SELECT %s FROM %s WHERE %s = '%s';" % (col0, table, col1, val)) + return db.fetchall() + + +def sql_select_col(col, table='stories'): + """ + Executes a SELECT query and returns the entire col. + """ + db.execute("SELECT %s FROM %s;" % (col, table)) + return db.fetchall() + + +def sql_delete_row_where(col, val, table='stories'): + """ + Executes a DELETE query where col=val. + """ + db.execute(""" + DELETE + FROM %s + WHERE %s = '%s'; + """ % (table, col, val)) + dbctx.commit() + + +def sql_update_row_where(vals, col, val, table='stories'): + """ + Executes an UPDATE query where col=val. + + vals is a list of tuples. + """ + length = len(vals) - 1 + valstring = '' + for i, j in enumerate(vals): + valstring += "%s = '%s'" % (j[0], j[1]) + if i < length: + valstring += ', ' + + db.execute(""" + UPDATE %s + SET %s + WHERE %s = '%s'; + """ % (table, valstring, col, val)) + dbctx.commit() + + +def sql_insert_story(args): + """ + Executes an sql INSERT query where *args are VALUES to insert. + + TODO: Make this more generic. + """ + db.execute(""" + INSERT INTO stories VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ); + """, (args[0], args[1], args[2], args[3], args[4], args[5], args[6], + args[7], args[8], args[9])) + dbctx.commit() diff --git a/diaspora.py b/diaspora.py @@ -19,357 +19,149 @@ Main diasporadiaries module """ from argparse import ArgumentParser -from random import randint, shuffle, SystemRandom -from string import ascii_uppercase, digits -from time import gmtime, strftime, time -import json -import sqlite3 +from random import choice +from time import time -from flask import (Flask, render_template, request, Markup, redirect) +from flask import Flask, render_template, request +from db import (sql_delete_row_where, sql_update_row_where, sql_insert_story, + sql_select_col, sql_select_col_where) +from utils import (get_story, makenav, randomstring, getcountryname, + get_multiple_stories, get_multiple_stories_filtered) -app = Flask(__name__) - - -def initdb(dbpath): - """ - Initializes the sqlite3 database and returns the db context and cursor. - - There are two tables, one which holds users, and the other holds stories. - """ - _dbctx = sqlite3.connect(dbpath, check_same_thread=False) - _db = _dbctx.cursor() - - udbinit = """ - CREATE TABLE IF NOT EXISTS users ( - id integer PRIMARY KEY, - email text NOT NULL, - name text NOT NULL, - password text NOT NULL, - timestamp integer NOT NULL - ); - """ - - sdbinit = """ - CREATE TABLE IF NOT EXISTS stories ( - id integer PRIMARY KEY, - name text NOT NULL, - embark text NOT NULL, - disembark text NOT NULL, - email text, - contact text, - story text NOT NULL, - timestamp integer NOT NULL, - visible integer NOT NULL, - deletekey text NOT NULL - ); - """ - - _db.execute(udbinit) - _db.execute(sdbinit) - - return _dbctx, _db - - -def randomascii(length): - """ - Returns a random uppercase string of given length. - """ - return ''.join(SystemRandom().choice(ascii_uppercase + digits) \ - for _ in range(length)) - - -def getcountryname(cc): - """ - Returns a country name which matches the given country code. - """ - return countrymap.get(cc, 'Unknown countries') - - -def query_all_where(col, val): - """ - Executes a query where col = val and returns all found rows. - """ - db.execute("SELECT * FROM stories WHERE %s = '%s';" % (col, val)) - return db.fetchall() - - -def query_col_where(col0, col1, val): - """ - Queries a specific column where col1 = val - """ - db.execute("SELECT %s FROM stories WHERE %s = '%s';" % (col0, col1, val)) - return db.fetchall() - - -def query_col(col): - """ - Executes a SELECT query and returns the entire col. - """ - db.execute("SELECT %s FROM stories;" % col) - return db.fetchall() - - -def delete_row_where(col, val): - """ - Executes a DELETE query. - """ - db.execute(""" - DELETE - FROM stories - WHERE %s = '%s'; - """ % (col, val)) - dbctx.commit() - - -def approvestory(storyid): - """ - Makes a story visible on the index. - """ - db.execute(""" - UPDATE stories - SET visible = 1 - WHERE id = '%s'; - """ % storyid) - dbctx.commit() - -def fillstory(row): - """ - Returns a story dict filled with the given row from the db. - """ - return { - 'id': row[0], - 'name': row[1], - 'embark': row[2], - 'embarkname': getcountryname(row[2]), - 'disembark': row[3], - 'disembarkname': getcountryname(row[3]), - 'email': row[4], - 'contact': row[5], - 'story': row[6], - 'date': strftime('%d.%m.%Y.', gmtime(row[7])), - 'time': strftime('%H:%M UTC', gmtime(row[7])), - 'visible': row[8], - 'deletekey': row[9], - } - - -def getstory(storyid): - """ - Returns a story dict with the given story id. - """ - return fillstory(query_all_where('id', storyid)[0]) - - -def getstories(col, cc): - """ - Returns a list of filled stories for viewing multiple ones in - the /country route. - """ - rows = query_all_where(col, cc) - - stories = [] - for row in rows: - sinstory = fillstory(row) - sinstory['story'] = Markup(sinstory['story']).striptags()[:128] - sinstory['story'] += '...' - stories.append(sinstory) - - return stories[::-1] +app = Flask(__name__) +app.add_template_global(makenav) -def getstoriesfiltered(cc, column, param): +@app.route('/submit', methods=['GET', 'POST']) +def submit(): """ - Returns a filtered list of filled stories for viewing multiple ones - in the /country route. + Route for submitting a story. """ - stories = getstories(cc, column) + if request.method != 'POST': + return render_template('submit.html') - filtered_stories = [] - for j in stories: - if j['embark'] == param: - filtered_stories.append(j) + delkey = randomstring(32) + storyargs = [ + None, + request.form['Name'], + request.form['Embark'], + request.form['Disembark'], + request.form['Email'], + request.form['Contact'], + request.form['Story'], + int(time()), + 0, + delkey, + ] + sql_insert_story(storyargs) - return filtered_stories + return render_template('success_submit.html', delkey=delkey) -def makenav(): +@app.route('/edit', methods=['GET', 'POST']) +def edit(): """ - Queries the disembark column for filling up the by-country dropdown - navigation. + Route for editing and redacting. """ - rows = query_col_where('disembark', 'visible', 1) - - havec = [] - for j in rows: - havec.append(j[0]) - - havec = list(set(havec)) - navlist = [] - for j in havec: - navlist.append((j, getcountryname(j))) - - shuffle(navlist) - return navlist - - -@app.route('/') -def main(): + if request.method != 'POST': + story_id = request.args.get('id') + if story_id: + return render_template('edit.html', story=get_story(story_id)) + return render_template('fail_edit.html') + + vals = [ + ('name', request.form['Name']), + ('embark', request.form['Embark']), + ('disembark', request.form['Disembark']), + ('email', request.form['Email']), + ('contact', request.form['Contact']), + ('story', request.form['Story']), + ] + sql_update_row_where(vals, 'id', request.form['Id']) + return render_template('success_edit.html') + + +@app.route('/view') +def view(): """ - Main route, the homepage. + Route for viewing a single story. + If no story is specified, it renders a random story. """ - return render_template('index.html', navlist=makenav()) + story_id = request.args.get('id') + if not story_id: + story_id = choice(sql_select_col('id'))[0] - -@app.route('/submit', methods=['GET', 'POST']) -def submit(): - """ - Route for submitting a story. - """ - if request.method == 'POST': - delkey = randomascii(32) - db.execute(""" - INSERT INTO stories VALUES ( - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?); - """, (None, - request.form['Name'], - request.form['Embark'], - request.form['Disembark'], - request.form['Email'], - request.form['Contact'], - request.form['Story'], - int(time()), - 0, - delkey)) - dbctx.commit() - return render_template('submitted.html', navlist=makenav(), - delkey=delkey) - - return render_template('submit.html', navlist=makenav()) + return render_template('view.html', story=get_story(story_id)) @app.route('/delete') def delete(): """ - Route for deleting a story. + Route for deleting a specific story. """ delkey = request.args.get('key') if delkey: - storyid = query_col_where('id', 'deletekey', delkey) - if storyid: - storyid = storyid[0][0] - delete_row_where('id', storyid) - return render_template('deleted.html', navlist=makenav()) + story_id = sql_select_col_where('id', 'deletekey', delkey) + if story_id: + story_id = story_id[0][0] + sql_delete_row_where('id', story_id) + return render_template('success_delete.html') - return render_template('deletefail.html', navlist=makenav()) + return render_template('fail_delete.html') -@app.route('/country', methods=['GET']) +@app.route('/country') def country(): """ Route for viewing stories for a specific country. - TODO: If no country is given, it will show a random country. + If no country is given, it will show a random country. """ cc = request.args.get('id') - # TODO: if not cc: + #if not cc + # cc = random(existing_cc) + # TODO: if not cc: return random existing cc ccfrom = request.args.get('from') + filtered = None if ccfrom: - filtered_stories = getstoriesfiltered('disembark', cc, ccfrom) + filtered = get_multiple_stories_filtered('disembark', cc, + ('embark', ccfrom), strip=True) - stories = getstories('disembark', cc) + stories = get_multiple_stories('disembark', cc, strip=True) clist = [] - for j in stories: - clist.append((j['embark'], j['embarkname'])) + for i in stories: + clist.append((i['embark'], i['embarkname'])) clist = list(set(clist)) - if not ccfrom: - return render_template('country.html', navlist=makenav(), - country=getcountryname(cc), cc=cc, - stories=stories, filtered_stories=None, - clist=clist) - - return render_template('country.html', navlist=makenav(), - country=getcountryname(cc), cc=cc, - stories=stories, filtered_stories=filtered_stories, + return render_template('country.html', country=getcountryname(cc), + cc=cc, stories=stories, filtered_stories=filtered, clist=clist) -@app.route('/view', methods=['GET']) -def view(): - """ - Route for viewing a single story. - If no story is specified, it will render a random story. - """ - story_id = request.args.get('id', randint(1, len(query_col('id')))) - story = getstory(story_id) - return render_template('view.html', navlist=makenav(), story=story) - -@app.route('/dashboard', methods=['GET', 'POST']) +@app.route('/dashboard') def dashboard(): """ Route for dashboard/admin view. """ - approve = request.args.get('approveid') - if approve: - approvestory(approve) - return render_template('approved.html', navlist=makenav()) + story_id = request.args.get('approveid') + if story_id: + sql_update_row_where([('visible', 1)], 'id', story_id) + return render_template('success_approve.html') - query = query_all_where('visible', 0) - print(query) - stories = [] - for i in query: - sinstory = fillstory(i) - sinstory['story'] = Markup(sinstory['story']).striptags()[:128] - sinstory['story'] += '...' - stories.append(sinstory) + stories = get_multiple_stories('visible', 0, strip=True) - return render_template('dashboard.html', navlist=makenav(), - stories=stories) + return render_template('dashboard.html', stories=stories) -@app.route('/edit', methods=['GET', 'POST']) -def edit(): +@app.route('/') +def main(): """ - Route for editing and redacting. + Main route, the homepage. """ - if request.method == 'POST': - db.execute(""" - UPDATE stories - SET name = '%s', - embark = '%s', - disembark = '%s', - email = '%s', - contact = '%s', - story = '%s' - WHERE id = '%s'; - """ % (request.form['Name'], - request.form['Embark'], - request.form['Disembark'], - request.form['Email'], - request.form['Contact'], - request.form['Story'], - request.form['Id'])) - dbctx.commit() - return render_template('redacted.html', navlist=makenav()) - - storyid = request.args.get('id') - if storyid: - story = getstory(storyid) - return render_template('edit.html', navlist=makenav(), story=story) - - return redirect('/dashboard') + return render_template('index.html') if __name__ == '__main__': @@ -378,19 +170,8 @@ if __name__ == '__main__': help='Address for listening (ex: 127.0.0.1)') parser.add_argument('-p', default='8000', help='Port for listening (ex: 8000)') - parser.add_argument('-db', default='stories.db', - help='Path to sqlite database (ex: stories.db)') parser.add_argument('-d', action='store_true', default=True, help='Debug mode') args = parser.parse_args() - countrymap = {} - with open('world.json') as f: - data = json.load(f) - for cname in data: - countrymap[cname['alpha2']] = cname['name'] - countrymap['xk'] = 'Kosovo' - - dbctx, db = initdb(args.db) - app.run(host=args.l, port=args.p, threaded=True, debug=args.d) diff --git a/templates/deletefail.html b/templates/fail_delete.html diff --git a/templates/fail_edit.html b/templates/fail_edit.html @@ -0,0 +1,17 @@ +{% include 'header.html' %} + + <title>Edit fail | Diaspora Diaries</title> + +{% include 'nav.html' %} + + <main role="main" class="container cover"> + + <h1 class="cover-heading">Error!</h1> + + <p class="lead">No story with this story id.</p> + + <p class="lead">You can return to the <a href="/">homepage</a> now.</p> + + </main> + +{% include 'footer.html' %} diff --git a/templates/footer.html b/templates/footer.html @@ -1,5 +1,5 @@ - <footer> - <img src="/static/img/slatkisi.png" width="150"> - </footer> + <footer> + <img src="/static/img/slatkisi.png" width="150"> + </footer> </body> </html> diff --git a/templates/nav.html b/templates/nav.html @@ -21,15 +21,11 @@ Stories </a> <div class="dropdown-menu" aria-labelledby="navbarDropdown"> - {% for i in navlist %} + {% for i in makenav() %} <a class="dropdown-item" href="/country?id={{ i[0] }}"> <img width="32" src="/static/img/flags/{{ i[0] }}.png"> {{ i[1]}} </a> {% endfor %} - <!-- <a class="dropdown-item" href="#">Country 1</a> - <a class="dropdown-item" href="#">Country 2</a> - <div class="dropdown-divider"></div> - <a class="dropdown-item" href="#">Country 3</a> --> </div> </li> <li class="nav-item"> diff --git a/templates/approved.html b/templates/success_approve.html diff --git a/templates/deleted.html b/templates/success_delete.html diff --git a/templates/redacted.html b/templates/success_edit.html diff --git a/templates/submitted.html b/templates/success_submit.html diff --git a/utils.py b/utils.py @@ -0,0 +1,136 @@ +# Copyright (c) 2019 Ivan Jelincic <parazyd@dyne.org> +# +# This file is part of diasporadiaries +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +Utility functions. +""" +from json import load +from random import SystemRandom, shuffle +from string import ascii_lowercase, digits +from time import gmtime, strftime + +from flask import Markup + +from db import sql_select_col_where + + +countrymap = {} +with open('world.json') as worldfile: + json_data = load(worldfile) +for cname in json_data: + countrymap[cname['alpha2']] = cname['name'] +countrymap['xk'] = 'Kosovo' +del json_data +del worldfile + + +def randomstring(length): + """ + Returns a random lowercase+digit string of given length. + """ + return ''.join(SystemRandom().choice(ascii_lowercase+digits) \ + for _ in range(length)) + + +def getcountryname(cc): + """ + Returns a country name matching the given country code. + """ + return countrymap.get(cc, 'Unknown countries') + + +def fill_story(row): + """ + Returns a story dict filled with the given row from the database. + """ + return { + 'id': row[0], + 'name': row[1], + 'embark': row[2], + 'embarkname': getcountryname(row[2]), + 'disembark': row[3], + 'disembarkname': getcountryname(row[3]), + 'email': row[4], + 'contact': row[5], + 'story': row[6], + 'date': strftime('%d.%m.%Y.', gmtime(row[7])), + 'time': strftime('%H:%M UTC', gmtime(row[7])), + 'visible': row[8], + 'deletekey': row[9], + } + + +def get_story(story_id): + """ + Returns a specific story matching the story_id. + """ + # TODO: test if story_id isn't in db. + return fill_story(sql_select_col_where('*', 'id', story_id)[0]) + + +def get_multiple_stories(col, val, reverse=True, strip=False): + """ + Returns a list of stories where col=val. + """ + rows = sql_select_col_where('*', col, val) + + stories = [] + for i in rows: + j = fill_story(i) + if strip: + j['story'] = Markup(j['story']).striptags()[:127] + j['story'] += '...' + stories.append(j) + + if reverse: + return stories[::-1] + return stories + + +def get_multiple_stories_filtered(col, val, param, reverse=True, strip=False): + """ + Returns a filtered list of stories where col=val. + + param is a tuple, where [0] will be compared to [1] for filtering. + """ + stories = get_multiple_stories(col, val, reverse=reverse, strip=strip) + # TODO: test if not stories + + filtered_stories = [] + for i in stories: + if i[param[0]] == param[1]: + filtered_stories.append(i) + + return filtered_stories + + +def makenav(randomize=True): + """ + Queries the disembark column for filling up the by-country dropdown + navigation. + """ + rows = sql_select_col_where('disembark', 'visible', 1) + + havec = [i[0] for i in rows] + havec = list(set(havec)) + + navlist = [] + for i in havec: + navlist.append((i, getcountryname(i))) + + if randomize: + shuffle(navlist) + return navlist