Unnamed Fossil Project

Check-in [8a259a1618]
Login

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

Overview
Comment:Initial shape of source repo
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256:8a259a161883828985fba46587bb37d0f7032359d9fa53509e2298ddb6e2424b
User & Date: vandys 2019-06-16 17:04:43
Context
2019-06-18
22:59
Start working up items, DB, and authentication check-in: 2fa5357a8e user: vandys tags: trunk
2019-06-16
17:04
Initial shape of source repo check-in: 8a259a1618 user: vandys tags: trunk
17:02
initial empty check-in check-in: b6d0fde39a user: vandys tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added css/shopr.css.

























>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
body {
 background: LightBlue;
}
div {
 width: 100%;
}
#items > span {
 flex-grow: 0;
}
.isel {
 flex-grow: 1;
}

Added etc/config.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
#
# List-with-swiping service
#
service "Swipr"

# Web interface
serve https
    port 8095
    publicCert certs/server.crt
    privateKey certs/server.key

Added 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
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
#
# get.py
#	Mixin to implement HTML GET operations
#
# Structure of paths in the server:
#  /
#	List of lists
#  /<listname>/<col#>
#	UI for a particular list at the given column
import sys, urllib
import ilist, delta

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

    def __init__(self):
	# /<listname>/<col #/name>
	#	Web page with view of this list's column
	self.dispatchers.append( ("GET", self.send_list) )

	# /rest/l.json
	#	JSON List of lists
	self.dispatchers.append( ("GET", self.send_json_lists) )

	# /rest/l/<listname>/<col# | name>.json
	#	JSON Dope for a given list
	self.dispatchers.append( ("GET", self.send_json_list) )

	# Long polling
	self.dispatchers.append( ("GET", self.send_json_changes) )

    # Root web page, redir to our static HTML to get the
    #  list of lists
    def send_top(self):
	return self.send_redir("/html/v_ilists.html")

    # Display the named list
    def send_list(self):
	pp = self.paths
	if (len(pp) != 3) or (pp[0] != 'l'):
	    return False,None

	# Not a known list
	nm = pp[1]
	l = ilist.ilists.get(nm)
	if l is None:
	    return False,None

	# Ok column #?
	col = l.getCol(urllib.unquote_plus(pp[2]))
	if col is None:
	    return False,None

	# Give them this static page now that we've made sure
	#  it's a reasonable list and column
	buf = self.readf("html/v_ilist.html")
	return True,self.send_result(buf, "text/html")

    # Encoded JSON list of available lists
    # /rest/l.json
    def send_json_lists(self):
	if not self.path_match("rest", "l.json"):
	    return False,None
	res = {"user": self.user}
	res["items"] = l = [ {"name": i.name, "owner": i.owner}
	    for i in ilist.ilists.itervalues() ]
	l.sort(key=lambda i: i["name"])
	return True,self.send_json(res)

    # Encoded JSON representing this column
    # /rest/l/<name>/<column# or name>.json
    def send_json_list(self):
	# Verify path, get list name
	pp = self.paths
	if (len(pp) != 4) or (pp[0] != "rest") or (pp[1] != "l"):
	    return False,None

	# Verify list name
	nm = pp[2]
	l = ilist.ilists.get(nm)
	if l is None:
	    return False,None

	# Verify colum name
	col = pp[3]
	if (not col.endswith(".json")) or (len(col) < 6):
	    return False,None
	colname = urllib.unquote_plus(col[:-5])
	col = l.getCol(colname)
	if col is None:
	    return False,None
	colidx = l.cols.index(col)

	# "l" points at our overall list, "c" at our selected column
	# Let the column encode most of the results
	res = l.asDict(colidx)
	res["user"] = self.user

	return True,self.send_json(res)

    # JSON of a new change
    # /rest/l/<name>/<column# or name>/changes.json?gen=XXX
    # We'll sleep if we don't have something new yet
    def send_json_changes(self):
	pp = self.paths
	if (len(pp) != 5) or (pp[0] != "rest") or (pp[1] != "l") or \
		(pp[4] != "changes.json") or ("gen" not in self.vals):
	    return False,None

	# List and column
	nm = pp[2]
	l = ilist.ilists.get(nm)
	if l is None:
	    return False,None
	colname = urllib.unquote_plus(pp[3])
	col = l.getCol(colname)
	if col is None:
	    return False,None
	colidx = l.cols.index(col)

	# Generation
	try:
	    gen = int(self.vals["gen"])
	except:
	    return False,None

	# Waiting for an update
	sys.stderr.write("json changes %d versus %d\n" % (gen, col.gen))
	if gen == (col.gen + 1):
	    resp = delta.await(l, colidx)
	    sys.stderr.write(" changes back %r\n" % (resp,))
	    # We blocked, here is the result

	# If they've fallen behind, tell them just to do a bulk
	#  catch-up
	else:
	    resp = [{"gen": -1}]

	return True,self.send_json(resp)

