webcalendar

Check-in [12ba32737d]
Login

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

Overview
Comment:Bring up a CSS3 mobile friendly "today" display. Do this by way of Python, adding a webcal.py module to bring in events and interpret them (including repeats).
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:12ba32737d4d52c7f32ae3c20a674a0d1662df6422039fc2adaed4e31f2f37fe
User & Date: ajv-899-334-8894@vsta.org 2017-05-30 03:18:41
Context
2017-05-30
04:29
Code comment from "Hamdi_C", issue 2729 at SF Webcalendar. Reuse of variable name interferes with correct deltion of repeating events. check-in: 54a93e819b user: ajv-899-334-8894@vsta.org tags: master, trunk
03:18
Bring up a CSS3 mobile friendly "today" display. Do this by way of Python, adding a webcal.py module to bring in events and interpret them (including repeats). check-in: 12ba32737d user: ajv-899-334-8894@vsta.org tags: master, trunk
2017-05-29
22:55
Add some Github goodies check-in: 814a656a3e user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added cgi-bin/.gitignore.



>
1
*.pyc

Added cgi-bin/phpsession.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
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
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
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
#
# phpsession.py
#	Read PHP session state from a file
#
# This code is from:
#	https://github.com/hackedd/python-phpsession
# And is used under its "MIT" style license.  See that project
#  for details.  Its use is gratefully acknowledged here.
#
from StringIO import StringIO
import string

class SessionData:
    PS_DELIMITER = "|"
    PS_UNDEF_MARKER = "!"

    def __init__(self, data):
        self.data = data
        self.length = len(data)

    def values(self):
        i = 0
        while i < self.length:
            delim = self.data.find(self.PS_DELIMITER, i)
            if delim == -1:
                break

            key = self.data[i:delim]
            value, value_len = self.unserialize(delim + 1)

            yield key, value

            i = delim + 1 + value_len

        if i < self.length:
            raise Exception("Unable to decode session: trailing data")

    def unserialize(self, offset):
        if self.data[offset] == self.PS_UNDEF_MARKER:
            return None, 1

        stream = StringIO(self.data[offset:])
        try:
            value = unserialize(stream)
            return value, stream.tell()
        except AssertionError as ex:
            raise Exception("Unable to decode session: %s at offset %d" %
                            (ex, offset + stream.tell()))


class PHPObject:
    def __init__(self, class_name, **kwargs):
        self.__class_name = class_name
        self.__attributes = kwargs

        self.__protected = {}
        self.__private = {}
        self.__public = {}

        for key, value in kwargs.iteritems():
            if key.startswith("\x00"):
                class_name, _, property_name = key[1:].partition("\x00")
                if class_name == "*":   # protected
                    self.__protected[property_name] = value
                else:
                    self.__private[(class_name, property_name)] = value
            else:
                self.__public[key] = value

    @property
    def class_name(self):
        return self.__class_name

    def get_attributes(self):
        return self.__attributes

    def __getattr__(self, name):
        if name in self.__public:
            return self.__public[name]
        if name in self.__protected:
            return self.__protected[name]
        for (class_name, property_name), value in self.__private.iteritems():
            if property_name == name:
                return value
        raise AttributeError(name)

    def __repr__(self):
        values = self.__attributes.items()
        args = [repr(self.__class_name)] + ["%s=%r" % i for i in values]
        return "PHPObject(%s)" % (", ".join(args))


def expect(stream, value):
    actual = stream.read(len(value))
    assert actual == value, "expected %r, got %r" % (value, actual)


def read_iv(stream, endchar=";"):
    """Read a signed integer value from the stream."""

    c = stream.read(1)
    if c == "-":
        sign = -1
        c = stream.read(1)
    elif c == "+":
        sign = 1
        c = stream.read(1)
    else:
        sign = 1

    value = 0
    while c in string.digits:
        value = value * 10 + int(c)
        c = stream.read(1)

    assert c == endchar, "read_iv: expected %r, got %r" % (endchar, c)
    return sign * value


def read_uiv(stream, endchar=";"):
    """Read a unsigned integer value from the stream."""

    c = stream.read(1)
    if c == "+":
        c = stream.read(1)

    value = 0
    while c in string.digits:
        value = value * 10 + int(c)
        c = stream.read(1)

    assert c == endchar, "read_uiv: expected %r, got %r" % (endchar, c)
    return value


