dockerized structure
This commit is contained in:
10
code/Dockerfile
Normal file
10
code/Dockerfile
Normal file
@@ -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
|
||||
2
code/requirements.txt
Normal file
2
code/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
gunicorn
|
||||
flask
|
||||
32
code/revprox.py
Normal file
32
code/revprox.py
Normal file
@@ -0,0 +1,32 @@
|
||||
class ReverseProxied(object):
|
||||
'''Wrap the application in this middleware and configure the
|
||||
front-end server to add these headers, to let you quietly bind
|
||||
this to a URL other than / and to an HTTP scheme that is
|
||||
different than what is used locally.
|
||||
|
||||
In nginx:
|
||||
location /myprefix {
|
||||
proxy_pass http://192.168.0.1:5001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_set_header X-Script-Name /myprefix;
|
||||
}
|
||||
|
||||
:param app: the WSGI application
|
||||
'''
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
||||
if script_name:
|
||||
environ['SCRIPT_NAME'] = script_name
|
||||
path_info = environ['PATH_INFO']
|
||||
if path_info.startswith(script_name):
|
||||
environ['PATH_INFO'] = path_info[len(script_name):]
|
||||
|
||||
scheme = environ.get('HTTP_X_SCHEME', '')
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
return self.app(environ, start_response)
|
||||
19
code/schema.sql
Normal file
19
code/schema.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
drop table if exists users;
|
||||
drop table if exists shops;
|
||||
drop table if exists shares;
|
||||
create table users (
|
||||
id integer primary key autoincrement,
|
||||
user text not null,
|
||||
pass text not null
|
||||
);
|
||||
create table shops (
|
||||
id integer primary key autoincrement,
|
||||
shop text not null,
|
||||
owner integer not null
|
||||
);
|
||||
create table shares (
|
||||
shopid integer not null,
|
||||
userid integer not null
|
||||
);
|
||||
|
||||
|
||||
613
code/shop.py
Normal file
613
code/shop.py
Normal file
@@ -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'*<span class="md_bold">\1</span>*', s)
|
||||
s = CODEFINDER.sub(r'`<span class="md_code">\1</span>`', s)
|
||||
return s
|
||||
|
||||
|
||||
def urlify(s):
|
||||
if URLPARSER.search(s):
|
||||
return URLPARSER.sub(r'[<a href="\4" target="_blank">\2</a>]', s)
|
||||
return URLFINDER.sub(r'<a href="\1" target="_blank">\1</a>', 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/<shopid>")
|
||||
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 = "<span class=md_head>" + row + "</span>"
|
||||
if row.startswith(">"):
|
||||
row = "<span class=md_quote>" + row + "</span>"
|
||||
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()
|
||||
BIN
code/static/favicon.ico
Normal file
BIN
code/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
152
code/static/script.js
Normal file
152
code/static/script.js
Normal file
@@ -0,0 +1,152 @@
|
||||
Array.prototype.stableSort = function(cmp) {
|
||||
cmp = !!cmp ? cmp : (a, b) => {
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
};
|
||||
let stabilizedThis = this.map((el, index) => [el, index]);
|
||||
let stableCmp = (a, b) => {
|
||||
let order = cmp(a[0], b[0]);
|
||||
if (order != 0) return order;
|
||||
return a[1] - b[1];
|
||||
}
|
||||
stabilizedThis.sort(stableCmp);
|
||||
for (let i=0; i<this.length; i++) {
|
||||
this[i] = stabilizedThis[i][0];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
function insertAtEnd(myField, myValue) {
|
||||
myField.value += myValue;
|
||||
}
|
||||
function insertDate(idName) {
|
||||
insertAtEnd(document.getElementById(idName),"\n"+getDate()+" ");
|
||||
growTextarea(idName)
|
||||
}
|
||||
|
||||
function getDate(){
|
||||
var d = new Date();
|
||||
var hr = d.getHours();
|
||||
var min = d.getMinutes();
|
||||
if (min < 10) { min = "0" + min; }
|
||||
var day = d.getDate();
|
||||
var month = 1+d.getMonth();
|
||||
if (month < 10) { month = "0" + month; }
|
||||
var year = d.getFullYear().toString().substr(-2);
|
||||
return year+"-"+month+"-"+day+" "+hr+":"+min
|
||||
}
|
||||
|
||||
|
||||
function growTextarea(name) {
|
||||
var el=document.getElementById(name);
|
||||
var rows=el.value.split(/\r?\n|\r/);
|
||||
el.rows=rows.length+1;
|
||||
var cols=40;
|
||||
for (var i = 0; i<rows.length; i++) {
|
||||
cols=Math.max(cols, rows[i].length);
|
||||
}
|
||||
el.cols=cols;
|
||||
}
|
||||
|
||||
function reload() {
|
||||
location.href=window.location.href;
|
||||
}
|
||||
|
||||
function hidetoggle(name) {
|
||||
if (document.getElementById(name).style.display=='inline-block') {
|
||||
document.getElementById(name).style.display='none';
|
||||
} else {
|
||||
document.getElementById(name).style.display='inline-block';
|
||||
document.getElementById(name).scrollIntoView();
|
||||
if (name == 'disp_add') {
|
||||
document.getElementById('add_md').focus();
|
||||
}
|
||||
if (name == 'disp_edit') {
|
||||
document.getElementById('edit_md').focus();
|
||||
}
|
||||
}
|
||||
hideOthers(name);
|
||||
}
|
||||
|
||||
function hideOthers(name) {
|
||||
var allElements = document.getElementsByTagName("*");
|
||||
for (var i = 0, n = allElements.length; i < n; ++i) {
|
||||
var el = allElements[i];
|
||||
if (el.id) {
|
||||
if ((el.id.startsWith("disp_")) && (el.id!=name)) {
|
||||
document.getElementById(el.id).style.display='none';
|
||||
}}
|
||||
}
|
||||
}
|
||||
function dropDown(name) {
|
||||
dropDownHide();
|
||||
document.getElementById(name).classList.toggle("show");
|
||||
hideOthers('foo');
|
||||
}
|
||||
function dropDownHide() {
|
||||
var dropdowns = document.getElementsByClassName("dropdown-content");
|
||||
var i;
|
||||
for (i = 0; i < dropdowns.length; i++) {
|
||||
var openDropdown = dropdowns[i];
|
||||
if (openDropdown.classList.contains('show')) {
|
||||
openDropdown.classList.remove('show');
|
||||
}
|
||||
}
|
||||
}
|
||||
function get_cookie(name) {
|
||||
var nameEQ = name+"=";
|
||||
var ca = document.cookie.split(';');
|
||||
for(var i=0;i < ca.length;i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
||||
if (c.indexOf(nameEQ) == 0) { return c.substring(nameEQ.length,c.length); }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function sortMarkdown(boughtOrder=false) {
|
||||
var divs = document.getElementsByClassName("entry");
|
||||
divs = Array.prototype.slice.call(divs, 0);
|
||||
if (boughtOrder) {
|
||||
divs.stableSort(orderStringSorter);
|
||||
} else {
|
||||
divs.stableSort(fullStringSorter);
|
||||
}
|
||||
var parent = document.getElementById('entry_loop');
|
||||
parent.innerHTML = "";
|
||||
for(var i = 0, l = divs.length; i < l; i++) {
|
||||
parent.appendChild(divs[i]);
|
||||
}
|
||||
}
|
||||
function fullStringSorter(a,b) {
|
||||
var aValue=a.children[1].innerHTML;
|
||||
var bValue=b.children[1].innerHTML;
|
||||
if (aValue < bValue) return -1;
|
||||
if (aValue > bValue) return 1;
|
||||
return 0;
|
||||
}
|
||||
function orderStringSorter(a,b) {
|
||||
var aValue=a.children[1].innerHTML.substring(0,3);
|
||||
var bValue=b.children[1].innerHTML.substring(0,3);
|
||||
if ( aValue == "[x]" && bValue == "[ ]" ) return 1;
|
||||
if ( aValue == "[ ]" && bValue == "[x]" ) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function scrollBack() {
|
||||
var position=parseInt(get_cookie("position"));
|
||||
window.scrollTo(0,position);
|
||||
}
|
||||
|
||||
// Close the dropdown menu if the user clicks outside of it
|
||||
window.onclick = function(event) {
|
||||
if (!event.target.matches('.dropbtn')) {
|
||||
dropDownHide();
|
||||
}
|
||||
}
|
||||
document.onscroll = function(event) {
|
||||
document.cookie = "position=" + window.scrollY + "; path=" + window.location.pathname;
|
||||
}
|
||||
window.onload = scrollBack;
|
||||
|
||||
97
code/static/style.css
Normal file
97
code/static/style.css
Normal file
@@ -0,0 +1,97 @@
|
||||
body { font-family: monospace; background: #fff; }
|
||||
a, h1, h2 { color: #377ba8; }
|
||||
h1, h2 { margin: 0; }
|
||||
h1 { border-bottom: 2px solid #eee; }
|
||||
h2 { font-size: 1.2em; border-top: 0px solid #eee; margin-top: 0.25em; margin-bottom: 0.25em;}
|
||||
td { height: 1.75em; }
|
||||
textarea { border: 2px solid #ccc; }
|
||||
textarea:active, textarea:focus { border: 2px solid #377ba8; }
|
||||
input[type=text]{ border: 2px solid #ccc; }
|
||||
hr { border-color: #377ba8; }
|
||||
|
||||
.submit { font-family: monospace; }
|
||||
.tickbox { margin-right: 0.5em; width:2em; height:1.8em; }
|
||||
.iconbox { margin-right: 0.5em; width: 1.5em; cursor: pointer;
|
||||
font-size:1.5em; color:#377ba8; }
|
||||
.noitem { background-color: transparent; border-color:transparent; }
|
||||
.page { border: 0px solid #ccc; padding: 0.5em;
|
||||
background: white; overflow:auto; min-height:16em;}
|
||||
.entries { margin-top: 1em; margin-bottom: 1em; }
|
||||
.entries input { margin-top: 0.125em; margin-bottom: 0.125em; }
|
||||
.entries li { margin: 0.8em 1.2em; }
|
||||
.entries h2 { margin-left: -1em; }
|
||||
.add-entry { font-size: 0.9em; }
|
||||
.add-entry dl { font-weight: bold; }
|
||||
.metanav { text-align: left; font-size: 1.2em;
|
||||
background: #ffffff; font-weight: normal; }
|
||||
.button-panel-row { margin-top: 1em; }
|
||||
.button-panel-col { float: left; display: block; margin-right: 1em;
|
||||
min-width: 23ex;}
|
||||
.flash { background: #cee5F5; padding: 0.5em;
|
||||
border: 1px solid #aacbe2; }
|
||||
.error { background: #f0d6d6; padding: 0.5em; }
|
||||
.hidden { display: none; }
|
||||
.pointer { cursor: pointer; text-decoration: underline; }
|
||||
.hspacer { width: 1em; display: inline-block;}
|
||||
.notify { -webkit-animation: notifier 500ms alternate infinite;
|
||||
-moz-animation: notifier 500ms alternate infinite;
|
||||
-o-animation: notifier 500ms alternate infinite;
|
||||
animation: notifier 500ms alternate infinite; }
|
||||
.date { font-size: 0.75em; margin-top: 1.2em; }
|
||||
.fixed_topright { position: fixed; top: 0.5em; right: -0.5em; }
|
||||
|
||||
/* MD rendering */
|
||||
|
||||
.md_head { color: #377ba8; font-weight: bold; }
|
||||
.md_quote { font-style: italic; }
|
||||
.md_bold { font-weight: bold; }
|
||||
.md_code { color: #a83737; }
|
||||
|
||||
/* dropdown */
|
||||
|
||||
.dropbtn {
|
||||
color: #377ba8;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
letter-spacing:-0.1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Dropdown button on hover & focus */
|
||||
.dropbtn:hover, .dropbtn:focus {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
/* The container <div> - needed to position the dropdown content */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Dropdown Content (Hidden by Default) */
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
/*right: 0px;*/
|
||||
position: absolute;
|
||||
background-color: #fafafa;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Links inside the dropdown */
|
||||
.dropdown-content a {
|
||||
padding: 8px 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Change color of dropdown links on hover */
|
||||
.dropdown-content a:hover {background-color: #f1f1f1}
|
||||
|
||||
/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
|
||||
.show {display:block;}
|
||||
|
||||
@keyframes notifier {
|
||||
0% { box-shadow: 0px 0px 4px 4px #fff; }
|
||||
100% { box-shadow: 0px 0px 4px 4px #cee5F5; }
|
||||
}
|
||||
320
code/static/table.js
Executable file
320
code/static/table.js
Executable file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
Table sorting script by Joost de Valk, check it out at http://www.joostdevalk.nl/code/sortable-table/.
|
||||
Based on a script from http://www.kryogenix.org/code/browser/sorttable/.
|
||||
Distributed under the MIT license: http://www.kryogenix.org/code/browser/licence.html .
|
||||
Edited, removed image support and a tags from headers, Ville Rantanen 2015
|
||||
|
||||
Copyright (c) 1997-2007 Stuart Langridge, Joost de Valk.
|
||||
|
||||
Version 1.5.7
|
||||
*/
|
||||
|
||||
/* You can change these values */
|
||||
var europeandate = true;
|
||||
var alternate_row_colors = false;
|
||||
|
||||
/* Don't change anything below this unless you know what you're doing */
|
||||
addEvent(window, "load", sortables_init);
|
||||
|
||||
var SORT_COLUMN_INDEX;
|
||||
var thead = false;
|
||||
|
||||
function sortables_init() {
|
||||
// Find all tables with class sortable and make them sortable
|
||||
if (!document.getElementsByTagName) return;
|
||||
tbls = document.getElementsByTagName("table");
|
||||
for (ti=0;ti<tbls.length;ti++) {
|
||||
thisTbl = tbls[ti];
|
||||
if (((' '+thisTbl.className+' ').indexOf("sortable") != -1) && (thisTbl.id)) {
|
||||
ts_makeSortable(thisTbl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ts_makeSortable(t) {
|
||||
if (t.rows && t.rows.length > 0) {
|
||||
if (t.tHead && t.tHead.rows.length > 0) {
|
||||
var firstRow = t.tHead.rows[t.tHead.rows.length-1];
|
||||
thead = true;
|
||||
} else {
|
||||
var firstRow = t.rows[0];
|
||||
}
|
||||
}
|
||||
if (!firstRow) return;
|
||||
|
||||
// We have a first row: assume it's the header, and make its contents clickable links
|
||||
for (var i=0;i<firstRow.cells.length;i++) {
|
||||
var cell = firstRow.cells[i];
|
||||
var txt = ts_getInnerText(cell);
|
||||
if (cell.className != "unsortable" && cell.className.indexOf("unsortable") == -1) {
|
||||
cell.innerHTML = '<span class="sortheader" onclick="ts_resortTable(this, '+i+');return false;">'+txt+'<span class="sortarrow"> ↕</span></span>';
|
||||
}
|
||||
}
|
||||
if (alternate_row_colors) {
|
||||
alternate(t);
|
||||
}
|
||||
}
|
||||
|
||||
function ts_getInnerText(el) {
|
||||
if (typeof el == "string") return el;
|
||||
if (typeof el == "undefined") { return el };
|
||||
if (el.innerText) return el.innerText; //Not needed but it is faster
|
||||
var str = "";
|
||||
|
||||
var cs = el.childNodes;
|
||||
var l = cs.length;
|
||||
for (var i = 0; i < l; i++) {
|
||||
switch (cs[i].nodeType) {
|
||||
case 1: //ELEMENT_NODE
|
||||
str += ts_getInnerText(cs[i]);
|
||||
break;
|
||||
case 3: //TEXT_NODE
|
||||
str += cs[i].nodeValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function ts_resortTable(lnk, clid) {
|
||||
var span;
|
||||
for (var ci=0;ci<lnk.childNodes.length;ci++) {
|
||||
if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
|
||||
}
|
||||
var spantext = ts_getInnerText(span);
|
||||
var td = lnk.parentNode;
|
||||
var column = clid || td.cellIndex;
|
||||
var t = getParent(td,'TABLE');
|
||||
// Work out a type for the column
|
||||
if (t.rows.length <= 1) return;
|
||||
var itm = "";
|
||||
var i = 0;
|
||||
while (itm == "" && i < t.tBodies[0].rows.length) {
|
||||
var itm = ts_getInnerText(t.tBodies[0].rows[i].cells[column]);
|
||||
itm = trim(itm);
|
||||
if (itm.substr(0,4) == "<!--" || itm.length == 0) {
|
||||
itm = "";
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (itm == "") return;
|
||||
sortfn = ts_sort_caseinsensitive;
|
||||
if (itm.match(/^\d\d[\/\.-][a-zA-z][a-zA-Z][a-zA-Z][\/\.-]\d\d\d\d$/)) sortfn = ts_sort_date;
|
||||
if (itm.match(/^\d\d[\/\.-]\d\d[\/\.-]\d\d\d{2}?$/)) sortfn = ts_sort_date;
|
||||
if (itm.match(/^-?[£$Û¢´]\d/)) sortfn = ts_sort_numeric;
|
||||
if (itm.match(/^-?(\d+[,\.]?)+(E[-+][\d]+)?%?$/)) sortfn = ts_sort_numeric;
|
||||
SORT_COLUMN_INDEX = column;
|
||||
var firstRow = new Array();
|
||||
var newRows = new Array();
|
||||
for (k=0;k<t.tBodies.length;k++) {
|
||||
for (i=0;i<t.tBodies[k].rows[0].length;i++) {
|
||||
firstRow[i] = t.tBodies[k].rows[0][i];
|
||||
}
|
||||
}
|
||||
for (k=0;k<t.tBodies.length;k++) {
|
||||
if (!thead) {
|
||||
// Skip the first row
|
||||
for (j=1;j<t.tBodies[k].rows.length;j++) {
|
||||
newRows[j-1] = t.tBodies[k].rows[j];
|
||||
}
|
||||
} else {
|
||||
// Do NOT skip the first row
|
||||
for (j=0;j<t.tBodies[k].rows.length;j++) {
|
||||
newRows[j] = t.tBodies[k].rows[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
newRows.sort(sortfn);
|
||||
if (span.getAttribute("sortdir") == 'down') {
|
||||
ARROW = ' ↑';
|
||||
newRows.reverse();
|
||||
span.setAttribute('sortdir','up');
|
||||
} else {
|
||||
ARROW = ' ↓';
|
||||
span.setAttribute('sortdir','down');
|
||||
}
|
||||
// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
|
||||
// don't do sortbottom rows
|
||||
for (i=0; i<newRows.length; i++) {
|
||||
if (!newRows[i].className || (newRows[i].className && (newRows[i].className.indexOf('sortbottom') == -1))) {
|
||||
t.tBodies[0].appendChild(newRows[i]);
|
||||
}
|
||||
}
|
||||
// do sortbottom rows only
|
||||
for (i=0; i<newRows.length; i++) {
|
||||
if (newRows[i].className && (newRows[i].className.indexOf('sortbottom') != -1))
|
||||
t.tBodies[0].appendChild(newRows[i]);
|
||||
}
|
||||
// Delete any other arrows there may be showing
|
||||
var allspans = document.getElementsByTagName("span");
|
||||
for (var ci=0;ci<allspans.length;ci++) {
|
||||
if (allspans[ci].className == 'sortarrow') {
|
||||
if (getParent(allspans[ci],"table") == getParent(lnk,"table")) { // in the same table as us?
|
||||
allspans[ci].innerHTML = ' ↕';
|
||||
}
|
||||
}
|
||||
}
|
||||
span.innerHTML = ARROW;
|
||||
alternate(t);
|
||||
}
|
||||
|
||||
function getParent(el, pTagName) {
|
||||
if (el == null) {
|
||||
return null;
|
||||
} else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase()) {
|
||||
return el;
|
||||
} else {
|
||||
return getParent(el.parentNode, pTagName);
|
||||
}
|
||||
}
|
||||
|
||||
function sort_date(date) {
|
||||
// y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
|
||||
dt = "00000000";
|
||||
if (date.length == 11) {
|
||||
mtstr = date.substr(3,3);
|
||||
mtstr = mtstr.toLowerCase();
|
||||
switch(mtstr) {
|
||||
case "jan": var mt = "01"; break;
|
||||
case "feb": var mt = "02"; break;
|
||||
case "mar": var mt = "03"; break;
|
||||
case "apr": var mt = "04"; break;
|
||||
case "may": var mt = "05"; break;
|
||||
case "jun": var mt = "06"; break;
|
||||
case "jul": var mt = "07"; break;
|
||||
case "aug": var mt = "08"; break;
|
||||
case "sep": var mt = "09"; break;
|
||||
case "oct": var mt = "10"; break;
|
||||
case "nov": var mt = "11"; break;
|
||||
case "dec": var mt = "12"; break;
|
||||
// default: var mt = "00";
|
||||
}
|
||||
dt = date.substr(7,4)+mt+date.substr(0,2);
|
||||
return dt;
|
||||
} else if (date.length == 10) {
|
||||
if (europeandate == false) {
|
||||
dt = date.substr(6,4)+date.substr(0,2)+date.substr(3,2);
|
||||
return dt;
|
||||
} else {
|
||||
dt = date.substr(6,4)+date.substr(3,2)+date.substr(0,2);
|
||||
return dt;
|
||||
}
|
||||
} else if (date.length == 8) {
|
||||
yr = date.substr(6,2);
|
||||
if (parseInt(yr) < 50) {
|
||||
yr = '20'+yr;
|
||||
} else {
|
||||
yr = '19'+yr;
|
||||
}
|
||||
if (europeandate == true) {
|
||||
dt = yr+date.substr(3,2)+date.substr(0,2);
|
||||
return dt;
|
||||
} else {
|
||||
dt = yr+date.substr(0,2)+date.substr(3,2);
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
|
||||
function ts_sort_date(a,b) {
|
||||
dt1 = sort_date(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
|
||||
dt2 = sort_date(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
|
||||
|
||||
if (dt1==dt2) {
|
||||
return 0;
|
||||
}
|
||||
if (dt1<dt2) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function ts_sort_numeric(a,b) {
|
||||
var aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
|
||||
aa = clean_num(aa);
|
||||
var bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
|
||||
bb = clean_num(bb);
|
||||
return compare_numeric(aa,bb);
|
||||
}
|
||||
function compare_numeric(a,b) {
|
||||
var a = parseFloat(a);
|
||||
a = (isNaN(a) ? 0 : a);
|
||||
var b = parseFloat(b);
|
||||
b = (isNaN(b) ? 0 : b);
|
||||
return a - b;
|
||||
}
|
||||
function ts_sort_caseinsensitive(a,b) {
|
||||
aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
|
||||
bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
|
||||
if (aa==bb) {
|
||||
return 0;
|
||||
}
|
||||
if (aa<bb) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function ts_sort_default(a,b) {
|
||||
aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
|
||||
bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
|
||||
if (aa==bb) {
|
||||
return 0;
|
||||
}
|
||||
if (aa<bb) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function addEvent(elm, evType, fn, useCapture)
|
||||
// addEvent and removeEvent
|
||||
// cross-browser event handling for IE5+, NS6 and Mozilla
|
||||
// By Scott Andrew
|
||||
{
|
||||
if (elm.addEventListener){
|
||||
elm.addEventListener(evType, fn, useCapture);
|
||||
return true;
|
||||
} else if (elm.attachEvent){
|
||||
var r = elm.attachEvent("on"+evType, fn);
|
||||
return r;
|
||||
} else {
|
||||
alert("Handler could not be removed");
|
||||
}
|
||||
}
|
||||
function clean_num(str) {
|
||||
str = str.replace(new RegExp(/[^-?0-9.]/g),"");
|
||||
return str;
|
||||
}
|
||||
function trim(s) {
|
||||
return s.replace(/^\s+|\s+$/g, "");
|
||||
}
|
||||
function alternate(table) {
|
||||
// Take object table and get all it's tbodies.
|
||||
var tableBodies = table.getElementsByTagName("tbody");
|
||||
// Loop through these tbodies
|
||||
for (var i = 0; i < tableBodies.length; i++) {
|
||||
// Take the tbody, and get all it's rows
|
||||
var tableRows = tableBodies[i].getElementsByTagName("tr");
|
||||
// Loop through these rows
|
||||
// Start at 1 because we want to leave the heading row untouched
|
||||
for (var j = 0; j < tableRows.length; j++) {
|
||||
// Check if j is even, and apply classes for both possible results
|
||||
if ( (j % 2) == 0 ) {
|
||||
if ( !(tableRows[j].className.indexOf('odd') == -1) ) {
|
||||
tableRows[j].className = tableRows[j].className.replace('odd', 'even');
|
||||
} else {
|
||||
if ( tableRows[j].className.indexOf('even') == -1 ) {
|
||||
tableRows[j].className += " even";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ( !(tableRows[j].className.indexOf('even') == -1) ) {
|
||||
tableRows[j].className = tableRows[j].className.replace('even', 'odd');
|
||||
} else {
|
||||
if ( tableRows[j].className.indexOf('odd') == -1 ) {
|
||||
tableRows[j].className += " odd";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
code/templates/layout.html
Normal file
52
code/templates/layout.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8/>
|
||||
<title>MDShop</title>
|
||||
<script language="javascript" src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
<script language="javascript" src="{{ url_for('static', filename='table.js') }}"></script>
|
||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<meta name="viewport" content="width=device-width, height=device-height, user-scalable=yes" />
|
||||
</head>
|
||||
<body>
|
||||
<div class=page>
|
||||
<div class=metanav>
|
||||
<a href="{{ url_for('list_shops') }}" id="shopsList">MDShops</a>
|
||||
<span class="hspacer"></span>
|
||||
<div class="dropdown">
|
||||
<p onclick="dropDown('userAct')" class="dropbtn">⊆⊇</p>
|
||||
<div id="userAct" class="dropdown-content">
|
||||
<a href="#" onclick="reload();">↻ Reload</a>
|
||||
<a href="{{ url_for('profile') }}">♙ Profile</a>
|
||||
<a href="{{ url_for('logout') }}">➲ Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if shop %}
|
||||
<span class="hspacer"></span>
|
||||
<div class="dropdown">
|
||||
<p onclick="dropDown('shopAct')" class="dropbtn">☝☔</p>
|
||||
<div id="shopAct" class="dropdown-content">
|
||||
<a id="h2_add" onclick="hidetoggle('disp_add')" class=pointer>± Add/remove</a>
|
||||
<a id="h2_sort" onclick="sortMarkdown()" class=pointer>↕ Sort once</a>
|
||||
<a id="h2_edit" onclick="hidetoggle('disp_edit');growTextarea('edit_md');" class=pointer>✎ Edit items</a>
|
||||
<a id="h2_share" onclick="hidetoggle('disp_share')" class=pointer>♟ Share shop</a>
|
||||
<a id="h2_restore" onclick="hidetoggle('disp_restore')" class=pointer>♻ Restore backup</a>
|
||||
<a id="h2_delete" onclick="hidetoggle('disp_delete')" class=pointer>⊗ Delete shop</a>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hspacer"></span>
|
||||
<span class="iconbox fixed_topright" onclick="hidetoggle('disp_add')" title="Add/remove">±</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class=flash>{{ message }}</div>
|
||||
{% endfor %}
|
||||
<hr/>
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
17
code/templates/list_shops.html
Normal file
17
code/templates/list_shops.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<table class="sortable" id="shoplist">
|
||||
<tr><th>Shop</th><th>Last edited</th><th>Owner</th></tr>
|
||||
{% for entry in entries %}
|
||||
<tr><td><a href="{{ url_for('show_shop',shopid=entry.shopid) }}">{{ entry.shop }}</a></td><td>{{ entry.date }}</td><td>{{ entry.owner }}</td>
|
||||
{% else %}
|
||||
<tr><td><em>Unbelievable. You dont have any shops</em>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
<form action="{{ url_for('add_shop') }}" method=post class=add-entry>
|
||||
<h2>Add new shop:</h2>
|
||||
<input class=submit type=text size=30 name=shop> <input class=submit type=submit value=Submit>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
15
code/templates/login.html
Normal file
15
code/templates/login.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h2>Login</h2>
|
||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||
<form action="{{ url_for('login') }}" method=post>
|
||||
<dl>
|
||||
<dt>Username:
|
||||
<dd><input type=text name=username autofocus>
|
||||
<dt>Password:
|
||||
<dd><input type=password name=password>
|
||||
<dd><input class=submit type=submit value=Login>
|
||||
</dl>
|
||||
</form>
|
||||
<!--a href="{{ url_for('register') }}">Register</a-->
|
||||
{% endblock %}
|
||||
14
code/templates/profile.html
Normal file
14
code/templates/profile.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h2>Profile</h2>
|
||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||
<form action="{{ url_for('profile') }}" method=post>
|
||||
<dl>
|
||||
<dt>Username:
|
||||
<dd>{{ user }}
|
||||
<dt>Password:
|
||||
<dd><input type=password name=password>
|
||||
<dd><input type=submit value=Update>
|
||||
</dl>
|
||||
</form>
|
||||
{% endblock %}
|
||||
14
code/templates/register.html
Normal file
14
code/templates/register.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h2>Register</h2>
|
||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||
<form action="{{ url_for('register') }}" method=post>
|
||||
<dl>
|
||||
<dt>Username:
|
||||
<dd><input type=text name=username>
|
||||
<dt>Password:
|
||||
<dd><input type=password name=password>
|
||||
<dd><input type=submit value=register>
|
||||
</dl>
|
||||
</form>
|
||||
{% endblock %}
|
||||
104
code/templates/show_shop.html
Normal file
104
code/templates/show_shop.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<H2># {{ shop }}</H2>
|
||||
<div class=entries>
|
||||
<form action="{{ url_for('toggle_item') }}" method=post class=toggle-item>
|
||||
<input type=hidden name=shopid value="{{ shopid }}" >
|
||||
<div id="entry_loop">
|
||||
{% for entry in entries %}
|
||||
<div class=entry><input class="submit tickbox {{ entry.extra_class }}" type=submit name=item{{ entry.row }} id=itemId{{ entry.row }} value="{{ entry.icon }}" ><span class=md_entry>{{ entry.text|safe }}</span><br></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
<div class=date>{{ date }}</div>
|
||||
</div>
|
||||
<div id=iconarea>
|
||||
<span class=iconbox> </span>
|
||||
<span class=iconbox onclick="hidetoggle('disp_add')" title="Add/remove">±</span>
|
||||
<span class=iconbox onclick="reload();" title="Reload">↻</span>
|
||||
<span class=iconbox onclick="sortMarkdown()" title="Sort once">↕</span>
|
||||
<span class=iconbox onclick="hidetoggle('disp_edit');growTextarea('edit_md');" title="Edit file">✎</span>
|
||||
<span class=iconbox onclick="hidetoggle('disp_share')" title="Share">♟</span>
|
||||
<span class=iconbox onclick="hidetoggle('disp_restore')" title="Restore">♻</span>
|
||||
<span class=iconbox onclick="hidetoggle('disp_delete')" title="Delete">⊗</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="disp_add">
|
||||
<form name="area_add" action="{{ url_for('add_items') }}" method=post class=add-entry>
|
||||
<dl>
|
||||
<dt><textarea cols=40 rows=1 id=add_md name=add_md onkeyup="growTextarea('add_md')"></textarea>
|
||||
<dt>
|
||||
<input type=hidden name=shopid value={{ shopid }}><input class="submit submitNextTo" type=submit value="Add items">
|
||||
<input class="submit" type=button value="Insert Date" onclick="insertDate('add_md');" >
|
||||
</dl>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="button-panel-row">
|
||||
<span class="button-panel-col">
|
||||
<form action="{{ url_for('toggle_item') }}" method=post>
|
||||
<input type=hidden name=shopid value="{{ shopid }}">
|
||||
<input class=submit type=submit name=toggleAll value="Invert ticks">
|
||||
</form>
|
||||
</span>
|
||||
<span class="button-panel">
|
||||
<form action="{{ url_for('toggle_item') }}" method=post>
|
||||
<input type=hidden name=shopid value="{{ shopid }}">
|
||||
<input class=submit type=submit name=unTickAll value="Clear ticks" onclick="return confirm('Do you really want to untick all entries?');">
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-panel-row">
|
||||
<span class="button-panel-col">
|
||||
<form action="{{ url_for('sort_flip') }}" method=post>
|
||||
<input type=hidden name=shopid value="{{ shopid }}">
|
||||
{% if autosort %}
|
||||
<input class=submit type=submit name=toggleAll value="Auto order: On">
|
||||
{% else %}
|
||||
<input class=submit type=submit name=toggleAll value="Auto order: Off">
|
||||
{% endif %}
|
||||
</form>
|
||||
</span>
|
||||
<span class="button-panel">
|
||||
<form action="{{ url_for('remove_toggled') }}" method=post>
|
||||
<input type=hidden name=shopid value={{ shopid }}>
|
||||
<input class=submit type=submit onclick="return confirm('Do you really want to remove ticked entries?');" value="Remove ticked">
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form id="disp_edit" class=hidden name="area_edit" action="{{ url_for('edit_md') }}" method=post class=add-entry>
|
||||
<dl>Edit content file:
|
||||
<dt><textarea cols=40 rows=5 id=edit_md name=edit_md onkeyup="growTextarea('edit_md')">{{ content }}</textarea>
|
||||
<dt>
|
||||
<input type=hidden name=shopid value={{ shopid }}><input class="submit notify" type=submit value=Save onclick="return confirm('Do you really want to replace content?');" >
|
||||
<input class="submit" type=button value="Insert Date" onclick="insertDate('edit_md');" >
|
||||
</dl>
|
||||
</form>
|
||||
<div class=hidden id=disp_share>
|
||||
<form action="{{ url_for('remove_share') }}" method=post class=toggle-item>
|
||||
<dl>Share the shop with other users:</dl>
|
||||
<input type=hidden name=shopid value="{{ shopid }}" >
|
||||
<ul>
|
||||
{% for user in shares %}
|
||||
<li> Remove: <input class="submit" type=submit name=user value="{{ user }}" onclick="return confirm('Do you really want to remove {{ user }}?');" ><br>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</form>
|
||||
<form name="area_share" action="{{ url_for('add_share') }}" method=post class=add-entry>
|
||||
<dl>
|
||||
<dt><input class=submit type=text length=10 name=share><input type=hidden name=shopid value={{ shopid }}><input class="submit notify" type=submit value=Share>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
<form id="disp_restore" class=hidden action="{{ url_for('restore_md') }}" method=post class=add-entry>
|
||||
<dl>Shop state is backed up with "Edit items" and "Remove ticked" actions. Last backup: <span class=date>{{ date_bkp }}</span></dl>
|
||||
<dl><input type=hidden name=shopid value={{ shopid }}><input class="submit notify" type=submit value=Restore onclick="return confirm('Do you really want to restore previous version?');">
|
||||
</dl></form>
|
||||
<form id="disp_delete" class=hidden action="{{ url_for('remove_shop') }}" method=post class=add-entry>
|
||||
<dl>Delete shop permanently. Can not be restored.</dl>
|
||||
<dl><input type=hidden name=shopid value={{ shopid }}><input class="submit notify" type=submit value=Delete onclick="return confirm('Do you really want to remove shop {{ shop }}?');">
|
||||
</dl></form>
|
||||
{% if autosort %}
|
||||
<script language=javascript>sortMarkdown(boughtOrder=true);</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user