Unnamed Fossil Project

Check-in [9f66b65036]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Bringup
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256:9f66b650360cd5c55648eaa948b4255c41c999da83dbb542a3a8f28b7b597999
User & Date: vandys 2019-06-20 22:36:19
Context
2019-06-20
23:54
Continue bringup; start fleshing out styling. check-in: 5cf6b89eed user: vandys tags: trunk
22:36
Bringup check-in: 9f66b65036 user: vandys tags: trunk
16:37
Get some of it running for the first time. check-in: 8a1ba772ee user: vandys tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to css/shopr.css.

14
15
16
17
18
19
20
21







 background: pink;
}
.clG {
 background: LightGreen;
}
.clY {
 background: yellow;
}















>
>
>
>
>
>
>
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 background: pink;
}
.clG {
 background: LightGreen;
}
.clY {
 background: yellow;
}
#scrlists2 > span {
 background: pink;
 font-size: large;
 border: 3px solid black;
 border-radius: 6px;
 padding: 4px;
}

Changes to get.py.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
..
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
..
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
..
60
61
62
63
64
65
66

67
68
69
70
71





72
73
74
75
76
77
#
# get.py
#	Mixin to implement HTML GET operations
#
import sys, urllib
import sqlite3
import items


# The GET part of our handling
class GET_mixin(object):

    def __init__(self):

	# /l/<listname>.json
................................................................................
	self.dispatchers.append( ("GET", self.send_list) )

	# /lists.json
	self.dispatchers.append( ("GET", self.send_lists) )

    # Root web page, single page webapp
    def send_top(self):
	return self.send_files("/html/shopr.html")

    # /lists.json
    #
    # Return list of lists
    def send_lists(self):
	if not self.path_match("lists.json"):
	    return False,None
................................................................................
	#  ever become noticeable, but if so I'll add a distinct
	#  DB table to enumerate lists (and ownership, and members,
	#  and...)
	approot = self.server.approot
	db = sqlite3.connect(approot.dbname)
	c = db.cursor()
	c.execute("select name from lists where owner=? or global=1",
	    (uid,))
	lists = set()
	for tup in c:
	    lists.add(tup[0])

	# Here they are
	return True,self.send_json(lists)

    # /l/<listname>.json[?gen=<number>]
    #
    # Get <listname>'s contents, waiting for new contents if
    #  nothing newer than generation <number> exists yet.
    def send_list(self):
	pp = self.paths
................................................................................
	# Logged in?  (This also verified that self.vals{} is there.)
	if not self.authenticated():
	    return True,self.send_error(403)

	# Known list?
	approot = self.server.approot
	nm = urllib.unquote_plus(pp[1][:-5])

	l = approot.get_list(nm)
	if l is None:
	    return True,self.send_error(404)

	# Wait for new revision?





	gen = self.vals.get("gen", -1)
	if l.gen <= gen:
	    l.await_gen()

	# How the list looks now
	return True.self.send_json(l.as_json())







>







 







|







 







|





|







 







>
|




>
>
>
>
>

|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
..
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
..
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#
# get.py
#	Mixin to implement HTML GET operations
#
import sys, urllib
import sqlite3
import items
import pdb

# The GET part of our handling
class GET_mixin(object):

    def __init__(self):

	# /l/<listname>.json
................................................................................
	self.dispatchers.append( ("GET", self.send_list) )

	# /lists.json
	self.dispatchers.append( ("GET", self.send_lists) )

    # Root web page, single page webapp
    def send_top(self):
	return self.send_files("html/shopr.html")

    # /lists.json
    #
    # Return list of lists
    def send_lists(self):
	if not self.path_match("lists.json"):
	    return False,None