Added html/shopr.html.



























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<html>
<head>
<title>
shopr
</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/shopr.css">
<script src="/js/shopr.js"></script>
</head>
<body>

<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>
  <input type="text" id="ifilter" placeholder="Choose or Add">
  <span>+</span>
  <img src="/img/menu.svg" />
 </div>
 <div id="items">
 </div>
</div>

<div id="scrmenu" style="display: none;">
</div>

<div id="scritem" style="display: none;">
</div>

<script>
init_page();
</script>

</body>
</html>

Added js/shopr.js.









>
>
>
>
1
2
3
4
//
// shopr.js
//	Main routines for shopping list API access
//

Added js/utils.js.











































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
//
// utils.js
//	Generic functions and wrappers
//
"use strict";

// XHR, PUT style
function xhr_put(url, fn, body = "", btype = null) {
    let req = new XMLHttpRequest();
    req.open("PUT", url);
    if (body) {
	req.setRequestHeader('Content-Type', btype || "text/plain");
    }
    req.onreadystatechange = function() {
	if (req.readyState != 4) {
	    return;
	}
	if (req.status != 200) {
	    // It'll be 0 if we aborted it ourselves
	    if (req.status != 0) {
		alert("Connection failed");
	    }
	    return;
	}

	// Completed operation
	fn(req);
    }
    req.send(body);
    return(req);
}

// XHR, POST style
function xhr_post(url, fn, body = "", btype = null) {
    let req = new XMLHttpRequest();
    req.open("POST", url);
    if (body) {
	req.setRequestHeader('Content-Type', btype || "text/plain");
    }
    req.onreadystatechange = function() {
	if (req.readyState != 4) {
	    return;
	}
	if (req.status != 200) {
	    // It'll be 0 if we aborted it ourselves
	    if (req.status != 0) {
		alert("Connection failed");
	    }
	    return;
	}

	// Completed operation
	fn(req);
    }
    req.send(body);
    return(req);
}

// XHR, GET style
function xhr_get(url, fn) {
    let req = new XMLHttpRequest();
    req.open("GET", url);
    req.onreadystatechange = function() {
	if (req.readyState != 4) {
	    return;
	}
	if (req.status != 200) {
	    if (req.status != 0) {
		alert("Connection failed");
	    }
	    return;
	}

	// Completed operation
	fn(req);
    }
    req.send();
    return(req);
}

// A bit of a shotgun approach to finding out that they're not
//  looking at this app any more.
function catch_bg(fn) {
    document.addEventListener("blur", fn);
    document.addEventListener("mouseleave", fn);
    document.addEventListener("visibilitychange", function () {
        if (document.hidden) {
            fn();
        }
    });
}
// ...and its inverse
function catch_fg(fn) {
    document.addEventListener("focus", fn);
    document.addEventListener("mouseenter", fn);
    document.addEventListener("visibilitychange", function () {
        if (!document.hidden) {
            fn();
        }
    });
}

Added main.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
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
#
# main.py
#	Main driver for list/swiping service
#
import sys
import chore
from chore.authen import Authen_mixin, Authen_Server_mixin
from get import GET_mixin
from post import POST_mixin
from put import PUT_mixin
import ilist

