Graham Northup 4 gadus atpakaļ
revīzija
32dd2d60fe

+ 4
- 0
.gitignore Parādīt failu

@@ -0,0 +1,4 @@
1
+*.pyc
2
+secrets.*
3
+*.db
4
+*.swp

+ 391
- 0
model.py Parādīt failu

@@ -0,0 +1,391 @@
1
+import os, sqlite3, json, urllib2, ssl, urllib, time, subprocess, socket
2
+
3
+from flask import render_template_string
4
+from util import *
5
+import secrets
6
+
7
+#db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'pluto.db'), check_same_thread = False)
8
+db = sqlite3.connect('/var/www/pluto/pluto.db', check_same_thread = False)
9
+cur = db.cursor()
10
+
11
+so = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
12
+so.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
13
+so6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
14
+so6.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
15
+
16
+class DBError(Exception):
17
+    pass
18
+
19
+class NoSuchEntity(DBError):
20
+    pass
21
+
22
+class TooManyEntities(DBError):
23
+    pass
24
+
25
+class DBObject(object):
26
+    __FIELDS__ = ()
27
+    __DEFAULTS__ = {}
28
+    __TABLE__ = ''
29
+    __TYPES__ = {}
30
+    AUTO_COMMIT = True
31
+
32
+    def __init__(self, rowid, *data):
33
+        self.rowid = rowid
34
+        for idx, field in enumerate(self.__FIELDS__):
35
+            default = self.__DEFAULTS__.get(field)
36
+            if idx < len(data):
37
+                setattr(self, field, data[idx])
38
+            else:
39
+                setattr(self, field, default)
40
+
41
+    @classmethod
42
+    def create_table(cls):
43
+        cur.execute('CREATE TABLE IF NOT EXISTS %(table)s (%(columns)s)'%\
44
+                {'table': cls.__TABLE__,
45
+                 'columns': ', '.join('%s%s'%(field, ' '+cls.__TYPES__[field] if field in cls.__TYPES__ else '') for field in cls.__FIELDS__)}
46
+        )
47
+
48
+    @classmethod
49
+    def create(cls, *data):
50
+        row = list(data)
51
+        for field in cls.__FIELDS__[len(data):]:
52
+            row.append(cls.__DEFAULTS__[field])
53
+        cur.execute('INSERT INTO %(table)s VALUES (%(fields)s)'%{
54
+            'table': cls.__TABLE__,
55
+            'fields': ', '.join(['?'] * len(cls.__FIELDS__))
56
+        }, row)
57
+        if cls.AUTO_COMMIT:
58
+            db.commit()
59
+        return cls(cur.lastrowid, *row)
60
+
61
+    def delete(self):
62
+        cur.execute('DELETE FROM %(table)s WHERE ROWID=?'%{'table': self.__TABLE__}, (self.rowid,))
63
+        if self.AUTO_COMMIT:
64
+            db.commit()
65
+
66
+    def update(self):
67
+        cur.execute('UPDATE %(table)s SET %(fields)s WHERE ROWID=?'%{
68
+            'table': self.__TABLE__,
69
+            'fields': ', '.join('%s=?'%(field,) for field in self.__FIELDS__)
70
+        }, tuple(getattr(self, field) for field in self.__FIELDS__) + (self.rowid,))
71
+        if self.AUTO_COMMIT:
72
+            db.commit()
73
+
74
+    @classmethod
75
+    def get(cls, **criteria):
76
+        pairs = criteria.items()
77
+        keys = [pair[0] for pair in pairs]
78
+        values = [pair[1] for pair in pairs]
79
+        cur.execute('SELECT ROWID, %(fields)s FROM %(table)s WHERE %(criteria)s'%{
80
+            'table': cls.__TABLE__,
81
+            'fields': ', '.join(cls.__FIELDS__),
82
+            'criteria': ' and '.join('%s=?'%(k,) for k in keys),
83
+        }, values)
84
+        return [cls(*row) for row in cur]
85
+
86
+    @classmethod
87
+    def all(cls):
88
+        cur.execute('SELECT ROWID, %(fields)s FROM %(table)s'%{
89
+            'table': cls.__TABLE__,
90
+            'fields': ', '.join(cls.__FIELDS__),
91
+        })
92
+        return [cls(*row) for row in cur]
93
+
94
+    @classmethod
95
+    def sorted(cls, by, limit=None):
96
+        cur.execute('SELECT ROWID, %(fields)s FROM %(table)s ORDER BY %(by)s %(limit)s'%{
97
+            'table': cls.__TABLE__,
98
+            'fields': ', '.join(cls.__FIELDS__),
99
+            'by': by,
100
+            'limit': ('' if limit is None else 'LIMIT %d'%(limit,)),
101
+        })
102
+        return [cls(*row) for row in cur]
103
+
104
+    @classmethod
105
+    def get_one(cls, **criteria):
106
+        res = cls.get(**criteria)
107
+        if len(res) < 1:
108
+            raise NoSuchEntity(cls, criteria)
109
+        elif len(res) > 1:
110
+            raise TooManyEntities(cls, criteria)
111
+        return res[0]
112
+
113
+    def __repr__(self):
114
+        return '<%(cls)s(%(table)s %(row)d %(items)s'%{
115
+                'table': self.__TABLE__,
116
+                'cls': type(self).__name__,
117
+                'row': self.rowid,
118
+                'items': ' '.join('%s=%r'%(field, getattr(self, field)) for field in self.__FIELDS__),
119
+        }
120
+
121
+
122
+class Log(DBObject):
123
+    __TABLE__ = 'log'
124
+    __FIELDS__ = ('time', 'path', 'headers', 'data', 'hooks')
125
+
126
+    @classmethod
127
+    def most_recent(cls, n=None):
128
+        return cls.sorted('time DESC', n)
129
+
130
+class DebugLog(DBObject):
131
+    __TABLE__ = 'debuglog'
132
+    __FIELDS__ = ('time', 'path', 'headers', 'data', 'hook', 'cond', 'act', 'success', 'message')
133
+
134
+    @classmethod
135
+    def most_recent(cls, n=None):
136
+        return cls.sorted('time DESC', n)
137
+
138
+class Hook(DBObject):
139
+    __TABLE__ = 'hooks'
140
+    __FIELDS__ = ('name', 'author', 'disabled', 'debugged')
141
+    __DEFAULTS__ = {
142
+        'disabled': 0,
143
+        'debugged': 0,
144
+    }
145
+
146
+    def trigger(self, path, headers, data, response):
147
+        if self.disabled:
148
+            return False
149
+        conditions = Condition.for_hook(self)
150
+        actions = Action.for_hook(self)
151
+        for condition in conditions:
152
+            result = condition.test_select(path, headers, data, response)
153
+            if self.debugged:
154
+                DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, condition.rowid, None, result, None)
155
+            if not result:
156
+                break
157
+        else:
158
+            for act in actions:
159
+                result = act.actuate(path, headers, data, response)
160
+                if self.debugged:
161
+                    DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, act.rowid, None, result)
162
+            if self.debugged:
163
+                DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, None, True, None)
164
+            return True
165
+        if self.debugged:
166
+            DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, None, False, None)
167
+        return False
168
+    
169
+class Condition(DBObject):
170
+    __TABLE__ = 'conditions'
171
+    __FIELDS__ = ('hook', 'selector', 's1', 's2', 's3', 'test', 't1', 't2', 't3', 'invert')
172
+
173
+    @classmethod
174
+    def for_hook(cls, hook):
175
+        return cls.get(hook=hook.rowid)
176
+
177
+    def get_hook(self):
178
+        return Hook.get_one(rowid=self.hook)
179
+
180
+    def select(self, path, headers, data, response):
181
+        return getattr(self, 'select_' + self.selector, self.no_select)(path, headers, data, response)
182
+
183
+    def no_select(self, path, headers, data, response):
184
+        return None
185
+
186
+    def select_header(self, path, headers, data, response):
187
+        return headers.get(self.s1, '')
188
+
189
+    def select_JSON(self, path, headers, data, response):
190
+        if not isinstance(data, dict):
191
+            return False
192
+        cur = data
193
+        for part in self.s1.split('.'):
194
+            cur = cur.get(part)
195
+            if cur is None:
196
+                return False
197
+        return str(cur)
198
+
199
+    def select_path(self, path, headers, data, response):
200
+        return path
201
+
202
+    def test_value(self, val):
203
+        try:
204
+            result = getattr(self, 'test_' + self.test, self.no_test)(val)
205
+        except (ValueError, TypeError):
206
+            result = False
207
+        if self.invert:
208
+            result = not result
209
+        return result
210
+
211
+    def no_test(self, val):
212
+        return False
213
+
214
+    def test_equal(self, val):
215
+        return str(val) == self.t1
216
+
217
+    def test_inrange(self, val):
218
+        return float(self.t1) <= float(val) <= float(self.t2)
219
+
220
+    def test_truthy(self, val):
221
+        return bool(val)
222
+
223
+    def test_contains(self, val):
224
+        return self.t1 in val
225
+
226
+    def test_select(self, path, headers, data, response):
227
+        return self.test_value(self.select(path, headers, data, response))
228
+
229
+class Action(DBObject):
230
+    __TABLE__ = 'actions'
231
+    __FIELDS__ = ('hook', 'action', 'a1', 'a2', 'a3')
232
+
233
+    GITLAB_API = 'https://gitlab.cosi.clarkson.edu/api/v3/'
234
+    GITLAB_TOKEN = secrets.GITLAB_TOKEN
235
+    PROTO = ssl.PROTOCOL_TLSv1_2
236
+
237
+    @classmethod
238
+    def for_hook(cls, hook):
239
+        return cls.get(hook=hook.rowid)
240
+
241
+    def get_hook(self):
242
+        return Hook.get_one(rowid=self.hook)
243
+
244
+    def actuate(self, path, headers, data, response):
245
+        try:
246
+            return getattr(self, 'act_' + self.action, self.no_act)(path, headers, data, response)
247
+        except (ValueError, TypeError):
248
+            pass
249
+
250
+    def no_act(self, path, headers, data, response):
251
+        return 'INTERNAL ERROR: ACTION NOT FOUND'
252
+
253
+    def act_post(self, path, headers, data, response):
254
+        args = {'path': path, 'headers': headers, 'data': data}
255
+        url = render_template_string(self.a1, **args)
256
+        postdata = render_template_string(self.a2, **args)
257
+        headers = json.loads(render_template_string(self.a3, **args))
258
+        print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
259
+        req = urllib2.Request(url, postdata, headers)
260
+        ctxt = ssl.SSLContext(self.PROTO)
261
+        result = urllib2.urlopen(req, context=ctxt)
262
+        out = result.read()
263
+        #out = None
264
+        print 'Complete, got', repr(out)
265
+        return out
266
+
267
+    def act_gitlab(self, path, headers, data, response):
268
+        args = {'path': path, 'headers': headers, 'data': data}
269
+        url = self.GITLAB_API + render_template_string(self.a1, **args)
270
+        params = json.loads(render_template_string(self.a2, **args))
271
+        headers = json.loads(render_template_string(self.a3, **args))
272
+        headers.update({'PRIVATE-TOKEN': self.GITLAB_TOKEN})
273
+        postdata = urllib.urlencode(params)
274
+        print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
275
+        req = urllib2.Request(url, postdata, headers)
276
+        ctxt = ssl.SSLContext(self.PROTO)
277
+        result = urllib2.urlopen(req, context=ctxt)
278
+        out = result.read()
279
+        #out = None
280
+        print 'Complete, got', repr(out)
281
+        return out
282
+
283
+    def act_system(self, path, headers, data, response):
284
+        args = {'path': path, 'headers': headers, 'data': data}
285
+        cmd = render_template_string(self.a1, **args)
286
+        if not self.a2:
287
+			proc = subprocess.Popen(cmd, shell=True)
288
+			return 'forked'
289
+        else:
290
+			try:
291
+				return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
292
+			except subprocess.CalledProcessError as e:
293
+				return e.output
294
+
295
+    def act_udp(self, path, headers, data, response):
296
+        args = {'path': path, 'headers': headers, 'data': data}
297
+        dest = render_template_string(self.a1, **args)
298
+        packet = render_template_string(self.a2, **args)
299
+        encoding = render_template_string(self.a3, **args)
300
+        try:
301
+            if encoding in (u'hex', u'base64'):
302
+                packet = packet.decode(encoding)
303
+            elif encoding == 'input':
304
+                packet = str(data)
305
+            elif encoding == 'json':
306
+                packet = jdumps(data)  # XXX HACKS
307
+            else:
308
+                packet = packet.encode(encoding)
309
+        except Exception as e:
310
+            return 'failed to encode packet: ' + str(e)
311
+        host, _, port = dest.partition(':')
312
+        if not _:
313
+            return 'illegal specification: no port in destination'
314
+        try:
315
+            port = int(port)
316
+        except ValueError:
317
+            return 'illegal port value: ' + port
318
+        if port < 0 or port > 65535:
319
+            return 'illegal port value: ' + str(port)
320
+        try:
321
+            res = socket.getaddrinfo(host, port)
322
+        except socket.gaierror:
323
+            return 'bad hostname:' + host
324
+        for fam, tp, proto, canon, addr in res:
325
+            if tp == socket.SOCK_DGRAM:
326
+                try:
327
+                    if fam == socket.AF_INET:
328
+                        so.sendto(packet, addr)
329
+                        return 'sent to {}: {}'.format(addr, packet.encode('hex'))
330
+                    elif fam == socket.AF_INET6:
331
+                        so6.sendto(packet, addr)
332
+                        return 'sent to {}: {}'.format(addr, packet.encode('hex'))
333
+                except Exception:
334
+                    pass
335
+        return 'no good address family found'
336
+
337
+    def act_tcp(self, path, headers, data, response):
338
+        args = {'path': path, 'headers': headers, 'data': data}
339
+        dest = render_template_string(self.a1, **args)
340
+        packet = render_template_string(self.a2, **args)
341
+        encoding = render_template_string(self.a3, **args)
342
+        try:
343
+            if encoding in (u'hex', u'base64'):
344
+                packet = packet.decode(encoding)
345
+            elif encoding == 'input':
346
+                packet = str(data)
347
+            elif encoding == 'json':
348
+                packet = jdumps(data)  # XXX HACKS
349
+            else:
350
+                packet = packet.encode(encoding)
351
+        except Exception as e:
352
+            return 'failed to encode packet: ' + str(e)
353
+        host, _, port = dest.partition(':')
354
+        if not _:
355
+            return 'illegal specification: no port in destination'
356
+        try:
357
+            port = int(port)
358
+        except ValueError:
359
+            return 'illegal port value: ' + port
360
+        if port < 0 or port > 65535:
361
+            return 'illegal port value: ' + str(port)
362
+        try:
363
+            res = socket.getaddrinfo(host, port)
364
+        except socket.gaierror:
365
+            return 'bad hostname:' + host
366
+        so = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
367
+        so.settimeout(0.1)
368
+        so6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
369
+        so6.settimeout(0.1)
370
+        for fam, tp, proto, canon, addr in res:
371
+            if tp == socket.SOCK_STREAM:
372
+                try:
373
+                    if fam == socket.AF_INET:
374
+                        so.connect(addr)
375
+                        so.send(packet)
376
+                        return 'sent to {}: {}'.format(addr, packet.encode('hex'))
377
+                    elif fam == socket.AF_INET6:
378
+                        so6.connect(addr)
379
+                        so6.send(packet)
380
+                        return 'sent to {}: {}'.format(addr, packet.encode('hex'))
381
+                except Exception:
382
+                    pass
383
+        return 'no good address family found'
384
+
385
+    def act_set_response(self, path, headers, data, response):
386
+        args = {'path': path, 'headers': headers, 'data': data}
387
+        content = render_template_string(self.a1, **args)
388
+        content_type = render_template_string(self.a2, **args)
389
+        response.set_data(content)
390
+        response.headers['Content-type'] = content_type
391
+        return 'response set to "' + content_type + '":\n' + content