def unserialize_str(stream, length):
    """Unserialize a string, processing any character escapes."""

    output = ""
    for i in range(length):
        c = stream.read(1)
        if c == "\\":
            output += chr(int(stream.read(2), 16))
        else:
            output += c
    return output


def read_nested_data(stream, elements, is_object=False, start_char="{"):
    """Reads a list of key, value pairs from the stream."""

    expect(stream, start_char)

    values = []
    for i in range(elements):
        key = unserialize(stream)
        assert isinstance(key, (basestring, int)), \
            "array or object key should be integer or string"

        if is_object:
            key = str(key)

        value = unserialize(stream)
        values.append((key, value))

    expect(stream, "}")

    return values


def unserialize_arrayobject(stream):
    """Reads and unserializes an PHP ArrayObject from the stream."""

    expect(stream, "x:")
    flags = unserialize(stream)

    peek = stream.read(1)
    stream.seek(stream.tell() - 1)

    if peek == "m":
        array = None
    elif peek in "aOC":
        array = unserialize(stream)
    else:
        raise ValueError("ArrayObject: expected 'm', array or object")
    expect(stream, ";")

    expect(stream, "m:")
    members = unserialize(stream)

    return PHPObject("ArrayObject", flags=flags, array=array, members=members)


custom_unserialize = {
    "ArrayObject": unserialize_arrayobject
}


def unserialize(stream):
    if isinstance(stream, basestring):
        stream = StringIO(stream)

    typechar = stream.read(1)

    # Allow N; for NULL
    if typechar == "N":
        expect(stream, ";")
    else:
        expect(stream, ":")

    if typechar in "Rr":
        id_ = read_iv(stream) - 1
        raise NotImplementedError("Reference")

    elif typechar == "N":
        return None

    elif typechar == "b":
        value = stream.read(1)
        expect(stream, ";")
        return value == "1"

    elif typechar == "i":
        value = read_iv(stream)
        return value

    elif typechar == "d":
        c = stream.read(1)
        value = ""
        while c != ";":
            value += c
            c = stream.read(1)
        assert c == ";", "double: expected ';', got %r" % (endchar, c)
        # Just convert ot float, this also works for NaN and [+-]Inf
        return float(value)

    elif typechar == "s":
        length = read_uiv(stream, ":")
        expect(stream, "\"")
        value = stream.read(length)
        expect(stream, "\";")
        return value

    elif typechar == "S":
        length = read_uiv(stream, ":")
        expect(stream, "\"")
        value = unserialize_str(stream, length)
        expect(stream, "\";")
        return value

    elif typechar == "a":
        length = read_iv(stream, ":")
        values = read_nested_data(stream, length)

        keys = [k for (k, v) in values]
        if sorted(keys) == range(length):
            return [v for (k, v) in values]
        else:
            return dict(values)

    elif typechar in "oOC":
        if typechar == "o":
            class_name = "stdClass"
        else:
            length = read_uiv(stream, ":")
            expect(stream, "\"")
            class_name = stream.read(length)
            expect(stream, "\"")
            expect(stream, ":")

        if typechar == "C":
            length = read_uiv(stream, ":")
            expect(stream, "{")
            if class_name in custom_unserialize:
                obj = custom_unserialize[class_name](stream)
            else:
                serialized = stream.read(length)
                obj = PHPObject(class_name, _serialized=serialized)
            expect(stream, "}")
            return obj

        length = read_iv(stream, ":")
        start_char = "\"" if typechar == "o" else "{"
        values = dict(read_nested_data(stream, length, True, start_char))
        return PHPObject(class_name, **values)

    else:
        raise ValueError("Unknown type character '%s'" % typechar)


def serialize(obj):
    raise NotImplemented


def load(fp):
    return loads(fp.read())


def loads(string):
    data = SessionData(string)
    return dict(data.values())


def dump(data, fp):
    raise NotImplemented


def dumps(data):
    raise NotImplemented

Added cgi-bin/today.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
#!/usr/bin/python
#
# today.py
#	Display events for today
#
import sys, time, os
import Cookie, webcal, phpsession
from webcal import Today, cmp_time, Today, CALBASE

# Common HTTP response header
print """Content-Type: text/html

<html>
"""