# Tie our various handlers together
class App_Handler(chore.handlers.Chore_Handler,
	GET_mixin, POST_mixin, PUT_mixin, Authen_mixin):
    def __init__(self, conn, tup, approot):
	chore.handlers.Chore_Handler.__init__(self, conn, tup, approot,
	    (GET_mixin.__init__,
	     POST_mixin.__init__,
	     PUT_mixin.__init__,
	     Authen_mixin.__init__,) )

# Load our configuration file
#
# This includes configuring our config file elements,
#  then processing the supplied file.
def load_cfg(fn):

    # A configurator
    c = chore.config.Config()

    # Let the web network side add its config entries
    chore.www.add_config(c)

    # Parse the input
    return c.load_cfg(fn)

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

    def __init__(self, config):

	# Let Chore handle most things
	chore.server.Server.__init__(self, config, App_Handler);

	# Authentication server
	Authen_Server_mixin.__init__(self)
	self.authentication.append( Authen_mixin.auth_server )
	self.init_acct_server()

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

    # Create the server with config
    app = App(load_cfg(sys.argv[1]))

    # Pull current list directory
    ilist.load_ilists()

    # It's an HTTP service
    app.start_http()

    # HTTP threads remain
    sys.exit(0)

Added post.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
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
#
# post.py
#	Mixin to implement HTML POST operations
#
# /l/<listname>
#	Create a new list
#
import urllib
import ilist, delta

# The POST part of our handling
class POST_mixin(object):

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

	# POST handlers
	self.dispatchers.append( ("POST", self.post_newlist) )
	self.dispatchers.append( ("POST", self.post_newitem) )

    # A new list requested?
    def post_newlist(self, buf):
	# /rest/l/LISTNAME
	pp = self.paths
	if (len(pp) != 3) or \
		(pp[0] != 'rest') or (pp[1] != 'l') or \
		(not pp[1]):
	    return False,None
	nm = urllib.unquote_plus(pp[2])
	if not ilist.ok_obname(nm):
	    resp = {"code": "Bad list name"}
	    return True,self.send_json(resp)

	# Can't already exist
	if nm in ilist.ilists:
	    resp = {"code": "List already exists"}
	    return True,self.send_json(resp)
	l = ilist.IList(self.user, nm)
	l.dump()

	# We're in business
	resp = {"code": "OK"}
	return True,self.send_json(resp)

    # A new item requested?
    def post_newitem(self, buf):
	# /rest/l/LISTNAME/COLUMN/ITEM
	#   0   1    2       3     4
	pp = self.paths
	if (len(pp) != 5) or \
		(pp[0] != 'rest') or (pp[1] != 'l') or \
		(not pp[2]) or (not pp[3]) or (not pp[4]):
	    return False,None

	# Verify list name
	lnm = urllib.unquote_plus(pp[2])
	l = ilist.ilists.get(lnm)
	if l is None:
	    resp = {"code": "No such list"}
	    return True,self.send_json(resp)

	# Column name
	colnm = urllib.unquote_plus(pp[3])
	col = l.getCol(colnm)
	if col is None:
	    resp = {"code": "No such column"}
	    return True,self.send_json(resp)
	colidx = l.cols.index(col)

	# Item name.  Can't already exist.
	nm = urllib.unquote_plus(pp[4])
	if nm in col.data:
	    resp = {"code": "Item already exists"}
	    return True,self.send_json(resp)
	if not ilist.ok_obname(nm):
	    resp = {"code": "Bad item name"}
	    return True,self.send_json(resp)

	# Ok, add to items and re-sort
	col.data.append(nm)
	col.data.sort()

	# Sync to filesystem
	l.dump()

	# The display of the item--for this user and all others--comes
	#  from the delta update
	col.gen += 1
	delta.changed("add", col.gen, ("l", lnm, colidx, nm))
	delta.commit()

	# We're in business
	resp = {"code": "OK"}
	return True,self.send_json(resp)