................................................................................
	#  ever become noticeable, but if so I'll add a distinct
	#  DB table to enumerate lists (and ownership, and members,
	#  and...)
	approot = self.server.approot
	db = sqlite3.connect(approot.dbname)
	c = db.cursor()
	c.execute("select name from lists where owner=? or global=1",
	    (self.uid,))
	lists = set()
	for tup in c:
	    lists.add(tup[0])

	# Here they are
	return True,self.send_json(list(lists))

    # /l/<listname>.json[?gen=<number>]
    #
    # Get <listname>'s contents, waiting for new contents if
    #  nothing newer than generation <number> exists yet.
    def send_list(self):
	pp = self.paths
................................................................................
	# Logged in?  (This also verified that self.vals{} is there.)
	if not self.authenticated():
	    return True,self.send_error(403)

	# Known list?
	approot = self.server.approot
	nm = urllib.unquote_plus(pp[1][:-5])
	pdb.set_trace()
	l = approot.get_list(self.uid, nm)
	if l is None:
	    return True,self.send_error(404)

	# Wait for new revision?
	#
	# Note, we give them an update unless they say that
	#  they're exactly at our current gen; this handles a client
	#  from before this server restarted (and started counting
	#  gens from 0 again).
	gen = self.vals.get("gen", -1)
	if l.gen == gen:
	    l.await_gen()

	# How the list looks now
	return True.self.send_json(l.as_json())

Changes to html/shopr.html.

12
13
14
15
16
17
18







19
20
21
22
23
24
25
<div id="scrlogin" style="display: none;">
 <input type="text" id="user" placeholder="User name">
 <br>
 <input type="password" id="password" placeholder="password">
 <br>
 <button onclick="try_login();">Login</button>
</div>








<div id="scrlist" style="display: none;">
 <div>
  <span>+RGY</span>
  <span>R</span>
  <span>G</span>
  <span>Y</span>







>
>
>
>
>
>
>







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<div id="scrlogin" style="display: none;">
 <input type="text" id="user" placeholder="User name">
 <br>
 <input type="password" id="password" placeholder="password">
 <br>
 <button onclick="try_login();">Login</button>
</div>

<div id="scrlists" style="display: none;">
 Select the list to use:
 <hr>
 <div id="scrlists2">
 </div>
</div>

<div id="scrlist" style="display: none;">
 <div>
  <span>+RGY</span>
  <span>R</span>
  <span>G</span>
  <span>Y</span>

Changes to items.py.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
..
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
..
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
...
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
	self.lid = lid
	self.isglob = isglob
	self.name = name
	self.items = []
	self.gen = 0

	# Load any existing state
	gen = -1
	db = self.get_db()
	c = db.cursor()
	c.execute("select name,idx,gen from items where list=?", name)
	for tup in c:
	    iname,idx,igen = tup

	    # Deleted items shouldn't be in the DB
	    assert idx >= 0

	    # New item, with the saved generation value
	    i = Item(self, idx, iname, igen)
	    gen = max(gen, igen)

	# The next generation is one beyond any previously allocated.
	# As a special case, we advance -1 to 0 to get an initial
	#  value for an empty DB.
	self.gen = gen+1

	c.close()
	db.close()

    # Return a DB connection
    def get_db(self):
	return sqlite3.connect(self.approot.dbname)
................................................................................
	    # Don't share deleted
	    if i.deleted():
		continue
	    items.append(i.for_json())
	return json.dumps(res)

class Item(object):
    def __init__(self, l, idx, name, gen):

	# Our parent list
	self.parent = l

	# Always start at first index type (usually red)
	# When -1, this is a deleted item
	self.idx = 0
................................................................................
	return {"name": self.name, "idx": self.idx}

    # Save ourselves to the DB
    def sync(self):
	db = self.parent.get_db()
	c = db.cursor()
	c.execute("insert or replace into items values(?,?,?) ",
	    (self.parent.name, self.name, self.idx))
	db.commit()
	c.close()
	db.close()

    def assign(self, idx):
	# No change
	if self.idx == idx:
................................................................................

	# Put into DB
	self.sync()

    # Tell if we're a deleted item slot
    def deleted(self):
	return self.idx == -1