+ 222
- 0
pluto.py Parādīt failu

@@ -0,0 +1,222 @@
1
+import time,string
2
+
3
+from flask import Flask, render_template, redirect, url_for, request, g, make_response
4
+
5
+from model import *
6
+from util import *
7
+
8
+app = Flask('pluto')
9
+app.debug = True
10
+
11
+@app.before_request
12
+def init_globals():
13
+    for cls in type.__subclasses__(DBObject):
14
+        setattr(g, cls.__name__, cls)
15
+
16
+app.jinja_env.filters['ctime'] = time.ctime
17
+app.jinja_env.filters['jloads'] = jloads
18
+app.jinja_env.filters['hloads'] = header_loads
19
+app.jinja_env.filters['split'] = string.split
20
+app.jinja_env.globals['safe_load'] = safe_load
21
+
22
+@app.errorhandler(404)
23
+def error_404(error):
24
+    return render_template('404.html', entity='page'), 404
25
+
26
+@app.route('/')
27
+def root():
28
+    return render_template('index.html')
29
+
30
+@app.route('/hook', methods=['GET', 'POST'])
31
+def hook():
32
+    response = make_response(render_template('api_hit.txt'))
33
+    response.headers['Content-type'] = 'text/json'
34
+    triggered = filter(lambda hook: hook.trigger(request.path, request.headers, jloads(request.data), response), Hook.all())
35
+    Log.create(time.time(), request.path, header_dumps(request.headers), request.data, ','.join(str(hook.rowid) for hook in triggered))
36
+    return response
37
+
38
+@app.route('/logs')
39
+def logs():
40
+    n = request.values.get('n', 10)
41
+    return render_template('logs.html', logs=Log.most_recent(n), n=n)
42
+
43
+@app.route('/debuglogs', methods=['GET', 'POST'])
44
+def debuglogs():
45
+    n = request.values.get('n', 10)
46
+    return render_template('debuglogs.html', logs=DebugLog.most_recent(n), n=n)
47
+
48
+@app.route('/hooks')
49
+def hooks():
50
+    return render_template('hooks.html', hooks=Hook.all())
51
+
52
+@app.route('/hooks/new', methods=['GET', 'POST'])
53
+def hooks_new():
54
+    if request.method == 'POST':
55
+        hook = Hook.create(
56
+            request.values['name'], None,  # FIXME no author yet
57
+            checkbox(request, 'disabled'),
58
+            checkbox(request, 'debugged'),
59
+        )
60
+        return redirect('/hook/%d'%(hook.rowid,))
61
+    return render_template('hooks_new.html')
62
+
63
+@app.route('/hook/<int:hookid>')
64
+def hook_obj(hookid):
65
+    try:
66
+        hook = Hook.get_one(rowid=hookid)
67
+    except NoSuchEntity:
68
+        return render_template('404.html', entity='hook'), 404
69
+    except TooManyEntities:
70
+        return render_template('500.html', cause='too many hooks'), 500
71
+    return render_template('hook.html', hook=hook)
72
+
73
+@app.route('/hook/<int:hookid>/edit', methods=['GET', 'POST'])
74
+def hook_edit(hookid):
75
+    try:
76
+        hook = Hook.get_one(rowid=hookid)
77
+    except NoSuchEntity:
78
+        return render_template('404.html', entity='hook'), 404
79
+    except TooManyEntities:
80
+        return render_template('500.html', cause='too many hooks'), 500
81
+    if request.method == 'POST':
82
+        hook.name = request.values['name']
83
+        hook.disabled = checkbox(request, 'disabled')
84
+        hook.debugged = checkbox(request, 'debugged')
85
+        hook.update()
86
+        return redirect('/hook/%d'%(hook.rowid,))
87
+    return render_template('hook_edit.html', hook=hook)
88
+    
89
+@app.route('/hook/<int:hookid>/newcond', methods=['GET', 'POST'])
90
+def hook_new_cond(hookid):
91
+    try:
92
+        hook = Hook.get_one(rowid=hookid)
93
+    except NoSuchEntity:
94
+        return render_template('404.html', entity='hook'), 404
95
+    except TooManyEntities:
96
+        return render_template('500.html', cause='too many hooks'), 500
97
+    if request.method == 'POST':
98
+        cond = Condition.create(
99
+            hook.rowid,
100
+            request.values['selector'], request.values['s1'], request.values['s2'], request.values['s3'],
101
+            request.values['test'], request.values['t1'], request.values['t2'], request.values['t3'],
102
+            checkbox(request, 'invert'),
103
+        )
104
+        return redirect('/cond/%d'%(cond.rowid,))
105
+    return render_template('hook_new_cond.html', hook=hook)
106
+
107
+@app.route('/hook/<int:hookid>/newact', methods=['GET', 'POST'])
108
+def hook_new_act(hookid):
109
+    try:
110
+        hook = Hook.get_one(rowid=hookid)
111
+    except NoSuchEntity:
112
+        return render_template('404.html', entity='hook'), 404
113
+    except TooManyEntities:
114
+        return render_template('500.html', cause='too many hooks'), 500
115
+    if request.method == 'POST':
116
+        act = Action.create(
117
+            hook.rowid,
118
+            request.values['action'], request.values['a1'], request.values['a2'], request.values['a3'],
119
+        )
120
+        return redirect('/act/%d'%(act.rowid,))
121
+    return render_template('hook_new_act.html', hook=hook)
122
+
123
+@app.route('/hook/<int:hookid>/delete', methods=['GET', 'POST'])
124
+def hook_delete(hookid):
125
+    try:
126
+        hook = Hook.get_one(rowid=hookid)
127
+    except NoSuchEntity:
128
+        return render_template('404.html', entity='hook'), 404
129
+    except TooManyEntities:
130
+        return render_template('500.html', cause='too many hooks'), 500
131
+    if request.method == 'POST':
132
+        hook.delete()
133
+        return redirect(url_for('hooks'))
134
+    return render_template('hook_delete.html', hook=hook)
135
+
136
+@app.route('/cond/<int:condid>')
137
+def cond_obj(condid):
138
+    try:
139
+        cond = Condition.get_one(rowid=condid)
140
+    except NoSuchEntity:
141
+        return render_template('404.html', entity='condition'), 404
142
+    except TooManyEntities:
143
+        return render_template('500.html', cause='too many conditions'), 500
144
+    return render_template('cond.html', cond=cond)
145
+
146
+@app.route('/cond/<int:condid>/edit', methods=['GET', 'POST'])
147
+def cond_edit(condid):
148
+    try:
149
+        cond = Condition.get_one(rowid=condid)
150
+    except NoSuchEntity:
151
+        return render_template('404.html', entity='condition'), 404
152
+    except TooManyEntities:
153
+        return render_template('500.html', cause='too many conditions'), 500
154
+    if request.method == 'POST':
155
+        cond.selector = request.values['selector']
156
+        cond.s1 = request.values['s1']
157
+        cond.s2 = request.values['s2']
158
+        cond.s3 = request.values['s3']
159
+        cond.test = request.values['test']
160
+        cond.t1 = request.values['t1']
161
+        cond.t2 = request.values['t2']
162
+        cond.t3 = request.values['t3']
163
+        cond.invert = checkbox(request, 'invert')
164
+        cond.update()
165
+        return redirect('/cond/%d'%(cond.rowid,))
166
+    return render_template('cond_edit.html', cond=cond)
167
+
168
+@app.route('/cond/<int:condid>/delete', methods=['GET', 'POST'])
169
+def cond_delete(condid):
170
+    try:
171
+        cond = Condition.get_one(rowid=condid)
172
+    except NoSuchEntity:
173
+        return render_template('404.html', entity='condition'), 404
174
+    except TooManyEntities:
175
+        return render_template('500.html', cause='too many conditions'), 500
176
+    if request.method == 'POST':
177
+        cond.delete()
178
+        return redirect('/hook/%d'%(cond.hook,))
179
+    return render_template('cond_delete.html', cond=cond)
180
+
181
+@app.route('/act/<int:actid>')
182
+def act_obj(actid):
183
+    try:
184
+        act = Action.get_one(rowid=actid)
185
+    except NoSuchEntity:
186
+        return render_template('404.html', entity='action'), 404
187
+    except TooManyEntities:
188
+        return render_template('500.html', cause='too many actions'), 500
189
+    return render_template('act.html', act=act)
190
+
191
+@app.route('/act/<int:actid>/edit', methods=['GET', 'POST'])
192
+def act_edit(actid):
193
+    try:
194
+        act = Action.get_one(rowid=actid)
195
+    except NoSuchEntity:
196
+        return render_template('404.html', entity='action'), 404
197
+    except TooManyEntities:
198
+        return render_template('500.html', cause='too many actions'), 500
199
+    if request.method == 'POST':
200
+        act.action = request.values['action']
201
+        act.a1 = request.values['a1']
202
+        act.a2 = request.values['a2']
203
+        act.a3 = request.values['a3']
204
+        act.update()
205
+        return redirect('/act/%d'%(act.rowid,))
206
+    return render_template('act_edit.html', act=act)
207
+
208
+@app.route('/act/<int:actid>/delete', methods=['GET', 'POST'])
209
+def act_delete(actid):
210
+    try:
211
+        act = Action.get_one(rowid=actid)
212
+    except NoSuchEntity:
213
+        return render_template('404.html', entity='action'), 404
214
+    except TooManyEntities:
215
+        return render_template('500.html', cause='too many actions'), 500
216
+    if request.method == 'POST':
217
+        act.delete()
218
+        return redirect('/hook/%d'%(act.hook,))
219
+    return render_template('act_delete.html', act=act)
220
+
221
+if __name__ == '__main__':
222
+    app.run(host='0.0.0.0', port=8080)