try:
    # Hopefully our PHP session cookie
    cookie = Cookie.SimpleCookie(os.environ["HTTP_COOKIE"])
    sessid = cookie["PHPSESSID"].value
    if ".." in sessid:
	sessid = "XXX"
    f = open("/var/lib/php5/sessions/sess_" + sessid, "r")
    sess = phpsession.load(f)
    uname = sess["webcal_login"]
    f.close()
except:
    print """<head>
    <title>Bad Webcalendar Session</title>
    <meta http-equiv="REFRESH" content="1;url=/webcal" />
   </head>
   <body>
    Redirecting you to Webcalendar for login...
   </body>
</html>
"""
    sys.exit(0)


# Shortcut; we use it a lot
w = sys.stdout.write

# Pixels per line of output
LINEPIX = 25

def filter_today(ev):
    global Today
    res = ev.happens(Today)
    return res

if __name__ == "__main__":
    evs = sorted(webcal.load_user(uname, filter_today),
	cmp=lambda ev1,ev2: cmp_time(ev1.begin, ev2.begin))

    # Assign columns
    rows = {}
    col = {}
    untimed = set()
    for ev in tuple(evs):

	# Don't tally untimed events (which look like 12AM)
	if ev.untimed:
	    untimed.add(ev)
	    evs.remove(ev)
	    continue

	hour = ev.begin.tm_hour
	hourend = ev.end.tm_hour
	while True:
	    depth = rows.get(hour, 0) + 1
	    rows[hour] = depth
	    if depth > col.get(ev, 0):
		col[ev] = depth
	    hour += 1
	    if hour > hourend:
		break

    hours = set(rows.iterkeys())
    nhour = (max(hours) - min(hours)) + 1
    w("""<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>%s</title>
</head>
<link rel="stylesheet" type="text/css" href="/webcal/includes/css/cal.css" />
<script src="/webcal/includes/js/cal.js"></script>

<body>
<table>
 <tr class="callist" style="height: %dpx;">
  <td id="caltimes">
""" % (time.strftime("%A %B %d %Y"), nhour*LINEPIX))

    # Label for untimed first
    idx = 0
    if untimed:
	w('<span style="left: 0px; top: 0px;">---</span>\n')
	idx += len(untimed)

    # An hour label for each row, in its own table column
    for hour in xrange(min(hours), max(hours)+1):
	w('<span style="left: 0px; top: %dpx;">%02d:00</span>\n' %
	     (idx*LINEPIX, hour))
	idx += 1
    w("""</td>
  <td id="calevents">
""")

    #
    # Now the actual events, each at their own hour and column offset
    #

    # Untimed events first
    houridx = 0
    for ev in untimed:
	w('<span onclick="view_ev(%d);"' % (ev.evid,))
	w(' style="left: 0px; top: %dpx;' %
	     (houridx * LINEPIX,))
	if ev.color:
	    w(' color: %s;' % (ev.color,))
	w('">%s</span>\n' % (ev.name,))
	houridx += 1

    # Divvy up display width across columns.  Assume some used
    #  for leading hour column.
    colpix = 80/(max(rows.itervalues()))
    maxcol = int(colpix * 1.2)

    curidx = 0
    for hour in xrange(min(hours), max(hours)+1):
	while (curidx < len(evs)) and (evs[curidx].begin.tm_hour == hour):
	    ev = evs[curidx]
	    w(
	     '<span onclick="view_ev(%d);" class="timedevent"' % (ev.evid,))
	    w(
	     ' style="left: %dvw; top: %dpx; height: %dpx;' %
	     (((col[ev] - 1) * colpix),
	      houridx * LINEPIX, max(ev.dur / 60.0, 0.75) * LINEPIX))
	    w(' max-width: %dvw; z-index: %d;' % (maxcol, col[ev]))
	    if ev.color:
		w(' color: %s;' % (ev.color,))
	    w('">%s</span>\n' % (ev.name,))
	    curidx += 1
	houridx += 1
    w("""</td>
 </tr>
</table>

</body>
</html>
""")

Added cgi-bin/webcal.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
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
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
#
# today.py
#	Pull all events from a icalendar, display today's
#
import time
import MySQLdb as mysql
import pdb

