sgfa 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1190 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Web interface to Binder
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'rack'
13
+ require 'time'
14
+
15
+ require_relative 'base'
16
+ require_relative '../error'
17
+
18
+ module Sgfa
19
+ module Web
20
+
21
+
22
+ #####################################################################
23
+ # Binder web interface
24
+ #
25
+ # @todo Add a docket view
26
+ class Binder < Base
27
+
28
+ #####################################
29
+ # Request
30
+ #
31
+ # @param env [Hash] The Rack environment for this request, with app
32
+ # specific options
33
+ # @option env [String] 'sgfa.binder.url' URL encoded binder name
34
+ # @option env [String] 'sgfa.binder.name' The name of the binder
35
+ # @option env [Binder] 'sgfa.binder' The binder
36
+ # @option env [String] 'sgfa.user' The user name
37
+ # @option env [Array] 'sgfa.groups' Array of groups the user belongs to
38
+ #
39
+ def call(env)
40
+ _call(env)
41
+ return response(env)
42
+
43
+ rescue Error::Permission => exp
44
+ env['sgfa.status'] = :deny
45
+ env['sgfa.html'] = _escape_html(exp.message)
46
+ return response(env)
47
+
48
+ rescue Error::Conflict => exp
49
+ env['sgfa.status'] = :conflict
50
+ env['sgfa.html'] = _escape_html(exp.message)
51
+ return response(env)
52
+
53
+ rescue Error::NonExistent => exp
54
+ env['sgfa.status'] = :notfound
55
+ env['sgfa.html'] = _escape_html(exp.message)
56
+ return response(env)
57
+
58
+ rescue Error::Limits => exp
59
+ env['sgfa.status'] = :badreq
60
+ env['sgfa.html'] = _escape_html(exp.message)
61
+ return response(env)
62
+
63
+ rescue Error::Corrupt => exp
64
+ env['sgfa.status'] = :servererror
65
+ env['sgfa.html'] = _escape_html(exp.message)
66
+ return response(env)
67
+
68
+ end # def request()
69
+
70
+
71
+ #####################################
72
+ # Process the request
73
+ #
74
+ # This is a seperate method to simplify the flow control by using return.
75
+ def _call(env)
76
+
77
+ # defaults
78
+ env['sgfa.status'] = :badreq
79
+ env['sgfa.title'] = 'SFGA Error'
80
+ env['sfga.navbar'] = ''
81
+ env['sgfa.html'] = 'Invalid request'
82
+
83
+ path = env['PATH_INFO'].split('/')
84
+ if path.empty?
85
+ jacket = nil
86
+ else
87
+ path.shift if path[0].empty?
88
+ jacket = path.shift
89
+ end
90
+
91
+ # just the binder
92
+ if !jacket
93
+ case env['REQUEST_METHOD']
94
+ when 'GET'; return _get_jackets(env, path)
95
+ when 'POST'; return _post_binder(env)
96
+ else; return
97
+ end
98
+ end
99
+
100
+ # special binder pages
101
+ if jacket[0] == '_'
102
+ return if env['REQUEST_METHOD'] != 'GET'
103
+ case jacket
104
+ when '_jackets'
105
+ return _get_jackets(env, path)
106
+ when '_users'
107
+ return _get_users(env, path)
108
+ when '_values'
109
+ return _get_values(env, path)
110
+ when '_info'
111
+ return _get_binder(env, path)
112
+ else
113
+ return
114
+ end
115
+ end
116
+
117
+ # jacket info stored
118
+ env['sgfa.jacket.url'] = jacket
119
+ env['sgfa.jacket.name'] = _escape_un(jacket)
120
+ cmd = path.shift
121
+
122
+ # just the jacket
123
+ if !cmd
124
+ case env['REQUEST_METHOD']
125
+ when 'GET'; return _get_tag(env, path)
126
+ when 'POST'; return _post_jacket(env)
127
+ else; return
128
+ end
129
+ end
130
+
131
+ # jacket stuff
132
+ return if env['REQUEST_METHOD'] != 'GET'
133
+ case cmd
134
+ when '_edit'; return _get_edit(env, path)
135
+ when '_entry'; return _get_entry(env, path)
136
+ when '_history'; return _get_history(env, path)
137
+ when '_attach'; return _get_attach(env, path)
138
+ when '_tag'; return _get_tag(env, path)
139
+ when '_log'; return _get_log(env, path)
140
+ when '_list'; return _get_list(env, path)
141
+ when '_info'; return _get_info(env, path)
142
+ else; return
143
+ end
144
+
145
+ end # def _call()
146
+
147
+
148
+ NavBarBinder = [
149
+ ['Jackets', '_jackets'],
150
+ ['Users', '_users'],
151
+ ['Values', '_values'],
152
+ ['Binder', '_info'],
153
+ ]
154
+
155
+
156
+ #####################################
157
+ # Generate navigation bar for a binder
158
+ def _navbar_binder(env, act)
159
+ env['sgfa.title'] = 'SGFA Binder %s &mdash; %s' %
160
+ [act, _escape_html(env['sgfa.binder.name'])]
161
+ base = env['SCRIPT_NAME']
162
+ txt = _navbar(env, act, NavBarBinder, base)
163
+ if env['sgfa.cabinet.url']
164
+ txt << "<div class='link'><a href='%s'>Cabinet</a></div>\n" %
165
+ env['sgfa.cabinet.url']
166
+ end
167
+ env['sgfa.navbar'] = txt
168
+ end # def _navbar_binder()
169
+
170
+
171
+ NavBarJacket = [
172
+ ['Tag', '_tag'],
173
+ ['List', '_list'],
174
+ ['Entry', nil],
175
+ ['Edit', '_edit'],
176
+ ['History', nil],
177
+ ['Attachment', nil],
178
+ ['Jacket', '_info'],
179
+ ['Log', '_log'],
180
+ ]
181
+
182
+ #####################################
183
+ # Generate navbar for a jacket
184
+ def _navbar_jacket(env, act)
185
+ env['sgfa.title'] = 'SGFA Jacket %s &mdash; %s : %s' % [
186
+ act,
187
+ _escape_html(env['sgfa.binder.name']),
188
+ _escape_html(env['sgfa.jacket.name'])
189
+ ]
190
+ base = env['SCRIPT_NAME'] + '/' + env['sgfa.jacket.url']
191
+ txt = _navbar(env, act, NavBarJacket, base)
192
+ txt << "<div class='link'><a href='%s'>Binder</a></div>\n" %
193
+ env['SCRIPT_NAME']
194
+ env['sgfa.navbar'] = txt
195
+ end # def _navbar_jacket()
196
+
197
+
198
+ #####################################
199
+ # Link to a jacket
200
+ def _link_jacket(env, jnam, disp)
201
+ "<a href='%s/%s'>%s</a>" % [env['SCRIPT_NAME'], _escape(jnam), disp]
202
+ end # def _link_jacket()
203
+
204
+
205
+ #####################################
206
+ # Link to edit entry
207
+ def _link_edit(env, enum, disp)
208
+ "<a href='%s/%s/_edit/%d'>%s</a>" % [
209
+ env['SCRIPT_NAME'],
210
+ env['sgfa.jacket.url'],
211
+ enum,
212
+ disp
213
+ ]
214
+ end # def _link_edit()
215
+
216
+
217
+ #####################################
218
+ # Link to display an entry
219
+ def _link_entry(env, enum, disp)
220
+ "<a href='%s/%s/_entry/%d'>%s</a>" % [
221
+ env['SCRIPT_NAME'],
222
+ env['sgfa.jacket.url'],
223
+ enum,
224
+ disp
225
+ ]
226
+ end # def _link_entry()
227
+
228
+
229
+ #####################################
230
+ # Link to a specific revision
231
+ def _link_revision(env, enum, rnum, disp)
232
+ "<a href='%s/%s/_entry/%d/%d'>%s</a>" % [
233
+ env['SCRIPT_NAME'],
234
+ env['sgfa.jacket.url'],
235
+ enum,
236
+ rnum,
237
+ disp
238
+ ]
239
+ end # def _link_revision()
240
+
241
+
242
+ #####################################
243
+ # Link to a history item
244
+ def _link_history(env, hnum, disp)
245
+ "<a href='%s/%s/_history/%d'>%s</a>" % [
246
+ env['SCRIPT_NAME'],
247
+ env['sgfa.jacket.url'],
248
+ hnum, disp
249
+ ]
250
+ end # def _link_history()
251
+
252
+
253
+ #####################################
254
+ # Link to a tag
255
+ def _link_tag(env, tag, disp)
256
+ "<a href='%s/%s/_tag/%s'>%s</a>" % [
257
+ env['SCRIPT_NAME'],
258
+ env['sgfa.jacket.url'],
259
+ _escape(tag),
260
+ disp
261
+ ]
262
+ end # def _link_tag()
263
+
264
+
265
+ #####################################
266
+ # Link to a tag prefix
267
+ def _link_prefix(env, pre, disp)
268
+ "<a href='%s/%s/_list/%s'>%s</a>" % [
269
+ env['SCRIPT_NAME'],
270
+ env['sgfa.jacket.url'],
271
+ _escape(pre),
272
+ disp
273
+ ]
274
+ end # def _link_prefix()
275
+
276
+
277
+ #####################################
278
+ # Link to an attachments
279
+ def _link_attach(env, enum, anum, hnum, name, disp)
280
+ "<a href='%s/%s/_attach/%d-%d-%d/%s'>%s</a>" % [
281
+ env['SCRIPT_NAME'],
282
+ env['sgfa.jacket.url'],
283
+ enum,
284
+ anum,
285
+ hnum,
286
+ _escape(name),
287
+ disp
288
+ ]
289
+ end # def _link_attach()
290
+
291
+
292
+ JacketsTable =
293
+ "<table class='list'>" +
294
+ "<tr><th>Jacket</th><th>Title</th><th>Perms</th></tr>\n" +
295
+ "%s</table>"
296
+
297
+ JacketsRow =
298
+ "<tr><td class='name'>%s</td>" +
299
+ "<td class='title'>%s</td><td class='perms'>%s</td></tr>\n"
300
+
301
+ JacketsForm =
302
+ "\n<hr>\n<form class='edit' method='post' action='%s' " +
303
+ "enctype='multipart/form-data'>\n" +
304
+ "<fieldset><legend>Create or Edit Jacket</legend>\n" +
305
+ "<label for='jacket'>Name:</label>" +
306
+ "<input class='jacket' name='jacket' type='text'><br>\n" +
307
+ "<label for='newname'>Rename:</label>" +
308
+ "<input class='jacket' name='newname' type='text'><br>\n" +
309
+ "<label for='title'>Title:</label>" +
310
+ "<input class='title' name='title' type='text'><br>\n" +
311
+ "<label for='perms'>Perms:</label>" +
312
+ "<input class='perms' name='perms' type='text'><br>\n" +
313
+ "</fieldset>\n" +
314
+ "<input type='submit' name='create' value='Create/Edit'>\n" +
315
+ "</form>\n"
316
+
317
+ #####################################
318
+ # Get jacket list
319
+ def _get_jackets(env, path)
320
+ _navbar_binder(env, 'Jackets')
321
+
322
+ if !path.empty?
323
+ env['sgfa.status'] = :badreq
324
+ env['sgfa.html'] = 'Invalid URL requested'
325
+ return
326
+ end
327
+
328
+ tr = _trans(env)
329
+ info = env['sgfa.binder'].binder_info(tr)
330
+
331
+ env['sgfa.status'] = :ok
332
+ env['sgfa.html'] = _disp_jackets(env, info[:jackets], tr)
333
+ end # def _get_jackets()
334
+
335
+
336
+ #####################################
337
+ # Display jacket list
338
+ def _disp_jackets(env, jackets, tr)
339
+
340
+ rows = ''
341
+ jackets.each do |jnam, jinfo|
342
+ perms = jinfo[:perms]
343
+ ps = perms.empty? ? '-' : _escape_html(perms.join(', '))
344
+ rows << JacketsRow % [_link_jacket(env, jnam, _escape_html(jnam)),
345
+ _escape_html(jinfo[:title]), ps]
346
+ end
347
+ html = JacketsTable % rows
348
+ html << JacketsForm % env['SCRIPT_NAME'] if tr[:perms].include?('manage')
349
+
350
+ return html
351
+ end # def _disp_jackets()
352
+
353
+
354
+ BinderTable =
355
+ "<table>\n" +
356
+ "<tr><td>Hash ID:</td><td>%s</td></tr>\n" +
357
+ "<tr><td>Text ID:</td><td>%s</td></tr>\n" +
358
+ "<tr><td>Jackets:</td><td>%d</td></tr>\n" +
359
+ "<tr><td>Users:</td><td>%d</td></tr>\n" +
360
+ "<tr><td>Values:</td><td>%d</td></tr>\n" +
361
+ "<tr><td>User Permissions:</td><td>%s</td></tr>\n" +
362
+ "</table>\n"
363
+
364
+ #####################################
365
+ # Get binder info
366
+ def _get_binder(env, path)
367
+ _navbar_binder(env, 'Binder')
368
+ return if !path.empty?
369
+
370
+ tr = _trans(env)
371
+ info = env['sgfa.binder'].binder_info(tr)
372
+
373
+ env['sgfa.status'] = :ok
374
+ env['sgfa.html'] = BinderTable % [
375
+ info[:id_hash], _escape_html(info[:id_text]),
376
+ info[:jackets].size, info[:users].size, info[:values].size,
377
+ _escape_html(tr[:perms].join(', '))
378
+ ]
379
+ end # def _get_binder()
380
+
381
+
382
+ UsersTable =
383
+ "<table class='list'>\n" +
384
+ "<tr><th>User</th><th>Permissions</th></tr>\n" +
385
+ "%s</table>\n"
386
+
387
+ UsersRow =
388
+ "<tr><td>%s</td><td>%s</td></tr>\n"
389
+
390
+ UsersForm =
391
+ "\n<hr>\n<form class='edit' method='post' action='%s' " +
392
+ "enctype='multipart/form-data'>\n" +
393
+ "<fieldset><legend>Set User or Group Permissions</legend>\n" +
394
+ "<label for='user'>Name:</label>" +
395
+ "<input class='user' name='user' type='text'><br>\n" +
396
+ "<label for='perms'>Perms:</label>" +
397
+ "<input class='perms' name='perms' type='text'><br>\n" +
398
+ "</fieldset>\n" +
399
+ "<input type='submit' name='set' value='Set'>\n" +
400
+ "</form>\n"
401
+
402
+ #####################################
403
+ # Get users
404
+ def _get_users(env, path)
405
+ _navbar_binder(env, 'Users')
406
+
407
+ if !path.empty?
408
+ env['sgfa.status'] = :badreq
409
+ env['sgfa.html'] = 'Invalid URL requested'
410
+ return
411
+ end
412
+
413
+ tr = _trans(env)
414
+ info = env['sgfa.binder'].binder_info(tr)
415
+
416
+ env['sgfa.status'] = :ok
417
+ env['sgfa.html'] = _disp_users(env, info[:users], tr)
418
+ end # def _get_users()
419
+
420
+
421
+ #####################################
422
+ # Display users
423
+ def _disp_users(env, users, tr)
424
+ rows = ''
425
+ users.each do |unam, pl|
426
+ perms = pl.empty? ? '-' : pl.join(', ')
427
+ rows << UsersRow % [
428
+ _escape_html(unam), _escape_html(perms)
429
+ ]
430
+ end
431
+ html = UsersTable % rows
432
+ html << (UsersForm % env['SCRIPT_NAME']) if tr[:perms].include?('manage')
433
+ return html
434
+ end # def _disp_users()
435
+
436
+
437
+ ValuesTable =
438
+ "<table class='list'>\n" +
439
+ "<tr><th>Value</th><th>State</th></tr>\n" +
440
+ "%s\n</table>\n"
441
+
442
+ ValuesRow =
443
+ "<tr><td>%s</td><td>%s</td></tr>\n"
444
+
445
+ ValuesForm =
446
+ "\n<hr>\n<form class='edit' method='post' action='%s' " +
447
+ "enctype='multipart/form-data'>\n" +
448
+ "<fieldset><legend>Assign Binder Values</legend>\n" +
449
+ "<label for='value'>Value:</label>" +
450
+ "<input class='value' name='value' type='text'><br>\n" +
451
+ "<label for='state'>State:</label>" +
452
+ "<input class='state' name='state' type='text'><br>\n" +
453
+ "</fieldset>\n" +
454
+ "<input type='submit' name='assign' value='Assign'>\n" +
455
+ "</form>\n"
456
+
457
+ #####################################
458
+ # Get values
459
+ def _get_values(env, path)
460
+ _navbar_binder(env, 'Values')
461
+
462
+ if !path.empty?
463
+ env['sgfa.status'] = :badreq
464
+ env['sgfa.html'] = 'Invalid URL requested'
465
+ return
466
+ end
467
+
468
+ tr = _trans(env)
469
+ info = env['sgfa.binder'].binder_info(tr)
470
+
471
+ env['sgfa.status'] = :ok
472
+ env['sgfa.html'] = _disp_values(env, info[:values], tr)
473
+ end # def _get_values()
474
+
475
+
476
+ #####################################
477
+ # Display values
478
+ def _disp_values(env, values, tr)
479
+ rows = ''
480
+ values.each do |vnam, vset|
481
+ rows << ValuesRow % [
482
+ _escape_html(vnam), _escape_html(vset)
483
+ ]
484
+ end
485
+ html = ValuesTable % rows
486
+ html << (ValuesForm % env['SCRIPT_NAME']) if tr[:perms].include?('manage')
487
+
488
+ return html
489
+ end # def _disp_values()
490
+
491
+
492
+ TagTable =
493
+ "<div class='title'>Tag: %s</div>\n" +
494
+ "<table class='list'>\n<tr>" +
495
+ "<th>Time</th><th>Title</th><th>Files</th><th>Tags</th><th>Edit</th>" +
496
+ "</tr>\n%s</table>\n"
497
+
498
+ TagRow =
499
+ "<tr><td class='time'>%s</td><td class='title'>%s</td>" +
500
+ "<td class='num'>%d</td><td class='num'>%d</td>" +
501
+ "<td class='act'>%s</td></tr>\n"
502
+
503
+ PageSize = 25
504
+ PageSizeMax = 100
505
+
506
+ #####################################
507
+ # Get a tag
508
+ def _get_tag(env, path)
509
+ _navbar_jacket(env, 'Tag')
510
+
511
+ if path.empty?
512
+ tag = '_all'
513
+ else
514
+ tag = _escape_un(path.shift)
515
+ end
516
+ page = path.empty? ? 1 : path.shift.to_i
517
+ page = 1 if page == 0
518
+ rck = Rack::Request.new(env)
519
+ params = rck.GET
520
+ per = params['perpage'] ? params['perpage'].to_i : 0
521
+ if per == 0 || per > PageSizeMax
522
+ per = PageSize
523
+ end
524
+
525
+ tr = _trans(env)
526
+ size, ents = env['sgfa.binder'].read_tag(tr, tag, (page-1)*per, per)
527
+ if ents.size == 0
528
+ html = 'No entries'
529
+ else
530
+ rows = ''
531
+ ents.reverse_each do |enum, rnum, time, title, tcnt, acnt|
532
+ rows << TagRow % [
533
+ time.localtime.strftime("%F %T %z"),
534
+ _link_entry(env, enum, _escape_html(title)),
535
+ acnt, tcnt,
536
+ _link_edit(env, enum, 'edit')
537
+ ]
538
+ end
539
+ html = TagTable % [_escape_html(tag), rows]
540
+ end
541
+
542
+ link = '%s/%s/_tag/%s' % [
543
+ env['SCRIPT_NAME'],
544
+ env['sgfa.jacket.url'],
545
+ _escape(tag)
546
+ ]
547
+ query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
548
+ pages = _link_pages(page, per, size, link, query)
549
+
550
+ env['sgfa.status'] = :ok
551
+ env['sgfa.html'] = html + pages
552
+ end # def _get_tag()
553
+
554
+
555
+ LogTable =
556
+ "<table class='list'>\n<tr>" +
557
+ "<th>History</th><th>Date/Time</th><th>User</th><th>Entries</th>" +
558
+ "<th>Attachs</th></tr>\n%s</table>\n"
559
+
560
+ LogRow =
561
+ "<tr><td class='hnum'>%d</td><td class='time'>%s</td>" +
562
+ "<td class='user'>%s</td><td>%d</td><td>%d</td></tr>\n"
563
+
564
+ #####################################
565
+ # Get the log
566
+ def _get_log(env, path)
567
+ _navbar_jacket(env, 'Log')
568
+
569
+ page = path.empty? ? 1 : path.shift.to_i
570
+ page = 1 if page == 0
571
+ return if !path.empty?
572
+ rck = Rack::Request.new(env)
573
+ params = rck.GET
574
+ per = params['perpage'] ? params['perpage'].to_i : 0
575
+ if per == 0 || per > PageSizeMax
576
+ per = PageSize
577
+ end
578
+
579
+ tr = _trans(env)
580
+ size, hsts = env['sgfa.binder'].read_log(tr, (page-1)*per, per)
581
+ if hsts.size == 0
582
+ env['sgfa.html'] = 'No history'
583
+ env['sgfa.status'] = :notfound
584
+ else
585
+ rows = ''
586
+ hsts.each do |hnum, time, user, ecnt, acnt|
587
+ rows << LogRow % [
588
+ hnum,
589
+ _link_history(env, hnum, time.localtime.strftime('%F %T %z')),
590
+ _escape_html(user),
591
+ ecnt, acnt
592
+ ]
593
+ end
594
+ link = '%s/%s/_log' % [env['SCRIPT_NAME'], env['sgfa.jacket.url']]
595
+ query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
596
+ env['sgfa.status'] = :ok
597
+ env['sgfa.html'] = (LogTable % rows) +
598
+ _link_pages(page, per, size, link, query)
599
+ end
600
+
601
+ end # def _get_log()
602
+
603
+
604
+ #####################################
605
+ # Get an entry
606
+ def _get_entry(env, path)
607
+ _navbar_jacket(env, 'Entry')
608
+
609
+ return if path.empty?
610
+ enum = path.shift.to_i
611
+ rnum = path.empty? ? 0 : path.shift.to_i
612
+ return if enum == 0
613
+
614
+ tr = _trans(env)
615
+ ent = env['sgfa.binder'].read_entry(tr, enum, rnum)
616
+
617
+ env['sgfa.status'] = :ok
618
+ env['sgfa.html'] = _disp_entry(env, ent)
619
+ end # def _get_entry()
620
+
621
+
622
+ EntryDisp =
623
+ "<div class='title'>%s</div>\n" +
624
+ "<div class='body'><pre>%s</pre></div>\n" +
625
+ "<div class='sidebar'>\n" +
626
+ "<div class='time'>%s</div>\n" +
627
+ "<div class='history'>Revision: %d %s %s<br>History: %s %s</div>\n" +
628
+ "<div class='tags'>%s</div>\n" +
629
+ "<div class='attach'>%s</div>\n" +
630
+ "</div>\n" +
631
+ "<div class='hash'>Hash: %s<br>Jacket: %s</div>\n"
632
+
633
+ #####################################
634
+ # Display an entry
635
+ def _disp_entry(env, ent)
636
+
637
+ enum = ent.entry
638
+ rnum = ent.revision
639
+ hnum = ent.history
640
+
641
+ tl = ent.tags
642
+ tags = "Tags:<br>\n"
643
+ if tl.empty?
644
+ tags << "none\n"
645
+ else
646
+ tl.each do |tag|
647
+ tags << _link_tag(env, tag, _escape_html(tag)) + "<br>\n"
648
+ end
649
+ end
650
+
651
+ al = ent.attachments
652
+ att = "Attachments:<br>\n"
653
+ if al.empty?
654
+ att << "none\n"
655
+ else
656
+ al.each do |anum, hnum, name|
657
+ att << _link_attach(env, enum, anum, hnum, name, _escape_html(name)) +
658
+ "<br>\n"
659
+ end
660
+ end
661
+ if rnum == 1
662
+ prev = 'previous'
663
+ else
664
+ prev = _link_revision(env, enum, rnum-1, 'previous')
665
+ end
666
+ curr = _link_entry(env, enum, 'current')
667
+ edit = _link_edit(env, enum, 'edit')
668
+
669
+ body = EntryDisp % [
670
+ _escape_html(ent.title),
671
+ _escape_html(ent.body),
672
+ ent.time.localtime.strftime('%F %T %z'),
673
+ rnum, prev, curr, _link_history(env, hnum, hnum.to_s), edit,
674
+ tags,
675
+ att,
676
+ ent.hash, ent.jacket
677
+ ]
678
+
679
+ return body
680
+ end # def _disp_entry()
681
+
682
+
683
+ HistoryDisp =
684
+ "<div class='title'>History %d</div>\n" +
685
+ "<div class='body'>%s</div>" +
686
+ "<div class='sidebar'>\n" +
687
+ "<div class='time'>%s</div>\n" +
688
+ "<div class='user'>%s</div>\n" +
689
+ "<div class='nav'>%s %s</div>\n" +
690
+ "</div>\n<div class='hash'>Hash: %s<br>Jacket: %s</div>\n"
691
+
692
+ HistoryTable =
693
+ "<table class='list'>\n<tr><th>Item</th><th>Hash</th></tr>\n" +
694
+ "%s</table>\n"
695
+
696
+ HistoryItem =
697
+ "<tr><td>%s</td><td class='hash'>%s</td></tr>\n"
698
+
699
+ #####################################
700
+ # Display a history item
701
+ def _get_history(env, path)
702
+ _navbar_jacket(env, 'History')
703
+
704
+ return if path.empty?
705
+ hnum = path.shift.to_i
706
+ return if hnum == 0
707
+
708
+ tr = _trans(env)
709
+ hst = env['sgfa.binder'].read_history(tr, hnum)
710
+ hnum = hst.history
711
+ plnk = (hnum == 1) ? 'Previous' : _link_history(env, hnum-1, 'Previous')
712
+ nlnk = _link_history(env, hnum+1, 'Next')
713
+
714
+ rows = ""
715
+ hst.entries.each do |enum, rnum, hash|
716
+ disp = "Entry %d-%d" % [enum, rnum]
717
+ rows << (HistoryItem % [_link_revision(env, enum, rnum, disp), hash])
718
+ end
719
+ hst.attachments.each do |enum, anum, hash|
720
+ disp = "Attach %d-%d-%d" % [enum, anum, hnum]
721
+ rows << HistoryItem %
722
+ [_link_attach(env, enum, anum, hnum, hash + '.bin', disp), hash]
723
+ end
724
+ tab = HistoryTable % rows
725
+
726
+ body = HistoryDisp % [
727
+ hnum, tab, hst.time.localtime.strftime('%F %T %z'),
728
+ _escape_html(hst.user), plnk, nlnk, hst.hash, hst.jacket
729
+ ]
730
+
731
+ env['sgfa.status'] = :ok
732
+ env['sgfa.html'] = body
733
+ end # def _get_history()
734
+
735
+
736
+ ListTable =
737
+ "<table class='list'>\n<tr>" +
738
+ "<th>Tag</th><th>Number</th></tr>\n" +
739
+ "%s\n</table>\n"
740
+
741
+ ListPrefix =
742
+ "<tr><td class='prefix'>%s: prefix</td><td>%d tags</td></tr>\n"
743
+
744
+ ListTag =
745
+ "<tr><td class='tag'>%s</td><td>%d entries</td></tr>\n"
746
+
747
+ #####################################
748
+ # Get list of tags
749
+ def _get_list(env, path)
750
+ _navbar_jacket(env, 'List')
751
+
752
+ bnd = env['sgfa.binder']
753
+ tr = _trans(env)
754
+ lst = bnd.read_list(tr)
755
+
756
+ # sort into prefixed & regular
757
+ prefix = {}
758
+ regular = []
759
+ lst.each do |tag|
760
+ idx = tag.index(':')
761
+ if !idx
762
+ regular.push tag
763
+ next
764
+ end
765
+ pre = tag[0,idx].strip
766
+ if prefix[pre]
767
+ prefix[pre].push tag
768
+ else
769
+ prefix[pre] = [tag]
770
+ end
771
+ end
772
+
773
+ # regular & prefix list
774
+ rows = ''
775
+ if path.empty?
776
+ prefix.keys.sort.each do |pre|
777
+ size = prefix[pre].size
778
+ rows << ListPrefix %
779
+ [_link_prefix(env, pre, _escape_html(pre)), size]
780
+ end
781
+ regular.sort.each do |tag|
782
+ size, ents = bnd.read_tag(tr, tag, 0, 0)
783
+ rows << ListTag %
784
+ [_link_tag(env, tag, _escape_html(tag)), size]
785
+ end
786
+
787
+ # list entire prefix
788
+ else
789
+ pre = _escape_un(path.shift)
790
+ return if !path.empty?
791
+ if !prefix[pre]
792
+ env['sgfa.status'] = :notfound
793
+ env['sgfa.html'] = 'Tag prefix not found'
794
+ return
795
+ end
796
+
797
+ prefix[pre].sort.each do |tag|
798
+ size, ents = bnd.read_tag(tr, tag, 0, 0)
799
+ rows << ListTag %
800
+ [_link_tag(env, tag, _escape_html(tag)), size]
801
+ end
802
+ end
803
+
804
+ env['sgfa.status'] = :ok
805
+ env['sgfa.html'] = ListTable % rows
806
+ end # def _get_list
807
+
808
+
809
+ InfoTable =
810
+ "<table>\n" +
811
+ "<tr><td>Text ID:</td><td>%s</td></tr>\n" +
812
+ "<tr><td>Hash ID:</td><td>%s</td></tr>\n" +
813
+ "<tr><td>Last Edit:</td><td>%s</td></tr>\n" +
814
+ "<tr><td>History:</td><td>%d</td></tr>\n" +
815
+ "<tr><td>Entries:</td><td>%d</td></tr>\n" +
816
+ "</table>\n"
817
+
818
+ #####################################
819
+ # Get jacket info
820
+ def _get_info(env, path)
821
+ _navbar_jacket(env, 'Jacket')
822
+ return if !path.empty?
823
+ tr = _trans(env)
824
+
825
+ info = env['sgfa.binder'].binder_info(tr)
826
+ hst = env['sgfa.binder'].read_history(tr, 0)
827
+ if hst
828
+ hmax = hst.history
829
+ emax = hst.entry_max
830
+ time = hst.time.localtime.strftime('%F %T %z')
831
+ else
832
+ hmax = 0
833
+ emax = 0
834
+ time = 'none'
835
+ end
836
+
837
+ jinf = info[:jackets][env['sgfa.jacket.name']]
838
+ env['sgfa.status'] = :ok
839
+ env['sgfa.html'] = InfoTable % [
840
+ jinf[:id_text], jinf[:id_hash],
841
+ time, hmax, emax
842
+ ]
843
+ end # def _get_info()
844
+
845
+
846
+ #####################################
847
+ # Get an attachment
848
+ def _get_attach(env, path)
849
+ _navbar_jacket(env, 'Attachment')
850
+
851
+ spec = path.shift
852
+ return if !spec
853
+ ma = /^(\d+)-(\d+)-(\d+)$/.match(spec)
854
+ return if !ma
855
+ name = path.shift
856
+ return if !name
857
+ return if !path.empty?
858
+ enum, anum, hnum = ma[1,3].map{|st| st.to_i}
859
+ name = _escape_un(name)
860
+
861
+ ext = name.rpartition('.')[2]
862
+ if ext.empty?
863
+ mime = 'application/octet-stream'
864
+ else
865
+ mime = Rack::Mime.mime_type('.' + ext)
866
+ end
867
+
868
+ tr = _trans(env)
869
+ file = env['sgfa.binder'].read_attach(tr, enum, anum, hnum)
870
+
871
+ env['sgfa.status'] = :ok
872
+ env['sgfa.headers'] = {
873
+ 'Content-Length' => file.size.to_s,
874
+ 'Content-Type' => mime,
875
+ 'Content-Disposition' => 'attachment',
876
+ }
877
+ env['sgfa.file'] = FileBody.new(file)
878
+
879
+ end # def _get_attach()
880
+
881
+
882
+ EditForm =
883
+ "<form class='edit' method='post' action='%s/%s' " +
884
+ "enctype='multipart/form-data'>\n" +
885
+ "<input name='entry' type='hidden' value='%d'>\n" +
886
+ "<input name='revision' type='hidden' value='%d'>\n" +
887
+
888
+ "<div class='edit'>\n" +
889
+
890
+ "<fieldset><legend>Basic Info</legend>\n" +
891
+
892
+ "<label for='title'>Title:</label>" +
893
+ "<input class='title' name='title' type='text' value='%s'><br>\n" +
894
+
895
+ "<label for='time'>Time:</label>" +
896
+ "<input name='time' type='text' value='%s'><br>\n" +
897
+
898
+ "<label for='body'>Body:</label>" +
899
+ "<textarea class='body' name='body'>%s</textarea>\n" +
900
+
901
+ "</fieldset>\n" +
902
+ "<fieldset><legend>Attachments</legend>\n%s</fieldset>\n" +
903
+ "<fieldset><legend>Tags</legend>\n%s</fieldset>\n" +
904
+
905
+ "<input type='submit' name='save' value='Save Changes'>\n" +
906
+ "</div></form>\n"
907
+
908
+ EditFilePre =
909
+ "<table class='edit_file'>\n" +
910
+ "<tr><th>Name</th><th>Upload/Replace</th></tr>\n"
911
+
912
+ EditFileEach =
913
+ "<tr><td><input name='attname%d' type='text' value='%s'>" +
914
+ "<input name='attnumb%d' type='hidden' value='%d'></td>" +
915
+ "<td><input name='attfile%d' type='file'></td></tr>\n"
916
+
917
+ EditFileCnt =
918
+ "</table>\n<input name='attcnt' type='hidden' value='%d'>\n"
919
+
920
+ EditTagOld =
921
+ "<input name='tag%d' type='text' value='%s'><br>\n"
922
+
923
+ EditTagNew =
924
+ "<input name='tag%d' type='text'><br>\n"
925
+
926
+ EditTagSel =
927
+ "%s: <select name='tag%d'>" +
928
+ "<option value='' selected></option>%s</select><br>\n"
929
+
930
+ EditTagOpt =
931
+ "<option value='%s: %s'>%s</option>"
932
+
933
+ EditTagCnt =
934
+ "<input name='tagcnt' type='hidden' value='%d'>\n"
935
+
936
+ #####################################
937
+ # Get edit form
938
+ def _get_edit(env, path)
939
+ _navbar_jacket(env, 'Edit')
940
+
941
+ tr = _trans(env)
942
+
943
+ if path.empty?
944
+ enum = 0
945
+ rnum = 0
946
+ ent = Entry.new
947
+ ent.title = 'Title'
948
+ ent.body = 'Body'
949
+ ent.time = Time.now
950
+ else
951
+ enum = path.shift.to_i
952
+ return if enum == 0 || !path.empty?
953
+ ent = env['sgfa.binder'].read_entry(tr, enum)
954
+ rnum = ent.revision
955
+ end
956
+
957
+ lst = env['sgfa.binder'].read_list(tr)
958
+ prefix = {}
959
+ lst.each do |tag|
960
+ idx = tag.index(':')
961
+ next unless idx
962
+ pre = tag[0,idx].strip
963
+ post = tag[idx+1..-1].strip
964
+ if prefix[pre]
965
+ prefix[pre].push post
966
+ else
967
+ prefix[pre] = [post]
968
+ end
969
+ end
970
+
971
+ tags = ''
972
+ cnt = 0
973
+ ent.tags.each do |tag|
974
+ tags << EditTagOld % [cnt, _escape_html(tag)]
975
+ cnt += 1
976
+ end
977
+ prefix.each do |pre, lst|
978
+ px = _escape_html(pre)
979
+ opts = ''
980
+ lst.each do |post|
981
+ ex = _escape_html(post)
982
+ opts << EditTagOpt % [px, ex, ex]
983
+ end
984
+ tags << EditTagSel % [px, cnt, opts]
985
+ cnt += 1
986
+ end
987
+ 5.times do |tg|
988
+ tags << EditTagNew % cnt
989
+ cnt += 1
990
+ end
991
+ tags << EditTagCnt % cnt
992
+
993
+ atts = "Attachments go here\n"
994
+ atts = EditFilePre.dup
995
+ cnt = 0
996
+ ent.attachments.each do |anum, hnum, name|
997
+ atts << EditFileEach % [cnt, _escape_html(name), cnt, anum, cnt]
998
+ cnt += 1
999
+ end
1000
+ 5.times do |ix|
1001
+ atts << EditFileEach % [cnt, '', cnt, 0, cnt]
1002
+ cnt += 1
1003
+ end
1004
+ atts << EditFileCnt % cnt
1005
+
1006
+ html = EditForm % [
1007
+ env['SCRIPT_NAME'], env['sgfa.jacket.url'], enum, rnum,
1008
+ _escape_html(ent.title), ent.time.localtime.strftime('%F %T %z'),
1009
+ _escape_html(ent.body), atts, tags,
1010
+ ]
1011
+
1012
+ env['sgfa.status'] = :ok
1013
+ env['sgfa.html'] = html
1014
+ end # def _get_edit
1015
+
1016
+ JacketPost = [
1017
+ 'entry',
1018
+ 'revision',
1019
+ 'time',
1020
+ 'tagcnt',
1021
+ 'attcnt',
1022
+ ]
1023
+
1024
+ #####################################
1025
+ # Handle jacket post
1026
+ def _post_jacket(env)
1027
+ _navbar_jacket(env, 'Edit')
1028
+
1029
+ rck = Rack::Request.new(env)
1030
+ params = rck.POST
1031
+
1032
+ # validate fields present
1033
+ JacketPost.each do |fn|
1034
+ next if params[fn]
1035
+ raise Error::Limits, 'Bad form submission'
1036
+ end
1037
+ tagcnt = params['tagcnt'].to_i
1038
+ attcnt = params['attcnt'].to_i
1039
+ tagcnt.times do |ix|
1040
+ next if params['tag%d' % ix]
1041
+ raise Error::Limits, 'Bad form submission'
1042
+ end
1043
+ attcnt.times do |ix|
1044
+ next if params['attnumb%d' % ix]
1045
+ raise Error::Limits, 'Bad form submission'
1046
+ end
1047
+
1048
+ # get the entry being edited
1049
+ enum = params['entry'].to_i
1050
+ rnum = params['revision'].to_i
1051
+ tr = _trans(env)
1052
+ if enum != 0
1053
+ ent = env['sgfa.binder'].read_entry(tr, enum, rnum)
1054
+ else
1055
+ ent = Entry.new
1056
+ end
1057
+
1058
+ # tags
1059
+ oldt = ent.tags
1060
+ newt = []
1061
+ tagcnt.times do |ix|
1062
+ tx = 'tag%d' % ix
1063
+ if !params[tx].empty?
1064
+ newt.push params[tx]
1065
+ end
1066
+ end
1067
+
1068
+ # attachments
1069
+ attcnt.times do |ix|
1070
+ anum = params['attnumb%d' % ix].to_i
1071
+ name = params['attname%d' % ix]
1072
+ file = params['attfile%d' % ix]
1073
+
1074
+ # copy uploaded file
1075
+ if file && file != ''
1076
+ ftmp = env['sgfa.binder'].temp
1077
+ IO::copy_stream(file[:tempfile], ftmp)
1078
+ file[:tempfile].close!
1079
+ else
1080
+ ftmp = nil
1081
+ end
1082
+
1083
+ # new file
1084
+ if anum == 0
1085
+ next if !ftmp
1086
+ name = file[:filename] if name == ''
1087
+ ent.attach(name, ftmp)
1088
+
1089
+ # old file
1090
+ else
1091
+ ent.rename(anum, name) if name != ''
1092
+ ent.replace(anum, ftmp) if ftmp
1093
+ end
1094
+
1095
+ end
1096
+
1097
+ # general
1098
+ ent.title = params['title']
1099
+ ent.body = params['body']
1100
+ begin
1101
+ time = Time.parse(params['time'])
1102
+ ent.time = time
1103
+ rescue ArgumentError
1104
+ end
1105
+ oldt.each{|tag| ent.untag(tag) if !newt.include?(tag) }
1106
+ newt.each{|tag| ent.tag(tag) if !oldt.include?(tag) }
1107
+ env['sgfa.binder'].write(tr, [ent])
1108
+
1109
+ env['sgfa.status'] = :ok
1110
+ env['sgfa.message'] = 'Entry edited.'
1111
+ env['sgfa.html'] = _disp_entry(env, ent)
1112
+
1113
+ end # def _post_jacket()
1114
+
1115
+
1116
+ #####################################
1117
+ # Handle binder post
1118
+ def _post_binder(env)
1119
+ _navbar_binder(env, 'Edit')
1120
+
1121
+ rck = Rack::Request.new(env)
1122
+ params = rck.POST
1123
+
1124
+ tr = _trans(env)
1125
+ tr[:title] = 'Test title'
1126
+ tr[:body] = 'Test description of action'
1127
+
1128
+ bnd = env['sgfa.binder']
1129
+
1130
+ # jacket
1131
+ if params['create']
1132
+ _navbar_binder(env, 'Jackets')
1133
+ ['jacket', 'newname', 'title', 'perms'].each do |fn|
1134
+ next if params[fn]
1135
+ raise Error::Limits, 'Bad form submission'
1136
+ end
1137
+ perms = params['perms'].split(',').map{|it| it.strip }
1138
+ title = params['title']
1139
+ newname = params['newname']
1140
+ jacket = params['jacket']
1141
+ tr[:jacket] = jacket
1142
+
1143
+ info = bnd.binder_info(tr)
1144
+ oj = info[:jackets][jacket]
1145
+ if oj
1146
+ newname ||= jacket
1147
+ title = oj['title'] if title.empty?
1148
+ jck = bnd.jacket_edit(tr, newname, title, perms)
1149
+ env['sgfa.message'] = 'Jacket edited.'
1150
+ else
1151
+ jck = bnd.jacket_create(tr, title, perms)
1152
+ env['sgfa.message'] = 'Jacket created.'
1153
+ end
1154
+ env['sgfa.status'] = :ok
1155
+ env['sgfa.html'] = _disp_jackets(env, jck, tr)
1156
+
1157
+ # user
1158
+ elsif params['set']
1159
+ _navbar_binder(env, 'Users')
1160
+ ['user', 'perms'].each do |fn|
1161
+ next if params[fn]
1162
+ raise Error::Limits, 'Bad form submission'
1163
+ end
1164
+ perms = params['perms'].split(',').map{|it| it.strip }
1165
+ users = bnd.binder_user(tr, params['user'], perms)
1166
+ env['sgfa.status'] = :ok
1167
+ env['sgfa.message'] = 'User edited.'
1168
+ env['sgfa.html'] = _disp_users(env, users, tr)
1169
+
1170
+ # binder
1171
+ elsif params['assign']
1172
+ _navbar_binder(env, 'Values')
1173
+ ['value', 'state'].each do |fn|
1174
+ next if params[fn]
1175
+ raise Error::Limits, 'Bad form submission'
1176
+ end
1177
+ vals = { params['value'] => params['state'] }
1178
+ values = bnd.binder_values(tr, vals)
1179
+ env['sgfa.status'] = :ok
1180
+ env['sgfa.message'] = 'Values assigned.'
1181
+ env['sgfa.html'] = _disp_values(env, values, tr)
1182
+
1183
+ end
1184
+
1185
+ end # def _post_binder()
1186
+
1187
+ end # class Binder
1188
+
1189
+ end # module Web
1190
+ end # module Sgfa