Binārs
static/add.jpg Parādīt failu


+ 60
- 0
static/edit.svg Parādīt failu

@@ -0,0 +1,60 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg
3
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
+   xmlns:cc="http://creativecommons.org/ns#"
5
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
+   xmlns:svg="http://www.w3.org/2000/svg"
7
+   xmlns="http://www.w3.org/2000/svg"
8
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
+   viewBox="0 -256 1792 1792"
11
+   id="svg3001"
12
+   version="1.1"
13
+   inkscape:version="0.48.3.1 r9886"
14
+   width="100%"
15
+   height="100%"
16
+   sodipodi:docname="pencil_font_awesome.svg">
17
+  <metadata
18
+     id="metadata3011">
19
+    <rdf:RDF>
20
+      <cc:Work
21
+         rdf:about="">
22
+        <dc:format>image/svg+xml</dc:format>
23
+        <dc:type
24
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
25
+      </cc:Work>
26
+    </rdf:RDF>
27
+  </metadata>
28
+  <defs
29
+     id="defs3009" />
30
+  <sodipodi:namedview
31
+     pagecolor="#ffffff"
32
+     bordercolor="#666666"
33
+     borderopacity="1"
34
+     objecttolerance="10"
35
+     gridtolerance="10"
36
+     guidetolerance="10"
37
+     inkscape:pageopacity="0"
38
+     inkscape:pageshadow="2"
39
+     inkscape:window-width="640"
40
+     inkscape:window-height="480"
41
+     id="namedview3007"
42
+     showgrid="false"
43
+     inkscape:zoom="0.13169643"
44
+     inkscape:cx="896"
45
+     inkscape:cy="896"
46
+     inkscape:window-x="0"
47
+     inkscape:window-y="25"
48
+     inkscape:window-maximized="0"
49
+     inkscape:current-layer="svg3001" />
50
+  <g
51
+     transform="matrix(1,0,0,-1,121.49153,1270.2373)"
52
+     id="g3003"
53
+     style="color: #ddd;">
54
+    <path
55
+       d="M 363,0 454,91 219,326 128,235 V 128 H 256 V 0 h 107 z m 523,928 q 0,22 -22,22 -10,0 -17,-7 L 305,401 q -7,-7 -7,-17 0,-22 22,-22 10,0 17,7 l 542,542 q 7,7 7,17 z M 832,1120 1248,704 416,-128 H 0 v 416 z m 683,-96 q 0,-53 -37,-90 l -166,-166 -416,416 166,165 q 36,38 90,38 53,0 91,-38 l 235,-234 q 37,-39 37,-91 z"
56
+       id="path3005"
57
+       inkscape:connector-curvature="0"
58
+       style="fill:currentColor" />
59
+  </g>
60
+</svg>

