commit ca7ce92a04973b7024ef2064779e04f44788547a Author: q Date: Sun Jul 3 23:47:54 2016 +0300 should work diff --git a/run.py b/run.py new file mode 100755 index 0000000..153eb17 --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +#!/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/schema.sql b/schema.sql new file mode 100644 index 0000000..20bc6a6 --- /dev/null +++ b/schema.sql @@ -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 +); + + diff --git a/shop.py b/shop.py new file mode 100644 index 0000000..ca9c757 --- /dev/null +++ b/shop.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +# all the imports +import sqlite3, time, datetime, hashlib, os +from shutil import copyfile +from flask import Flask, request, session, g, redirect, url_for, \ + abort, render_template, flash + +# configuration +DATABASE = 'shop.db' +DATADIR = 'data' +DEBUG = False +SECRET_KEY = 'development key' +USERNAME = 'admin' +PASSWORD = 'default' + +# create our little application :) +app = Flask(__name__) +app.config.from_object(__name__) + +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).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 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() + +@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") + entries=[] + content=open(data_file, 'rt').read() + for i,row in enumerate(open( data_file, 'rt').read().split("\n")): + # any parsing magick would be here + if row=="": + continue + icon=" " + extra_class="noitem" + if "[ ]" in row or "[x]" in row: + icon=u"\u2714" + extra_class="" + 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])) + return render_template('show_shop.html', entries=entries, shop=shopname, shopid=shopid, content=content,shares=shared_to) + +@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') + for row in request.form['edit_md'].split("\n"): + contents_file.write("%s\n"%row) + contents_file.close() + flash('Saved new file.') + 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 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") + 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: + if '[ ]' in row: + contents[i]=row.replace('[ ]','[x]') + if '[x]' in row: + contents[i]=row.replace('[x]','[ ]') + if row!=contents[i]: + changed=True + break + if changed: + 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('/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") + 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) + changed=True + contents_file.close() + if changed: + 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('/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 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('/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/script.js b/static/script.js new file mode 100644 index 0000000..80e0c4c --- /dev/null +++ b/static/script.js @@ -0,0 +1,24 @@ +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, row; row = rows[i]; i++) { + cols=Math.max(cols, row.length); + } + el.cols=cols; +} + +function reload() { + location.href=window.location.href; +} + +function hidetoggle(name,button) { + document.getElementById(name).style.display='none'; + document.getElementById(button).onclick=function(){showtoggle(name,button);}; +} +function showtoggle(name,button) { + document.getElementById(name).style.display='inline-block'; + document.getElementById(button).onclick=function(){hidetoggle(name,button);}; +} + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..3ee5065 --- /dev/null +++ b/static/style.css @@ -0,0 +1,24 @@ +body { font-family: monospace; background: #eee; } +a, h1, h2 { color: #377ba8; } +h1, h2 { margin: 0; } +h1 { border-bottom: 2px solid #eee; } +h2 { font-size: 1.2em; border-top: 2px solid #eee; margin-top: 0.25em; margin-bottom: 0.25em;} +td { height: 1.75em; } + +.submit { font-family: monospace; } +.tickbox { margin-right: 1em; } +.noitem { background-color: transparent; border-color:transparent; } +.page { border: 5px solid #ccc; padding: 0.5em; + background: white; } +.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: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } +.flash { background: #cee5F5; padding: 0.5em; + border: 1px solid #aacbe2; } +.error { background: #f0d6d6; padding: 0.5em; } +.hidden { display: none; } diff --git a/static/table.js b/static/table.js new file mode 100755 index 0000000..4c9ea12 --- /dev/null +++ b/static/table.js @@ -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 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'+txt+'  ↕'; + } + } + 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 + + + +Shop + + + + + + + + +
+ List shops + Reload page + Logout + + +
+
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block body %}{% endblock %} +
+ + + diff --git a/templates/list_shops.html b/templates/list_shops.html new file mode 100644 index 0000000..8138d2f --- /dev/null +++ b/templates/list_shops.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} +{% block body %} + + + {% for entry in entries %} + + {% else %} +
ShopLast editedOwner
{{ entry.shop }}{{ entry.date }}{{ entry.owner }}
Unbelievable. You dont have any shops + {% endfor %} + +
+
+

Add new shop:

+ +
+ +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..bfb582b --- /dev/null +++ b/templates/login.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} +{% block body %} +

Login

+ {% if error %}

Error: {{ error }}{% endif %} +

+
+
Username: +
+
Password: +
+
+
+
+ +{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..8a0753a --- /dev/null +++ b/templates/register.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} +{% block body %} +

Register

+ {% if error %}

Error: {{ error }}{% endif %} +

+
+
Username: +
+
Password: +
+
+
+
+{% endblock %} diff --git a/templates/show_shop.html b/templates/show_shop.html new file mode 100644 index 0000000..86e0164 --- /dev/null +++ b/templates/show_shop.html @@ -0,0 +1,51 @@ +{% extends "layout.html" %} +{% block body %} +

# {{ shop }}

+
+
+ + {% for entry in entries %} + {{ entry.text }}
+ {% endfor %} +
+
+ +

Add items:

+
+
+

+
+
+

Remove ticked:

+ +

Edit items:

+ +

Share shop:

+ +

Delete shop:

+ + +{% endblock %}