# Shared state for any given list name
Lists = {}
def get_list(approot, name):
    global Lists

    l = Lists.get(name)
    if l is None:
	l = List(approot, name)
    else:
	assert l.approot is approot
    return l







<


|

|





|
<
<
<
<
<
<







 







|







 







|







 







<
<
<
<
<
<
<
<
<
<
<
<
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28






29
30
31
32
33
34
35
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
..
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
...
108
109
110
111
112
113
114












	self.lid = lid
	self.isglob = isglob
	self.name = name
	self.items = []
	self.gen = 0

	# Load any existing state

	db = self.get_db()
	c = db.cursor()
	c.execute("select name,idx from items where list=?", lid)
	for tup in c:
	    iname,idx = tup

	    # Deleted items shouldn't be in the DB
	    assert idx >= 0

	    # New item, with the saved generation value
	    i = Item(self, idx, iname)







	c.close()
	db.close()

    # Return a DB connection
    def get_db(self):
	return sqlite3.connect(self.approot.dbname)
................................................................................
	    # Don't share deleted
	    if i.deleted():
		continue
	    items.append(i.for_json())
	return json.dumps(res)

class Item(object):
    def __init__(self, l, idx, name):

	# Our parent list
	self.parent = l

	# Always start at first index type (usually red)
	# When -1, this is a deleted item
	self.idx = 0
................................................................................
	return {"name": self.name, "idx": self.idx}

    # Save ourselves to the DB
    def sync(self):
	db = self.parent.get_db()
	c = db.cursor()
	c.execute("insert or replace into items values(?,?,?) ",
	    (self.parent.lid, self.name, self.idx))
	db.commit()
	c.close()
	db.close()

    def assign(self, idx):
	# No change
	if self.idx == idx:
................................................................................

	# Put into DB
	self.sync()

    # Tell if we're a deleted item slot
    def deleted(self):
	return self.idx == -1












Changes to js/shopr.js.

1
2
3
4










































































































































































































//
// shopr.js
//	Main routines for shopping list API access
//














































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//
// shopr.js
//	Main routines for shopping list API access
//
"use strict";

// User/pass
var uname = null, pw = null;
// Pre-calculated user/pw as URL arguments
var authURL = null;

// Current list name, null if need to choose one
var cur_list = null;

// Currently displayed page elem
var curdiv = null;
function setcur(e) {
    if (curdiv != null) {
	curdiv.style.display = "none";
    }
    e.style.display = "block";
    curdiv = e;
}

// Calculate URL args for authentication
function calc_authURL() {
    authURL = "?user=" + encodeURIComponent(user.value) +
	"&pw=" + encodeURIComponent(password.value);
}

// Try logging in by making sure there's a user/pass, filling
//  in our saved state, then trying to get a list
function try_login() {
    if (!user.value) {
	alert("Please fill in a user name");
	return;
    }
    if (!password.value) {
	alert("Please fill in a password");
	return;
    }
    localStorage.setItem("user", user.value);
    localStorage.setItem("password", password.value);
    calc_authURL();
    get_lists();
}

// Paint server's description of list elements
function show_list(ls) {
    // Clear old list display
    while (items.firstChild != null) {
	items.removeChild(items.firstChild);
    }

    // Display received items
    for (let i = 0; i < ls.length; ++i) {
	const l = ls[i];

	// Red/Green/Yellow tappables
	const s1 = document.createElement("span");
	s1.onclick = () => item_click(i, 0);
	const s2 = document.createElement("span");
	s2.onclick = () => item_click(i, 1);
	const s3 = document.createElement("span");
	s3.onclick = () => item_click(i, 2);

	// The text goes in the one selected
	if (l.idx == 0) {
	    s1.textContent = l.name;
	    s1.style.class = "selected";
	} else if (l.idx == 1) {
	    s2.textContent = l.name;
	    s2.style.class = "selected";
	} else {
	    s3.textContent = l.name;
	    s3.style.class = "selected";
	}

	// Add to DOM
	elems.appendChild(s1);
	elems.appendChild(s2);
	elems.appendChild(s3);
    }

    // Show list of items
    setcur(scrlist);
}