# Indices into webcal_entry
WE_ID = 0	# Entry's ID
WE_WHEN = 4	# Date (and time at 5)
WE_DUR = 8	# Duration (minutes)
WE_NAME = 14	# Event's name

# Indices into webcal_entry_repeats
WER_TYP = 1	# Type (weekly/monthly/etc.)
WER_END = 2	# End date/time (including time at 3)
WER_FREQ = 4	# "frequency" (actually more of an interval)
WER_WDAY = 8	# Day of week, optionally "nth"
WER_YDAY = 12	# Day of week within year
WER_CNT = 13	# Count

# Indices in webcal_entry_repeats_not
WEX_WHEN = 1	# Date of exclusion

# Map from abbreviated weekday name to ical standard index
DAYIDX = {"mo": 0, "tu": 1, "we": 2, "th": 3, "fr": 4, "sa": 5, "su": 6}

# Load Webcalendar config into cfg{}
HTBASE = "/webcal/"
CALBASE = "/var/www/vatclip/htdocs" + HTBASE
CFG = CALBASE + "includes/settings.php"
cfg = {}
def load_cfg(fn):
    global cfg

    f = open(CFG, "r")
    for l in f:
	tup = l.strip().split(": ")
	if len(tup) != 2:
	    continue
	cfg[tup[0]] = tup[1]
    f.close()
load_cfg(CFG)

# Database connection
db = mysql.connect(cfg["db_host"],
    cfg["db_login"], cfg["db_password"], db=cfg["db_database"])

# Map from abbreviated weekday name to ical standard index
DAYIDX = {"mo": 0, "tu": 1, "we": 2, "th": 3, "fr": 4, "sa": 5, "su": 6}

# Precalcs
#
# Seconds in a day
DAYSECS = 24*60*60
WEEKSECS = DAYSECS*7

# Everybody is going to look at this; grab it once
# Calculate for *beginning* of current day
today = Today = None
def set_today():
    global DAYSECS
    global today, Today

    # Calculate start of today
    t = time.time()
    tm = list(time.localtime(t))
    tm[3] = tm[4] = 0
    today = time.mktime(tm)
    Today = time.localtime(today)
set_today()

# Return for this day of the week, which week it is
# 1..7 -> 1, 8..15 -> 2, etc.
def week_idx(mday):
    return ((mday-1) / 7)+1

# Return mktime of a plain date
def daydecode(tup, idx):
    # YYYYMMDD
    dtstart = int(tup[idx])
    assert dtstart > 0
    year = dtstart / 10000
    month = (dtstart / 100) % 100
    day = dtstart % 100

    # Technically, an untimed event--no timezone offset is applied
    return time.mktime( (year, month, day, 0, 0, 0, 0, 0, -1) )

# Return mktime of date/time database fields
def tmdecode(tup, idx):
    # Is it just a day?
    tmstart = int(tup[idx+1])
    if tmstart < 0:
	return daydecode(tup, idx)

    # YYYYMMDD
    dtstart = int(tup[idx])
    assert dtstart > 0
    year = dtstart / 10000
    month = (dtstart / 100) % 100
    day = dtstart % 100

    # HHMMSS
    hour = tmstart / 10000
    min = (tmstart / 100) % 100
    sec = tmstart % 100

    # Convert to system time (everything in the DB is in Zulu)
    tm = time.mktime( (year, month, day, hour, min, sec, 0, 0, -1) )
    return tm-time.timezone

# Tell if these two match to the day level
def daymatch(tm1, tm2):
    return (tm1.tm_year == tm2.tm_year) and \
	    (tm1.tm_mon == tm2.tm_mon) and \
	    (tm1.tm_mday == tm2.tm_mday)

# Tell if @evtm is beyond the day of @targtm
def beyond(evtm, targtm):
    t = cmp(evtm.tm_year, targtm.tm_year)
    if t < 0:
	return False
    if t > 0:
	return True
    t = cmp(evtm.tm_mon, targtm.tm_mon)
    if t < 0:
	return False
    if t > 0:
	return True
    return (evtm.tm_mday > targtm.tm_mday)

# Compare strictly based on time fields, ignoring date
def cmp_time(tm1, tm2):
    res = cmp(tm1.tm_hour, tm2.tm_hour)
    if res:
	return res
    res = cmp(tm1.tm_min, tm2.tm_min)
    if res:
	return res
    return cmp(tm1.tm_sec, tm2.tm_sec)

