shopr

Check-in [470fc733db]
Login

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

Overview
Comment:Get item changes and updating with visual cues working.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256:470fc733dbf192e489af032792104400df40dc822a39e3d1e393dafb6b4e8133
User & Date: vandys 2019-06-21 23:55:27
Context
2019-06-22
17:28
Start working up filter text check-in: 35b0eed1a0 user: vandys tags: trunk
2019-06-21
23:55
Get item changes and updating with visual cues working. check-in: 470fc733db user: vandys tags: trunk
17:03
Start bringing up item clicking and transition check-in: 6a9d6c9f50 user: vandys tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to css/shopr.css.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
}
#items {
 display: flex;
 flex-wrap: wrap;
}
#items > span {
 flex-grow: 0;
 min-width: 25px;
 text-align: center;
 border-bottom: 3px solid black;
}
.isel {
 flex-grow: 1 !important;
 min-width: 50% !important;
}
#items > span:nth-child(3n+1) {
 background: pink;
}
#items > span:nth-child(3n+2) {
 background: LightGreen;
}







|





|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
}
#items {
 display: flex;
 flex-wrap: wrap;
}
#items > span {
 flex-grow: 0;
 width: 9%;
 text-align: center;
 border-bottom: 3px solid black;
}
.isel {
 flex-grow: 1 !important;
 width: 78% !important;
}
#items > span:nth-child(3n+1) {
 background: pink;
}
#items > span:nth-child(3n+2) {
 background: LightGreen;
}

Changes to get.py.

71
72
73
74
75
76
77
78



79
80
81
82
83

	# 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.for_json())







|
>
>
>





71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

	# 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).
	try:
	    gen = int(self.vals["gen"])
	except:
	    gen = -1
	if l.gen == gen:
	    l.await_gen()

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

Changes to items.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
..
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
..
58
59
60
61
62
63
64
65
66
67
68
69
70
71











72
73
74
75
76
77
78
#
# items.py
#	Represent the items on a shopping list
#
import json
import sqlite3

class List(object):
    def __init__(self, approot, lid, isglob, name):
	self.approot = approot
	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
................................................................................
    #  be sure to put your changes in before rev'ing the
    #  generation counter.
    def next_gen(self):
	self.gen += 1
	res = self.gen

	# Release anybody waiting for new changes
	waiters = set(self.waiters)
	self.waiters.clear()
	for sema in waiters:
	    sema.release()

	return res

    # Wait for new change
................................................................................
	self.waiters.add(sema)
	sema.acquire()
	# Resume here after next_gen()

    # A JSON wrap-up of our content
    def for_json(self):
	items = []
	res = {"name": self.name, "items": items}
	for i in self.items:
	    # Don't share deleted
	    if i.deleted():
		continue
	    items.append(i.for_json())
	return res












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

	# Our parent list
	self.parent = l





|










>







 







|







 







|
|





>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
..
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
..
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
#
# items.py
#	Represent the items on a shopping list
#
import json, threading
import sqlite3

class List(object):
    def __init__(self, approot, lid, isglob, name):
	self.approot = approot
	self.lid = lid
	self.isglob = isglob
	self.name = name
	self.items = []
	self.gen = 0
	self.waiters = set()

	# 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
................................................................................
    #  be sure to put your changes in before rev'ing the
    #  generation counter.
    def next_gen(self):
	self.gen += 1
	res = self.gen

	# Release anybody waiting for new changes
	waiters = frozenset(self.waiters)
	self.waiters.clear()
	for sema in waiters:
	    sema.release()

	return res

    # Wait for new change
................................................................................
	self.waiters.add(sema)
	sema.acquire()
	# Resume here after next_gen()

    # A JSON wrap-up of our content
    def for_json(self):
	items = []
	res = {"name": self.name, "items": items, "gen": self.gen}
	for i in sorted(self.items, key=lambda _i: _i.name):
	    # Don't share deleted
	    if i.deleted():
		continue
	    items.append(i.for_json())
	return res

    # Update idx for the given item
    def set_idx(self, row, col):
	if (row < 0) or (row >= len(self.items)):
	    return False
	l = self.items[row]

	# Update item, which flags change to list
	l.assign(col)

	return True

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

	# Our parent list
	self.parent = l

Changes to js/shopr.js.

7
8
9
10
11
12
13






14
15
16
17
18
19
20
...
115
116
117
118
119
120
121
122






123
124




125




126
127
128
129
130
131
132
...
155
156
157
158
159
160
161
162


163

164
165









































































166
167
168
169
170
171
172
...
187
188
189
190
191
192
193
194
195
196
197









198
199
200
201
202
203

204
205
206
207
208
209
210
// 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";
    }
................................................................................
    const tup = e.id.substring(4).split('_');
    const row = parseInt(tup[0]), col = parseInt(tup[1]);

    // We don't switch the item; we flag it as having a change
    //  on the way, and tell the server.  It'll wake up our XHR
    //  and let us update that way.
    set_bright(e);
}







// 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.items.length; ++i) {
................................................................................
	    s3.classList.add("isel");
	}

	// Add to DOM
	items.appendChild(s1);
	items.appendChild(s2);
	items.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;
	}

	// 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.







>
>
>
>
>
>







 







|
>
>
>
>
>
>
|
<
>
>
>
>
|
>
>
>
>







 







|
>
>
|
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 








|
|
|
>
>
>
>
>
>
>
>
>






>







7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
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
...
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
...
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
// 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;

// Current generation of cur_list we hold
var curgen = null;

// List of elems as last delivered by the server
var cur_names = null;