// Display the current list
function paint_list() {
    const req = new XMLHttpRequest();
    req.onreadystatechange = () => {
	if (req.readyState != 4) {
	    return;
	}
	if (req.status != 200) {
	    if (req.status == 403) {
		// Changed, password, deleted account, ???
		setcur(scrlogin);
		alert("Login credentials failed, please try again");
	    } else {
		alert("Failed to get list " + cur_list);
	    }
	    return;
	}

	// Server seems happy
	show_list(JSON.parse(req.responseText));
	return;
    };

    // Requesting URL, including authentication
    req.open("GET", "/l/" + encodeURIComponent(cur_list) +
	".json" + authURL);
    req.send();
}

// Choose a list
function sel_list(e) {
    cur_list = e.currentTarget.textContent;
    paint_list();
    return false;
}

// Display choice of lists
//
// scrlists is the outer div, whose display we toggle.
// There's some helpful text, then scrlists2 is the actual
//  element holding the list of list names.
function show_lists(names) {

    // Just in case there's old cruft
    while (scrlists2.firstChild != null) {
	scrlists2.removeChild(scrlists2.firstChild);
    }

    // Make new list
    for (let nm of names) {
	const s = document.createElement("span");
	s.textContent = nm;
	s.onclick = (e) => {sel_list(e);};
	scrlists2.appendChild(s);
    }

    // Show it
    setcur(scrlists);
}

// Ask for list of lists
function get_lists() {
    const req = new XMLHttpRequest();
    req.onreadystatechange = () => {
	if (req.readyState != 4) {
	    return;
	}
	if (req.status != 200) {
	    if (req.status == 403) {
		// Changed, password, deleted account, ???
		setcur(scrlogin);
		alert("Login credentials failed, please try again");
	    } else {
		alert("Failed to get lists directory");
	    }
	    return;
	}

	// Server seems happy
	show_lists(JSON.parse(req.responseText));
	return;
    };

    // Requesting URL, including authentication
    req.open("GET", "/lists.json" + authURL);
    req.send();
}

// Initial load of shopr app page
function init_page() {
    // Saved values?
    const u = localStorage.getItem("user");
    if (u != null) {
	uname = user.value = u;
    }
    const p = localStorage.getItem("password");
    if (p != null) {
	pw = password.value = p;
    }

    // Log in?
    if ((u == null) || (p == null)) {
	setcur(scrlogin);
	return;
    }
    calc_authURL();

    // See if we can continue where they left off
    const cl = localStorage.getItem("curlist");
    if (cl != null) {
	cur_list = cl;
	paint_list();
	return;
    }

    // Nope, start them in the list-of-lists
    get_lists();
}

Changes to main.py.

5
6
7
8
9
10
11

12
13
14
15
16
17
18
..
32
33
34
35
36
37
38
39

40
41
42
43
44
45
46

47
48
49
50
51
52

53
54
55

56
57
58
59
60
61
62
63
64
65
66
67
68
69
..
94
95
96
97
98
99
100
101
102
103
104

105
106
107
108
109
110
111
112
113
114

115
116
117
118
119
120
121
import sys
import sqlite3
import chore
from get import GET_mixin
from post import POST_mixin
from put import PUT_mixin
import items


# Our config (pretty basic)
def load_cfg(fn):
    c = chore.config.Config()
    chore.www.add_config(c)
    c.onearg.add( ("db",) )
    return c.load_cfg(fn)