+ 59
- 0
static/home.svg Parādīt failu

@@ -0,0 +1,59 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg
3
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
+   xmlns:cc="http://creativecommons.org/ns#"
5
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
+   xmlns:svg="http://www.w3.org/2000/svg"
7
+   xmlns="http://www.w3.org/2000/svg"
8
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
+   viewBox="0 -256 1792 1792"
11
+   id="svg3013"
12
+   version="1.1"
13
+   inkscape:version="0.48.3.1 r9886"
14
+   width="100%"
15
+   height="100%"
16
+   sodipodi:docname="home_font_awesome.svg">
17
+  <metadata
18
+     id="metadata3023">
19
+    <rdf:RDF>
20
+      <cc:Work
21
+         rdf:about="">
22
+        <dc:format>image/svg+xml</dc:format>
23
+        <dc:type
24
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
25
+      </cc:Work>
26
+    </rdf:RDF>
27
+  </metadata>
28
+  <defs
29
+     id="defs3021" />
30
+  <sodipodi:namedview
31
+     pagecolor="#ffffff"
32
+     bordercolor="#666666"
33
+     borderopacity="1"
34
+     objecttolerance="10"
35
+     gridtolerance="10"
36
+     guidetolerance="10"
37
+     inkscape:pageopacity="0"
38
+     inkscape:pageshadow="2"
39
+     inkscape:window-width="640"
40
+     inkscape:window-height="480"
41
+     id="namedview3019"
42
+     showgrid="false"
43
+     inkscape:zoom="0.13169643"
44
+     inkscape:cx="896"
45
+     inkscape:cy="896"
46
+     inkscape:window-x="0"
47
+     inkscape:window-y="25"
48
+     inkscape:window-maximized="0"
49
+     inkscape:current-layer="svg3013" />
50
+  <g
51
+     transform="matrix(1,0,0,-1,68.338983,1285.4237)"
52
+     id="g3015">
53
+    <path
54
+       d="M 1408,544 V 64 Q 1408,38 1389,19 1370,0 1344,0 H 960 V 384 H 704 V 0 H 320 q -26,0 -45,19 -19,19 -19,45 v 480 q 0,1 0.5,3 0.5,2 0.5,3 l 575,474 575,-474 q 1,-2 1,-6 z m 223,69 -62,-74 q -8,-9 -21,-11 h -3 q -13,0 -21,7 L 832,1112 140,535 q -12,-8 -24,-7 -13,2 -21,11 l -62,74 q -8,10 -7,23.5 1,13.5 11,21.5 l 719,599 q 32,26 76,26 44,0 76,-26 l 244,-204 v 195 q 0,14 9,23 9,9 23,9 h 192 q 14,0 23,-9 9,-9 9,-23 V 840 l 219,-182 q 10,-8 11,-21.5 1,-13.5 -7,-23.5 z"
55
+       id="path3017"
56
+       inkscape:connector-curvature="0"
57
+       style="fill:currentColor" />
58
+  </g>
59
+</svg>

+ 60
- 0
static/minus.svg Parādīt failu

@@ -0,0 +1,60 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg
3
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
+   xmlns:cc="http://creativecommons.org/ns#"
5
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
+   xmlns:svg="http://www.w3.org/2000/svg"
7
+   xmlns="http://www.w3.org/2000/svg"
8
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
+   viewBox="0 -256 1792 1792"
11
+   id="svg2989"
12
+   version="1.1"
13
+   inkscape:version="0.48.3.1 r9886"
14
+   width="100%"
15
+   height="100%"
16
+   sodipodi:docname="minus_sign_font_awesome.svg">
17
+  <metadata
18
+     id="metadata2999">
19
+    <rdf:RDF>
20
+      <cc:Work
21
+         rdf:about="">
22
+        <dc:format>image/svg+xml</dc:format>
23
+        <dc:type
24
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
25
+      </cc:Work>
26
+    </rdf:RDF>
27
+  </metadata>
28
+  <defs
29
+     id="defs2997" />
30
+  <sodipodi:namedview
31
+     pagecolor="#ffffff"
32
+     bordercolor="#666666"
33
+     borderopacity="1"
34
+     objecttolerance="10"
35
+     gridtolerance="10"
36
+     guidetolerance="10"
37
+     inkscape:pageopacity="0"
38
+     inkscape:pageshadow="2"
39
+     inkscape:window-width="640"
40
+     inkscape:window-height="480"
41
+     id="namedview2995"
42
+     showgrid="false"
43
+     inkscape:zoom="0.13169643"
44
+     inkscape:cx="896"
45
+     inkscape:cy="896"
46
+     inkscape:window-x="0"
47
+     inkscape:window-y="25"
48
+     inkscape:window-maximized="0"
49
+     inkscape:current-layer="svg2989" />
50
+  <g
51
+     transform="matrix(1,0,0,-1,129.08475,1277.8305)"
52
+     id="g2991"
53
+     style="color: #ddd;">
54
+    <path
55
+       d="m 1216,576 v 128 q 0,26 -19,45 -19,19 -45,19 H 384 q -26,0 -45,-19 -19,-19 -19,-45 V 576 q 0,-26 19,-45 19,-19 45,-19 h 768 q 26,0 45,19 19,19 19,45 z m 320,64 Q 1536,431 1433,254.5 1330,78 1153.5,-25 977,-128 768,-128 559,-128 382.5,-25 206,78 103,254.5 0,431 0,640 0,849 103,1025.5 206,1202 382.5,1305 559,1408 768,1408 977,1408 1153.5,1305 1330,1202 1433,1025.5 1536,849 1536,640 z"
56
+       id="path2993"
57
+       inkscape:connector-curvature="0"
58
+       style="fill:currentColor" />
59
+  </g>
60
+</svg>

+ 60
- 0
static/plus.svg Parādīt failu

@@ -0,0 +1,60 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg
3
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
+   xmlns:cc="http://creativecommons.org/ns#"
5
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
+   xmlns:svg="http://www.w3.org/2000/svg"
7
+   xmlns="http://www.w3.org/2000/svg"
8
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
+   viewBox="0 -256 1792 1792"
11
+   id="svg3025"
12
+   version="1.1"
13
+   inkscape:version="0.48.3.1 r9886"
14
+   width="100%"
15
+   height="100%"
16
+   sodipodi:docname="plus_sign_font_awesome.svg">
17
+  <metadata
18
+     id="metadata3035">
19
+    <rdf:RDF>
20
+      <cc:Work
21
+         rdf:about="">
22
+        <dc:format>image/svg+xml</dc:format>
23
+        <dc:type
24
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
25
+      </cc:Work>
26
+    </rdf:RDF>
27
+  </metadata>
28
+  <defs
29
+     id="defs3033" />
30
+  <sodipodi:namedview
31
+     pagecolor="#ffffff"
32
+     bordercolor="#666666"
33
+     borderopacity="1"
34
+     objecttolerance="10"
35
+     gridtolerance="10"
36
+     guidetolerance="10"
37
+     inkscape:pageopacity="0"
38
+     inkscape:pageshadow="2"
39
+     inkscape:window-width="640"
40
+     inkscape:window-height="480"
41
+     id="namedview3031"
42
+     showgrid="false"
43
+     inkscape:zoom="0.13169643"
44
+     inkscape:cx="896"
45
+     inkscape:cy="896"
46
+     inkscape:window-x="0"
47
+     inkscape:window-y="25"
48
+     inkscape:window-maximized="0"
49
+     inkscape:current-layer="svg3025" />
50
+  <g
51
+     transform="matrix(1,0,0,-1,113.89831,1277.8305)"
52
+     id="g3027"
53
+     style="color: #ddd;">
54
+    <path
55
+       d="m 1216,576 v 128 q 0,26 -19,45 -19,19 -45,19 H 896 v 256 q 0,26 -19,45 -19,19 -45,19 H 704 q -26,0 -45,-19 -19,-19 -19,-45 V 768 H 384 q -26,0 -45,-19 -19,-19 -19,-45 V 576 q 0,-26 19,-45 19,-19 45,-19 H 640 V 256 q 0,-26 19,-45 19,-19 45,-19 h 128 q 26,0 45,19 19,19 19,45 v 256 h 256 q 26,0 45,19 19,19 19,45 z m 320,64 Q 1536,431 1433,254.5 1330,78 1153.5,-25 977,-128 768,-128 559,-128 382.5,-25 206,78 103,254.5 0,431 0,640 0,849 103,1025.5 206,1202 382.5,1305 559,1408 768,1408 977,1408 1153.5,1305 1330,1202 1433,1025.5 1536,849 1536,640 z"
56
+       id="path3029"
57
+       inkscape:connector-curvature="0"
58
+       style="fill:currentColor" />
59
+  </g>
60
+</svg>

