diff --git a/README.md b/README.md
index 3be3bf9..6a4dfb3 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,8 @@
A simple shopping list app:
-* Runs on gunicorn+flask (python2.7)
+* Runs on gunicorn+flask (python3)
+* Dockerizes
* Multi-user
* Data saved as flat files, can contain markdown
* Syntaxes parsed:
diff --git a/code/Dockerfile b/code/Dockerfile
new file mode 100644
index 0000000..f47f8bf
--- /dev/null
+++ b/code/Dockerfile
@@ -0,0 +1,10 @@
+#FROM alpine:3.10
+FROM python:3
+COPY requirements.txt /requirements.txt
+RUN python3 -m venv /venv && \
+ . /venv/bin/activate && \
+ pip3 install --upgrade pip && \
+ pip3 install -r /requirements.txt
+COPY . /code/
+WORKDIR /code
+CMD . /venv/bin/activate && ./start.me
diff --git a/requirements.txt b/code/requirements.txt
similarity index 100%
rename from requirements.txt
rename to code/requirements.txt
diff --git a/revprox.py b/code/revprox.py
similarity index 100%
rename from revprox.py
rename to code/revprox.py
diff --git a/schema.sql b/code/schema.sql
similarity index 100%
rename from schema.sql
rename to code/schema.sql
diff --git a/code/shop.py b/code/shop.py
new file mode 100644
index 0000000..58b000b
--- /dev/null
+++ b/code/shop.py
@@ -0,0 +1,613 @@
+# -*- coding: utf-8 -*-
+# all the imports
+import sqlite3, time, datetime, hashlib, os, re
+from shutil import copyfile, move
+from flask import (
+ Flask,
+ request,
+ session,
+ g,
+ redirect,
+ url_for,
+ abort,
+ render_template,
+ flash,
+)
+from revprox import ReverseProxied
+
+# configuration
+DATABASE = "/data/shop.db"
+DATADIR = "/data"
+DEBUG = False
+SECRET_KEY = os.getenv("SECRET_KEY", "development key")
+USERNAME = os.getenv("ADMIN_USER", "admin")
+PASSWORD = os.getenv("ADMIN_PASSWD", "default")
+URLFINDER = re.compile("((news|telnet|nttp|file|http|ftp|https)://[^ ]+)")
+URLPARSER = re.compile(r"(\[)([^\]]+)(\])\(([^\)]+)\)")
+BOLDFINDER = re.compile(r"\*([^\*]+)\*")
+CODEFINDER = re.compile(r"\`([^\`]+)\`")
+
+# create our little application :)
+app = Flask(__name__)
+app.config.from_object(__name__)
+app.config["SESSION_COOKIE_NAME"] = os.getenv("SESSION_COOKIE_NAME", "mdshop")
+app.config["register"] = os.getenv("ENABLE_REGISTER", "true") != "false"
+print(app.config)
+app.wsgi_app = ReverseProxied(app.wsgi_app)
+
+
+def connect_db():
+ if not os.path.exists(app.config["DATABASE"]):
+ db = sqlite3.connect(app.config["DATABASE"])
+ for command in open("schema.sql", "rt").read().split(";"):
+ db.execute(command)
+ db.commit()
+ return sqlite3.connect(app.config["DATABASE"])
+
+
+def password_hash(s):
+ return hashlib.sha224(s.encode("utf8")).hexdigest()
+
+
+def get_username(id):
+ cur = g.db.execute("select * from users")
+ for row in cur.fetchall():
+ if id == row[0]:
+ return row[1]
+ return None
+
+
+def get_userid(name):
+ cur = g.db.execute("select * from users")
+ for row in cur.fetchall():
+ if name == row[1]:
+ return row[0]
+ return None
+
+
+def get_shares(id):
+ cur = g.db.execute("select * from shares")
+ shares = []
+ for row in cur.fetchall():
+ if id == row[1]:
+ shares.append(row[0])
+ return shares
+
+
+def get_shop_date(id):
+ date = ""
+ cur = g.db.execute("select * from shops")
+ for row in cur.fetchall():
+ if id == row[0]:
+ data_dir = os.path.join(DATADIR, get_username(row[2]))
+ data_file = os.path.join(data_dir, row[1] + ".md")
+ if os.path.exists(data_file):
+ date = datetime.datetime.fromtimestamp(
+ os.path.getmtime(data_file)
+ ).strftime("%m/%d %H:%M")
+ return date
+
+
+def get_shop_backup_date(id):
+ date = ""
+ cur = g.db.execute("select * from shops")
+ for row in cur.fetchall():
+ if id == row[0]:
+ data_dir = os.path.join(DATADIR, get_username(row[2]))
+ data_file = os.path.join(data_dir, row[1] + ".md.bkp")
+ if os.path.exists(data_file):
+ date = datetime.datetime.fromtimestamp(
+ os.path.getmtime(data_file)
+ ).strftime("%m/%d %H:%M")
+ return date
+
+
+def scan_for_new_documents(id):
+ user = get_username(id)
+ data_dir = os.path.join(DATADIR, user)
+ if not os.path.exists(data_dir):
+ return
+ cur = g.db.execute("select * from shops")
+ existing = []
+ non_existing = []
+ for row in cur.fetchall():
+ if row[2] != id:
+ continue
+ existing.append(row[1])
+ for row in os.listdir(data_dir):
+ if row.endswith(".md"):
+ if row[:-3] not in existing:
+ non_existing.append(row[:-3])
+ for shop in non_existing:
+ g.db.execute("insert into shops (shop,owner) values (?, ?)", [shop, id])
+ g.db.commit()
+
+
+def markdown_parse(s):
+ s = s.decode("utf8")
+ s = BOLDFINDER.sub(r'*\1*', s)
+ s = CODEFINDER.sub(r'`\1`', s)
+ return s
+
+
+def urlify(s):
+ if URLPARSER.search(s):
+ return URLPARSER.sub(r'[\2]', s)
+ return URLFINDER.sub(r'\1', s)
+
+
+@app.before_request
+def before_request():
+ g.db = connect_db()
+
+
+@app.teardown_request
+def teardown_request(exception):
+ db = getattr(g, "db", None)
+ if db is not None:
+ db.close()
+
+
+@app.route("/shop/")
+def show_shop(shopid):
+ if not session.get("logged_in"):
+ return redirect(url_for("login", error=None))
+ try:
+ shopid = int(shopid)
+ except ValueError:
+ return redirect(url_for("login", error=None))
+ has_access = False
+ cur = g.db.execute("select * from shops")
+ shared = get_shares(session.get("user"))
+ for row in cur.fetchall():
+ if row[0] == shopid:
+ if row[2] == session.get("user") or row[0] in shared:
+ has_access = True
+ shopname = row[1]
+ break
+ if not has_access:
+ return redirect(url_for("list_shops"))
+ data_dir = os.path.join(DATADIR, get_username(row[2]))
+ data_file = os.path.join(data_dir, row[1] + ".md")
+ if not os.path.exists(data_file):
+ open(data_file, "wt").close()
+ entries = []
+ content = open(data_file, "rt").read() # .decode('utf-8')
+ for i, row in enumerate(open(data_file, "rt").read().split("\n")):
+ # any parsing magick would be here
+ row = row.rstrip()
+ if row == "":
+ continue
+ icon = " "
+ extra_class = "noitem"
+ if "[ ]" in row:
+ icon = u" "
+ extra_class = ""
+ if "[x]" in row:
+ icon = u"\u2714"
+ extra_class = ""
+ row = urlify(row).encode("ascii", "xmlcharrefreplace")
+ row = markdown_parse(row)
+ if row.startswith("#"):
+ row = "" + row + ""
+ if row.startswith(">"):
+ row = "" + row + ""
+ entries.append(dict(row=i, text=row, icon=icon, extra_class=extra_class))
+ shared_to = []
+ cur = g.db.execute("select * from shares")
+ for row in cur.fetchall():
+ if row[0] == shopid:
+ shared_to.append(get_username(row[1]))
+ # invalidate autosort in 60 minutes:
+ if session.get("sort_update"):
+ if time.time() - session.get("sort_update") > 3600:
+ session["sort_view"] = False
+ session["sort_update"] = time.time()
+
+ return render_template(
+ "show_shop.html",
+ entries=entries,
+ shop=shopname,
+ shopid=shopid,
+ content=content,
+ shares=shared_to,
+ date=get_shop_date(shopid),
+ date_bkp=get_shop_backup_date(shopid),
+ autosort=session.get("sort_view", False),
+ )
+
+
+@app.route("/")
+def list_shops():
+ if not session.get("logged_in"):
+ return redirect(url_for("login", error=None))
+ scan_for_new_documents(session.get("user"))
+ cur = g.db.execute("select * from shops order by shop")
+ entries = []
+ for row in cur.fetchall():
+ if session.get("user") == row[2]: # owner
+ date = get_shop_date(row[0])
+ entries.append(
+ dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date)
+ )
+ cur = g.db.execute("select * from shops order by shop")
+ shares = get_shares(session.get("user"))
+ for row in cur.fetchall():
+ if row[0] in shares: # Has been shared to
+ date = get_shop_date(row[0])
+ entries.append(
+ dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date)
+ )
+
+ return render_template("list_shops.html", entries=entries)
+
+
+@app.route("/add", methods=["POST"])
+def add_items():
+ if not session.get("logged_in"):
+ abort(401)
+ shopid = int(request.form["shopid"])
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ shopname = g.db.execute(
+ "select shop from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ ownername = get_username(ownerid)
+ data_dir = os.path.join(DATADIR, ownername)
+ data_file = os.path.join(data_dir, shopname + ".md")
+ count = 0
+ contents_file = open(data_file, "at")
+ for row in request.form["add_md"].split("\n"):
+ if row.strip() == "":
+ continue
+ count += 1
+ contents_file.write("[ ] %s\n" % row.strip())
+ contents_file.close()
+ flash("Added %d items" % (count))
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/edit", methods=["POST"])
+def edit_md():
+ if not session.get("logged_in"):
+ abort(401)
+ shopid = int(request.form["shopid"])
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ shopname = g.db.execute(
+ "select shop from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ ownername = get_username(ownerid)
+ data_dir = os.path.join(DATADIR, ownername)
+ data_file = os.path.join(data_dir, shopname + ".md")
+ backup = data_file + ".bkp"
+ copyfile(data_file, backup)
+ contents_file = open(data_file, "wt")
+ contents_list = request.form["edit_md"].split("\n")
+ while contents_list[-1].strip() == "":
+ contents_list.pop()
+ for row in contents_list:
+ contents_file.write("%s\n" % (row,))
+ contents_file.close()
+ flash("Saved new file.")
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/restore", methods=["POST"])
+def restore_md():
+ if not session.get("logged_in"):
+ abort(401)
+ shopid = int(request.form["shopid"])
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ shopname = g.db.execute(
+ "select shop from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ ownername = get_username(ownerid)
+ data_dir = os.path.join(DATADIR, ownername)
+ data_file = os.path.join(data_dir, shopname + ".md")
+ backup = data_file + ".bkp"
+ backup_tmp = data_file + ".tmp"
+ if not os.path.exists(backup):
+ flash("Backup does not exist")
+ return redirect(url_for("show_shop", shopid=shopid))
+ copyfile(data_file, backup_tmp)
+ copyfile(backup, data_file)
+ move(backup_tmp, backup)
+ flash("Backup restored")
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/toggle", methods=["POST"])
+def toggle_item():
+ if not session.get("logged_in"):
+ abort(401)
+ shopid = int(request.form["shopid"])
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ shopname = g.db.execute(
+ "select shop from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ ownername = get_username(ownerid)
+ req_row = None
+ for key in request.form:
+ if key.startswith("item"):
+ req_row = int(key[4:])
+ if key == "toggleAll":
+ # Special meaning: toggle all rows
+ req_row = -1
+ if key == "unTickAll":
+ # Special meaning: untick all rows
+ req_row = -2
+ if req_row == None:
+ return redirect(url_for("show_shop", shopid=shopid))
+ data_dir = os.path.join(DATADIR, ownername)
+ data_file = os.path.join(data_dir, shopname + ".md")
+ backup = data_file + ".bkp"
+ contents_file = open(data_file, "rt")
+ contents = contents_file.read().split("\n")
+ contents_file.close()
+ changed = False
+ for i, row in enumerate(contents):
+ if i == req_row or req_row < 0:
+ if req_row != -2: # no ticking if unticking all
+ if "[ ]" in row:
+ contents[i] = row.replace("[ ]", "[x]")
+ if "[x]" in row:
+ contents[i] = row.replace("[x]", "[ ]")
+ if row != contents[i]:
+ changed = True
+ if changed:
+ if req_row == -1 or req_row == -2:
+ copyfile(data_file, backup)
+ contents_file = open(data_file, "wt")
+ contents_file.write("\n".join(contents))
+ contents_file.close()
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/remove_toggled", methods=["POST"])
+def remove_toggled():
+ if not session.get("logged_in"):
+ abort(401)
+ shopid = int(request.form["shopid"])
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ shopname = g.db.execute(
+ "select shop from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ ownername = get_username(ownerid)
+ data_dir = os.path.join(DATADIR, ownername)
+ data_file = os.path.join(data_dir, shopname + ".md")
+ backup = data_file + ".bkp"
+ contents_file = open(data_file, "rt")
+ contents = []
+ changed = False
+ for i, row in enumerate(contents_file.read().split("\n")):
+ if "[x]" not in row:
+ contents.append(row)
+ else:
+ changed = True
+ contents_file.close()
+ if changed:
+ copyfile(data_file, backup)
+ contents_file = open(data_file, "wt")
+ contents_file.write("\n".join(contents))
+ contents_file.close()
+ # ~ flash('successfully posted %s (%d)'%(row,req_row))
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/add_shop", methods=["POST"])
+def add_shop():
+ if not session.get("logged_in"):
+ abort(401)
+ import re, string
+
+ pattern = re.compile("[\W]+")
+ shopname = pattern.sub("", request.form["shop"])
+ if shopname == "":
+ flash("Shop name empty!")
+ return redirect(url_for("list_shops"))
+ cur = g.db.execute("select * from shops order by shop")
+ for row in cur.fetchall():
+ if shopname == row[1]:
+ flash("Shop already exists! " + shopname)
+ return redirect(url_for("list_shops"))
+ g.db.execute(
+ "insert into shops (shop,owner) values (?, ?)", [shopname, session["user"]]
+ )
+ g.db.commit()
+ new_dir = os.path.join(DATADIR, get_username(session["user"]))
+ new_file = os.path.join(new_dir, shopname + ".md")
+ if not os.path.exists(new_dir):
+ os.mkdir(new_dir)
+ open(new_file, "at")
+ flash("successfully created new shop: " + shopname)
+ return redirect(url_for("list_shops"))
+
+
+@app.route("/add_share", methods=["POST"])
+def add_share():
+ if not session.get("logged_in"):
+ abort(401)
+ import re, string
+
+ pattern = re.compile("[\W]+")
+ username = pattern.sub("", request.form["share"])
+ shopid = pattern.sub("", request.form["shopid"])
+ if username == "":
+ flash("User name empty!")
+ return redirect(url_for("show_shop", shopid=shopid))
+ userid = get_userid(username)
+ if userid == None:
+ flash("No such user!")
+ return redirect(url_for("show_shop", shopid=shopid))
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ if session.get("user") != ownerid:
+ flash("Not your shop!")
+ return redirect(url_for("show_shop", shopid=shopid))
+ g.db.execute("insert into shares (shopid,userid) values (?, ?)", [shopid, userid])
+ g.db.commit()
+ flash("Shared to %s" % (username))
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/remove_share", methods=["POST"])
+def remove_share():
+ if not session.get("logged_in"):
+ abort(401)
+ import re, string
+
+ pattern = re.compile("[\W]+")
+ username = pattern.sub("", request.form["user"])
+ shopid = pattern.sub("", request.form["shopid"])
+ if username == "":
+ flash("User name empty!")
+ return redirect(url_for("show_shop", shopid=shopid))
+ userid = get_userid(username)
+ if userid == None:
+ flash("No such user!")
+ return redirect(url_for("show_shop", shopid=shopid))
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ if session.get("user") != ownerid:
+ flash("Not your shop!")
+ return redirect(url_for("show_shop", shopid=shopid))
+
+ g.db.execute("delete from shares where shopid=? and userid=?", [shopid, userid])
+ g.db.commit()
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/remove_shop", methods=["POST"])
+def remove_shop():
+ if not session.get("logged_in"):
+ abort(401)
+ shopid = int(request.form["shopid"])
+ ownerid = g.db.execute(
+ "select owner from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ shopname = g.db.execute(
+ "select shop from shops where id=?", (request.form["shopid"],)
+ ).fetchall()[0][0]
+ ownername = get_username(ownerid)
+ data_dir = os.path.join(DATADIR, ownername)
+ data_file = os.path.join(data_dir, shopname + ".md")
+ if session.get("user") != ownerid:
+ flash("Not your shop!")
+ return redirect(url_for("show_shop", shopid=shopid))
+ # remove shop DB
+ g.db.execute("delete from shops where id=?", [shopid])
+ g.db.commit()
+ # backup data, and remove
+ backup = data_file + ".bkp"
+ copyfile(data_file, backup)
+ os.remove(data_file)
+ # remove shares
+ g.db.execute("delete from shares where shopid=?", [shopid])
+ g.db.commit()
+ flash("successfully deleted shop %s" % (shopname))
+ return redirect(url_for("list_shops"))
+
+
+@app.route("/sort_flip", methods=["POST"])
+def sort_flip():
+ if not session.get("sort_view"):
+ session["sort_view"] = True
+ session["sort_update"] = time.time()
+ else:
+ session["sort_view"] = False
+ session["sort_update"] = time.time()
+ shopid = int(request.form["shopid"])
+ return redirect(url_for("show_shop", shopid=shopid))
+
+
+@app.route("/login", methods=["GET", "POST"])
+def login():
+ error = None
+ if request.method == "POST":
+ cur = g.db.execute("select * from users")
+ for row in cur.fetchall():
+ if request.form["username"] == row[1]:
+ if password_hash(request.form["password"]) == row[2]:
+ session["logged_in"] = True
+ session["user"] = row[0]
+ # scan_for_new_documents(row[0])
+ return redirect(url_for("list_shops"))
+ error = "Invalid user/pass"
+ return render_template("login.html", error=error)
+
+
+@app.route("/register", methods=["GET", "POST"])
+def register():
+ error = None
+ if not app.config["register"]:
+ return ""
+ if request.method == "POST":
+ import re, string
+
+ pattern = re.compile("[\W]+")
+ username = pattern.sub("", request.form["username"])
+ password = password_hash(request.form["password"])
+ if len(username) == 0:
+ error = "No username given"
+ return render_template("register.html", error=error)
+ if len(request.form["password"]) < 5:
+ error = "Password too short"
+ return render_template("register.html", error=error)
+ cur = g.db.execute("select * from users")
+ for row in cur.fetchall():
+ if username == row[1]:
+ error = "Username already exists"
+ return render_template("register.html", error=error)
+ g.db.execute(
+ "insert into users (user,pass) values (?, ?)", [username, password]
+ )
+ g.db.commit()
+ flash('successfully registered user "%s". Now login.' % username)
+ return redirect(url_for("login"))
+ return render_template("register.html", error=error)
+
+
+@app.route("/profile", methods=["GET", "POST"])
+def profile():
+ if not session.get("logged_in"):
+ return redirect(url_for("login"))
+ error = None
+ user = get_username(session.get("user"))
+ if request.method == "POST":
+ import re, string
+
+ pattern = re.compile("[\W]+")
+ password = password_hash(request.form["password"])
+ if len(request.form["password"]) < 5:
+ error = "Password too short"
+ return render_template("profile.html", error=error, user=user)
+ g.db.execute(
+ "update users set pass=? where id=?", [password, session.get("user")]
+ )
+ g.db.commit()
+ flash("successfully updated profile.")
+ return redirect(url_for("profile"))
+ return render_template("profile.html", error=error, user=user)
+
+
+@app.route("/logout")
+def logout():
+ session.pop("logged_in", None)
+ session.pop("user", None)
+ flash("You were logged out")
+ return render_template("login.html", error=None)
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/static/favicon.ico b/code/static/favicon.ico
similarity index 100%
rename from static/favicon.ico
rename to code/static/favicon.ico
diff --git a/static/script.js b/code/static/script.js
similarity index 100%
rename from static/script.js
rename to code/static/script.js
diff --git a/static/style.css b/code/static/style.css
similarity index 100%
rename from static/style.css
rename to code/static/style.css
diff --git a/static/table.js b/code/static/table.js
similarity index 100%
rename from static/table.js
rename to code/static/table.js
diff --git a/templates/layout.html b/code/templates/layout.html
similarity index 100%
rename from templates/layout.html
rename to code/templates/layout.html
diff --git a/templates/list_shops.html b/code/templates/list_shops.html
similarity index 100%
rename from templates/list_shops.html
rename to code/templates/list_shops.html
diff --git a/templates/login.html b/code/templates/login.html
similarity index 100%
rename from templates/login.html
rename to code/templates/login.html
diff --git a/templates/profile.html b/code/templates/profile.html
similarity index 100%
rename from templates/profile.html
rename to code/templates/profile.html
diff --git a/templates/register.html b/code/templates/register.html
similarity index 100%
rename from templates/register.html
rename to code/templates/register.html
diff --git a/templates/show_shop.html b/code/templates/show_shop.html
similarity index 100%
rename from templates/show_shop.html
rename to code/templates/show_shop.html
diff --git a/debug.py b/debug.py
deleted file mode 100755
index 153eb17..0000000
--- a/debug.py
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/python
-from shop import app
-print('http://127.0.0.1:5000/')
-app.run(debug=True,host="0.0.0.0")
diff --git a/docker-compose.example b/docker-compose.example
new file mode 100644
index 0000000..ab0236f
--- /dev/null
+++ b/docker-compose.example
@@ -0,0 +1,15 @@
+version: "3.5"
+
+services:
+ app:
+ build:
+ context: code
+ ports:
+ - "127.0.0.1:8166:8166"
+ volumes:
+ - ./data/:/data/
+ environment:
+ - SECRET_KEY=gifodjgoifdjgoejr903
+ user: "1000:1000"
+ restart: unless-stopped
+
diff --git a/run.example b/run.example
deleted file mode 100755
index 67cb308..0000000
--- a/run.example
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-
-export SESSION_COOKIE_NAME=mdshop
-#export ENABLE_REGISTER=false
-exec gunicorn -b 127.0.0.1:8000 -w 2 shop:app
diff --git a/shop.py b/shop.py
deleted file mode 100644
index 4ac7c98..0000000
--- a/shop.py
+++ /dev/null
@@ -1,540 +0,0 @@
-# -*- coding: utf-8 -*-
-# all the imports
-import sqlite3, time, datetime, hashlib, os, re
-from shutil import copyfile, move
-from flask import Flask, request, session, g, redirect, url_for, \
- abort, render_template, flash
-from revprox import ReverseProxied
-
-# configuration
-DATABASE = 'shop.db'
-DATADIR = 'data'
-DEBUG = False
-SECRET_KEY = 'development key'
-USERNAME = 'admin'
-PASSWORD = 'default'
-URLFINDER = re.compile("((news|telnet|nttp|file|http|ftp|https)://[^ ]+)")
-URLPARSER = re.compile(r'(\[)([^\]]+)(\])\(([^\)]+)\)')
-BOLDFINDER = re.compile(r'\*([^\*]+)\*')
-CODEFINDER = re.compile(r'\`([^\`]+)\`')
-
-# create our little application :)
-app = Flask(__name__)
-app.config.from_object(__name__)
-app.config['SESSION_COOKIE_NAME'] = os.getenv('SESSION_COOKIE_NAME', 'mdshop')
-app.config['register'] = os.getenv('ENABLE_REGISTER', 'true') != 'false'
-print(app.config)
-app.wsgi_app = ReverseProxied(app.wsgi_app)
-
-def connect_db():
- if not os.path.exists(app.config['DATABASE']):
- db=sqlite3.connect(app.config['DATABASE'])
- for command in open('schema.sql','rt').read().split(';'):
- db.execute(command)
- db.commit()
- return sqlite3.connect(app.config['DATABASE'])
-
-def password_hash(s):
- return hashlib.sha224(s.encode('utf8')).hexdigest()
-
-def get_username(id):
- cur = g.db.execute('select * from users')
- for row in cur.fetchall():
- if id==row[0]:
- return row[1]
- return None
-
-def get_userid(name):
- cur = g.db.execute('select * from users')
- for row in cur.fetchall():
- if name==row[1]:
- return row[0]
- return None
-
-def get_shares(id):
- cur = g.db.execute('select * from shares')
- shares=[]
- for row in cur.fetchall():
- if id==row[1]:
- shares.append(row[0])
- return shares
-
-def get_shop_date(id):
- date=""
- cur = g.db.execute('select * from shops')
- for row in cur.fetchall():
- if id==row[0]:
- data_dir=os.path.join(DATADIR, get_username(row[2]))
- data_file=os.path.join(data_dir, row[1]+".md")
- if os.path.exists(data_file):
- date=datetime.datetime.fromtimestamp(
- os.path.getmtime(data_file)).strftime('%m/%d %H:%M')
- return date
-
-def get_shop_backup_date(id):
- date=""
- cur = g.db.execute('select * from shops')
- for row in cur.fetchall():
- if id==row[0]:
- data_dir=os.path.join(DATADIR, get_username(row[2]))
- data_file=os.path.join(data_dir, row[1]+".md.bkp")
- if os.path.exists(data_file):
- date=datetime.datetime.fromtimestamp(
- os.path.getmtime(data_file)).strftime('%m/%d %H:%M')
- return date
-
-def scan_for_new_documents(id):
- user=get_username(id)
- data_dir=os.path.join(DATADIR, user)
- if not os.path.exists(data_dir):
- return
- cur = g.db.execute('select * from shops')
- existing=[]
- non_existing=[]
- for row in cur.fetchall():
- if row[2]!=id:
- continue
- existing.append(row[1])
- for row in os.listdir(data_dir):
- if row.endswith(".md"):
- if row[:-3] not in existing:
- non_existing.append(row[:-3])
- for shop in non_existing:
- g.db.execute('insert into shops (shop,owner) values (?, ?)',
- [shop, id])
- g.db.commit()
-
-def markdown_parse(s):
- s = s.decode('utf8')
- s=BOLDFINDER.sub(r'*\1*',s)
- s=CODEFINDER.sub(r'`\1`',s)
- return s
-
-def urlify(s):
- if URLPARSER.search(s):
- return URLPARSER.sub(r'[\2]',s)
- return URLFINDER.sub(r'\1', s)
-
-@app.before_request
-def before_request():
- g.db = connect_db()
-@app.teardown_request
-def teardown_request(exception):
- db = getattr(g, 'db', None)
- if db is not None:
- db.close()
-
-@app.route('/shop/')
-def show_shop(shopid):
- if not session.get('logged_in'):
- return redirect(url_for('login',error=None))
- try:
- shopid=int(shopid)
- except ValueError:
- return redirect(url_for('login',error=None))
- has_access=False
- cur = g.db.execute('select * from shops')
- shared=get_shares(session.get('user'))
- for row in cur.fetchall():
- if row[0]==shopid:
- if row[2]==session.get('user') or row[0] in shared:
- has_access=True
- shopname=row[1]
- break
- if not has_access:
- return redirect(url_for('list_shops'))
- data_dir=os.path.join(DATADIR, get_username(row[2]))
- data_file=os.path.join(data_dir, row[1]+".md")
- if not os.path.exists(data_file):
- open(data_file, 'wt').close()
- entries=[]
- content=open(data_file, 'rt').read()#.decode('utf-8')
- for i,row in enumerate(open( data_file, 'rt').read().split("\n")):
- # any parsing magick would be here
- row=row.rstrip()
- if row=="":
- continue
- icon=" "
- extra_class="noitem"
- if "[ ]" in row:
- icon=u" "
- extra_class=""
- if "[x]" in row:
- icon=u"\u2714"
- extra_class=""
- row=urlify(row).encode('ascii', 'xmlcharrefreplace')
- row=markdown_parse(row)
- if row.startswith("#"):
- row=""+row+""
- if row.startswith(">"):
- row=""+row+""
- entries.append( dict(row=i, text=row, icon=icon, extra_class=extra_class) )
- shared_to=[]
- cur = g.db.execute('select * from shares')
- for row in cur.fetchall():
- if row[0]==shopid:
- shared_to.append(get_username(row[1]))
- # invalidate autosort in 60 minutes:
- if session.get('sort_update'):
- if time.time() - session.get('sort_update') > 3600:
- session['sort_view'] = False
- session['sort_update'] = time.time()
-
- return render_template(
- 'show_shop.html',
- entries = entries,
- shop = shopname,
- shopid = shopid,
- content = content,
- shares = shared_to,
- date = get_shop_date(shopid),
- date_bkp = get_shop_backup_date(shopid),
- autosort = session.get('sort_view', False)
- )
-
-@app.route('/')
-def list_shops():
- if not session.get('logged_in'):
- return redirect(url_for('login',error=None))
- scan_for_new_documents(session.get('user'))
- cur = g.db.execute('select * from shops order by shop')
- entries=[]
- for row in cur.fetchall():
- if session.get('user')==row[2]: # owner
- date=get_shop_date(row[0])
- entries.append( dict(shop=row[1], shopid=row[0], owner=get_username(row[2]),date=date) )
- cur = g.db.execute('select * from shops order by shop')
- shares=get_shares(session.get('user'))
- for row in cur.fetchall():
- if row[0] in shares: # Has been shared to
- date=get_shop_date(row[0])
- entries.append( dict(shop=row[1], shopid=row[0], owner=get_username(row[2]),date=date) )
-
- return render_template('list_shops.html', entries=entries)
-
-@app.route('/add', methods=['POST'])
-def add_items():
- if not session.get('logged_in'):
- abort(401)
- shopid=int(request.form['shopid'])
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- ownername=get_username(ownerid)
- data_dir=os.path.join(DATADIR, ownername)
- data_file=os.path.join(data_dir, shopname+".md")
- count=0
- contents_file=open(data_file,'at')
- for row in request.form['add_md'].split("\n"):
- if row.strip()=="":
- continue
- count+=1
- contents_file.write("[ ] %s\n"%row.strip())
- contents_file.close()
- flash('Added %d items'%(count))
- return redirect(url_for('show_shop',shopid=shopid))
-
-@app.route('/edit', methods=['POST'])
-def edit_md():
- if not session.get('logged_in'):
- abort(401)
- shopid=int(request.form['shopid'])
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- ownername=get_username(ownerid)
- data_dir=os.path.join(DATADIR, ownername)
- data_file=os.path.join(data_dir, shopname+".md")
- backup=data_file+".bkp"
- copyfile(data_file, backup)
- contents_file=open(data_file,'wt')
- contents_list=request.form['edit_md'].split("\n")
- while contents_list[-1].strip()=='':
- contents_list.pop()
- for row in contents_list:
- contents_file.write("%s\n"%(row,))
- contents_file.close()
- flash('Saved new file.')
- return redirect(url_for('show_shop',shopid=shopid))
-
-@app.route('/restore', methods=['POST'])
-def restore_md():
- if not session.get('logged_in'):
- abort(401)
- shopid=int(request.form['shopid'])
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- ownername=get_username(ownerid)
- data_dir=os.path.join(DATADIR, ownername)
- data_file=os.path.join(data_dir, shopname+".md")
- backup=data_file+".bkp"
- backup_tmp=data_file+".tmp"
- if not os.path.exists(backup):
- flash('Backup does not exist')
- return redirect(url_for('show_shop',shopid=shopid))
- copyfile(data_file,backup_tmp)
- copyfile(backup, data_file)
- move(backup_tmp, backup)
- flash('Backup restored')
- return redirect(url_for('show_shop',shopid=shopid))
-
-@app.route('/toggle', methods=['POST'])
-def toggle_item():
- if not session.get('logged_in'):
- abort(401)
- shopid=int(request.form['shopid'])
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- ownername=get_username(ownerid)
- req_row=None
- for key in request.form:
- if key.startswith('item'):
- req_row=int(key[4:])
- if key=='toggleAll':
- # Special meaning: toggle all rows
- req_row=-1
- if key=='unTickAll':
- # Special meaning: untick all rows
- req_row=-2
- if req_row==None:
- return redirect(url_for('show_shop',shopid=shopid))
- data_dir=os.path.join(DATADIR, ownername)
- data_file=os.path.join(data_dir, shopname+".md")
- backup=data_file+".bkp"
- contents_file=open(data_file,'rt')
- contents=contents_file.read().split("\n")
- contents_file.close()
- changed=False
- for i,row in enumerate(contents):
- if i==req_row or req_row<0:
- if req_row!=-2: # no ticking if unticking all
- if '[ ]' in row:
- contents[i]=row.replace('[ ]','[x]')
- if '[x]' in row:
- contents[i]=row.replace('[x]','[ ]')
- if row!=contents[i]:
- changed=True
- if changed:
- if req_row==-1 or req_row==-2:
- copyfile(data_file, backup)
- contents_file=open(data_file,'wt')
- contents_file.write("\n".join(contents))
- contents_file.close()
- return redirect(url_for('show_shop',shopid=shopid))
-
-@app.route('/remove_toggled', methods=['POST'])
-def remove_toggled():
- if not session.get('logged_in'):
- abort(401)
- shopid=int(request.form['shopid'])
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- ownername=get_username(ownerid)
- data_dir=os.path.join(DATADIR, ownername)
- data_file=os.path.join(data_dir, shopname+".md")
- backup=data_file+".bkp"
- contents_file=open(data_file,'rt')
- contents=[]
- changed=False
- for i,row in enumerate(contents_file.read().split("\n")):
- if '[x]' not in row:
- contents.append(row)
- else:
- changed=True
- contents_file.close()
- if changed:
- copyfile(data_file, backup)
- contents_file=open(data_file,'wt')
- contents_file.write("\n".join(contents))
- contents_file.close()
- #~ flash('successfully posted %s (%d)'%(row,req_row))
- return redirect(url_for('show_shop',shopid=shopid))
-
-@app.route('/add_shop', methods=['POST'])
-def add_shop():
- if not session.get('logged_in'):
- abort(401)
- import re, string
- pattern = re.compile('[\W]+')
- shopname=pattern.sub('', request.form['shop'])
- if shopname=="":
- flash('Shop name empty!')
- return redirect(url_for('list_shops'))
- cur = g.db.execute('select * from shops order by shop')
- for row in cur.fetchall():
- if shopname==row[1]:
- flash('Shop already exists! '+shopname)
- return redirect(url_for('list_shops'))
- g.db.execute('insert into shops (shop,owner) values (?, ?)',
- [shopname, session['user']])
- g.db.commit()
- new_dir=os.path.join(DATADIR, get_username(session['user']))
- new_file=os.path.join(new_dir, shopname+".md")
- if not os.path.exists(new_dir):
- os.mkdir(new_dir)
- open( new_file, 'at')
- flash('successfully created new shop: '+shopname)
- return redirect(url_for('list_shops'))
-
-@app.route('/add_share', methods=['POST'])
-def add_share():
- if not session.get('logged_in'):
- abort(401)
- import re, string
- pattern = re.compile('[\W]+')
- username=pattern.sub('', request.form['share'])
- shopid=pattern.sub('', request.form['shopid'])
- if username=="":
- flash('User name empty!')
- return redirect(url_for('show_shop',shopid=shopid))
- userid=get_userid(username)
- if userid==None:
- flash('No such user!')
- return redirect(url_for('show_shop',shopid=shopid))
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- if session.get('user')!=ownerid:
- flash('Not your shop!')
- return redirect(url_for('show_shop',shopid=shopid))
- g.db.execute('insert into shares (shopid,userid) values (?, ?)',
- [shopid, userid])
- g.db.commit()
- flash('Shared to %s'%(username))
- return redirect(url_for('show_shop',shopid=shopid))
-
-@app.route('/remove_share', methods=['POST'])
-def remove_share():
- if not session.get('logged_in'):
- abort(401)
- import re, string
- pattern = re.compile('[\W]+')
- username=pattern.sub('', request.form['user'])
- shopid=pattern.sub('', request.form['shopid'])
- if username=="":
- flash('User name empty!')
- return redirect(url_for('show_shop',shopid=shopid))
- userid=get_userid(username)
- if userid==None:
- flash('No such user!')
- return redirect(url_for('show_shop',shopid=shopid))
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- if session.get('user')!=ownerid:
- flash('Not your shop!')
- return redirect(url_for('show_shop',shopid=shopid))
-
- g.db.execute('delete from shares where shopid=? and userid=?',
- [shopid, userid])
- g.db.commit()
- return redirect(url_for('show_shop',shopid=shopid))
-
-@app.route('/remove_shop', methods=['POST'])
-def remove_shop():
- if not session.get('logged_in'):
- abort(401)
- shopid=int(request.form['shopid'])
- ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0]
- ownername=get_username(ownerid)
- data_dir=os.path.join(DATADIR, ownername)
- data_file=os.path.join(data_dir, shopname+".md")
- if session.get('user')!=ownerid:
- flash('Not your shop!')
- return redirect(url_for('show_shop',shopid=shopid))
- # remove shop DB
- g.db.execute('delete from shops where id=?',
- [shopid])
- g.db.commit()
- # backup data, and remove
- backup=data_file+".bkp"
- copyfile(data_file, backup)
- os.remove(data_file)
- # remove shares
- g.db.execute('delete from shares where shopid=?',
- [shopid])
- g.db.commit()
- flash('successfully deleted shop %s'%(shopname))
- return redirect(url_for('list_shops'))
-
-@app.route('/sort_flip',methods=['POST'])
-def sort_flip():
- if not session.get('sort_view'):
- session['sort_view'] = True
- session['sort_update'] = time.time()
- else:
- session['sort_view'] = False
- session['sort_update'] = time.time()
- shopid = int(request.form['shopid'])
- return redirect(
- url_for('show_shop', shopid = shopid)
- )
-
-@app.route('/login', methods=['GET', 'POST'])
-def login():
- error = None
- if request.method == 'POST':
- cur = g.db.execute('select * from users')
- for row in cur.fetchall():
- if request.form['username'] == row[1]:
- if password_hash(request.form['password']) == row[2]:
- session['logged_in'] = True
- session['user'] = row[0]
- # scan_for_new_documents(row[0])
- return redirect(url_for('list_shops'))
- error='Invalid user/pass'
- return render_template('login.html', error=error)
-
-@app.route('/register', methods=['GET', 'POST'])
-def register():
- error = None
- if not app.config['register']:
- return ""
- if request.method == 'POST':
- import re, string
- pattern = re.compile('[\W]+')
- username=pattern.sub('', request.form['username'])
- password=password_hash(request.form['password'])
- if len(username)==0:
- error="No username given"
- return render_template('register.html', error=error)
- if len(request.form['password'])<5:
- error="Password too short"
- return render_template('register.html', error=error)
- cur = g.db.execute('select * from users')
- for row in cur.fetchall():
- if username == row[1]:
- error="Username already exists"
- return render_template('register.html', error=error)
- g.db.execute('insert into users (user,pass) values (?, ?)',
- [username,password])
- g.db.commit()
- flash('successfully registered user "%s". Now login.'%username)
- return redirect(url_for('login'))
- return render_template('register.html', error=error)
-
-@app.route('/profile', methods=['GET', 'POST'])
-def profile():
- if not session.get('logged_in'):
- return redirect(url_for('login'))
- error = None
- user=get_username(session.get('user'))
- if request.method == 'POST':
- import re, string
- pattern = re.compile('[\W]+')
- password=password_hash(request.form['password'])
- if len(request.form['password'])<5:
- error="Password too short"
- return render_template('profile.html', error=error,user=user)
- g.db.execute('update users set pass=? where id=?',
- [password,session.get('user')])
- g.db.commit()
- flash('successfully updated profile.')
- return redirect(url_for('profile'))
- return render_template('profile.html', error=error,user=user)
-
-@app.route('/logout')
-def logout():
- session.pop('logged_in', None)
- session.pop('user', None)
- flash('You were logged out')
- return render_template('login.html', error=None)
-
-
-if __name__ == '__main__':
- app.run()