# Tell if tm2 overlaps at all with tm1's range
# (tm2 is assumed to lie at or later than tm1 due to pre-ordering.)
def overlap_time(tm1, dur, tm2):
    pdb.set_trace()

# Each Event gets one of these
class Event(object):
    def __repr__(self):
	return "Event(%s)" % (self.name,)

    def __init__(self, evid, name, begin, dur, rep):
	self.evid = evid
	self.name = name
	self.begin = time.localtime(begin)
	self.dur = dur
	self.end = time.localtime(begin + 60*dur)
	self.rep = rep
	self.untimed = self.color = None

    # Tell if this event happens on the day of @dt
    def happens(self, dt):
	beg = self.begin

	# If we start beyond the @dt, nope
	if beyond(beg, dt):
	    return False

	# On this day
	if daymatch(dt, beg):
	    return True

	# Or under a repetition thereof
	if self.rep is None:
	    return False
	return self.rep.applies(dt)

# An excluded/included date
class Exclusion(object):

    def __init__(self, tup):
	self.when = time.localtime(daydecode(tup, WEX_WHEN))
	self.excluded = bool(int(tup[2]))

    def applies(self, tm):
	when = self.when
	return daymatch(when, tm)

# Represent repetition of events, including exceptions
class Repeat(object):
    def __init__(self, base, rep, nots):
	self.base = base
	self.basetm = time.localtime(base)
	if rep[WER_END]:
	    self.end = tmdecode(rep, WER_END)
	    self.endtm = time.localtime(self.end)
	else:
	    self.endtm = self.end = None
	self.intvl = int(rep[WER_FREQ])
	if nots:
	    self.excluded = set(Exclusion(e) for e in nots)
	else:
	    self.excluded = ()
	c = rep[WER_CNT]
	if c is None:
	    self.count = None
	else:
	    self.count = int(c)

	# Hook for recoding specific dope out of the repetition tuple
	self.init2(rep)

    # Default, no-op
    def init2(self, rep):
	pass

    # Tell if this repetition applies to @target date
    def applies(self, target):

	# Past final applicable date?
	if self.endtm is not None:
	    if target > self.endtm:
		return False

	# Can't apply if expressly excluded
	for e in self.excluded:
	    if not e.excluded:
		continue
	    if e.applies(target):
		return False

	# Repetition-specific reason
	return self._applies(target)

# Daily repeat
class RepeatDaily(Repeat):

    def _applies(self, target):
	assert self.intvl == 1

	# This day or one within the counted range?
	days = (time.mktime(target) - self.base) / DAYSECS
	return int(days) < (self.count or 1)

# Weekly repeat
class RepeatWeekly(Repeat):

    def _applies(self, target):

	# Not right day of week
	basetm = self.basetm
	if self.basetm.tm_wday != target.tm_wday:
	    return False

	# This many weeks
	if self.count is not None:
	    t = self.base
	    for x in xrange(self.count):
		t2 = t + (x * WEEKSECS * self.intvl)
		t2tm = time.localtime(t2)
		if daymatch(t2tm, target):
		    return True
	    return False

	assert self.intvl == 1
	return True

# Monthly, by day of week
class RepeatMonthlyDay(Repeat):

    def init2(self, rep):
	# "2TU" means 2nd Tuesday of the month
	wday = rep[WER_WDAY].lower()
	dow = wday[-2:]
	assert self.basetm.tm_wday == DAYIDX[dow]
	wday = wday[:-2]
	if wday:
	    self.offset = int(wday)
	else:
	    self.offset = None

    def _applies(self, target):

	# Not right day of week
	basetm = self.basetm
	if self.basetm.tm_wday != target.tm_wday:
	    return False

	# Every Friday, etc.
	if self.offset == None:
	    return True

	# This one?
	return self.offset == week_idx(target.tm_mday)

# Monthly, by date in month
class RepeatMonthlyDate(Repeat):

    def _applies(self, target):
	basetm = self.basetm
	if basetm.tm_mday != target.tm_mday:
	    return False
	if self.intvl == 1:
	    return True
	mdiff = (12 + (target.tm_mday - basetm.tm_mday)) % 12
	return ((mdiff + 1) % self.intvl) == 0