+ 59
- 0
static/search.svg Parādīt failu

@@ -0,0 +1,59 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<svg
3
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
+   xmlns:cc="http://creativecommons.org/ns#"
5
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
+   xmlns:svg="http://www.w3.org/2000/svg"
7
+   xmlns="http://www.w3.org/2000/svg"
8
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
+   viewBox="0 -256 1792 1792"
11
+   id="svg3025"
12
+   version="1.1"
13
+   inkscape:version="0.48.3.1 r9886"
14
+   width="100%"
15
+   height="100%"
16
+   sodipodi:docname="search_font_awesome.svg">
17
+  <metadata
18
+     id="metadata3035">
19
+    <rdf:RDF>
20
+      <cc:Work
21
+         rdf:about="">
22
+        <dc:format>image/svg+xml</dc:format>
23
+        <dc:type
24
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
25
+      </cc:Work>
26
+    </rdf:RDF>
27
+  </metadata>
28
+  <defs
29
+     id="defs3033" />
30
+  <sodipodi:namedview
31
+     pagecolor="#ffffff"
32
+     bordercolor="#666666"
33
+     borderopacity="1"
34
+     objecttolerance="10"
35
+     gridtolerance="10"
36
+     guidetolerance="10"
37
+     inkscape:pageopacity="0"
38
+     inkscape:pageshadow="2"
39
+     inkscape:window-width="640"
40
+     inkscape:window-height="480"
41
+     id="namedview3031"
42
+     showgrid="false"
43
+     inkscape:zoom="0.13169643"
44
+     inkscape:cx="896"
45
+     inkscape:cy="896"
46
+     inkscape:window-x="0"
47
+     inkscape:window-y="25"
48
+     inkscape:window-maximized="0"
49
+     inkscape:current-layer="svg3025" />
50
+  <g
51
+     transform="matrix(1,0,0,-1,60.745763,1201.8983)"
52
+     id="g3027">
53
+    <path
54
+       d="m 1152,704 q 0,185 -131.5,316.5 Q 889,1152 704,1152 519,1152 387.5,1020.5 256,889 256,704 256,519 387.5,387.5 519,256 704,256 889,256 1020.5,387.5 1152,519 1152,704 z m 512,-832 q 0,-52 -38,-90 -38,-38 -90,-38 -54,0 -90,38 L 1103,124 Q 924,0 704,0 561,0 430.5,55.5 300,111 205.5,205.5 111,300 55.5,430.5 0,561 0,704 q 0,143 55.5,273.5 55.5,130.5 150,225 94.5,94.5 225,150 130.5,55.5 273.5,55.5 143,0 273.5,-55.5 130.5,-55.5 225,-150 94.5,-94.5 150,-225 Q 1408,847 1408,704 1408,484 1284,305 l 343,-343 q 37,-37 37,-90 z"
55
+       id="path3029"
56
+       inkscape:connector-curvature="0"
57
+       style="fill:currentColor" />
58
+  </g>
59
+</svg>

+ 69
- 0
static/site.js Parādīt failu

@@ -0,0 +1,69 @@
1
+const ACTION_SCHEMA = {
2
+    'post': ['URL template', 'POST data template', 'Headers JSON template'],
3
+    'gitlab': ['API subpath', 'POST data JSON template', 'Headers JSON template'],
4
+    'system': ['Command', "Don't fork"],
5
+	'udp': ['Destination', 'Packet Data', 'Codec'],
6
+	'tcp': ['Destination', 'Packet Data', 'Codec'],
7
+    'set_response': ['Content', 'Content Type'],
8
+};
9
+
10
+const SELECTOR_SCHEMA = {
11
+    'header': ['Header name'],
12
+    'JSON': ['Object path'],
13
+    'path': [],
14
+};
15
+
16
+const TEST_SCHEMA = {
17
+    'equal': ['String value'],
18
+    'inrange': ['Lower bound', 'Upper bound'],
19
+    'truthy': [],
20
+    'contains': ['String value'],
21
+};
22
+
23
+function reg_action_select(select, laba1, laba2, laba3) {
24
+    select.addEventListener("change", function() {
25
+        var schema = ACTION_SCHEMA[this.value];
26
+        if(!schema) {
27
+            laba1.textContent = "(Unknown schema)";
28
+            laba2.textContent = "(Unknown schema)";
29
+            laba3.textContent = "(Unknown schema)";
30
+        } else {
31
+            laba1.textContent = (schema.length > 0 ? schema[0] : "Unused");
32
+            laba2.textContent = (schema.length > 1 ? schema[1] : "Unused");
33
+            laba3.textContent = (schema.length > 2 ? schema[2] : "Unused");
34
+        }
35
+    });
36
+    select.dispatchEvent(new Event("change"));
37
+}
38
+
39
+function reg_selector_select(select, labs1, labs2, labs3) {
40
+    select.addEventListener("change", function() {
41
+        var schema = SELECTOR_SCHEMA[this.value];
42
+        if(!schema) {
43
+            labs1.textContent = "(Unknown schema)";
44
+            labs2.textContent = "(Unknown schema)";
45
+            labs3.textContent = "(Unknown schema)";
46
+        } else {
47
+            labs1.textContent = (schema.length > 0 ? schema[0] : "Unused");
48
+            labs2.textContent = (schema.length > 1 ? schema[1] : "Unused");
49
+            labs3.textContent = (schema.length > 2 ? schema[2] : "Unused");
50
+        }
51
+    });
52
+    select.dispatchEvent(new Event("change"));
53
+}
54
+
55
+function reg_test_select(select, labt1, labt2, labt3) {
56
+    select.addEventListener("change", function() {
57
+        var schema = TEST_SCHEMA[this.value];
58
+        if(!schema) {
59
+            labt1.textContent = "(Unknown schema)";
60
+            labt2.textContent = "(Unknown schema)";
61
+            labt3.textContent = "(Unknown schema)";
62
+        } else {
63
+            labt1.textContent = (schema.length > 0 ? schema[0] : "Unused");
64
+            labt2.textContent = (schema.length > 1 ? schema[1] : "Unused");
65
+            labt3.textContent = (schema.length > 2 ? schema[2] : "Unused");
66
+        }
67
+    });
68
+    select.dispatchEvent(new Event("change"));
69
+}

+ 195
- 0
static/style.css Parādīt failu