// Currently displayed page elem
var curdiv = null;
function setcur(e) {
    if (curdiv != null) {
	curdiv.style.display = "none";
    }
................................................................................
    const tup = e.id.substring(4).split('_');
    const row = parseInt(tup[0]), col = parseInt(tup[1]);

    // We don't switch the item; we flag it as having a change
    //  on the way, and tell the server.  It'll wake up our XHR
    //  and let us update that way.
    set_bright(e);

    // So tell the server...
    const msg = {op: "idx", row: row, col: col};
    const req = new XMLHttpRequest();
    req.open("PUT", "/l/" + encodeURIComponent(cur_list) +
	".json" + authURL);
    req.send(JSON.stringify(msg));
}


// Tear down old list display, paint new one
//
// This is from scratch, tossing out any enhancements
function show_new_list(ls) {

    // This is what'll be on the screen now
    cur_names = ls;

    // Clear old list display
    while (items.firstChild != null) {
	items.removeChild(items.firstChild);
    }

    // Display received items
    for (let i = 0; i < ls.items.length; ++i) {
................................................................................
	    s3.classList.add("isel");
	}

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

	// Make easy to reference for future updates
	l.elems = [s1, s2, s3];
    }

    // Show list of items
    setcur(scrlist);

    // Hang a request to hear about subsequent changes
    curgen = ls.gen;
    paint_list();
}

// We're going to display a server's update of our item
//  list.  Paint some color at the change point(s).
function show_list(ls) {
    // Just a timeout, probably to keep NAT happy
    if (ls.gen == curgen) {
	return;
    }

    // If there's no current content, just paint the new stuff now
    if ((cur_names == null) || (cur_names.items.length == 0)) {
	show_new_list(ls);
	return;
    }

    // Walk the old and new in parallel, noting new/changed/deleted
    //  as we progress, and placing a visual cue as appropriate.
    // i1 walks the new/latest values, i2 points at our
    //  older counterpart as currently displayed.
    let changed = false;
    let oidx = 0;
    let i2 = null, e2 = null;
    for (let i1 of ls.items) {

	// Adding beyond last, so make bottom
	//  of last elem a green bar
	if (oidx == cur_names.items.length) {
	    i2 = cur_names.items[oidx-1];
	    e2 = i2.elems[i2.idx];
	    e2.style.background = "green";
	    changed = true;
	    break;
	}

	// Same elem before/after?
	i2 = cur_names.items[oidx];
	e2 = i2.elems[i2.idx];
	if (i1.name == i2.name) {
	    // Changing idx/color
	    if (i1.idx != i2.idx) {
		e2.style.background = "white";
		changed = true;
	    }
	    oidx += 1;
	    continue;
	}

	// Inserted cell?
	if (i1.name < i2.name) {
	    e2.style.background = "green";
	    changed = true;
	    continue;
	}

	// Deleted cell
	e2.style.background = "red";
	oidx += 1;
	changed = true;
    }

    // Let our highlights show for a bit, then switch
    //  to the new list.
    if (changed) {
	setTimeout( () => { show_new_list(ls); }, 1000 );
    } else {
	// Didn't spot anything new, so paint now.
	show_new_list(ls);
    }
}

// Display the current list
function paint_list() {
    const req = new XMLHttpRequest();
    req.onreadystatechange = () => {
	if (req.readyState != 4) {
................................................................................
	    return;
	}

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

    // Construct URL; our list, authen
    let url = "/l/" + encodeURIComponent(cur_list) +
	".json" + authURL;

    // If we've received previous generations, note our
    //  current position
    if (curgen != null) {
	url += ("&gen=" + curgen.toString());
    }

    // Requesting URL, including authentication
    req.open("GET", url);
    req.send();
}

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

// Display choice of lists
//
// scrlists is the outer div, whose display we toggle.

Changes to put.py.

1
2
3
4
5
6

7
8
9
10
11
12
13
..
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#
# put.py
#	Mixin to implement HTML PUT operations
#
import urllib, json
import items


# The PUT part of our handling
class PUT_mixin(object):

    # Configure our WPlayer GET treatment
    def __init__(self):

................................................................................
	    return True,self.send_error(404)

	# Decode op
	try:
	    d = json.loads(buf)
	    op = str(d["op"])
	    if op == "idx":
		item = str(d["name"])
		val = int(d["val"])
	    else:
		return True,self.send_error(400, "No such op")
	except:
	    return True,self.send_error(400, "Malformed JSON")

	# Update item, and churn the list
	if op == "idx":
	    if l.set_idx(item, val):
		return True,self.send_json({"ok": True})
	    return True,self.send_error(400, "Bad item")

	# "Can't Happen"
	return True,self.send_error(500)






>







 







|
|







|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#
# put.py
#	Mixin to implement HTML PUT operations
#
import urllib, json
import items
import pdb

# The PUT part of our handling
class PUT_mixin(object):

    # Configure our WPlayer GET treatment
    def __init__(self):

................................................................................
	    return True,self.send_error(404)

	# Decode op
	try:
	    d = json.loads(buf)
	    op = str(d["op"])
	    if op == "idx":
		row = int(d["row"])
		col = int(d["col"])
	    else:
		return True,self.send_error(400, "No such op")
	except:
	    return True,self.send_error(400, "Malformed JSON")

	# Update item, and churn the list
	if op == "idx":
	    if l.set_idx(row, col):
		return True,self.send_json({"ok": True})
	    return True,self.send_error(400, "Bad item")

	# "Can't Happen"
	return True,self.send_error(500)