# Yearly
class RepeatYearly(Repeat):
    def init2(self, rep):
	assert rep[WER_YDAY].lower() in DAYIDX
	assert self.intvl == 1

    def _applies(self, target):
	basetm = self.basetm
	return (target.tm_mon == basetm.tm_mon) and \
	    (target.tm_mday == basetm.tm_mday)

# Manually entered dates
class RepeatManual(Repeat):

    # In our base date, or any extra ones?
    def _applies(self, target):
	basetm = self.basetm
	if daymatch(target, basetm):
	    return True
	for e in self.excluded:
	    if e.excluded:
		continue
	    if e.applies(target):
		return True
	return False

# Convert SQL repetition table into local data structure
def as_rep(base, tup):
    global db

    c = db.cursor()
    entid = int(tup[WE_ID])
    c.execute("""select * from webcal_entry_repeats
	    where cal_id = %s""", (tup[WE_ID],))
    tup2 = c.fetchone()
    c.close()
    if not tup2:
	return None

    c = db.cursor()
    c.execute("""select * from webcal_entry_repeats_not
	    where cal_id = %s""", (tup[WE_ID],))
    nots = c.fetchall()
    c.close()

    # Decode repetition type
    typ = tup2[WER_TYP]
    if typ == "daily":
	cl = RepeatDaily
    elif typ == "weekly":
	cl = RepeatWeekly
    elif typ == "monthlyByDay":
	cl = RepeatMonthlyDay
    elif typ == "monthlyByDate":
	cl = RepeatMonthlyDate
    elif typ == "yearly":
	cl = RepeatYearly
    elif typ == "manual":
	cl = RepeatManual
    else:
	raise Exception, "Unsupported repetition: " + typ

    # Create a suitable Repetition instance
    return cl(base, tup2, nots)

# Convert SQL result into a local data structure
def as_event(tup):

    # Timestamp of event start and end
    tmstart = tmdecode(tup, WE_WHEN)
    dur = int(tup[WE_DUR])

    # Repetition?
    rep = as_rep(tmstart, tup)

    # Represent the Event
    ev = Event(int(tup[WE_ID]), tup[WE_NAME], tmstart, dur, rep)

    # Note if it was an untimed Event
    ev.untimed = ((tup[WE_WHEN+1] < 0) or (ev.dur == 0))

    return ev

def load_cal(calname, filter):

    c = db.cursor()
    c.execute("""select we.* from
	    webcal_entry we, webcal_entry_user weu where
	    we.cal_id = weu.cal_id and weu.cal_login = %s""",
	(calname,))
    res = set()
    for tup in c:
	c2 = db.cursor()
	c2.execute("""select cal_status from webcal_entry_user
		where cal_id = %s and cal_login = %s""",
	    (int(tup[0]), calname))

	# Check for deleted events
	tup2 = c2.fetchone()
	c2.close()
	if tup2 is not None:
	    if tup2[0] == 'D':
		continue

	# Encode the event
	ev = as_event(tup)

	# Gather relevant ones
	if (filter is None) or filter(ev):
	    res.add(ev)

    c.close()

    return res

# Load user's calendar, along with any layers they like to include
def load_user(uname, filter):

    # Get our own entries
    entries = load_cal(uname, filter)
    c = db.cursor()

    # And any layers we desire
    c.execute("""select cal_layeruser,cal_color from webcal_user_layers
	    where cal_login = %s""", (uname,))
    for tup in c:
	for ev in load_cal(tup[0], filter):
	    ev.color = tup[1]
	    entries.add(ev)
    c.close()

    return entries

Added includes/css/cal.css.

















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
body {
    background-color: LightGray;
}
.callist {
    width: 100%;
    vertical-align: top;
}
.callist td {
    position: relative;
}
.callist > td:first-child {
    width: 5vw;
}
.callist > td:nth-child(2) {
    width: 90vw;
}
.callist span {
    position: absolute;
}
.timedevent {
    /* Just a color I pulled out of a picker... */
    background-color: #d3a5ac;
    border: 1px solid black;
}

Added includes/js/cal.js.



















>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
//
// cal.js
//	Supporting JS functions for Webcalendar access
//

// View details of this event ID
function view_ev(idx) {
    window.location.href = "/webcal/view_entry.php?id=" + idx.toString();
}