@@ -0,0 +1,195 @@
1
+body {
2
+    background-color: #eee;
3
+    color: #222;
4
+    padding: 0 10%;
5
+    font-size: 125%;
6
+}
7
+
8
+@media (max-width: 768px) {
9
+    body {
10
+        padding: 0 2%;
11
+    }
12
+}
13
+
14
+table {
15
+    background-color: #ddd;
16
+}
17
+
18
+span.monospace {
19
+    font-family: monospace;
20
+    border: 1px solid #777;
21
+    border-radius: 3px;
22
+    white-space: pre;
23
+    display: inline-block;
24
+    padding: 5px;
25
+    background-color: #fff;
26
+}
27
+
28
+table {
29
+    border-collapse: collapse;
30
+    border: 1px solid #777;
31
+}
32
+
33
+table td, table th {
34
+    padding: 10px;
35
+    border: 1px solid #777;
36
+}
37
+
38
+#logs.debug {
39
+    font-size: 75%;
40
+}
41
+
42
+#logs .headers, #logs .data {
43
+    white-space: pre-wrap;
44
+    font-family: monospace;
45
+    background-color: #eee;
46
+}
47
+
48
+.nodata {
49
+    font-style: italic;
50
+    color: #700;
51
+}
52
+
53
+.positive {
54
+    color: #0a0;
55
+}
56
+
57
+.negative {
58
+    color: #a00;
59
+}
60
+
61
+div.error {
62
+    background-color: #edd;
63
+    border: 1px solid #a77;
64
+    border-radius: 15px;
65
+    width: 100%;
66
+    padding: 25px;
67
+}
68
+
69
+.posbutton a {
70
+    display: inline-block;
71
+    background-color: #080;
72
+    color: #ddd;
73
+    border: 2px solid #060;
74
+    border-radius: 5px;
75
+    padding: 10px;
76
+    margin: 5px;
77
+    font-weight: bolder;
78
+    text-decoration: none;
79
+}
80
+
81
+.posbutton a:hover {
82
+    background-color: #0a0;
83
+}
84
+
85
+.posbutton a:active {
86
+    background-color: #ddd;
87
+    color: #0a0;
88
+}
89
+
90
+.negbutton a {
91
+    display: inline-block;
92
+    background-color: #800;
93
+    color: #ddd;
94
+    border: 2px solid #600;
95
+    border-radius: 5px;
96
+    padding: 10px;
97
+    margin: 5px;
98
+    font-weight: bolder;
99
+    text-decoration: none;
100
+}
101
+
102
+.negbutton a:hover {
103
+    background-color: #a00;
104
+}
105
+
106
+.negbutton a:active {
107
+    background-color: #ddd;
108
+    color: #a00;
109
+}
110
+
111
+.neutbutton a {
112
+    display: inline-block;
113
+    background-color: #008;
114
+    color: #ddd;
115
+    border: 2px solid #006;
116
+    border-radius: 5px;
117
+    padding: 10px;
118
+    margin: 5px;
119
+    font-weight: bolder;
120
+    text-decoration: none;
121
+}
122
+
123
+.neutbutton a:hover {
124
+    background-color: #00a;
125
+}
126
+
127
+.neutbutton a:active {
128
+    background-color: #ddd;
129
+    color: #00a;
130
+}
131
+
132
+.addbutton a::after {
133
+    margin-left: 10px;
134
+    margin-bottom: -3px;
135
+    background-image: url('/static/plus.svg');
136
+    background-size: 25px 25px;
137
+    width: 25px;
138
+    height: 25px;
139
+    display: inline-block;
140
+    content: "";
141
+    color: #f0f;
142
+}
143
+
144
+.rembutton a::after {
145
+    margin-left: 10px;
146
+    margin-bottom: -5px;
147
+    background-image: url('/static/minus.svg');
148
+    background-size: 25px 25px;
149
+    width: 25px;
150
+    height: 25px;
151
+    display: inline-block;
152
+    content: "";
153
+}
154
+
155
+.edbutton a::after {
156
+    margin-left: 10px;
157
+    margin-bottom: -5px;
158
+    background-image: url('/static/edit.svg');
159
+    background-size: 25px 25px;
160
+    width: 25px;
161
+    height: 25px;
162
+    display: inline-block;
163
+    content: "";
164
+}
165
+
166
+.srchbutton a::after {
167
+    margin-left: 10px;
168
+    margin-bottom: -5px;
169
+    background-image: url('/static/search.svg');
170
+    background-size: 25px 25px;
171
+    width: 25px;
172
+    height: 25px;
173
+    display: inline-block;
174
+    content: "";
175
+}
176
+
177
+.hacks-flr {
178
+    float: right;
179
+}
180
+
181
+#home {
182
+    width: 46px;
183
+    height: 46px;
184
+    padding-right: 10px;
185
+    float: left;
186
+}
187
+
188
+.flex-center {
189
+    display: flex;
190
+    justify-content: center;
191
+}
192
+
193
+.flex-center > div {
194
+    margin: 0 50px;
195
+}

+ 8
- 0
templates/404.html Parādīt failu

@@ -0,0 +1,8 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Error 404{% endblock %}
3
+{% block content %}
4
+    <div class="error">
5
+        There is no such {{ entity | default('entity') }} identified by your request.
6
+    </div>
7
+{% endblock %}
8
+

+ 13
- 0
templates/500.html Parādīt failu

@@ -0,0 +1,13 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Error 500{% endblock %}
3
+{% block content %}
4
+    <div class="error">
5
+        An error occurred while processing your request.<br/>
6
+        The cause is:
7
+        {% if cause %}
8
+            <span class="monospace">{{ cause }}</span>
9
+        {% else %}
10
+            <span class="nodata">No cause given.</span>
11
+        {% endif %}
12
+    </div>
13
+{% endblock %}

+ 8
- 0
templates/act.html Parādīt failu

@@ -0,0 +1,8 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Action {{act.rowid}}{% endblock %}
3
+{% block content %}
4
+    <p>This is action {{ act.rowid }} for {{ macros.hook(act.get_hook()) }}:</p>
5
+    <div class="neutbutton edbutton hacks-flr"><a href="/act/{{ act.rowid }}/edit">Edit</a></div>
6
+    <div class="negbutton rembutton hacks-flr"><a href="/act/{{ act.rowid }}/delete">Delete</a></div>
7
+    <p>Action: {{ act.action }} ({{ act.a1 | pprint }}, {{ act.a2 | pprint }}, {{ act.a3 | pprint }})</p>
8
+{% endblock %}

+ 9
- 0
templates/act_delete.html Parādīt failu

@@ -0,0 +1,9 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Delete Action {{ act.rowid }}{% endblock %}
3
+{% block content %}
4
+    <p>Are you sure you want to delete {{ macros.act(act) }} of {{ macros.hook(act.get_hook()) }}?</p>
5
+    <form action="?" method="POST">
6
+        <button type="submit">Confirm</button>
7
+    </form>
8
+{% endblock %}
9
+

+ 9
- 0
templates/act_edit.html Parādīt failu

@@ -0,0 +1,9 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Edit Action {{ act.rowid }}{% endblock %}
3
+{% block content %}
4
+    <p>Edit action {{ macros.act(act) }}:</p>
5
+    <form action="?" method="POST">
6
+        {{ macros.act_params(act) }}
7
+        <button type="submit">Submit</button>
8
+    </form>
9
+{% endblock %}

+ 22
- 0
templates/api_hit.txt Parādīt failu

@@ -0,0 +1,22 @@
1
+{
2
+  "version": "1.0",
3
+  "sessionAttributes": {},
4
+  "response": {
5
+    "outputSpeech": {
6
+      "type": "PlainText",
7
+      "text": "Welcome to the Alexa Skills Kit sample, Please tell me your favorite color by saying, my favorite color is red"
8
+    },
9
+    "card": {
10
+      "type": "Simple",
11
+      "title": "SessionSpeechlet - Welcome",
12
+      "content": "SessionSpeechlet - Welcome to the Alexa Skills Kit sample, Please tell me your favorite color by saying, my favorite color is red"
13
+    },
14
+    "reprompt": {
15
+      "outputSpeech": {
16
+        "type": "PlainText",
17
+        "text": "Please tell me your favorite color by saying, my favorite color is red"
18
+      }
19
+    },
20
+    "shouldEndSession": false
21
+  }
22
+}

+ 21
- 0
templates/base.html Parādīt failu

@@ -0,0 +1,21 @@
1
+{% import "macros.html" as macros %}
2
+<!DOCTYPE HTML>
3
+<html>
4
+    <head>
5
+        {% block head %}
6
+        <title>{% block title %}{% endblock %}</title>
7
+        <link rel="stylesheet" type="text/css" href="/static/style.css"/>
8
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
+        <script type="text/javascript" src="/static/site.js"></script>
10
+        {% endblock %}
11
+    </head>
12
+    <body>
13
+        {% block header %}
14
+        <h1><a href="/"><img id="home" src="/static/home.svg"></a></h1>
15
+        <h1>{{ self.title() }}</h1>
16
+        {% endblock %}
17
+        <div style="clear: left;"></div>
18
+        {% block content %}
19
+        {% endblock %}
20
+    </body>
21
+</html>

+ 10
- 0
templates/cond.html Parādīt failu

@@ -0,0 +1,10 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Condition {{cond.rowid}}{% endblock %}
3
+{% block content %}
4
+    <p>This is condition {{ cond.rowid }} for {{ macros.hook(cond.get_hook()) }}:</p>
5
+    <div class="neutbutton edbutton hacks-flr"><a href="/cond/{{ cond.rowid }}/edit">Edit</a></div>
6
+    <div class="negbutton rembutton hacks-flr"><a href="/cond/{{ cond.rowid }}/delete">Delete</a></div>
7
+    <p>Selector: {{ cond.selector }} ({{ cond.s1 | pprint }}, {{ cond.s2 | pprint }}, {{ cond.s3 | pprint }})</p>
8
+    <p>Test: {{ cond.test }} ({{ cond.t1 | pprint }}, {{ cond.t2 | pprint }}, {{ cond.t3 | pprint }})</p>
9
+    <p>Inverted: {% if cond.invert %}Yes{% else %}No{% endif %}</p>
10
+{% endblock %}

+ 8
- 0
templates/cond_delete.html Parādīt failu

@@ -0,0 +1,8 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Delete Condition {{ cond.rowid }}{% endblock %}
3
+{% block content %}
4
+    <p>Are you sure you want to delete {{ macros.cond(cond) }} of {{ macros.hook(cond.get_hook()) }}?</p>
5
+    <form action="?" method="POST">
6
+        <button type="submit">Confirm</button>
7
+    </form>
8
+{% endblock %}

