commit c278f9558efcc15bb5ef459323163425c73bc5bd
parent e345d743e09c7b5cf76a42c414fa9510eb400e5a
Author: parazyd <parazyd@dyne.org>
Date: Wed, 16 Jan 2019 20:35:10 +0100
Refactor into separate files.
Diffstat:
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