................................................................................
    #  authenticated name.
    #
    # Yes, passwords in the clear.  This is a personal service, and
    #  I need to recover passwords often, and don't really care if
    #  somebody breaks into the FS and discovers the passwords.
    def authenticated(self):
	self.user = None
	if not self.vals:

	    return False

	# Verify user/pass
	u = vals.get("user")
	pw = vals.get("pw")
	if (not u) or (not pw):
	    return False

	db = sqlite3.connect(self.approot.dbname)
	c = cursor()
	c.execute("select uid from users where name=? and pw=?",
	    (u, pw))
	tup = c.find_one()
	if not tup:

	    return False

	# Success

	self.user = name

	# Cache resolution
	approot = self.server.approot
	self.uid = uid = int(tup[0])
	approot.uids[uid] = name
	approot.uids[name] = uid

	return True

# Root of our app server
class App(chore.server.Server):

    def __init__(self, cfg):
................................................................................
	    return res

	# Try from DB, owned by us
	db = sqlite3.connect(self.dbname)
	c = db.cursor()
	c.execute("select lid,global from lists where owner=? and name=?",
	    (uid, name))
	tup = c.find_one()
	if tup is not None:
	    lid,isglob = tup
	    res = self.lists[ (uid, name) ] = List(self, lid, isglob, name)

	    return res

	# Now global DB's with this name
	c.execute("select lid from lists where name=? and global=1",
	    (name,))
	tup = c.find_one()
	if tup is None:
	    return None
	lid = tup[0]
	res = self.lists[name] = List(self, lid, True, name)

	return res

if __name__ == "__main__":
    if len(sys.argv) != 2:
	sys.stderr.write("Usage is: %s <config>\n" %
	    (sys.argv[0],))
	sys.exit(1)







>







 







|
>







>
|
|


|

>



>
|


<

|
|







 







|


|
>





|



|
>







5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
..
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
..
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import sys
import sqlite3
import chore
from get import GET_mixin
from post import POST_mixin
from put import PUT_mixin
import items
import pdb

# Our config (pretty basic)
def load_cfg(fn):
    c = chore.config.Config()
    chore.www.add_config(c)
    c.onearg.add( ("db",) )
    return c.load_cfg(fn)
................................................................................
    #  authenticated name.
    #
    # Yes, passwords in the clear.  This is a personal service, and
    #  I need to recover passwords often, and don't really care if
    #  somebody breaks into the FS and discovers the passwords.
    def authenticated(self):
	self.user = None
	vals = self.vals
	if not vals:
	    return False

	# Verify user/pass
	u = vals.get("user")
	pw = vals.get("pw")
	if (not u) or (not pw):
	    return False
	approot = self.server.approot
	db = sqlite3.connect(approot.dbname)
	c = db.cursor()
	c.execute("select uid from users where name=? and pw=?",
	    (u, pw))
	tup = c.fetchone()
	if not tup:
	    sys.stderr.write("auth fail %s\n" % (u,))
	    return False

	# Success
	sys.stderr.write("auth success %s\n" % (u,))
	self.user = u

	# Cache resolution

	self.uid = uid = int(tup[0])
	approot.uids[uid] = u
	approot.uids[u] = uid

	return True

# Root of our app server
class App(chore.server.Server):

    def __init__(self, cfg):
................................................................................
	    return res

	# Try from DB, owned by us
	db = sqlite3.connect(self.dbname)
	c = db.cursor()
	c.execute("select lid,global from lists where owner=? and name=?",
	    (uid, name))
	tup = c.fetchone()
	if tup is not None:
	    lid,isglob = tup
	    res = self.lists[ (uid, name) ] = \
		items.List(self, lid, bool(isglob), name)
	    return res

	# Now global DB's with this name
	c.execute("select lid from lists where name=? and global=1",
	    (name,))
	tup = c.fetchone()
	if tup is None:
	    return None
	lid = tup[0]
	res = self.lists[name] = \
	    items.List(self, lid, True, name)
	return res

if __name__ == "__main__":
    if len(sys.argv) != 2:
	sys.stderr.write("Usage is: %s <config>\n" %
	    (sys.argv[0],))
	sys.exit(1)