Added put.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
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
#
# put.py
#	Mixin to implement HTML PUT operations
#
# /rest/l/<listname>/<colname>?dx=<dir>
#	Move item right/left (dx = 1/-1)
#
import urllib
import ilist, delta

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

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

	# PUT handlers
	self.dispatchers.append( ("PUT", self.put_dx) )
	self.dispatchers.append( ("PUT", self.put_colname) )

    # /l/<listname>/<colname>
    #  0     1         2
    # Change column's name
    def put_colname(self, buf):
	pp = self.paths
	if (len(pp) != 3) or (pp[0] != 'l'):
	    return False,None

	# List and column name/index
	lnm = urllib.unquote_plus(pp[1])
	l = ilist.ilists.get(lnm)
	if l is None:
	    return False,None
	colnm = urllib.unquote_plus(pp[2]);
	col = l.getCol(colnm)
	if col is None:
	    return False,None
	colidx = l.cols.index(col)

	# Ok name?
	if not ilist.ok_obname(buf):
	    res = {"code": "Illegal name"}
	    return True,self.send_json(res)

	# Change data structure
	col.name = buf
	l.dump()

	# Tell about the change
	delta.changed("change", col.gen, ["l", lnm, colidx], buf)
	if colidx > 0:
	    col2 = l.cols[colidx-1]
	    col2.gen += 1
	    delta.changed("change", col2.gen,
		["l", lnm, colidx-1, "next"], buf)
	if colidx < (len(l.cols)-1):
	    col2 = l.cols[colidx+1]
	    col2.gen += 1
	    delta.changed("change", col2.gen,
		["l", lnm, colidx+1, "prev"], buf)
	delta.commit()

	# Success
	res = {"code": "OK"}
	return True,self.send_json(res)

    # Item motion?
    # /rest/l/<listname>/<colname>/<item>?dx=<dir>
    #   0   1     2          3       4
    def put_dx(self, buf):
	pp = self.paths
	if (len(pp) != 5) or \
		(pp[0] != 'rest') or (pp[1] != 'l') or \
		(not pp[2]) or (not pp[3]) or (not pp[4]):
	    return False,None

	# Direction
	try:
	    dx = int(self.vals["dx"])
	    if dx not in (-1, 1):
		return False,None
	except:
	    return False,None

	# List name
	lnm = urllib.unquote_plus(pp[2])
	l = ilist.ilists.get(lnm)
	if l is None:
	    return False,None

	# Column name & current index
	colnm = urllib.unquote_plus(pp[3]);
	col = l.getCol(colnm)
	if col is None:
	    return False,None
	colidx = l.cols.index(col)

	# Item name
	nm = urllib.unquote_plus(pp[4])
	if (not nm) or (nm not in col.data):
	    return False,None
	nmidx = col.data.index(nm)

	# Not going to be here
	del col.data[nmidx]
	col.gen += 1
	delta.changed("del", col.gen, ["l", lnm, colidx, nm])

	# Going left?
	if dx < 0:
	    # Move to previously column (it's deleted
	    #  once you swipe it off column 0)
	    if colidx > 0:
		col2idx = colidx-1
		col2 = l.cols[col2idx]
	    else:
		col2idx = col2 = None
	else:
	    # Going right
	    col2idx = colidx+1
	    if colidx == (len(l.cols)-1):
		# Need a new column
		col2name = "%d" % (col2idx,)
		col2 = ilist.ICol(col2name)
		l.cols.append(col2)

		# The list changes with a new column
		l.gen += 1
		delta.changed("add", l.gen, ["l", lnm, col2idx])

		# And the notion of "next" becomes real for
		#  the current column
		col.gen += 1
		delta.changed("change", col.gen,
		    ["l", lnm, colidx, "next"], col2name)

	    else:
		col2 = l.cols[col2idx]

	# If our item moved to a different column, update it
	if col2 is not None:
	    col2.data.append(nm)
	    col2.data.sort()
	    col2.gen += 1
	    delta.changed("add", col2idx, ["l", lnm, col2idx, nm])

	# We're in business
	l.dump()
	delta.commit()
	resp = {"code": "OK"}
	return True,self.send_json(resp)