+ 10
- 0
templates/cond_edit.html Parādīt failu

@@ -0,0 +1,10 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Edit Condition {{ cond.rowid }}{% endblock %}
3
+{% block content %}
4
+    <p>Edit condition {{ macros.cond(cond) }}:</p>
5
+    <form action="?" method="POST">
6
+        {{ macros.cond_params(cond) }}
7
+        <button type="submit">Submit</button>
8
+    </form>
9
+{% endblock %}
10
+

+ 4
- 0
templates/cond_new.html Parādīt failu

@@ -0,0 +1,4 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - New Condition{% endblock %}
3
+{% block content %}
4
+    <p>

+ 44
- 0
templates/debuglogs.html Parādīt failu

@@ -0,0 +1,44 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Logs{% endblock %}
3
+{% block content %}
4
+    <p>
5
+        Here are the most recent {{ n }} entries in the debug log:
6
+    </p>
7
+
8
+    <table id="logs" class="debug">
9
+        <tr>
10
+            <th>Time</th>
11
+            <th>Path</th>
12
+            <th>Headers</th>
13
+            <th>Data</th>
14
+            <th>Hook</th>
15
+            <th>Actor</th>
16
+            <th>Success</th>
17
+            <th>Message</th>
18
+        </tr>
19
+    {% for log in logs %}
20
+        <tr>
21
+            <td class="time">{{ log.time | ctime }}</td>
22
+            <td class="path">{{ log.path }}</td>
23
+            <td class="headers">{{ log.headers | hloads | pprint(True) }}</td>
24
+            <td class="data">{{ log.data | jloads | pprint(True) }}</td>
25
+            <td class="hook">{{ macros.hook_id(log.hook) }}</td>
26
+            <td class="actor">
27
+                {% if log.cond %}
28
+                    {{ macros.cond_id(log.cond) }}
29
+                {% elif log.act %}
30
+                    {{ macros.act_id(log.act) }}
31
+                {% else %}
32
+                    {{ macros.hook_id(log.hook) }}
33
+                {% endif %}
34
+            </td>
35
+            <td class="success">{{ macros.yesno(log.success) }}</td>
36
+            <td class="message">{{ log.message }}</td>
37
+        </tr>
38
+    {% else %}
39
+        <tr>
40
+            <td colspan="8" class="nodata">No data.</td>
41
+        </tr>
42
+    {% endfor %}
43
+    </table>
44
+{% endblock %}

+ 25
- 0
templates/hook.html Parādīt failu

@@ -0,0 +1,25 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Hook {{hook.name}}{% endblock %}
3
+{% block content %}
4
+    <p>This is hook {{ hook.rowid }}, named "{{ hook.name }}", presently {% if hook.disabled %}<span class="negative">disabled</span>{% else %}<span class="positive">enabled</span>{% endif %}, presently {% if hook.debugged %}<span class="positive">debugged (<a href="/debuglogs">see entries</a>)</span>{% else %}<span class="negative">not debugged</span>{% endif %}.</p>
5
+    <div class="neutbutton edbutton hacks-flr"><a href="/hook/{{ hook.rowid }}/edit">Edit</a></div>
6
+    <div class="negbutton rembutton hacks-flr"><a href="/hook/{{ hook.rowid }}/delete">Delete</a></div>
7
+    <p>Conditions for activation:</p>
8
+    <ul>
9
+    {% for cond in g.Condition.for_hook(hook) %}
10
+        <li>{{ macros.cond(cond) }}</li>
11
+    {% else %}
12
+        <li class="nodata">No conditions</li>
13
+    {% endfor %}
14
+        <li class="posbutton addbutton"><a href="/hook/{{ hook.rowid }}/newcond">Add condition</a></li>
15
+    </ul>
16
+    <p>Actions upon activation:</p>
17
+    <ul>
18
+    {% for act in g.Action.for_hook(hook) %}
19
+        <li>{{ macros.act(act) }}</li>
20
+    {% else %}
21
+        <li class="nodata">No actions</li>
22
+    {% endfor %}
23
+        <li class="posbutton addbutton"><a href="/hook/{{ hook.rowid }}/newact">Add action</a></li>
24
+    </ul>
25
+{% endblock %}

+ 8
- 0
templates/hook_delete.html Parādīt failu

@@ -0,0 +1,8 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Delete Hook {{ hook.name }}{% endblock %}
3
+{% block content %}
4
+    <p>Are you sure you want to delete {{ macros.hook(hook) }}?</p>
5
+    <form action="?" method="POST">
6
+        <button type="submit">Confirm</button>
7
+    </form>
8
+{% endblock %}

+ 10
- 0
templates/hook_edit.html Parādīt failu

@@ -0,0 +1,10 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Edit Hook {{ hook.name }}{% endblock %}
3
+{% block content %}
4
+    <p>Edit hook {{ macros.hook(hook) }}:</p>
5
+    <form action="?" method="POST">
6
+        {{ macros.hook_params(hook) }}
7
+        <button type="submit">Submit</button>
8
+    </form>
9
+{% endblock %}
10
+

+ 10
- 0
templates/hook_new_act.html Parādīt failu

@@ -0,0 +1,10 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - New Action{% endblock %}
3
+{% block content %}
4
+    <p>Add an action to {{ macros.hook(hook) }}:</p>
5
+    <form action="?" method="POST">
6
+        {{ macros.act_params(None) }}
7
+        <button type="submit">Add</button>
8
+    </form>
9
+{% endblock %}
10
+

+ 9
- 0
templates/hook_new_cond.html Parādīt failu

@@ -0,0 +1,9 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - New Condition{% endblock %}
3
+{% block content %}
4
+    <p>Add a condition to {{ macros.hook(hook) }}:</p>
5
+    <form action="?" method="POST">
6
+        {{ macros.cond_params(None) }}
7
+        <button type="submit">Add</button>
8
+    </form>
9
+{% endblock %}

+ 16
- 0
templates/hooks.html Parādīt failu

@@ -0,0 +1,16 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Hooks{% endblock %}
3
+{% block content %}
4
+    <p>
5
+        Here are the active hooks: 
6
+    </p>
7
+
8
+    <ul>
9
+    {% for hook in hooks %}
10
+        <li>{{ macros.hook(hook) }}</li>
11
+    {% else %}
12
+        <li class="nodata">No data.</li>
13
+    {% endfor %}
14
+        <li class="posbutton addbutton"><a href="/hooks/new">Add hook</a></li>
15
+    </ul>
16
+{% endblock %}

+ 9
- 0
templates/hooks_new.html Parādīt failu

@@ -0,0 +1,9 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - New Hook{% endblock %}
3
+{% block content %}
4
+    <p>Add a new hook:</p>
5
+    <form action="?" method="POST">
6
+        {{ macros.hook_params(None) }}
7
+        <button type="submit">Add</button>
8
+    </form>
9
+{% endblock %}

+ 30
- 0
templates/index.html Parādīt failu

@@ -0,0 +1,30 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto{% endblock %}
3
+{% block content %}
4
+    <h2>A Webhook Hacking Server</h2>
5
+    <h3>Sponsored by <a href="https://gitlab.cosi.clarkson.edu/COSI/">COSI</a></h3>
6
+
7
+    <p>
8
+        Welcome to Pluto! This server is a test server useful for deploying
9
+        quick, one-off scripts to integrate with "webhooks"&mdash;HTTP
10
+        requests initiated by services in response to events.
11
+    </p>
12
+
13
+    <div class="flex-center">
14
+        <div class="neutbutton srchbutton"><a href="/logs">Logs</a></div>
15
+        <div class="neutbutton srchbutton"><a href="/debuglogs">Debug Logs</a></div>
16
+        <div class="neutbutton srchbutton"><a href="/hooks">Hooks</a></div>
17
+    </div>
18
+
19
+    <p>
20
+        If you'd like to use Pluto for one of your projects, point the URL
21
+        for a webhook at <span
22
+            class="monospace">http://pluto.cslabs.clarkson.edu/hook</span> or
23
+            <span
24
+            class="monospace">https://pluto.cslabs.clarkson.edu/hook</span>,
25
+            then actuate the hook a few times, and <a href="/logs">check the
26
+            logs</a> to see what kind of requests are being sent. Then, <a
27
+            href="/hooks">add a hook</a>
28
+        in the hooks table.
29
+    </p>
30
+{% endblock %}

+ 40
- 0
templates/logs.html Parādīt failu

@@ -0,0 +1,40 @@
1
+{% extends "base.html" %}
2
+{% block title %}Pluto - Logs{% endblock %}
3
+{% block content %}
4
+    <p>
5
+        Here are the last {{ n }} requests observed to <span class="monospace">/hook</span>:
6
+    </p>
7
+
8
+    <table id="logs">
9
+        <tr>
10
+            <th>Time</th>
11
+            <th>Path</th>
12
+            <th>Headers</th>
13
+            <th>Data</th>
14
+            <th>Hooks</th>
15
+        </tr>
16
+    {% for log in logs %}
17
+        <tr>
18
+            <td class="time">{{ log.time | ctime }}</td>
19
+            <td class="path">{{ log.path }}</td>
20
+            <td class="headers">{{ log.headers | hloads | pprint(True) }}</td>
21
+            <td class="data">{{ log.data | jloads | pprint(True) }}</td>
22
+            <td class="hooks">
23
+                {% if log.hooks %}
24
+                    {% for hookid in log.hooks.split(',') %}
25
+                        {% set num = hookid|int %}
26
+                        {% set hook = g.Hook.get_one(rowid = num) %}
27
+                        {{ macros.hook(hook) }}
28
+                    {% endfor %}
29
+                {% else %}
30
+                    <span class="nodata">No hooks.</span>
31
+                {% endif %}
32
+            </td>
33
+        </tr>
34
+    {% else %}
35
+        <tr>
36
+            <td colspan="4" class="nodata">No data.</td>
37
+        </tr>
38
+    {% endfor %}
39
+    </table>
40
+{% endblock %}

+ 123
- 0
templates/macros.html Parādīt failu

@@ -0,0 +1,123 @@
1
+{% macro checked_if(value) -%}{% if value %}checked{% endif %}{%- endmacro %}
2
+
3
+{% macro text_in(desc, name, value) -%}
4
+    <p>{{desc}}: <input type="text" name="{{ name }}" value="{{ value }}"/></p>
5
+{%- endmacro %}
6
+
7
+{% macro text_in_id(desc, id, name, value) -%}
8
+    <p><span id="{{ id }}">{{desc}}</span>: <input type="text" name="{{ name }}" value="{{ value }}"/></p>
9
+{%- endmacro %}
10
+
11
+{% macro select_in(desc, name, value, displays, options) -%}
12
+    <p>{{desc}}:
13
+        <select name="{{ name }}" value="{{ value }}">
14
+            {% for disp in displays %}
15
+                {% set opt = options[loop.index0] %}
16
+                <option value="{{ opt }}">{{ disp }}</option>
17
+            {% endfor %}
18
+        </select>
19
+    </p>
20
+{%- endmacro %}
21
+
22
+{% macro select_in_id(desc, id, name, value, displays, options) -%}
23
+    <p>{{desc}}:
24
+        <select name="{{ name }}" value="{{ value }}" id="{{ id }}">
25
+            {% for disp in displays %}
26
+                {% set opt = options[loop.index0] %}
27
+                <option value="{{ opt }}">{{ disp }}</option>
28
+            {% endfor %}
29
+        </select>
30
+    </p>
31
+{%- endmacro %}
32
+
33
+{% macro checkbox_in(desc, name, value) -%}
34
+    <p><label><input type="checkbox" name="{{ name }}" value="1" {{ checked_if(value) }}/>{{ desc }}</label></p>
35
+{%- endmacro %}
36
+
37
+{% macro hook(hook) -%}
38
+    {% if hook %}
39
+        <a class="hook" href="/hook/{{ hook.rowid }}">hook {{ hook.name }} ({{ hook.rowid }})</a>
40
+    {% else %}
41
+        <span class="nodata">Bad hook</span>
42
+    {% endif %}
43
+{%- endmacro %}
44
+
45
+{% macro hook_id(rowid) -%}
46
+    {{ hook(safe_load('Hook', rowid)) }}
47
+{%- endmacro %}
48
+
49
+{% macro hook_params(hook) -%}
50
+    {{ text_in('Name', 'name', hook.name) }}
51
+    {{ checkbox_in('Disable hook', 'disabled', hook.disabled) }}
52
+    {{ checkbox_in('Debug hook', 'debugged', hook.debugged) }}
53
+{%- endmacro %}
54
+    
55
+
56
+{% macro cond(cond) -%}
57
+    {% if cond %}
58
+    <a class="cond" href="/cond/{{ cond.rowid }}">condition {{ cond.rowid }}, selector {{ cond.selector }} ({{ cond.s1 | pprint }}, {{ cond.s2 | pprint }}, {{ cond.s3 | pprint }}), test {{ cond.test }} ({{ cond.t1 | pprint }}, {{ cond.t2 | pprint }}, {{ cond.t3 | pprint}})</a>
59
+    {% else %}
60
+        <span class="nodata">Bad condition</span>
61
+    {% endif %}
62
+{%- endmacro %}
63
+
64
+{% macro cond_id(rowid) -%}
65
+    {{ cond(safe_load('Condition', rowid)) }}
66
+{%- endmacro %}
67
+
68
+{% macro cond_params(cond) -%}
69
+    {{ select_in_id('Selector', 'select_selector', 'selector', cond.selector, ['header', 'JSON', 'path'], ['header', 'JSON', 'path']) }}
70
+    {{ text_in_id('Selector parameter 1', 'label_s1', 's1', cond.s1) }}
71
+    {{ text_in_id('Selector parameter 2', 'label_s2', 's2', cond.s2) }}
72
+    {{ text_in_id('Selector parameter 3', 'label_s3', 's3', cond.s3) }}
73
+    {{ select_in_id('Test', 'select_test', 'test', cond.test, ['equal', 'inrange', 'truthy', 'contains'], ['equal', 'inrange', 'truthy', 'contains']) }}
74
+    {{ text_in_id('Test parameter 1', 'label_t1', 't1', cond.t1) }}
75
+    {{ text_in_id('Test parameter 2', 'label_t2', 't2', cond.t2) }}
76
+    {{ text_in_id('Test parameter 3', 'label_t3', 't3', cond.t3) }}
77
+    {{ checkbox_in('Invert this condition', 'invert', cond.invert) }}
78
+    <script type="text/javascript">
79
+        reg_selector_select(
80
+            document.querySelector("#select_selector"),
81
+            document.querySelector("#label_s1"),
82
+            document.querySelector("#label_s2"),
83
+            document.querySelector("#label_s3")
84
+        );
85
+        reg_test_select(
86
+            document.querySelector("#select_test"),
87
+            document.querySelector("#label_t1"),
88
+            document.querySelector("#label_t2"),
89
+            document.querySelector("#label_t3")
90
+        );
91
+    </script>
92
+{%- endmacro %}
93
+
94
+{% macro act(act) -%}
95
+    {% if act %}
96
+    <a class="act" href="/act/{{ act.rowid }}">action {{ act.rowid }}, action {{ act.action }} ({{ act.a1 | pprint }}, {{ act.a2 | pprint }}, {{ act.a3 | pprint }})</a>
97
+    {% else %}
98
+        <span class="nodata">Bad action</span>
99
+    {% endif %}
100
+{%- endmacro %}
101
+
102
+{% macro act_id(rowid) -%}
103
+    {{ act(safe_load('Action', rowid)) }}
104
+{%- endmacro %}
105
+
106
+{% macro act_params(act) -%}
107
+    {{ select_in_id('Action', 'select_action', 'action', act.action, ['post', 'gitlab', 'system', 'udp', 'tcp', 'set_response'], ['post', 'gitlab', 'system', 'udp', 'tcp', 'set_response']) }}
108
+    {{ text_in_id('Action parameter 1', 'label_a1', 'a1', act.a1) }}
109
+    {{ text_in_id('Action parameter 2', 'label_a2', 'a2', act.a2) }}
110
+    {{ text_in_id('Action parameter 3', 'label_a3', 'a3', act.a3) }}
111
+    <script type="text/javascript">
112
+        reg_action_select(
113
+            document.querySelector("#select_action"),
114
+            document.querySelector("#label_a1"),
115
+            document.querySelector("#label_a2"),
116
+            document.querySelector("#label_a3")
117
+        );
118
+    </script>
119
+{%- endmacro %}
120
+
121
+{% macro yesno(booly) -%}
122
+    {% if booly %}<span class="positive">Yes</span>{% else %}<span class="negative">No</span>{% endif %}
123
+{%- endmacro %}

+ 34
- 0
util.py Parādīt failu

@@ -0,0 +1,34 @@
1
+import json
2
+
3
+def header_dumps(hdr):
4
+    return '\n'.join('%s: %s'%(k, v) for k, v in hdr.items())
5
+
6
+def header_loads(hdr):
7
+    d = {}
8
+    for line in hdr.split('\n'):
9
+        k, _, v = line.partition(': ')
10
+        if _:
11
+            d[k] = v
12
+    return d
13
+
14
+def jloads(s):
15
+    try:
16
+        return json.loads(s)
17
+    except ValueError:
18
+        return s
19
+
20
+def jdumps(obj):
21
+    try:
22
+        return json.dumps(obj)
23
+    except ValueError:
24
+        return obj
25
+
26
+def checkbox(request, name):
27
+    return (True if request.values.get(name, 0, int) else False)
28
+
29
+def safe_load(tpn, rowid):
30
+    import model
31
+    try:
32
+        return getattr(model, tpn).get_one(rowid=rowid)
33
+    except (AttributeError, model.DBError):
34
+        return None

Notiek ielāde…
Atcelt
Saglabāt