icfs 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,73 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
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
+ #
13
+ module ICFS
14
+ module Web
15
+
16
+ ##########################################################################
17
+ # Authtication using SSL client certificates - Rack Middleware
18
+ #
19
+ class AuthSsl
20
+
21
+ ###############################################
22
+ # New instance
23
+ #
24
+ # @param app [Object] The rack app
25
+ # @param map [Object] Maps DN to user name
26
+ # @param api [ICFS::Api] the Api
27
+ #
28
+ def initialize(app, map, api)
29
+ @app = app
30
+ @map = map
31
+ @api = api
32
+ end
33
+
34
+
35
+ ###############################################
36
+ # Handle requests
37
+ #
38
+ # Expects SSL_CLIENT_VERIFY to be set to SUCCESS and SSL_CLIENT_S_DN
39
+ # to contain the client subject DN
40
+ #
41
+ def call(env)
42
+
43
+ # check if verified
44
+ unless env['SSL_CLIENT_VERIFY'] == 'SUCCESS'
45
+ return [
46
+ 400,
47
+ {'Content-Type' => 'text/plain'.freeze},
48
+ ['Client certificate required.'.freeze]
49
+ ]
50
+ end
51
+
52
+ # lookup
53
+ user = @map[env['SSL_CLIENT_S_DN']]
54
+ if user.nil?
55
+ return [
56
+ 400,
57
+ {'Content-Type' => 'text/plain'.freeze},
58
+ ['User not found for %s' % env['SSL_CLIENT_S_DN']]
59
+ ]
60
+ end
61
+
62
+ # pass to app
63
+ @api.user = user
64
+ env['icfs'] = @api
65
+ return @app.call(env)
66
+ end # def call()
67
+
68
+
69
+ end # class ICFS::Web::AuthSsl
70
+
71
+ end # module ICFS::Web
72
+
73
+ end # module ICFS
@@ -0,0 +1,4498 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
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
+
14
+ module ICFS
15
+
16
+ ##########################################################################
17
+ # Web interface using Rack
18
+ #
19
+ module Web
20
+
21
+ ##########################################################################
22
+ # Web Client
23
+ #
24
+ # @todo Improve time handling for web interface
25
+ # @todo Scrub the javascript
26
+ #
27
+ class Client
28
+
29
+ ###############################################
30
+ # New instance
31
+ #
32
+ # @param css [String] the URL for the stylesheet
33
+ # @param js [String] the URL for the javascript
34
+ #
35
+ def initialize(css, js)
36
+ @css = css.freeze
37
+ @js = js.freeze
38
+ end
39
+
40
+
41
+ ###############################################
42
+ # A Rack call
43
+ #
44
+ # @param env [Hash] the Rack environment
45
+ #
46
+ def call(env)
47
+
48
+ # grab the path components
49
+ path = env['PATH_INFO']
50
+ if path.empty?
51
+ cmps = ['']
52
+ else
53
+ cmps = path.split('/'.freeze, -1)
54
+ cmps.shift if cmps[0].empty?
55
+ cmps = [''] if cmps.empty?
56
+ end
57
+ env['icfs.cmps'] = cmps
58
+
59
+ # reset
60
+ env['icfs'].reset
61
+
62
+ case cmps[0]
63
+
64
+ # search
65
+ when 'case_search'
66
+ return _call_search(env,
67
+ 'Case Search'.freeze,
68
+ 'Case Search'.freeze,
69
+ QueryCase,
70
+ ListCase,
71
+ :case_search,
72
+ Proc.new{|qu, txt| _a_case_search(env, qu, txt) }
73
+ )
74
+
75
+ when 'entry_search'
76
+ return _call_search(env,
77
+ 'Entry Search'.freeze,
78
+ 'Entry Search'.freeze,
79
+ QueryEntry,
80
+ ListEntry,
81
+ :entry_search,
82
+ Proc.new{|qu, txt| _a_entry_search(env, qu, txt) }
83
+ )
84
+
85
+ when 'log_search'
86
+ return _call_search(env,
87
+ 'Log Search'.freeze,
88
+ 'Log Search'.freeze,
89
+ QueryLog,
90
+ ListLog,
91
+ :log_search,
92
+ Proc.new{|qu, txt| _a_log_search(env, qu, txt) }
93
+ )
94
+
95
+ when 'action_search'
96
+ return _call_search(env,
97
+ 'Action Search'.freeze,
98
+ 'Action Search'.freeze,
99
+ QueryAction,
100
+ ListAction,
101
+ :action_search,
102
+ Proc.new{|qu, txt| _a_action_search(env, qu, txt) }
103
+ )
104
+
105
+ when 'index_search'
106
+ return _call_search(env,
107
+ 'Index Search'.freeze,
108
+ 'Index Search'.freeze,
109
+ QueryIndex,
110
+ ListIndex,
111
+ :index_search,
112
+ Proc.new{|qu, txt| _a_index_search(env, qu, txt) }
113
+ )
114
+
115
+ when 'index_lookup'; return _call_index_lookup(env)
116
+
117
+ # aggregations
118
+ when 'stats'
119
+ return _call_search(env,
120
+ 'Stats Search'.freeze,
121
+ 'Stats Search'.freeze,
122
+ QueryStats,
123
+ ListStats,
124
+ :stats,
125
+ nil
126
+ )
127
+
128
+ when 'case_tags'
129
+ return _call_search(env,
130
+ 'Case Tags'.freeze,
131
+ 'Case Tags Search'.freeze,
132
+ QueryCaseTags,
133
+ ListCaseTags,
134
+ :case_tags,
135
+ nil
136
+ )
137
+
138
+ when 'entry_tags'
139
+ return _call_search(env,
140
+ 'Entry Tags'.freeze,
141
+ 'Entry Tag Search'.freeze,
142
+ QueryEntryTags,
143
+ ListEntryTags,
144
+ :entry_tags,
145
+ nil
146
+ )
147
+
148
+ when 'action_tags'
149
+ return _call_search(env,
150
+ 'Action Tags'.freeze,
151
+ 'Action Tag Search'.freeze,
152
+ QueryActionTags,
153
+ ListActionTags,
154
+ :action_tags,
155
+ nil
156
+ )
157
+
158
+ when 'index_tags'
159
+ return _call_search(env,
160
+ 'Index Tags'.freeze,
161
+ 'Index Tag Search'.freeze,
162
+ QueryIndexTags,
163
+ ListIndexTags,
164
+ :index_tags,
165
+ nil
166
+ )
167
+
168
+ # forms
169
+ when 'case_create'; return _call_case_create(env)
170
+ when 'case_edit'; return _call_case_edit(env)
171
+ when 'entry_edit'; return _call_entry_edit(env)
172
+ when 'index_edit'; return _call_index_edit(env)
173
+
174
+ # view
175
+ when 'home', ''; return _call_home(env)
176
+ when 'case'; return _call_case(env)
177
+ when 'entry'; return _call_entry(env)
178
+ when 'log'; return _call_log(env)
179
+ when 'action'; return _call_action(env)
180
+ when 'index'; return _call_index(env)
181
+ when 'file'; return _call_file(env)
182
+
183
+ # info page
184
+ when 'info'; return _call_info(env)
185
+
186
+ # not supported path
187
+ else
188
+ env['icfs.page'] = 'Invalid'.freeze
189
+ raise(Error::NotFound, 'Invalid request'.freeze)
190
+ end
191
+
192
+ rescue Error::NotFound => e
193
+ return _resp_notfound( env, 'Not found: %s'.freeze %
194
+ Rack::Utils.escape_html(e.message) )
195
+
196
+ rescue Error::Perms => e
197
+ return _resp_forbidden( env, 'Forbidden: %s'.freeze %
198
+ Rack::Utils.escape_html(e.message) )
199
+
200
+ rescue Error::Conflict => e
201
+ return _resp_conflict( env, 'Conflict: %s'.freeze %
202
+ Rack::Utils.escape_html(e.message) )
203
+
204
+ rescue Error::Value => e
205
+ return _resp_badreq( env, 'Invalid values: %s'.freeze %
206
+ Rack::Utils.escape_html(e.message) )
207
+
208
+ rescue Error::Interface => e
209
+ return _resp_badreq(env, Rack::Utils.escape_html(e.message))
210
+
211
+ end # def call()
212
+
213
+ private
214
+
215
+
216
+ ###########################################################
217
+ # Handle calls
218
+ ###########################################################
219
+
220
+ ###############################################
221
+ # Info page
222
+ def _call_info(env)
223
+ env['icfs.page'] = 'Info'.freeze
224
+ api = env['icfs']
225
+ _verb_get(env)
226
+ body = [
227
+ _div_nav(env),
228
+ _div_desc('Info'.freeze, ''.freeze),
229
+ _div_info(env)
230
+ ].join(''.freeze)
231
+ return _resp_success(env, body)
232
+ end # def _call_info()
233
+
234
+
235
+ ###############################################
236
+ # Common search, tags, stats code
237
+ #
238
+ def _call_search(env, page, type, query_get,
239
+ list, api_meth, page_proc)
240
+ env['icfs.page'] = page
241
+ api = env['icfs']
242
+ _verb_get(env)
243
+ act = '%s/%s'.freeze % [env['SCRIPT_NAME'], env['icfs.cmps'][0]]
244
+
245
+ # form
246
+ if env['QUERY_STRING'].empty?
247
+ body = [
248
+ _div_nav(env),
249
+ _div_desc(type, ''.freeze),
250
+ _form_query(env, query_get, {}, act, true)
251
+ ]
252
+
253
+ # do the query
254
+ else
255
+ query = _util_get_query(env, query_get)
256
+ resp = api.send(api_meth, query)
257
+ if query[:caseid]
258
+ env['icfs.cid' ] = query[:caseid]
259
+ end
260
+ body = [
261
+ _div_nav(env),
262
+ _div_query(env, type, query_get, resp[:query]),
263
+ _form_query(env, query_get, resp[:query], act),
264
+ _div_list(env, resp, list),
265
+ ]
266
+ if page_proc
267
+ body << _div_page(resp, page_proc)
268
+ end
269
+ end
270
+
271
+ return _resp_success(env, body.join(''.freeze))
272
+ end # def _call_search()
273
+
274
+
275
+ ###############################################
276
+
277
+ # Case query options
278
+ QueryCase = [
279
+ ['title'.freeze, :title, :string].freeze,
280
+ ['tags'.freeze, :tags, :string].freeze,
281
+ ['status'.freeze, :status, :boolean].freeze,
282
+ ['template'.freeze, :template, :boolean].freeze,
283
+ ['grantee'.freeze, :grantee, :string].freeze,
284
+ ['perm'.freeze, :perm, :string].freeze,
285
+ ['size'.freeze, :size, :integer].freeze,
286
+ ['page'.freeze, :page, :integer].freeze,
287
+ ['purpose'.freeze, :purpose, :string].freeze,
288
+ ].freeze
289
+
290
+
291
+ # List case search
292
+ ListCase = [
293
+ [:caseid, :current].freeze,
294
+ [:tags, nil].freeze,
295
+ [:title, :case].freeze,
296
+ [:snippet, nil].freeze
297
+ ].freeze
298
+
299
+
300
+ # Entry query options
301
+ QueryEntry = [
302
+ ['title'.freeze, :title, :string].freeze,
303
+ ['content'.freeze, :content, :string].freeze,
304
+ ['tags'.freeze, :tags, :string].freeze,
305
+ ['caseid'.freeze, :caseid, :string].freeze,
306
+ ['action'.freeze, :action, :integer].freeze,
307
+ ['after'.freeze, :after, :time].freeze,
308
+ ['before'.freeze, :before, :time].freeze,
309
+ ['stat'.freeze, :stat, :string].freeze,
310
+ ['credit'.freeze, :credit, :string].freeze,
311
+ ['size'.freeze, :size, :integer].freeze,
312
+ ['page'.freeze, :page, :integer].freeze,
313
+ ['sort'.freeze, :sort, :string].freeze,
314
+ ['purpose'.freeze, :purpose, :string].freeze,
315
+ ].freeze
316
+
317
+
318
+ # Entry query display
319
+ ListEntry = [
320
+ [:caseid, :mixed].freeze,
321
+ [:entry, :current].freeze,
322
+ [:action, :current].freeze,
323
+ [:time, :entry].freeze,
324
+ [:tags, nil].freeze,
325
+ [:index, :entry].freeze,
326
+ [:files, nil].freeze,
327
+ [:stats, nil].freeze,
328
+ [:title, :entry].freeze,
329
+ [:snippet, nil].freeze,
330
+ ].freeze
331
+
332
+
333
+ # Log query options
334
+ QueryLog = [
335
+ ['caseid'.freeze, :caseid, :string].freeze,
336
+ ['after'.freeze, :after, :time].freeze,
337
+ ['before'.freeze, :before, :time].freeze,
338
+ ['user'.freeze, :user, :string].freeze,
339
+ ['entry'.freeze, :entry, :integer].freeze,
340
+ ['index'.freeze, :index, :integer].freeze,
341
+ ['action'.freeze, :action, :integer].freeze,
342
+ ['size'.freeze, :size, :integer].freeze,
343
+ ['page'.freeze, :page, :integer].freeze,
344
+ ['sort'.freeze, :sort, :string].freeze,
345
+ ['purpose'.freeze, :purpose, :string].freeze,
346
+ ].freeze
347
+
348
+
349
+ # Log query display
350
+ ListLog = [
351
+ [:caseid, :mixed].freeze,
352
+ [:log, nil].freeze,
353
+ [:time, :log].freeze,
354
+ [:user, nil].freeze,
355
+ [:entry, :log].freeze,
356
+ [:action, :log].freeze,
357
+ [:index, :log].freeze,
358
+ ].freeze
359
+
360
+
361
+ # Task query options
362
+ QueryAction = [
363
+ ['assigned'.freeze, :assigned, :string].freeze,
364
+ ['caseid'.freeze, :caseid, :string].freeze,
365
+ ['title'.freeze, :title, :string].freeze,
366
+ ['status'.freeze, :status, :boolean].freeze,
367
+ ['flag'.freeze, :flag, :boolean].freeze,
368
+ ['before'.freeze, :before, :time].freeze,
369
+ ['after'.freeze, :after, :time].freeze,
370
+ ['tags'.freeze, :tags, :string].freeze,
371
+ ['purpose'.freeze, :purpose, :string].freeze,
372
+ ['sort'.freeze, :sort, :string].freeze,
373
+ ].freeze
374
+
375
+
376
+ # Task list options
377
+ ListAction = [
378
+ [:time, nil].freeze,
379
+ [:tags, nil].freeze,
380
+ [:caseid, :mixed].freeze,
381
+ [:title, :action].freeze,
382
+ [:snippet, nil].freeze,
383
+ ].freeze
384
+
385
+
386
+
387
+ ###############################################
388
+ # Do an index lookup
389
+ #
390
+ def _call_index_lookup(env)
391
+ env['icfs.page'] = 'Index Lookup'.freeze
392
+ api = env['icfs']
393
+ _verb_get(env)
394
+
395
+ # query required
396
+ if env['QUERY_STRING'].empty?
397
+ raise(Error::Interface, 'Query string required'.freeze)
398
+ end
399
+
400
+ # do the query
401
+ query = _util_get_query(env, QueryIndex)
402
+ resp = api.index_search(query)
403
+ first = resp[:list][0]
404
+
405
+ # raw rack return
406
+ if first
407
+ body = {
408
+ 'index' => first[:object][:index],
409
+ 'title' => first[:object][:title],
410
+ }
411
+ else
412
+ body = {
413
+ 'index' => nil
414
+ }
415
+ end
416
+ body = JSON.generate(body)
417
+ head = {
418
+ 'Content-Type' => 'application/json'.freeze,
419
+ 'Content-Length' => body.bytesize.to_s
420
+ }
421
+ return [200, head, [body]]
422
+ end # def _call_index_lookup()
423
+
424
+
425
+ # Index query options
426
+ QueryIndex = [
427
+ ['caseid'.freeze, :caseid, :string].freeze,
428
+ ['title'.freeze, :title, :string].freeze,
429
+ ['prefix'.freeze, :prefix, :string].freeze,
430
+ ['content'.freeze, :content, :string].freeze,
431
+ ['tags'.freeze, :tags, :string].freeze,
432
+ ['purpose'.freeze, :purpose, :string].freeze,
433
+ ['sort'.freeze, :sort, :string].freeze,
434
+ ].freeze
435
+
436
+
437
+ # Task list options
438
+ ListIndex = [
439
+ [:caseid, :mixed].freeze,
440
+ [:index, :current].freeze,
441
+ [:title, :index].freeze,
442
+ [:snippet, nil].freeze,
443
+ ].freeze
444
+
445
+
446
+ # Stats query options
447
+ QueryStats = [
448
+ ['credit'.freeze, :credit, :string].freeze,
449
+ ['caseid'.freeze, :caseid, :string].freeze,
450
+ ['before'.freeze, :before, :time].freeze,
451
+ ['after'.freeze, :after, :time].freeze,
452
+ ['purpose'.freeze, :purpose, :string].freeze,
453
+ ].freeze
454
+
455
+ # Stats list options
456
+ ListStats = [
457
+ [:stat, nil].freeze,
458
+ [:count, nil].freeze,
459
+ [:sum, nil].freeze,
460
+ ].freeze
461
+
462
+
463
+ # Query for case tags
464
+ QueryCaseTags = [
465
+ ['status'.freeze, :status, :boolean].freeze,
466
+ ['template'.freeze, :template, :boolean].freeze,
467
+ ['grantee'.freeze, :grantee, :string].freeze,
468
+ ['purpose'.freeze, :purpose, :string].freeze,
469
+ ].freeze
470
+
471
+
472
+ # Case Tags list
473
+ ListCaseTags = [
474
+ [:tag, :case].freeze,
475
+ [:count, nil].freeze,
476
+ ].freeze
477
+
478
+
479
+ # Entry tags query options
480
+ QueryEntryTags = [
481
+ ['caseid'.freeze, :caseid, :string].freeze,
482
+ ['purpose'.freeze, :purpose, :string].freeze,
483
+ ].freeze
484
+
485
+
486
+ # Entry Tags list
487
+ ListEntryTags = [
488
+ [:tag, :entry].freeze,
489
+ [:count, nil].freeze,
490
+ ].freeze
491
+
492
+
493
+ # Action Tag query
494
+ QueryActionTags = [
495
+ ['caseid'.freeze, :caseid, :string].freeze,
496
+ ['assigned'.freeze, :assigned, :string].freeze,
497
+ ['status'.freeze, :status, :boolean].freeze,
498
+ ['flag'.freeze, :flag, :boolean].freeze,
499
+ ['before'.freeze, :before, :time].freeze,
500
+ ['after'.freeze, :after, :time].freeze,
501
+ ['purpose'.freeze, :purpose, :string].freeze,
502
+ ].freeze
503
+
504
+
505
+ # Action Tags list
506
+ ListActionTags = [
507
+ [:tag, :action].freeze,
508
+ [:count, nil].freeze
509
+ ].freeze
510
+
511
+
512
+ # Index tags query
513
+ QueryIndexTags = [
514
+ ['caseid'.freeze, :caseid, :string].freeze,
515
+ ['purpose'.freeze, :purpose, :string].freeze,
516
+ ].freeze
517
+
518
+
519
+ # Index tags list
520
+ ListIndexTags = [
521
+ [:tag, :index].freeze,
522
+ [:count, nil].freeze,
523
+ ].freeze
524
+
525
+
526
+ ###############################################
527
+ # Create a new case
528
+ #
529
+ def _call_case_create(env)
530
+ env['icfs.page'] = 'Case Create'.freeze
531
+ api = env['icfs']
532
+ tid = _util_case(env)
533
+ _verb_getpost(env)
534
+
535
+ # get the form
536
+ if env['REQUEST_METHOD'] == 'GET'.freeze
537
+ tpl = api.case_read(tid)
538
+ tpl['title'] = ''.freeze
539
+ parts = [
540
+ _form_entry(env, tid, nil),
541
+ _form_create(env),
542
+ _form_case(env, tpl),
543
+ ]
544
+ body = [
545
+ _div_nav(env),
546
+ _div_desc(
547
+ 'Create New Case'.freeze,
548
+ '<i>template:</i> %s'.freeze % Rack::Utils.escape_html(tid),
549
+ ),
550
+ _div_form(env, '/case_create/'.freeze, tid, parts, 'Create Case'.freeze)
551
+ ].join(''.freeze)
552
+ return _resp_success(env, body)
553
+
554
+ # post the form
555
+ elsif env['REQUEST_METHOD'] == 'POST'.freeze
556
+ para = _util_post(env)
557
+
558
+ # process
559
+ cse = _post_case(env, para)
560
+ cid = para['create_cid']
561
+ cse['template'] = (para['create_tmpl'] == 'true'.freeze) ? true : false
562
+
563
+ # process entry
564
+ ent = _post_entry(env, para)
565
+ Validate.validate(tid, 'Template ID'.freeze, Items::FieldCaseid)
566
+ ent['caseid'] = cid
567
+
568
+ # create
569
+ api.case_create(ent, cse, tid)
570
+
571
+ # display the new case
572
+ env['icfs.cid'] = cid
573
+ body = _div_nav(env) + _div_case(env, cse)
574
+ return _resp_success(env, body)
575
+ end
576
+ end # def _call_case_create()
577
+
578
+
579
+ ###############################################
580
+ # Edit a case
581
+ #
582
+ def _call_case_edit(env)
583
+ env['icfs.page'] = 'Case Edit'.freeze
584
+ cid = _util_case(env)
585
+ api = env['icfs']
586
+ _verb_getpost(env)
587
+
588
+ # get the form
589
+ if env['REQUEST_METHOD'] == 'GET'.freeze
590
+ cse = api.case_read(cid)
591
+ parts = [
592
+ _form_entry(env, cid, nil),
593
+ _form_case(env, cse),
594
+ ]
595
+ body = [
596
+ _div_nav(env),
597
+ _div_desc('Edit Case'.freeze, ''.freeze),
598
+ _div_form(env, '/case_edit/'.freeze, cid, parts, 'Record Case'.freeze),
599
+ ].join(''.freeze)
600
+ return _resp_success(env, body)
601
+
602
+ # post the form
603
+ elsif env['REQUEST_METHOD'] == 'POST'.freeze
604
+ para = _util_post(env)
605
+
606
+ # process
607
+ cse = _post_case(env, para)
608
+ ent = _post_entry(env, para)
609
+ act = _post_action(env, para)
610
+ if act.is_a?(Integer)
611
+ ent['action'] = act if act != 0
612
+ act = nil
613
+ end
614
+ ent['caseid'] = cid
615
+ cse_old = api.case_read(cid)
616
+ cse['template'] = cse_old['template']
617
+ api.record(ent, act, nil, cse)
618
+
619
+ # display the case
620
+ body = _div_nav(env) + _div_case(env, cse)
621
+ return _resp_success(env, body)
622
+ end
623
+ end # def _call_case_edit()
624
+
625
+
626
+ ###############################################
627
+ # Edit an entry
628
+ #
629
+ def _call_entry_edit(env)
630
+ env['icfs.page'] = 'Entry Edit'.freeze
631
+ api = env['icfs']
632
+ _verb_getpost(env)
633
+
634
+ cid = _util_case(env)
635
+
636
+ # get the form
637
+ if env['REQUEST_METHOD'] == 'GET'.freeze
638
+ enum = _util_num(env, 2)
639
+ anum = _util_num(env, 3)
640
+
641
+ # entry or action specified
642
+ if enum != 0
643
+ desc = 'Edit Entry'.freeze
644
+ ent = api.entry_read(cid, enum)
645
+ elsif anum != 0
646
+ desc = 'New Entry in Action'.freeze
647
+ act = api.action_read(cid, anum)
648
+ else
649
+ desc = 'New Entry'.freeze
650
+ end
651
+
652
+ # see if editing is possible
653
+ unless( api.access_list(cid).include?(ICFS::PermWrite) || (
654
+ (anum != 0) && api.tasked?(cid, anum)))
655
+ raise(Error::Perms, 'Not able to edit this entry.'.freeze)
656
+ end
657
+
658
+ # build form
659
+ parts = [ _form_entry(env, cid, ent) ]
660
+ if !ent &&
661
+ (act || api.access_list(cid).include?(ICFS::PermAction))
662
+ parts << _form_action(env, cid, act, {edit: false})
663
+ end
664
+ body = [
665
+ _div_nav(env),
666
+ _div_desc(desc, ''.freeze),
667
+ _div_form(env, '/entry_edit/'.freeze, cid, parts,
668
+ 'Record Entry'.freeze),
669
+ ].join(''.freeze)
670
+ return _resp_success(env, body)
671
+
672
+ # post the form
673
+ elsif env['REQUEST_METHOD'] == 'POST'.freeze
674
+ para = _util_post(env)
675
+
676
+ # process
677
+ ent = _post_entry(env, para)
678
+ act = _post_action(env, para)
679
+ if act.is_a?(Integer)
680
+ ent['action'] = act if act != 0
681
+ act = nil
682
+ end
683
+ ent['caseid'] = cid
684
+ api.record(ent, act, nil, nil)
685
+
686
+ # display the entry
687
+ body = [
688
+ _div_nav(env),
689
+ _div_entry(env, ent)
690
+ ]
691
+ body << _div_action(env, act) if act
692
+ return _resp_success(env, body.join(''.freeze))
693
+ end
694
+ end # def _call_entry_edit()
695
+
696
+
697
+ ###############################################
698
+ # Edit an Index
699
+ #
700
+ def _call_index_edit(env)
701
+ env['icfs.page'] = 'Index Edit'.freeze
702
+ api = env['icfs']
703
+ _verb_getpost(env)
704
+
705
+ cid = _util_case(env)
706
+
707
+ # get the form
708
+ if env['REQUEST_METHOD'] == 'GET'.freeze
709
+
710
+ # see if editing is possible
711
+ unless api.access_list(cid).include?(ICFS::PermWrite)
712
+ raise(Error::Perms, 'Not able to edit this index.'.freeze)
713
+ end
714
+
715
+ xnum = _util_num(env, 2)
716
+ idx = api.index_read(cid, xnum) if xnum != 0
717
+ parts = [
718
+ _form_entry(env, cid, nil),
719
+ _form_index(env, cid, idx),
720
+ ]
721
+ desc = idx ? 'Edit Index'.freeze : 'New Index'.freeze
722
+ body = [
723
+ _div_nav(env),
724
+ _div_desc(desc, ''.freeze),
725
+ _div_form(env, '/index_edit/'.freeze, cid, parts,
726
+ 'Record Index'.freeze),
727
+ ].join(''.freeze)
728
+ return _resp_success(env, body)
729
+
730
+ # post the form
731
+ elsif env['REQUEST_METHOD'] == 'POST'.freeze
732
+ para = _util_post(env)
733
+
734
+ # process
735
+ ent = _post_entry(env, para)
736
+ act = _post_action(env, para)
737
+ idx = _post_index(env, para)
738
+ if act.is_a?(Integer)
739
+ ent['action'] = act if act != 0
740
+ act = nil
741
+ end
742
+ ent['caseid'] = cid
743
+ api.record(ent, act, idx, nil)
744
+
745
+ # display the index
746
+ body = [
747
+ _div_nav(env),
748
+ _div_entry(env, ent),
749
+ _div_index(env, idx)
750
+ ].join(''.freeze)
751
+ return _resp_success(env, body)
752
+ end
753
+ end # def _call_index_edit()
754
+
755
+
756
+ ###############################################
757
+ # User Home page
758
+ def _call_home(env)
759
+ env['icfs.page'] = 'Home'.freeze
760
+ _verb_get(env)
761
+ body = [
762
+ _div_nav(env),
763
+ _div_desc('User Home'.freeze, ''.freeze),
764
+ _div_home(env),
765
+ ].join(''.freeze)
766
+ return _resp_success(env, body)
767
+ end # def _call_home()
768
+
769
+
770
+ ###############################################
771
+ # Display a Case
772
+ #
773
+ def _call_case(env)
774
+ env['icfs.page'] = 'Case View'.freeze
775
+ api = env['icfs']
776
+ _verb_get(env)
777
+ cid = _util_case(env)
778
+ lnum = _util_num(env, 2)
779
+ cse = api.case_read(cid, lnum)
780
+ if lnum != 0
781
+ msg = 'This is a historical version of this Case'.freeze
782
+ else
783
+ msg = ''.freeze
784
+ end
785
+ body = [
786
+ _div_nav(env),
787
+ _div_desc('Case Information'.freeze, msg),
788
+ _div_case(env, cse),
789
+ ].join(''.freeze)
790
+ return _resp_success(env, body)
791
+ end # def _call_case()
792
+
793
+
794
+ ###############################################
795
+ # Display an Entry
796
+ #
797
+ def _call_entry(env)
798
+ env['icfs.page'] = 'Entry View'.freeze
799
+ api = env['icfs']
800
+ _verb_get(env)
801
+ cid = _util_case(env)
802
+ enum = _util_num(env, 2)
803
+ lnum = _util_num(env, 3)
804
+ raise(Error::Interface, 'No Entry requested'.freeze) if enum == 0
805
+ ent = api.entry_read(cid, enum, lnum)
806
+ if lnum != 0
807
+ msg = 'This is a historical version of this Entry'.freeze
808
+ else
809
+ msg = ''.freeze
810
+ end
811
+ body = [
812
+ _div_nav(env),
813
+ _div_desc('View Entry'.freeze, msg),
814
+ _div_entry(env, ent),
815
+ ].join(''.freeze)
816
+ return _resp_success(env, body)
817
+ end # def _call_entry()
818
+
819
+
820
+ ###############################################
821
+ # Display a Log
822
+ #
823
+ def _call_log(env)
824
+ env['icfs.page'] = 'Log View'.freeze
825
+ api = env['icfs']
826
+ _verb_get(env)
827
+ cid = _util_case(env)
828
+ lnum = _util_num(env, 2)
829
+ raise(Error::Interface, 'No log requested'.freeze) if lnum == 0
830
+ log = api.log_read(cid, lnum)
831
+ body = [
832
+ _div_nav(env),
833
+ _div_desc('View Log'.freeze, ''.freeze),
834
+ _div_log(env, log)
835
+ ].join(''.freeze)
836
+ return _resp_success(env, body)
837
+ end # def _call_log()
838
+
839
+
840
+ ###############################################
841
+ # Display an Action
842
+ #
843
+ def _call_action(env)
844
+ env['icfs.page'] = 'Action View'.freeze
845
+ api = env['icfs']
846
+ _verb_get(env)
847
+ cid = _util_case(env)
848
+ anum = _util_num(env, 2)
849
+ lnum = _util_num(env, 3)
850
+ raise(Error::Interface, 'No Action requested'.freeze) if anum == 0
851
+
852
+ # get the action
853
+ act = api.action_read(cid, anum, lnum)
854
+ if lnum != 0
855
+ msg = 'This is a historical version of this Action'.freeze
856
+ else
857
+ msg = ''.freeze
858
+ end
859
+
860
+ # get the entries
861
+ query = {
862
+ caseid: cid,
863
+ action: anum,
864
+ purpose: 'Action Entries'.freeze,
865
+ }
866
+ resp = api.entry_search(query)
867
+
868
+ # display
869
+ body = [
870
+ _div_nav(env),
871
+ _div_desc('View Action'.freeze, msg),
872
+ _div_action(env, act),
873
+ _div_list(env, resp, ListEntry),
874
+ _div_page(resp){|qu, txt| _a_entry_search(env, qu, txt)},
875
+ ].join(''.freeze)
876
+ return _resp_success(env, body)
877
+ end # def _call_action()
878
+
879
+
880
+ ###############################################
881
+ # Display an Index
882
+ #
883
+ def _call_index(env)
884
+ env['icfs.page'] = 'Index View'.freeze
885
+ api = env['icfs']
886
+ _verb_get(env)
887
+ cid = _util_case(env)
888
+ xnum = _util_num(env, 2)
889
+ lnum = _util_num(env, 3)
890
+ raise(Error::Interface, 'No Index requested'.freeze) if xnum == 0
891
+
892
+ # get the index
893
+ idx = api.index_read(cid, xnum, lnum)
894
+ if lnum != 0
895
+ msg = 'This is a historical version of this Index'.freeze
896
+ else
897
+ msg = ''.freeze
898
+ end
899
+
900
+ # get the entries
901
+ query = {
902
+ caseid: cid,
903
+ index: xnum
904
+ }
905
+ resp = api.entry_search(query)
906
+
907
+ # display
908
+ body = [
909
+ _div_nav(env) +
910
+ _div_desc('View Index'.freeze, msg),
911
+ _div_index(env, idx),
912
+ _div_list(env, resp, ListEntry),
913
+ _div_page(resp){|qu, txt| _a_entry_search(env, qu, txt)},
914
+ ].join(''.freeze)
915
+ return _resp_success(env, body)
916
+ end # def _call_index()
917
+
918
+
919
+ ###############################################
920
+ # Get a file
921
+ def _call_file(env)
922
+ env['icfs.page'] = 'File Download'.freeze
923
+ api = env['icfs']
924
+ _verb_get(env)
925
+ cid = _util_case(env)
926
+
927
+ # get filename
928
+ cmps = env['icfs.cmps']
929
+ if cmps.size < 3 || cmps[2].empty?
930
+ raise(Error::Interface, 'No file specified in the URL'.freeze)
931
+ end
932
+ fnam = Rack::Utils.unescape(cmps[2])
933
+ ma = /^(\d+)-(\d+)-(\d+)-(.+)$/.match fnam
934
+ if !ma
935
+ raise(Error::Interface, 'File not properly specified in URL'.freeze)
936
+ end
937
+ enum = ma[1].to_i
938
+ lnum = ma[2].to_i
939
+ fnum = ma[3].to_i
940
+ ext = ma[4].rpartition('.'.freeze)[2]
941
+
942
+ # get MIME-type by extension
943
+ if ext.empty?
944
+ mime = 'application/octet-stream'.freeze
945
+ else
946
+ mime = Rack::Mime.mime_type('.' + ext)
947
+ end
948
+
949
+ # return the file
950
+ file = api.file_read(cid, enum, lnum, fnum)
951
+ fr = Web::FileResp.new(file)
952
+ headers = {
953
+ 'Content-Length' => file.size.to_s,
954
+ 'Content-Type' => mime,
955
+ 'Content-Disposition' => 'attachment'.freeze,
956
+ }
957
+ return [200, headers, fr]
958
+
959
+ end # def _call_file()
960
+
961
+
962
+ ###########################################################
963
+ # Generate HTML divs
964
+ ###########################################################
965
+
966
+ ###############################################
967
+ # Navbar div
968
+ #
969
+ def _div_nav(env)
970
+
971
+ unam = env['icfs'].user
972
+ cid = env['icfs.cid']
973
+
974
+ # with case
975
+ if cid
976
+ tc = _a_case(env, cid, 0, cid)
977
+ tabs = [
978
+ _a_entry_search(env, {
979
+ caseid: cid,
980
+ purpose: 'Case Entries'.freeze,
981
+ }, 'Entries'.freeze),
982
+ _a_index_search(env, {
983
+ caseid: cid,
984
+ purpose: 'Case Indexes'.freeze,
985
+ }, 'Indexes'.freeze),
986
+ _a_stats(env, {
987
+ caseid: cid,
988
+ purpose: 'Case Stats'.freeze,
989
+ }, 'Stats'.freeze),
990
+ _a_entry_tags(env, {
991
+ caseid: cid,
992
+ purpose: 'Entry Tags'.freeze,
993
+ }, 'Entry Tags'.freeze),
994
+ _a_index_tags(env, {
995
+ caseid: cid,
996
+ purpose: 'Index Tags'.freeze,
997
+ }, 'Index Tags'.freeze),
998
+ _a_entry_edit(env, cid, 0, 0, 'New Entry'.freeze),
999
+ _a_index_edit(env, cid, 0, 'New Index'.freeze),
1000
+ ]
1001
+
1002
+ # no case
1003
+ else
1004
+ tc = ''.freeze
1005
+ tabs = [
1006
+ _a_action_search(env, {
1007
+ assigned: unam,
1008
+ status: true,
1009
+ flag: true,
1010
+ purpose: 'Flagged Actions'.freeze,
1011
+ }, 'Actions'.freeze),
1012
+ _a_case_search(env, {
1013
+ grantee: unam,
1014
+ status: true,
1015
+ template: false,
1016
+ purpose: 'Open Cases'.freeze,
1017
+ }, 'Cases'.freeze),
1018
+ _a_stats(env, {
1019
+ credit: unam,
1020
+ after: Time.now.to_i - 60*60*24*30,
1021
+ purpose: 'User Stats - Last 30 days'.freeze,
1022
+ }, 'Stats'.freeze),
1023
+ _a_info(env, 'Info'.freeze),
1024
+ ]
1025
+ end
1026
+
1027
+ # tab divs
1028
+ tabs = tabs.map{|aa| DivNavTab % aa}.join(''.freeze)
1029
+
1030
+ return DivNav % [
1031
+ _a_home(env, 'ICFS'.freeze),
1032
+ tc,
1033
+ tabs
1034
+ ]
1035
+ end # def _div_nav()
1036
+
1037
+
1038
+ # navbar div
1039
+ DivNav = '
1040
+ <div class="nav">
1041
+ <div class="nav-icfs">%s</div>
1042
+ <div class="nav-case">%s</div>%s
1043
+ </div>'.freeze
1044
+
1045
+
1046
+ # navbar tab
1047
+ DivNavTab = '
1048
+ <div class="nav-tab">%s</div>'.freeze
1049
+
1050
+
1051
+ ###############################################
1052
+ # Message div
1053
+ #
1054
+ def _div_msg(env, msg)
1055
+ DivMsg % msg
1056
+ end # def _div_msg()
1057
+
1058
+
1059
+ # message div
1060
+ DivMsg = '
1061
+ <div class="message">%s
1062
+ </div>'.freeze
1063
+
1064
+
1065
+ ###############################################
1066
+ # Info div
1067
+ #
1068
+ def _div_info(env)
1069
+ api = env['icfs']
1070
+ tz = env['icfs.tz']
1071
+
1072
+ # roles/groups/perms
1073
+ roles = api.roles.map{|rol| DivInfoList % Rack::Utils.escape_html(rol)}
1074
+ grps = api.groups.map{|grp| DivInfoList % Rack::Utils.escape_html(grp)}
1075
+ perms = api.perms.map{|pm| DivInfoList % Rack::Utils.escape_html(pm)}
1076
+
1077
+ # global stats
1078
+ gstats = api.gstats.map{|st| DivInfoList % Rack::Utils.escape_html(st)}
1079
+
1080
+ return DivInfo % [
1081
+ Rack::Utils.escape_html(tz),
1082
+ Rack::Utils.escape_html(api.user),
1083
+ roles.join(''.freeze),
1084
+ grps.join(''.freeze),
1085
+ perms.join(''.freeze),
1086
+ gstats.join(''.freeze),
1087
+ ]
1088
+ end # def _div_info()
1089
+
1090
+
1091
+ # info div
1092
+ DivInfo = '
1093
+ <div class="info">
1094
+ <div class="list">
1095
+ <div class="list-row">
1096
+ <div class="list-label">Timezone:</div>
1097
+ <div class="list-text-s">%s</div>
1098
+ </div>
1099
+ <div class="list-row">
1100
+ <div class="list-label">User:</div>
1101
+ <div class="list-text-m">%s</div>
1102
+ </div>
1103
+ <div class="list-row">
1104
+ <div class="list-label">Roles:</div>
1105
+ <div class="user-list">%s
1106
+ </div>
1107
+ </div>
1108
+ <div class="list-row">
1109
+ <div class="list-label">Groups:</div>
1110
+ <div class="user-list">%s
1111
+ </div>
1112
+ </div>
1113
+ <div class="list-row">
1114
+ <div class="list-label">Perms:</div>
1115
+ <div class="user-list">%s
1116
+ </div>
1117
+ </div>
1118
+ <div class="list-row">
1119
+ <div class="list-label">Stats:</div>
1120
+ <div class="user-list">%s
1121
+ </div>
1122
+ </div>
1123
+ </div>
1124
+ </div>'.freeze
1125
+
1126
+
1127
+ # List items in the info div
1128
+ DivInfoList = '
1129
+ <div>%s</div>'.freeze
1130
+
1131
+
1132
+ # Column classes by symbol
1133
+ ListColClass = {
1134
+ entry: 'list-int'.freeze,
1135
+ action: 'list-int'.freeze,
1136
+ index: 'list-int'.freeze,
1137
+ log: 'list-int'.freeze,
1138
+ tags: 'list-int'.freeze,
1139
+ tag: 'list-tag'.freeze,
1140
+ stats: 'list-int'.freeze,
1141
+ time: 'list-time'.freeze,
1142
+ title: 'list-title'.freeze,
1143
+ caseid: 'list-caseid'.freeze,
1144
+ stat: 'list-stat'.freeze,
1145
+ sum: 'list-float'.freeze,
1146
+ count: 'list-int'.freeze,
1147
+ files: 'list-int'.freeze,
1148
+ user: 'list-usergrp'.freeze,
1149
+ }.freeze
1150
+
1151
+
1152
+ ###############################################
1153
+ # Search results list div
1154
+ #
1155
+ # @param env [Hash] Rack environment
1156
+ # @param resp [Hash] Search response
1157
+ # @param list [Array] List of object items to display and how
1158
+ #
1159
+ def _div_list(env, resp, list)
1160
+ return _div_msg(env, 'No results found'.freeze) if resp[:list].size == 0
1161
+
1162
+ # did we query with caseid?
1163
+ qcid = resp[:query].key?(:caseid)
1164
+
1165
+ # copy the query
1166
+ qu = resp[:query].dup
1167
+
1168
+ # header row
1169
+ hcols = list.map do |sym, opt|
1170
+ if sym == :caseid && qcid
1171
+ ''.freeze
1172
+ else
1173
+ DivListHeadItems[sym]
1174
+ end
1175
+ end
1176
+ head = DivListHead % hcols.join(''.freeze)
1177
+
1178
+ # search results into rows
1179
+ rows = resp[:list].map do |sr|
1180
+ obj = sr[:object]
1181
+ cid = obj[:caseid]
1182
+
1183
+ cols = list.map do |sym, opt|
1184
+ it = obj[sym]
1185
+ cc = ListColClass[sym]
1186
+
1187
+ # snippets are special non-column, not in the object itself
1188
+ if sym == :snippet
1189
+ if sr[:snippet]
1190
+ next( DivListItem % ['list-snip'.freeze, sr[:snippet]])
1191
+ else
1192
+ next(''.freeze)
1193
+ end
1194
+
1195
+ # redacted result
1196
+ elsif it.nil?
1197
+ next( DivListItem % [cc, '&mdash;'.freeze])
1198
+ end
1199
+
1200
+ # normal result
1201
+ case sym
1202
+
1203
+ # snippets - a special non-column, not in the object itself
1204
+ when :snippet
1205
+ if sr[:snippet]
1206
+ cd = sr[:snippet]
1207
+ else
1208
+ cd = nil
1209
+ end
1210
+
1211
+ # entry
1212
+ when :entry
1213
+ case opt
1214
+ when :current
1215
+ cd = _a_entry(env, cid, it, 0, it.to_s)
1216
+ when :log
1217
+ cd = _a_entry(env, cid, it, obj[:log], it.to_s)
1218
+ else
1219
+ cd = it.to_s
1220
+ end
1221
+
1222
+ # action
1223
+ when :action
1224
+ case opt
1225
+ when :current
1226
+ cd = (it == 0) ? ''.freeze : _a_action(env, cid, it, 0, it.to_s)
1227
+ when :log
1228
+ if it != 0
1229
+ cd = _a_action(env, cid, it, obj[:log], it.to_s)
1230
+ else
1231
+ cd = ''.freeze
1232
+ end
1233
+ else
1234
+ cd = it == 0 ? ''.freeze : it.to_s
1235
+ end
1236
+
1237
+ # index
1238
+ when :index
1239
+ case opt
1240
+ when :entry
1241
+ cd = (it == 0) ? ''.freeze : it.to_s
1242
+ when :current
1243
+ cd = _a_index(env, cid, it, 0, it.to_s)
1244
+ when :log
1245
+ if it != 0
1246
+ cd = _a_index(env, cid, it, obj[:log], it.to_s)
1247
+ else
1248
+ cd = ''.freeze
1249
+ end
1250
+ else
1251
+ cd = it.to_s
1252
+ end
1253
+
1254
+ # log
1255
+ when :log
1256
+ case opt
1257
+ when :link
1258
+ cd = _a_log(env, cid, it, it.to_s)
1259
+ else
1260
+ cd = it.to_s
1261
+ end
1262
+
1263
+ # tags
1264
+ when :tags
1265
+ if it.size == 1 && it[0] == ICFS::TagNone
1266
+ cd = ''.freeze
1267
+ else
1268
+ cd = it.size.to_s
1269
+ end
1270
+
1271
+ # tag - the result of a tags aggregation
1272
+ when :tag
1273
+ qu[:tags] = it
1274
+
1275
+ case opt
1276
+ when :entry
1277
+ qu[:purpose] = 'Entry Tag Search'.freeze
1278
+ cd = _a_entry_search(env, qu, it)
1279
+ when :index
1280
+ qu[:purpose] = 'Index Tag Search'.freeze
1281
+ cd = _a_index_search(env, qu, it)
1282
+ when :case
1283
+ qu[:purpose] = 'Case Tag Search'.freeze
1284
+ cd = _a_case_search(env, qu, it)
1285
+ when :action
1286
+ qu[:purpose] = 'Action Tag Search'.freeze
1287
+ cd = _a_action_search(env, qu, it)
1288
+ end
1289
+
1290
+ # time
1291
+ when :time
1292
+ case opt
1293
+ when :entry
1294
+ cd = _a_entry(env, cid, obj[:entry], 0, _util_time(env, it))
1295
+ when :log
1296
+ cd = _a_log(env, cid, obj[:log], _util_time(env, it))
1297
+ else
1298
+ cd = _util_time(env, it)
1299
+ end
1300
+
1301
+ # title
1302
+ when :title
1303
+ case opt
1304
+ when :entry
1305
+ cd = _a_entry(env, cid, obj[:entry], 0, it)
1306
+ when :action
1307
+ cd = _a_action(env, cid, obj[:action], 0, it)
1308
+ when :case
1309
+ cd = _a_case(env, cid, 0, it)
1310
+ when :index
1311
+ cd = _a_index(env, cid, obj[:index], 0, it)
1312
+ when :action
1313
+ cd = _a_action(env, cid, obj[:action], 0, it)
1314
+ else
1315
+ cd = Rack::Utils.escape_html(it)
1316
+ end
1317
+
1318
+ # caseid
1319
+ when :caseid
1320
+ case opt
1321
+ when :current
1322
+ cd = _a_case(env, it, 0, it)
1323
+ when :mixed
1324
+ cd = qcid ? nil : _a_case(env, it, 0, it)
1325
+ else
1326
+ cd = Rack::Utils.escape_html(cid)
1327
+ end
1328
+
1329
+ # stat - only on stats aggregation
1330
+ when :stat
1331
+ qu[:stat] = it
1332
+ qu[:purpose] = 'Entry Stat Search'.freeze
1333
+ cd = _a_entry_search(env, qu, it)
1334
+
1335
+ # sum - only on stats aggregation
1336
+ when :sum
1337
+ cd = it.to_s
1338
+
1339
+ # count - only on stats aggregation
1340
+ when :count
1341
+ cd = it.to_s
1342
+
1343
+ # files
1344
+ when :files
1345
+ cd = it == 0 ? ''.freeze : it.to_s
1346
+
1347
+ # user
1348
+ when :user
1349
+ cd = Rack::Utils.escape_html(it)
1350
+
1351
+ # stats
1352
+ when :stats
1353
+ cd = it == 0 ? ''.freeze : it.to_s
1354
+
1355
+ # huh?
1356
+ else
1357
+ raise NotImplementedError, sym.to_s
1358
+ end
1359
+
1360
+ cd ? (DivListItem % [cc, cd]) : ''.freeze
1361
+ end
1362
+
1363
+ DivListRow % cols.join(''.freeze)
1364
+ end
1365
+
1366
+ return DivList % [head, rows.join(''.freeze)]
1367
+
1368
+ end # def _div_list()
1369
+
1370
+
1371
+ # Search results list
1372
+ DivList = '
1373
+ <div class="list">%s%s
1374
+ </div>'.freeze
1375
+
1376
+ # Search results row
1377
+ DivListRow = '
1378
+ <div class="list-row">%s
1379
+ </div>'.freeze
1380
+
1381
+ # Search results header
1382
+ DivListHead = '
1383
+ <div class="list-head">%s
1384
+ </div>'.freeze
1385
+
1386
+ # Search results header items
1387
+ DivListHeadItems = {
1388
+ tags: '
1389
+ <div class="list-int">Tags</div>'.freeze,
1390
+ tag: '
1391
+ <div class="list-tag">Tag</div>'.freeze,
1392
+ entry: '
1393
+ <div class="list-int">Entry</div>'.freeze,
1394
+ index: '
1395
+ <div class="list-int">Index</div>'.freeze,
1396
+ action: '
1397
+ <div class="list-int">Action</div>'.freeze,
1398
+ log: '
1399
+ <div class="list-int">Log</div>'.freeze,
1400
+ title: '
1401
+ <div class="list-title">Title</div>'.freeze,
1402
+ caseid: '
1403
+ <div class="list-caseid">Case ID</div>'.freeze,
1404
+ stats: '
1405
+ <div class="list-int">Stats</div>'.freeze,
1406
+ time: '
1407
+ <div class="list-time">Date/Time</div>'.freeze,
1408
+ stat: '
1409
+ <div class="list-stat">Stat Name</div>'.freeze,
1410
+ sum: '
1411
+ <div class="list-float">Total</div>'.freeze,
1412
+ count: '
1413
+ <div class="list-int">Count</div>'.freeze,
1414
+ files: '
1415
+ <div class="list-int">Files</div>'.freeze,
1416
+ user: '
1417
+ <div class="list-usergrp">User</div>'.freeze,
1418
+ snippet: ''.freeze
1419
+ }.freeze
1420
+
1421
+ # search results item
1422
+ DivListItem = '
1423
+ <div class="%s">%s</div>'.freeze
1424
+
1425
+
1426
+ ###############################################
1427
+ # Page description div
1428
+ #
1429
+ def _div_desc(head, body)
1430
+ DivDesc % [ head, body ]
1431
+ end # def _div_desc()
1432
+
1433
+ # Div description
1434
+ DivDesc = '
1435
+ <div class="desc">
1436
+ <div class="desc-head">%s</div>
1437
+ %s
1438
+ </div>'.freeze
1439
+
1440
+
1441
+ ###############################################
1442
+ # Page navigation div
1443
+ #
1444
+ # @param resp [Array<Hash>] search results
1445
+ # @param pr [Proc] A Proc which is called instead of yield
1446
+ # @yield [query, disp] the query to run and what to display
1447
+ # @yieldparam query [Hash] the query to execute
1448
+ # @yieldparam disp [String] what to display in the link
1449
+ # @yieldreturn [String] the HTML link for the query page
1450
+ # @return [String] a HTML div for page nav
1451
+ #
1452
+ def _div_page(resp, pr=nil)
1453
+
1454
+ hits = resp[:hits]
1455
+ page_size = resp[:size]
1456
+ tot_pages = ((hits - 1) / page_size) + 1
1457
+ disp_pages = (tot_pages > 10) ? 10 : tot_pages
1458
+
1459
+ query = resp[:query].dup
1460
+ if query.key?(:page)
1461
+ cur = query[:page].to_i
1462
+ else
1463
+ cur = 1
1464
+ end
1465
+ if cur > disp_pages
1466
+ cur = disp_pages
1467
+ end
1468
+
1469
+ # numeric links
1470
+ ary = []
1471
+ disp_pages.times do |pg|
1472
+ page = pg + 1
1473
+ if page == cur
1474
+ ary << '<b>%d</b>'.freeze % page
1475
+ else
1476
+ query[:page] = page
1477
+ if pr
1478
+ val = pr.call(query, page.to_s)
1479
+ else
1480
+ val = yield(query, page.to_s)
1481
+ end
1482
+ ary << val
1483
+ end
1484
+ end
1485
+
1486
+ # previous
1487
+ if cur == 1
1488
+ prev_page = ''.freeze
1489
+ else
1490
+ query[:page] = cur - 1
1491
+ if pr
1492
+ prev_page = pr.call(query, '(Prev)'.freeze)
1493
+ else
1494
+ prev_page = yield(query, '(Prev)'.freeze)
1495
+ end
1496
+ end
1497
+
1498
+ # next
1499
+ if cur == disp_pages
1500
+ next_page = ''.freeze
1501
+ else
1502
+ query[:page] = cur + 1
1503
+ if pr
1504
+ next_page = pr.call(query, '(Next)'.freeze)
1505
+ else
1506
+ next_page = yield(query, '(Next)'.freeze)
1507
+ end
1508
+ end
1509
+
1510
+ return DivPage % [
1511
+ prev_page, ary.join(' '.freeze), next_page,
1512
+ hits, tot_pages
1513
+ ]
1514
+ end # def _div_page()
1515
+
1516
+
1517
+ # Pageing div
1518
+ DivPage = '
1519
+ <div class="pagenav">
1520
+ &lt;&lt; %s %s %s &gt;&gt;<br>
1521
+ Hits: %d Pages: %d
1522
+ </div>'.freeze
1523
+
1524
+
1525
+ ###############################################
1526
+ # Home div
1527
+ #
1528
+ def _div_home(env)
1529
+ api = env['icfs']
1530
+
1531
+ # get the user & roles
1532
+ ur = [ api.user ]
1533
+ ur.concat api.roles
1534
+ now = Time.now.to_i
1535
+
1536
+ # actions
1537
+ useract = ur.map do |ug|
1538
+ cl = [
1539
+ _a_case_search(env, {
1540
+ grantee: ug,
1541
+ status: true,
1542
+ template: false,
1543
+ purpose: 'Open Cases'.freeze
1544
+ }, 'open'.freeze),
1545
+ _a_case_search(env, {
1546
+ grantee: ug,
1547
+ status: false,
1548
+ template: false,
1549
+ purpose: 'Closed Cases'.freeze
1550
+ }, 'closed'.freeze),
1551
+ _a_case_search(env, {
1552
+ grantee: ug,
1553
+ perm: ICFS::PermAction,
1554
+ status: true,
1555
+ template: false,
1556
+ purpose: 'Action Manager Cases'.freeze
1557
+ }, 'action mgr'.freeze),
1558
+ _a_case_tags(env, {
1559
+ grantee: ug,
1560
+ status: true,
1561
+ template: false,
1562
+ purpose: 'Open Case Tags'.freeze
1563
+ }, 'tags'.freeze),
1564
+ ].map{|lk| DivHomeLink % lk }.join(''.freeze)
1565
+
1566
+ al = [
1567
+ _a_action_search(env, {
1568
+ assigned: ug,
1569
+ status: true,
1570
+ flag: true,
1571
+ purpose: 'Flagged Actions'.freeze
1572
+ }, 'flagged'.freeze),
1573
+ _a_action_search(env, {
1574
+ assigned: ug,
1575
+ status: true,
1576
+ before: now,
1577
+ sort: 'time_asc'.freeze,
1578
+ purpose: 'Actions - Past Date'.freeze,
1579
+ }, 'past'.freeze),
1580
+ _a_action_search(env, {
1581
+ assigned: ug,
1582
+ status: true,
1583
+ after: now,
1584
+ sort: 'time_desc'.freeze,
1585
+ purpose: 'Actions - Future Date'.freeze,
1586
+ }, 'future'.freeze),
1587
+ _a_action_search(env, {
1588
+ assigned: ug,
1589
+ status: true,
1590
+ purpose: 'Open Actions'.freeze
1591
+ }, 'all open'.freeze),
1592
+ _a_action_tags(env, {
1593
+ assigned: ug,
1594
+ status: true,
1595
+ purpose: 'Open Action Tags'.freeze
1596
+ }, 'tags'.freeze),
1597
+ ].map{|lk| DivHomeLink % lk }.join(''.freeze)
1598
+
1599
+ ol = [
1600
+ _a_case_search(env, {
1601
+ grantee: ug,
1602
+ perm: ICFS::PermManage,
1603
+ status: true,
1604
+ template: false,
1605
+ purpose: 'Managed Cases'.freeze,
1606
+ }, 'managed'.freeze),
1607
+ _a_case_search(env, {
1608
+ grantee: ug,
1609
+ perm: ICFS::PermManage,
1610
+ status: true,
1611
+ template: true,
1612
+ purpose: 'Templates'.freeze,
1613
+ }, 'templates'.freeze),
1614
+ _a_stats(env, {
1615
+ credit: ug,
1616
+ after: Time.now.to_i - 60*60*24*30,
1617
+ purpose: 'User/Role Stats - 30 days'.freeze,
1618
+ }, '30-day stats'.freeze),
1619
+ ].map{|lk| DivHomeLink % lk }.join(''.freeze)
1620
+
1621
+
1622
+ DivHomeUr % [Rack::Utils.escape_html(ug), al, cl, ol ]
1623
+ end
1624
+
1625
+ DivHome % useract.join(''.freeze)
1626
+ end # def _div_home()
1627
+
1628
+
1629
+ # Home div
1630
+ DivHome = '
1631
+ <div class="home list">
1632
+ <div class="list-head">
1633
+ <div class="list-usergrp">User/Role</div>
1634
+ <div class="list-text-s">Actions</div>
1635
+ <div class="list-text-s">Cases</div>
1636
+ <div class="list-text-s">Other</div>
1637
+ </div>%s
1638
+ </div>'.freeze
1639
+
1640
+
1641
+ # Home user/role
1642
+ DivHomeUr = '
1643
+ <div class="list-row">
1644
+ <div class="list-usergrp">%s</div>
1645
+ <div class="links-list">%s
1646
+ </div>
1647
+ <div class="links-list">%s
1648
+ </div>
1649
+ <div class="links-list">%s
1650
+ </div>
1651
+ </div>'.freeze
1652
+
1653
+
1654
+ # Home Link
1655
+ DivHomeLink = '
1656
+ <div class="list-text-s">%s</div>'.freeze
1657
+
1658
+
1659
+ ###############################################
1660
+ # Case Create Form
1661
+ #
1662
+ def _form_create(env)
1663
+ [ FormCaseCreate, ''.freeze ]
1664
+ end # def _form_create()
1665
+
1666
+
1667
+ # Form to create a new case
1668
+ FormCaseCreate = '
1669
+ <div class="sect">
1670
+ <div class="sect-main">
1671
+ <div class="sect-label">Create Case</div>
1672
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
1673
+ Describe the new case to be created.
1674
+ </div></div>
1675
+ <div class="sect-fill"> </div>
1676
+ </div>
1677
+ <div class="form-row">
1678
+ <div class="list-label">Case ID:</div>
1679
+ <input class="form-caseid" name="create_cid" type="text">
1680
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
1681
+ A unique identifier for the case. This is fixed at case creation and
1682
+ cannot be changed.
1683
+ </div></div>
1684
+ </div>
1685
+ <div class="form-row">
1686
+ <div class="list-label">Template:</div>
1687
+ <input class="form-check" name="create_tmpl" type="checkbox"
1688
+ value="true">
1689
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
1690
+ Select to make this a template which will provide default values
1691
+ when making new cases.
1692
+ </div></div>
1693
+ </div>
1694
+ </div>'.freeze
1695
+
1696
+
1697
+ ###############################################
1698
+ # Form div
1699
+ #
1700
+ # @param env [Hash] Rack environment
1701
+ # @param path [String] the path to submit
1702
+ # @param cid [String, NilClass] case ID
1703
+ # @param parts [Array<String>] the form sections
1704
+ # @param button [String] the submit button text
1705
+ def _div_form(env, path, cid, parts, button)
1706
+ spath = env['SCRIPT_NAME'] + path
1707
+ spath += Rack::Utils.escape(cid) if cid
1708
+ return DivForm % [
1709
+ spath,
1710
+ parts.join(''.freeze),
1711
+ button,
1712
+ ]
1713
+ end # def _div_form()
1714
+
1715
+ # Form
1716
+ DivForm = '
1717
+ <div class="form"><form method="post" action="%s"
1718
+ enctype="multipart/form-data" accept-charset="utf-8">
1719
+ %s
1720
+ <input class="submit" type="submit" value="%s">
1721
+ </form></div>'.freeze
1722
+
1723
+
1724
+
1725
+ ###############################################
1726
+ # Case div
1727
+ #
1728
+ def _div_case(env, cse)
1729
+ api = env['icfs']
1730
+ urg = api.urg
1731
+ cid = cse['caseid']
1732
+ al = api.access_list(cid)
1733
+
1734
+ status = cse['status'] ? 'Open'.freeze : 'Closed'.freeze
1735
+ template = cse['template'] ? 'Yes'.freeze : 'No'.freeze
1736
+
1737
+ # case links
1738
+ links = [
1739
+ _a_log_search(env, {caseid: cid}, 'History of Case'.freeze),
1740
+ ]
1741
+ if al.include?(ICFS::PermManage)
1742
+ links << _a_case_edit(env, cid, 'Edit This Case'.freeze)
1743
+ if cse['template']
1744
+ links << _a_case_create(env, cid, 'Create New Case'.freeze)
1745
+ end
1746
+ end
1747
+ links.map!{|aa| DivCaseLink % aa}
1748
+
1749
+ # action section
1750
+ if al.include?(ICFS::PermAction)
1751
+ now = Time.now.to_i
1752
+ actions = [
1753
+ _a_action_search(env, {
1754
+ caseid: cid,
1755
+ assigned: ICFS::UserCase,
1756
+ status: true,
1757
+ flag: true,
1758
+ purpose: 'Flagged Actions'.freeze,
1759
+ }, 'flagged'.freeze),
1760
+ _a_action_search(env, {
1761
+ caseid: cid,
1762
+ assigned: ICFS::UserCase,
1763
+ status: true,
1764
+ before: now,
1765
+ sort: 'time_asc'.freeze,
1766
+ purpose: 'Actions - Past Date'.freeze,
1767
+ }, 'past'.freeze),
1768
+ _a_action_search(env, {
1769
+ caseid: cid,
1770
+ assigned: ICFS::UserCase,
1771
+ status: true,
1772
+ after: now,
1773
+ sort: 'time_desc'.freeze,
1774
+ purpose: 'Actions - Future Date'.freeze,
1775
+ }, 'future'.freeze),
1776
+ _a_action_search(env, {
1777
+ caseid: cid,
1778
+ assigned: ICFS::UserCase,
1779
+ status: true,
1780
+ purpose: 'Open Actions'.freeze,
1781
+ }, 'all open'.freeze),
1782
+ _a_action_tags(env, {
1783
+ caseid: cid,
1784
+ assigned: ICFS::UserCase,
1785
+ status: true,
1786
+ purpose: 'Open Action Tags'.freeze,
1787
+ }, 'tags'.freeze),
1788
+ ].map{|lk| DivCaseLink % lk}
1789
+ actions = DivCaseActions % actions.join(''.freeze)
1790
+ else
1791
+ actions = ''.freeze
1792
+ end
1793
+
1794
+ # tags
1795
+ tags = cse['tags'].map do |tg|
1796
+ DivCaseTag % _a_case_search(env, {tags: tg},
1797
+ Rack::Utils.escape_html(tg) )
1798
+ end
1799
+
1800
+ # access control
1801
+ acc = cse['access'].map do |ac|
1802
+ pm = Rack::Utils.escape_html(ac['perm'])
1803
+ ugl = ac['grant'].map do |ug|
1804
+ DivCaseGrant % Rack::Utils.escape_html(ug)
1805
+ end
1806
+ DivCaseAccess % [ pm, ugl.join(''.freeze) ]
1807
+ end
1808
+
1809
+ # stats
1810
+ if cse['stats']
1811
+ stats = cse['stats'].map do |st|
1812
+ DivCaseStatEach % _a_entry_search(env, { caseid: cid, stat: st,
1813
+ purpose: 'Entries with Stat'.freeze },
1814
+ Rack::Utils.escape_html(st) )
1815
+ end
1816
+ stats = DivCaseStats % stats.join(''.freeze)
1817
+ else
1818
+ stats = ''.freeze
1819
+ end
1820
+
1821
+ return DivCase % [
1822
+ Rack::Utils.escape_html(cid),
1823
+ _a_log(env, cid, cse['log'], cse['log'].to_s),
1824
+ status,
1825
+ template,
1826
+ links.join(''.freeze),
1827
+ Rack::Utils.escape_html(cse['title']),
1828
+ acc.join(''.freeze),
1829
+ tags.join(''.freeze),
1830
+ stats,
1831
+ actions,
1832
+ ]
1833
+ end # def _div_case()
1834
+
1835
+ # Case div
1836
+ DivCase = '
1837
+ <div class="sbar">
1838
+ <div class="sbar-side">
1839
+ <div class="sbar-side-head">Case</div>
1840
+ <div class="list">
1841
+ <div class="list-row">
1842
+ <div class="list-label">Case ID:</div>
1843
+ <div class="list-caseid">%s</div>
1844
+ </div>
1845
+ <div class="list-row">
1846
+ <div class="list-label">Log:</div>
1847
+ <div class="list-int">%s</div>
1848
+ </div>
1849
+ <div class="list-row">
1850
+ <div class="list-label">Status:</div>
1851
+ <div class="list-text-s">%s</div>
1852
+ </div>
1853
+ <div class="list-row">
1854
+ <div class="list-label">Template:</div>
1855
+ <div class="list-text-s">%s</div>
1856
+ </div>
1857
+ </div>
1858
+ <div class="sect">%s
1859
+ </div>
1860
+ </div>
1861
+ <div class="sbar-main">
1862
+ <div class="sbar-main-head">%s</div>
1863
+ <div class="sect">
1864
+ <div class="sect-head">Access Control</div>
1865
+ <div class="list">
1866
+ <div class="list-head">
1867
+ <div class="list-perm">Permission</div>
1868
+ <div class="list-usergrp">Users/Groups</div>
1869
+ </div>%s
1870
+ </div>
1871
+ </div>
1872
+ <div class="sect">
1873
+ <div class="sect-head">Tags</div>%s
1874
+ </div>%s%s
1875
+ </div>
1876
+ </div>'.freeze
1877
+
1878
+
1879
+ # Case div action links
1880
+ DivCaseActions = '
1881
+ <div class="sect">
1882
+ <div class="sect-head">Actions</div>%s
1883
+ </div>'.freeze
1884
+
1885
+
1886
+ # Case div links
1887
+ DivCaseLink = '
1888
+ <div>%s</div>'.freeze
1889
+
1890
+ # Case div each access
1891
+ DivCaseAccess = '
1892
+ <div class="list-row">
1893
+ <div class="list-perm">%s</div>
1894
+ <div class="list-vert list-usergrp">%s
1895
+ </div>
1896
+ </div>'.freeze
1897
+
1898
+
1899
+ # Case div each grant
1900
+ DivCaseGrant = '
1901
+ <div>%s</div>'.freeze
1902
+
1903
+
1904
+ # Case div each tag
1905
+ DivCaseTag = '
1906
+ <div class="item-tag">%s</div>'.freeze
1907
+
1908
+
1909
+ # Case div stats section
1910
+ DivCaseStats = '
1911
+ <div class="sect">
1912
+ <div class="sect-head">Stats</div>
1913
+ <div class="list">%s
1914
+ </div>
1915
+ </div>'.freeze
1916
+
1917
+
1918
+ # Case div each stat
1919
+ DivCaseStatEach = '
1920
+ <div class="list-perm">%s</div>'.freeze
1921
+
1922
+
1923
+ ###############################################
1924
+ # Entry div
1925
+ #
1926
+ def _div_entry(env, ent)
1927
+ api = env['icfs']
1928
+ cid = ent['caseid']
1929
+
1930
+ links = []
1931
+
1932
+ enum = ent['entry']
1933
+ links << _a_entry_edit(env, cid, enum, 0, 'Edit This Entry'.freeze)
1934
+
1935
+ lnum = ent['log']
1936
+ links << _a_log_search(env, {
1937
+ 'caseid' => cid,
1938
+ 'entry' => enum,
1939
+ 'purpose' => 'History of Entry'.freeze,
1940
+ }, 'History of Entry'.freeze)
1941
+
1942
+ if ent['action']
1943
+ anum = ent['action']
1944
+ action = DivEntryAction % _a_action(env, cid, anum, 0, anum.to_s)
1945
+ links << _a_entry_edit(env, cid, 0, anum, 'New Entry in Action'.freeze)
1946
+ else
1947
+ action = ''.freeze
1948
+ end
1949
+
1950
+ if ent['index']
1951
+ indexes = ent['index'].map do |xnum|
1952
+ idx = api.index_read(cid, xnum)
1953
+ DivEntryIndexEach % _a_index(env, cid, xnum, 0, idx['title'])
1954
+ end
1955
+ index = DivEntryIndex % indexes.join(''.freeze)
1956
+ else
1957
+ index = ''.freeze
1958
+ end
1959
+
1960
+ tags = ent['tags'].map do |tag|
1961
+ DivEntryTag % _a_entry_search(env, {
1962
+ 'caseid' => cid,
1963
+ 'tags' => tag,
1964
+ 'purpose' => 'Tag Entries'.freeze,
1965
+ }, tag)
1966
+ end
1967
+
1968
+ if ent['perms']
1969
+ pa = ent['perms'].map do |pm|
1970
+ DivEntryPermEach % Rack::Utils.escape_html(pm)
1971
+ end
1972
+ perms = DivEntryPerms % pa.join("\n".freeze)
1973
+ else
1974
+ perms = ''.freeze
1975
+ end
1976
+
1977
+ if ent['stats']
1978
+ sa = ent['stats'].map do |st|
1979
+ ca = st['credit'].map do |ug|
1980
+ Rack::Utils.escape_html(ug)
1981
+ end
1982
+ DivEntryStatEach % [
1983
+ Rack::Utils.escape_html(st['name']),
1984
+ st['value'],
1985
+ ca.join(', '.freeze)
1986
+ ]
1987
+ end
1988
+ stats = DivEntryStats % sa.join("\n".freeze)
1989
+ else
1990
+ stats = ''.freeze
1991
+ end
1992
+
1993
+ if ent['files']
1994
+ fa = ent['files'].map do |fd|
1995
+ DivEntryFileEach % _a_file(env, cid, enum, fd['log'],
1996
+ fd['num'], fd['name'], fd['name'])
1997
+ end
1998
+ files = DivEntryFiles % fa.join("\n".freeze)
1999
+ else
2000
+ files = ''.freeze
2001
+ end
2002
+
2003
+ return DivEntry % [
2004
+ _a_case(env, cid, 0, cid),
2005
+ _a_entry(env, cid, enum, 0, enum.to_s),
2006
+ _a_log(env, cid, lnum, lnum.to_s),
2007
+ Rack::Utils.escape_html(ent['user']),
2008
+ action,
2009
+ links.map{|lk| DivEntryLink % lk }.join(''.freeze),
2010
+ Rack::Utils.escape_html(ent['title']),
2011
+ _util_time(env, ent['time']),
2012
+ Rack::Utils.escape_html(ent['content']),
2013
+ tags.join("\n".freeze),
2014
+ index,
2015
+ perms,
2016
+ stats,
2017
+ files
2018
+ ]
2019
+ end # def _div_entry()
2020
+
2021
+ # entry div
2022
+ DivEntry = '
2023
+ <div class="sbar">
2024
+ <div class="sbar-side">
2025
+ <div class="sbar-side-head">Entry</div>
2026
+ <div class="list">
2027
+ <div class="list-row">
2028
+ <div class="list-label">Case:</div>
2029
+ <div class="list-caseid">%s</div>
2030
+ </div>
2031
+ <div class="list-row">
2032
+ <div class="list-label">Entry:</div>
2033
+ <div class="list-int">%s</div>
2034
+ </div>
2035
+ <div class="list-row">
2036
+ <div class="list-label">Log:</div>
2037
+ <div class="list-int">%s</div>
2038
+ </div>
2039
+ <div class="list-row">
2040
+ <div class="list-label">User:</div>
2041
+ <div class="list-text-m">%s</div>
2042
+ </div>%s
2043
+ <div class="sect">%s
2044
+ </div>
2045
+ </div>
2046
+ </div>
2047
+ <div class="sbar-main">
2048
+ <div class="sbar-main-head">%s
2049
+ <div class="sbar-main-sub">%s</div>
2050
+ </div>
2051
+ <pre class="sbar-main-content">%s</pre>
2052
+ <div class="sect">
2053
+ <div class="sect-head">Tags</div>
2054
+ <div class="tags-list">%s
2055
+ </div>
2056
+ </div>%s%s%s%s
2057
+ </div>
2058
+ </div>'.freeze
2059
+
2060
+
2061
+ # entry tag each
2062
+ DivEntryTag = '
2063
+ <div>%s</div>'.freeze
2064
+
2065
+
2066
+ # entry link each
2067
+ DivEntryLink = '
2068
+ <div>%s</div>'.freeze
2069
+
2070
+
2071
+ # entry action
2072
+ DivEntryAction = '
2073
+ <div class="list-row">
2074
+ <div class="list-label">Action:</div>
2075
+ <div class="list-int">%s</div>
2076
+ </div>'.freeze
2077
+
2078
+
2079
+ # entry index
2080
+ DivEntryIndex = '
2081
+ <div class="sect">
2082
+ <div class="sect-head">Indexes</div>
2083
+ <div class="index-list">%s
2084
+ </div>
2085
+ </div>'.freeze
2086
+
2087
+
2088
+ # entry index each
2089
+ DivEntryIndexEach = '
2090
+ <div>%s</div>'.freeze
2091
+
2092
+
2093
+ # entry perms
2094
+ DivEntryPerms = '
2095
+ <div class="sect">
2096
+ <div class="sect-head">Permissions</div>
2097
+ <div class="perms-list">%s
2098
+ </div>
2099
+ </div>'.freeze
2100
+
2101
+
2102
+ # entry perm each
2103
+ DivEntryPermEach = '
2104
+ <div>%s</div>'.freeze
2105
+
2106
+
2107
+ # entry stats
2108
+ DivEntryStats = '
2109
+ <div class="sect">
2110
+ <div class="sect-head">Stats</div>
2111
+ <div class="stats-list">%s
2112
+ </div>
2113
+ </div>'.freeze
2114
+
2115
+
2116
+ # entry each stat
2117
+ DivEntryStatEach = '
2118
+ <div>%s %f %s</div>'.freeze
2119
+
2120
+
2121
+ # entry files
2122
+ DivEntryFiles = '
2123
+ <div class="sect">
2124
+ <div class="sect-head">Files</div>
2125
+ <div class="files-list">%s
2126
+ </div>
2127
+ </div>'.freeze
2128
+
2129
+
2130
+ # entry each file
2131
+ DivEntryFileEach = '
2132
+ <div>%s</div>'.freeze
2133
+
2134
+
2135
+ ###############################################
2136
+ # Log div
2137
+ #
2138
+ def _div_log(env, log)
2139
+ cid = log['caseid']
2140
+ lnum = log['log']
2141
+ enum = log['entry']['num']
2142
+
2143
+ navp = (lnum == 1) ? 'prev'.freeze : _a_log(env, cid, lnum-1, 'prev'.freeze)
2144
+ navn = _a_log(env, cid, lnum + 1, 'next'.freeze)
2145
+
2146
+ time = _util_time(env, log['time'])
2147
+
2148
+ if log['case_hash']
2149
+ chash = DivLogCase % _a_case(env, cid, lnum, log['case_hash'])
2150
+ else
2151
+ chash = ''.freeze
2152
+ end
2153
+
2154
+ if log['action']
2155
+ action = DivLogAction % [
2156
+ _a_action(env, cid, log['action']['num'], lnum, log['action']['hash']),
2157
+ log['action']['num'],
2158
+ ]
2159
+ else
2160
+ action = ''.freeze
2161
+ end
2162
+
2163
+ if log['index']
2164
+ index = DivLogIndex % [
2165
+ _a_index(env, cid, log['index']['num'], lnum, log['index']['hash']),
2166
+ log['index']['num'],
2167
+ ]
2168
+ else
2169
+ index = ''.freeze
2170
+ end
2171
+
2172
+ if log['files_hash']
2173
+ ha = log['files_hash']
2174
+ fa = []
2175
+ ha.each_index do |ix|
2176
+ fa << DivLogFileEach % [
2177
+ _a_file(env, cid, enum, lnum, ix, 'file.bin'.freeze, ha[ix]),
2178
+ ix
2179
+ ]
2180
+ end
2181
+ files = DivLogFiles % fa.join("\n".freeze)
2182
+ else
2183
+ files = ''.freeze
2184
+ end
2185
+
2186
+ return DivLog % [
2187
+ Rack::Utils.escape_html(cid),
2188
+ log['log'],
2189
+ navp,
2190
+ navn,
2191
+ _util_time(env, log['time']),
2192
+ Rack::Utils.escape_html(log['user']),
2193
+ log['prev'],
2194
+ _a_entry(env, cid, enum, lnum, log['entry']['hash']),
2195
+ enum,
2196
+ chash,
2197
+ action,
2198
+ index,
2199
+ files
2200
+ ]
2201
+ end # def _div_log()
2202
+
2203
+ # log div
2204
+ DivLog = '
2205
+ <div class="sbar">
2206
+ <div class="sbar-side">
2207
+ <div class="sbar-side-head">Log</div>
2208
+ <div class="list">
2209
+ <div class="list-row">
2210
+ <div class="list-label">Case:</div>
2211
+ <div class="list-caseid">%s</div>
2212
+ </div>
2213
+ <div class="list-row">
2214
+ <div class="list-label">Log:</div>
2215
+ <div class="list-int">%d</div>
2216
+ </div>
2217
+ </div>
2218
+ <div class="nav_links">
2219
+ <div class="prev">%s</div>
2220
+ <div class="next">%s</div>
2221
+ </div>
2222
+ </div>
2223
+ <div class="sbar-main list">
2224
+ <div class="list-row">
2225
+ <div class="list-label">Time:</div>
2226
+ <div class="list-time">%s</div>
2227
+ </div>
2228
+ <div class="list-row">
2229
+ <div class="list-label">User:</div>
2230
+ <div class="list-usergrp">%s</div>
2231
+ </div>
2232
+ <div class="list-row">
2233
+ <div class="list-label">Prev:</div>
2234
+ <div class="list-hash">%s</div>
2235
+ </div>
2236
+ <div class="list-row">
2237
+ <div class="list-label">Entry:</div>
2238
+ <div class="list-hash">%s</div>
2239
+ <div class="list-int">%d</div>
2240
+ </div>%s%s%s%s
2241
+ </div>
2242
+ </div>'.freeze
2243
+
2244
+
2245
+ # log action
2246
+ DivLogAction = '
2247
+ <div class="list-row">
2248
+ <div class="list-label">Action:</div>
2249
+ <div class="list-hash">%s</div>
2250
+ <div class="list-int">%d</div>
2251
+ </div>'.freeze
2252
+
2253
+
2254
+ # log index
2255
+ DivLogIndex = '
2256
+ <div class="list-row">
2257
+ <div class="list-label">Index:</div>
2258
+ <div class="list-hash">%s</div>
2259
+ <div class="list-int">%d</div>
2260
+ </div>'.freeze
2261
+
2262
+
2263
+ # log case
2264
+ DivLogCase = '
2265
+ <div class="list-row">
2266
+ <div class="list-label">Case:</div>
2267
+ <div class="list-hash">%s</div>
2268
+ </div>
2269
+ '.freeze
2270
+
2271
+ # log file
2272
+ DivLogFiles = '
2273
+ <div class="list-row">
2274
+ <div class="list-label">Files:</div>
2275
+ <div class="files-list">%s
2276
+ </div>
2277
+ </div>'.freeze
2278
+
2279
+
2280
+ # log file
2281
+ DivLogFileEach = '
2282
+ <div>
2283
+ <div class="list-hash">%s</div>
2284
+ <div class="list-int">%d</div>
2285
+ </div>'.freeze
2286
+
2287
+
2288
+ ###############################################
2289
+ # Action div
2290
+ #
2291
+ def _div_action(env, act)
2292
+ api = env['icfs']
2293
+ cid = act['caseid']
2294
+
2295
+ # get perms & user/roles
2296
+ al = api.access_list(act['caseid'])
2297
+ perm_act = al.include?(ICFS::PermAction)
2298
+ ur = Set.new
2299
+ ur.add api.user
2300
+ ur.merge api.roles
2301
+
2302
+ links = []
2303
+ anum = act['action']
2304
+ links << _a_entry_edit(env, cid, 0, anum, 'New Entry in Action'.freeze)
2305
+
2306
+ lnum = act['log']
2307
+ links << _a_log_search(env, {
2308
+ 'caseid' => cid,
2309
+ 'action' => anum,
2310
+ 'purpose' => 'Action History'.freeze,
2311
+ }, 'History of Action'.freeze)
2312
+
2313
+ # each task
2314
+ tasks = []
2315
+ ta = act['tasks']
2316
+ ta.each_index do |ixr|
2317
+ ix = ixr + 1
2318
+ tk = ta[ixr]
2319
+
2320
+ # if we can edit
2321
+ edit = (ixr == 0) ? perm_act : ur.include?(tk['assigned'])
2322
+
2323
+ # tags
2324
+ tags = tk['tags'].map do |tg|
2325
+ qu = {
2326
+ assigned: tk['assigned'],
2327
+ tags: tg,
2328
+ }
2329
+ qu[:caseid] = cid if tk['assigned'] == ICFS::UserCase
2330
+ DivActionTag % _a_action_search(env, qu, tg)
2331
+ end
2332
+
2333
+ tasks << DivActionTask % [
2334
+ edit ? 'task-ed'.freeze : 'task-ro'.freeze,
2335
+ Rack::Utils.escape_html(tk['assigned']),
2336
+ Rack::Utils.escape_html(tk['title']),
2337
+ tk['status'] ? 'Open'.freeze : 'Closed'.freeze,
2338
+ tk['flag'] ? 'Raised'.freeze : 'Normal'.freeze,
2339
+ _util_time(env, tk['time']),
2340
+ tags.join(''.freeze),
2341
+ ]
2342
+ end
2343
+
2344
+ return DivAction % [
2345
+ _a_case(env, cid, 0, cid),
2346
+ _a_action(env, cid, anum, 0, anum.to_s),
2347
+ _a_log(env, cid, lnum, lnum.to_s),
2348
+ links.map{|lk| DivActionLink % lk }.join(''.freeze),
2349
+ tasks.join(''.freeze)
2350
+ ]
2351
+ end # def _div_action()
2352
+
2353
+
2354
+ # Action div
2355
+ DivAction = '
2356
+ <div class="sbar">
2357
+
2358
+ <div class="sbar-side">
2359
+ <div class="sbar-side-head">Action</div>
2360
+ <div class="list">
2361
+ <div class="list-row">
2362
+ <div class="list-label">Case:</div>
2363
+ <div class="list-caseid">%s</div>
2364
+ </div>
2365
+ <div class="list-row">
2366
+ <div class="list-label">Action:</div>
2367
+ <div class="list-int">%s</div>
2368
+ </div>
2369
+ <div class="list-row">
2370
+ <div class="list-label">Log:</div>
2371
+ <div class="list-int">%s</div>
2372
+ </div>
2373
+ <div class="sect">%s
2374
+ </div>
2375
+ </div>
2376
+ </div>
2377
+
2378
+ <div class="sbar-main">%s
2379
+ </div>
2380
+
2381
+ </div>'.freeze
2382
+
2383
+
2384
+ # Action link
2385
+ DivActionLink = '
2386
+ <div>%s</div>'.freeze
2387
+
2388
+
2389
+ # Action task
2390
+ DivActionTask = '
2391
+ <div class="list task">
2392
+ <div class="list-row %s">
2393
+ <div class="list-label">Tasked:</div>
2394
+ <div class="list-usergrp">%s</div>
2395
+ </div>
2396
+ <div class="list-row">
2397
+ <div class="list-label">Title:</div>
2398
+ <div class="list-title">%s</div>
2399
+ </div>
2400
+ <div class="list-row">
2401
+ <div class="list-label">Open:</div>
2402
+ <div class="list-text-s">%s</div>
2403
+ </div>
2404
+ <div class="list-row">
2405
+ <div class="list-label">Flag:</div>
2406
+ <div class="list-text-s">%s</div>
2407
+ </div>
2408
+ <div class="list-row">
2409
+ <div class="list-label">Time:</div>
2410
+ <div class="list-time">%s</div>
2411
+ </div>
2412
+ <div class="list-row">
2413
+ <div class="list-label">Tags:</div>
2414
+ <div class="list-vert">%s
2415
+ </div>
2416
+ </div>
2417
+ </div>'.freeze
2418
+
2419
+
2420
+ # Action Tag
2421
+ DivActionTag = '
2422
+ <div>%s</div>'.freeze
2423
+
2424
+
2425
+ ###############################################
2426
+ # Index div
2427
+ #
2428
+ def _div_index(env, idx)
2429
+ cid = idx['caseid']
2430
+
2431
+ links = []
2432
+ xnum = idx['index']
2433
+ links << _a_index_edit(env, cid, xnum, 'Edit This Index'.freeze)
2434
+
2435
+ lnum = idx['log']
2436
+ links << _a_log_search(env, {
2437
+ 'caseid' => cid,
2438
+ 'index' => xnum,
2439
+ 'purpose' => 'Index History'.freeze,
2440
+ }, 'History of Index')
2441
+
2442
+ tags = idx['tags'].map do |tg|
2443
+ DivIndexTag % _a_index_search(env, {
2444
+ 'caseid' => cid,
2445
+ 'tags' => tg,
2446
+ 'purpose' => 'Index Entries'.freeze,
2447
+ }, tg)
2448
+ end
2449
+
2450
+ return DivIndex % [
2451
+ _a_case(env, cid, 0, cid),
2452
+ _a_index(env, cid, xnum, 0, xnum.to_s),
2453
+ _a_log(env, cid, lnum, lnum.to_s),
2454
+ links.map{|lk| DivIndexLink % lk }.join(''.freeze),
2455
+ Rack::Utils.escape_html(idx['title']),
2456
+ Rack::Utils.escape_html(idx['content']),
2457
+ tags.join(''.freeze),
2458
+ ]
2459
+ end # def _div_index()
2460
+
2461
+
2462
+ # Index div
2463
+ DivIndex = '
2464
+ <div class="sbar">
2465
+ <div class="sbar-side">
2466
+ <div class="sbar-side-head">Index</div>
2467
+ <div class="list">
2468
+ <div class="list-row">
2469
+ <div class="list-label">Case:</div>
2470
+ <div class="list-caseid">%s</div>
2471
+ </div>
2472
+ <div class="list-row">
2473
+ <div class="list-label">Index:</div>
2474
+ <div class="list-int">%s</div>
2475
+ </div>
2476
+ <div class="list-row">
2477
+ <div class="list-label">Log:</div>
2478
+ <div class="list-int">%s</div>
2479
+ </div>
2480
+ </div>
2481
+ <div class="sect">%s
2482
+ </div>
2483
+ </div>
2484
+ <div class="sbar-main">
2485
+ <div class="sbar-main-head">%s</div>
2486
+ <pre class="sbar-main-content">%s</pre>
2487
+ <div class="sect">
2488
+ <div class="sect-head">Tags</div>
2489
+ <div class="tags-list">%s
2490
+ </div>
2491
+ </div>
2492
+ </div>
2493
+ </div>'.freeze
2494
+
2495
+
2496
+ # Index Links
2497
+ DivIndexLink = '
2498
+ <div>%s</div>'.freeze
2499
+
2500
+ # Index tags
2501
+ DivIndexTag = '
2502
+ <div>%s</div>'.freeze
2503
+
2504
+
2505
+ ###############################################
2506
+ # Description div for queries
2507
+ #
2508
+ def _div_query(env, type, sup, query, disp=false)
2509
+
2510
+ # query purpose
2511
+ if query[:purpose]
2512
+ purp = Rack::Utils.escape_html(query[:purpose])
2513
+ else
2514
+ purp = type
2515
+ end
2516
+
2517
+ # display the query parameters
2518
+ list = []
2519
+ sup.each do |txt, sym, pr|
2520
+ next if !query.key?(sym) || (sym == :purpose) || (sym == :page)
2521
+ val = query[sym]
2522
+
2523
+ case pr
2524
+ when :string
2525
+ para = Rack::Utils.escape_html(val)
2526
+ when :boolean
2527
+ para = val ? 'true'.freeze : 'false'.freeze
2528
+ when :integer
2529
+ para = val.to_s
2530
+ when :time
2531
+ para = _util_time(env, val)
2532
+ else
2533
+ raise NotImplementedError, pr.to_s
2534
+ end
2535
+
2536
+ list << '<i>%s:</i> %s'.freeze % [sym, para]
2537
+ end
2538
+ if list.empty?
2539
+ paras = ''.freeze
2540
+ else
2541
+ paras = ' &ndash; ' + list.join(', '.freeze)
2542
+ end
2543
+
2544
+ # enable value
2545
+ value = disp ? 'true'.freeze : 'false'.freeze
2546
+
2547
+ return _div_desc(purp, DivQuery % [ type, paras, value ])
2548
+ end # def _div_query()
2549
+
2550
+
2551
+ # Query div
2552
+ DivQuery = '<span class="desc-query"
2553
+ onclick="enaDiv(&quot;que-form&quot;, &quot;que-ena&quot;)"
2554
+ >%s</span>
2555
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2556
+ What kind of search was performed. Query terms are displayed with name
2557
+ of the term in italics followed by the search value.
2558
+ Click to display search form.
2559
+ </div></div>
2560
+ %s
2561
+ <input name="que-enable" id="que-ena" type="hidden" value="%s">'.freeze
2562
+
2563
+
2564
+ ###########################################################
2565
+ # Generate forms
2566
+ ###########################################################
2567
+
2568
+
2569
+ ###############################################
2570
+ # Query form
2571
+ #
2572
+ def _form_query(env, sup, query, act, disp=false)
2573
+
2574
+ # supported params
2575
+ inputs = sup.map do |txt, sym, pr|
2576
+
2577
+ case sym
2578
+ when :caseid
2579
+ ilabel = 'Case ID'.freeze
2580
+ iclass = 'form-caseid'.freeze
2581
+ ihint = 'Filter for a specific case.'.freeze
2582
+ when :title
2583
+ ilabel = 'Title'.freeze
2584
+ iclass = 'form-title'.freeze
2585
+ ihint = 'Text search within the title.'.freeze
2586
+ when :prefix
2587
+ ilabel = 'Prefix'.freeze
2588
+ iclass = 'form-title'.freeze
2589
+ ihint = 'Filter for titles starting with fixed text.'.freeze
2590
+ when :content
2591
+ ilabel = 'Content'.freeze
2592
+ iclass = 'form-content'.freeze
2593
+ ihint = 'Text search within the content.'.freeze
2594
+ when :tags
2595
+ ilabel = 'Tag'.freeze
2596
+ iclass = 'form-tag'.freeze
2597
+ ihint = 'Filter for only a specific tag.'.freeze
2598
+ when :action
2599
+ ilabel = 'Action'.freeze
2600
+ iclass = 'form-int'.freeze
2601
+ ihint = 'Filter for a specific action (by number).'.freeze
2602
+ when :before
2603
+ ilabel = 'Before'.freeze
2604
+ iclass = 'form-time'.freeze
2605
+ ihint = 'Filter for items occuring before this date and time.'.freeze
2606
+ when :after
2607
+ ilabel = 'After'.freeze
2608
+ iclass = 'form-time'.freeze
2609
+ ihint = 'Filter for items occuring after this date and time.'.freeze
2610
+ when :credit
2611
+ ilabel = 'Credit'.freeze
2612
+ iclass = 'form-usergrp'.freeze
2613
+ ihint = 'Filter for stats crediting this user or role.'.freeze
2614
+ when :size
2615
+ ilabel = 'Size'.freeze
2616
+ iclass = 'form-int'.freeze
2617
+ ihint = 'Number of results to be returned per page.'.freeze
2618
+ when :page
2619
+ next
2620
+ when :sort
2621
+ ilabel = 'Sort'.freeze
2622
+ iclass = 'form-sort'.freeze
2623
+ ihint = 'How to sort the results.'.freeze
2624
+ when :purpose
2625
+ next
2626
+ when :user
2627
+ ilabel = 'User'.freeze
2628
+ iclass = 'form-usergrp'.freeze
2629
+ ihint = 'Filter for logs authored by this user.'.freeze
2630
+ when :grantee
2631
+ ilabel = 'Grantee'.freeze
2632
+ iclass = 'form-usergrp'.freeze
2633
+ ihint = 'Filter for cases granting this user or role a permission.'.freeze
2634
+ when :perm
2635
+ ilabel = 'Permission'.freeze
2636
+ iclass = 'form-perm'.freeze
2637
+ ihint = 'Filter for cases granting this permission.'.freeze
2638
+ when :entry
2639
+ ilabel = 'Entry'.freeze
2640
+ iclass = 'form-int'.freeze
2641
+ ihint = 'Filter for logs recording specified entry (by number).'.freeze
2642
+ when :index
2643
+ ilabel = 'Index'.freeze
2644
+ iclass = 'form-int'.freeze
2645
+ ihint = 'Filter for specified index (by number).'.freeze
2646
+ when :assigned
2647
+ ilabel = 'Assigned'.freeze
2648
+ iclass = 'form-usergrp'.freeze
2649
+ ihint = 'Filter for tasks assigned to specified user or role.'.freeze
2650
+ when :status
2651
+ ilabel = 'Status'.freeze
2652
+ iclass = 'form-boolean'.freeze
2653
+ ihint = 'Filter for open items. Use true or false.'.freeze
2654
+ when :flag
2655
+ ilabel = 'Flag'.freeze
2656
+ iclass = 'form-boolean'.freeze
2657
+ ihint = 'Filter for flagged tasks. Use true or false.'.freeze
2658
+ when :template
2659
+ ilabel = 'Template'.freeze
2660
+ iclass = 'form-boolean'.freeze
2661
+ ihint = 'Filter for template cases. Use true or false.'.freeze
2662
+ when :stat
2663
+ ilabel = 'Stat'.freeze
2664
+ iclass = 'form-boolean'.freeze
2665
+ ihint = 'Filter for stats by name. Use true or false.'.freeze
2666
+ else
2667
+ raise NotImplementedError, sym.to_s
2668
+ end
2669
+
2670
+ case pr
2671
+ when :string
2672
+ itype = 'text'.freeze
2673
+ ivalue = query[sym] || ''.freeze
2674
+ when :boolean
2675
+ itype = 'text'.freeze
2676
+ if query[sym].nil?
2677
+ ivalue = ''.freeze
2678
+ else
2679
+ ivalue = query[sym] ? 'true'.freeze : 'false'.freeze
2680
+ end
2681
+ when :integer
2682
+ itype = 'text'.freeze
2683
+ ivalue = query[sym] ? query[sym].to_s : ''
2684
+ when :time
2685
+ itype = 'text'.freeze
2686
+ ivalue = query[sym] ? _util_time(env, query[sym]) : ''.freeze
2687
+ else
2688
+ raise NotImplementedError, pr.to_s
2689
+ end
2690
+
2691
+ FormQueryItem % [ilabel, txt, iclass, itype, ivalue, ihint]
2692
+ end
2693
+
2694
+ # display the form
2695
+ formClass = disp ? ''.freeze : ' hidden'.freeze
2696
+
2697
+ return FormQuery % [
2698
+ formClass,
2699
+ act,
2700
+ inputs.join(''.freeze)
2701
+ ]
2702
+ end # def _form_query
2703
+
2704
+
2705
+ # Query Form
2706
+ FormQuery = '
2707
+ <div class="form%s" id="que-form"><form method="get" action="%s">%s
2708
+ <input class="submit" type="submit" value="Search">
2709
+ </form></div>'.freeze
2710
+
2711
+
2712
+ # Query form item
2713
+ FormQueryItem = '
2714
+ <div class="form-row">
2715
+ <div class="list-label">%s:</div>
2716
+ <input name="%s" class="%s" type="%s" value="%s">
2717
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2718
+ %s
2719
+ </div></div>
2720
+ </div>'.freeze
2721
+
2722
+
2723
+ ###############################################
2724
+ # Case form
2725
+ def _form_case(env, cse)
2726
+
2727
+ status = ' checked'.freeze if cse['status']
2728
+
2729
+ # tags
2730
+ tags_cnt = 0
2731
+ if cse['tags'][0] != ICFS::TagNone
2732
+ tags_list = cse['tags'].map do |tg|
2733
+ tags_cnt = tags_cnt + 1
2734
+ FormCaseTag % [ tags_cnt, Rack::Utils.escape_html(tg) ]
2735
+ end
2736
+ tags = tags_list.join(''.freeze)
2737
+ else
2738
+ tags = ''.freeze
2739
+ end
2740
+
2741
+ # stats
2742
+ stats_cnt = 0
2743
+ if cse['stats']
2744
+ stats_list = cse['stats'].map do |st|
2745
+ stats_cnt += 1
2746
+ FormCaseStat % [stats_cnt, Rack::Utils.escape_html(st)]
2747
+ end
2748
+ stats = stats_list.join(''.freeze)
2749
+ else
2750
+ stats = ''.freeze
2751
+ end
2752
+
2753
+ # access
2754
+ acc_cnt = 0
2755
+ acc_list = cse['access'].map do |ad|
2756
+ acc_cnt = acc_cnt + 1
2757
+
2758
+ grant_cnt = 0
2759
+ grants = ad['grant'].map do |ug|
2760
+ grant_cnt = grant_cnt + 1
2761
+ FormCaseGrant % [ acc_cnt, grant_cnt,
2762
+ Rack::Utils.escape_html(ug)
2763
+ ]
2764
+ end
2765
+
2766
+ FormCaseAccess % [
2767
+ acc_cnt, grant_cnt,
2768
+ acc_cnt, Rack::Utils.escape_html(ad['perm']),
2769
+ grants.join(''.freeze),
2770
+ ]
2771
+ end
2772
+
2773
+ return FormCase % [
2774
+ Rack::Utils.escape_html(cse['title']),
2775
+ status,
2776
+ tags_cnt, tags,
2777
+ acc_cnt, acc_list.join(''.freeze),
2778
+ stats_cnt, stats,
2779
+ ]
2780
+ end # def _form_case()
2781
+
2782
+ # Case form
2783
+ FormCase = '
2784
+ <div class="sect">
2785
+ <div class="sect-main">
2786
+ <div class="sect-label">Case</div>
2787
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2788
+ The current state of a case.
2789
+ </div></div>
2790
+ <div class="sect-fill"> </div>
2791
+ </div>
2792
+ <div class="form-row">
2793
+ <div class="list-label">Title:</div>
2794
+ <input class="form-title" name="cse-title" type="text" spellcheck="true"
2795
+ value="%s">
2796
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2797
+ One line description of the case.
2798
+ </div></div>
2799
+ </div>
2800
+ <div class="form-row">
2801
+ <div class="list-label">Open:</div>
2802
+ <input class="form-check" name="cse-status" type="checkbox"
2803
+ value="true"%s>
2804
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2805
+ Select to indicate the case is currently open.
2806
+ </div></div>
2807
+ </div>
2808
+ </div>
2809
+
2810
+ <div class="sect">
2811
+ <div class="sect-head">
2812
+ <div class="sect-label">Tags</div>
2813
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2814
+ A way to group related cases together.
2815
+ </div></div>
2816
+ <div class="sect-fill"> </div>
2817
+ <button class="tag-add" type="button"
2818
+ onClick="addTag(&quot;cse-tag-list&quot;)">+</button>
2819
+ </div>
2820
+ <div class="tags-list" id="cse-tag-list">
2821
+ <input type="hidden" name="cse-tag" value="%d">%s
2822
+ </div>
2823
+ </div>
2824
+
2825
+ <div class="sect">
2826
+ <div class="sect-head">
2827
+ <div class="sect-label">Access</div>
2828
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2829
+ Grant permissions to a set of users, roles, or groups. This is
2830
+ controls access for this specific case.
2831
+ </div></div>
2832
+ <div class="sect-fill"> </div>
2833
+ <button class="access-add" type="button" onClick="cseAddAcc()">+
2834
+ </button>
2835
+ </div>
2836
+ <input type="hidden" id="cse-acc-cnt" name="cse-acc-cnt" value="%d">
2837
+ <div class="access list" id="cse-acc-list">
2838
+ <div class="list-head">
2839
+ <div class="list-perm">Permission</div>
2840
+ <div class="list-usergrp">Grants to</div>
2841
+ </div>%s
2842
+ </div>
2843
+ </div>
2844
+
2845
+ <div class="sect">
2846
+ <div class="sect-head">
2847
+ <div class="sect-label">Stats</div>
2848
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
2849
+ List of case-specific things which can be assigned numerical
2850
+ values. This is combined with the global stats list to get the
2851
+ list of allowed stats.
2852
+ </div></div>
2853
+ <div class="sect-fill"> </div>
2854
+ <button class="stats-add" type="button" onclick="cseAddStat()">+
2855
+ </button>
2856
+ </div>
2857
+ <div class="stat-list" id="cse-stat-list">
2858
+ <input type="hidden" name="cse-stat" value="%d">%s
2859
+ </div>
2860
+ </div>'.freeze
2861
+
2862
+
2863
+ # Case form Tag each
2864
+ FormCaseTag = '
2865
+ <div>
2866
+ <input class="form-tag" type="text" name="cse-tag-%d" value="%s">
2867
+ <button class="form-del" type="button" onclick="delDiv(this)">X
2868
+ </button>
2869
+ </div>'.freeze
2870
+
2871
+
2872
+ # Case form Stat each
2873
+ FormCaseStat = '
2874
+ <div>
2875
+ <input class="form-stat" type="text" name="cse-stat-%d" value="%s">
2876
+ <button class="form-del" type="button" onclick="delDiv(this)">X
2877
+ </button>
2878
+ </div>'.freeze
2879
+
2880
+
2881
+ # Case form Access each
2882
+ FormCaseAccess = '
2883
+ <div class="list-row">
2884
+ <input type="hidden" name="cse-acc-%d" value="%d">
2885
+ <input class="form-perm" type="text" name="cse-acc-%d-perm"
2886
+ value="%s">
2887
+ <div class="grant-list">%s
2888
+ </div>
2889
+ <button class="add-grant" type="button"
2890
+ onclick="cseAddGrant(this)">+
2891
+ </button>
2892
+ </div>'.freeze
2893
+
2894
+ # Case form Grant each
2895
+ FormCaseGrant = '
2896
+ <div>
2897
+ <input class="form-usergrp" type="text" name="cse-acc-%d-%d"
2898
+ value="%s">
2899
+ </div>'.freeze
2900
+
2901
+
2902
+ #############################################
2903
+ # New entry form
2904
+ #
2905
+ def _form_entry(env, cid, ent=nil)
2906
+ api = env['icfs']
2907
+
2908
+ # title
2909
+ if ent && ent['title']
2910
+ title = Rack::Utils.escape_html(ent['title'])
2911
+ else
2912
+ title = ''.freeze
2913
+ end
2914
+
2915
+ # time
2916
+ if ent && ent['time']
2917
+ time = _util_time(env, ent['time'])
2918
+ else
2919
+ time = ''.freeze
2920
+ end
2921
+
2922
+ # content
2923
+ if ent && ent['content']
2924
+ content = Rack::Utils.escape_html(ent['content'])
2925
+ else
2926
+ content = ''.freeze
2927
+ end
2928
+
2929
+ # files
2930
+ files_cnt = 0
2931
+ files_list = []
2932
+ if ent && ent['files']
2933
+ ent['files'].each do |fd|
2934
+ files_cnt = files_cnt + 1
2935
+ files_list << FormEntryFileEach % [
2936
+ files_cnt, Rack::Utils.escape_html(fd['name']),
2937
+ files_cnt,
2938
+ files_cnt, fd['num'], fd['log']
2939
+ ]
2940
+ end
2941
+ files = files_list.join("\n".freeze)
2942
+ else
2943
+ files = ''.freeze
2944
+ end
2945
+
2946
+ # tags
2947
+ tags_cnt = 0
2948
+ if ent && ent['tags'][0] != ICFS::TagNone
2949
+ tags_list = ent['tags'].map do |tg|
2950
+ tags_cnt = tags_cnt + 1
2951
+ FormEntryTagEach % [tags_cnt, Rack::Utils.escape_html(tg)]
2952
+ end
2953
+ tags = tags_list.join(''.freeze)
2954
+ else
2955
+ tags = ''.freeze
2956
+ end
2957
+
2958
+ # indexes
2959
+ index_cnt = 0
2960
+ if ent && ent['index']
2961
+ idx_list = ent['index'].map do |xnum|
2962
+ index_cnt += 1
2963
+ idx = api.index_read(cid, xnum, 0)
2964
+ FormEntryIndexEach % [
2965
+ index_cnt, xnum,
2966
+ Rack::Utils.escape_html(idx['title'])
2967
+ ]
2968
+ end
2969
+ index = idx_list.join(''.freeze)
2970
+ else
2971
+ index = ''.freeze
2972
+ end
2973
+
2974
+ # stats select
2975
+ stats_sel = api.stats_list(cid).to_a.sort.map do |stat|
2976
+ esc = Rack::Utils.escape_html(stat)
2977
+ FormEntryStatOpt % [esc, esc]
2978
+ end
2979
+ stats_sel = FormEntryStatSel % stats_sel.join(''.freeze)
2980
+
2981
+ # stats count & list
2982
+ stats_cnt = 0
2983
+ stats_list = []
2984
+ if ent && ent['stats']
2985
+ stats_list = ent['stats'].map do |st|
2986
+ stats_cnt = stats_cnt + 1
2987
+
2988
+ claim_cnt = 0
2989
+ claims = st['credit'].map do |ug|
2990
+ claim_cnt = claim_cnt + 1
2991
+ FormEntryClaim % [stats_cnt, claim_cnt,
2992
+ Rack::Utils.escape_html(ug)]
2993
+ end
2994
+
2995
+ esc = Rack::Utils.escape_html(st['name'])
2996
+ FormEntryStatEach % [
2997
+ stats_cnt, claim_cnt,
2998
+ stats_cnt, esc, esc,
2999
+ stats_cnt, st['val'].to_s,
3000
+ claims.join(''.freeze)
3001
+ ]
3002
+ end
3003
+ stats = stats_list.join(''.freeze)
3004
+ else
3005
+ stats = ''.freeze
3006
+ end
3007
+
3008
+ # perms select
3009
+ al = env['icfs'].access_list(cid)
3010
+ perms_sel = al.sort.map do |pm|
3011
+ esc = Rack::Utils.escape_html(pm)
3012
+ FormEntryPermOpt % [esc, esc]
3013
+ end
3014
+ perms_sel = perms_sel.join(''.freeze)
3015
+
3016
+ # perms count & list
3017
+ perms_cnt = 0
3018
+ if ent && ent['perms']
3019
+ perms_list = ent['perms'].map do |pm|
3020
+ perms_cnt = perms_cnt + 1
3021
+ esc = Rack::Utils.escape_html(pm)
3022
+ FormEntryPermEach % [esc, perms_cnt, esc]
3023
+ end
3024
+ perms = perms_list.join(''.freeze)
3025
+ else
3026
+ perms = ''.freeze
3027
+ end
3028
+
3029
+ return FormEntry % [
3030
+ ent ? ent['entry'] : 0,
3031
+ (ent && ent['action']) ? ent['action'] : 0,
3032
+ title, time, content,
3033
+ tags_cnt, tags,
3034
+ files_cnt, files,
3035
+ Rack::Utils.escape(env['SCRIPT_NAME']),
3036
+ Rack::Utils.escape(cid),
3037
+ index_cnt, index,
3038
+ stats_sel, stats_cnt, stats,
3039
+ perms_sel, perms_cnt, perms
3040
+ ]
3041
+ end # def _form_entry
3042
+
3043
+
3044
+ # entry edit form
3045
+ FormEntry = '
3046
+ <div class="sect">
3047
+ <div class="sect-main">
3048
+ <div class="sect-label">Entry</div>
3049
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3050
+ Describe the activity.
3051
+ </div></div>
3052
+ <div class="sect-fill"> </div>
3053
+ <input name="ent-num" type="hidden" value="%d">
3054
+ <input name="ent-act" type="hidden" value="%d">
3055
+ </div>
3056
+ <div class="form-row">
3057
+ <div class="list-label">Title:</div>
3058
+ <input class="form-title" name="ent-title" type="text"
3059
+ spellcheck="true" value="%s">
3060
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3061
+ One line description of the entry.
3062
+ </div></div>
3063
+ </div>
3064
+ <div class="form-row">
3065
+ <div class="list-label">Time:</div>
3066
+ <input class="form-time" name="ent-time" type="text" value="%s">
3067
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3068
+ When the entry occured.
3069
+ </div></div>
3070
+ </div>
3071
+ <div class="form-row">
3072
+ <div class="list-label">Content:</div>
3073
+ <textarea class="form-content" name="ent-content">%s</textarea>
3074
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3075
+ A complete description of the entry.
3076
+ </div></div>
3077
+ </div>
3078
+ </div>
3079
+
3080
+ <div class="sect">
3081
+ <div class="sect-head">
3082
+ <div class="sect-label">Tags</div>
3083
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3084
+ A way to group related entries together.
3085
+ </div></div>
3086
+ <div class="sect-fill"> </div>
3087
+ <div class="sect-right">
3088
+ <button class="tag-add" type="button"
3089
+ onClick="addTag(&quot;ent-tag-list&quot;)">+</button>
3090
+ </div>
3091
+ </div>
3092
+ <div class="tags-list" id="ent-tag-list">
3093
+ <input type="hidden" name="ent-tag" value="%d">%s
3094
+ </div>
3095
+ </div>
3096
+
3097
+ <div class="sect">
3098
+ <div class="sect-head">
3099
+ <div class="sect-label">Files</div>
3100
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3101
+ Attach files to this entry.
3102
+ </div></div>
3103
+ <div class="sect-fill"> </div>
3104
+ <div class="sect-right">
3105
+ <button class="file-add" type="button" onClick="entAddFile()">+
3106
+ </button>
3107
+ </div>
3108
+ </div>
3109
+ <input type="hidden" id="ent-file-cnt" name="ent-file-cnt" value="%d">
3110
+ <div class="files-list" id="ent-file-list">%s
3111
+ </div>
3112
+ </div>
3113
+
3114
+ <div class="sect">
3115
+ <div class="sect-head">
3116
+ <div class="sect-label">Indexes</div>
3117
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3118
+ Real-world factors that appear in a case multiple times.
3119
+ </div></div>
3120
+ <div class="sect-fill"> </div>
3121
+ <div class="sect-right">
3122
+ <input class="form-index" type="text" id="ent-idx-lu"
3123
+ name="ent-idx-lu">
3124
+ <button class="index-add" type="button"
3125
+ onclick="entAddIndex()">?</button>
3126
+ </div>
3127
+ </div>
3128
+ <input type="hidden" id="ent-idx-script" value="%s">
3129
+ <input type="hidden" id="ent-idx-caseid" value="%s">
3130
+ <input type="hidden" id="ent-idx-cnt" name="ent-idx-cnt" value="%d">
3131
+ <div class="index-list" id="ent-idx-list">%s
3132
+ </div>
3133
+ </div>
3134
+
3135
+ <div class="sect">
3136
+ <div class="sect-head">
3137
+ <div class="sect-label">Stats</div>
3138
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3139
+ Numerical measurements of things that have occured, associated with
3140
+ this entry.
3141
+ </div></div>
3142
+ <div class="sect-fill"> </div>
3143
+ <div class="sect-right">%s
3144
+ <button class="stat-add" type="button" onClick="entAddStat()">+
3145
+ </button>
3146
+ </div>
3147
+ </div>
3148
+ <input type="hidden" name="ent-stats-cnt" id="ent-stats-cnt" value="%d">
3149
+ <div class="stats-list" id="ent-stats-list">%s
3150
+ </div>
3151
+ </div>
3152
+
3153
+ <div class="sect">
3154
+ <div class="sect-head">
3155
+ <div class="sect-label">Permissions</div>
3156
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3157
+ Permissions needed to access this entry.
3158
+ </div></div>
3159
+ <div class="sect-fill"> </div>
3160
+ <div class="sect-right">
3161
+ <select class="perm-sel" id="ent-perm-sel" name="ent-perm-sel">%s
3162
+ </select>
3163
+ <button class="perm-add" type="button" onClick="entAddPerm()">+
3164
+ </button>
3165
+ </div>
3166
+ </div>
3167
+ <input type="hidden" name="ent-perm-cnt" id="ent-perm-cnt" value="%d">
3168
+ <div class="perms-list" id="ent-perm-list">%s
3169
+ </div>
3170
+ </div>'.freeze
3171
+
3172
+
3173
+ # Entry form tag each
3174
+ FormEntryTagEach = '
3175
+ <div>
3176
+ <input class="form-tag" type="text" name="ent-tag-%d" value="%s">
3177
+ <button class="form-del" type="button" onclick="delDiv(this)">X
3178
+ </button>
3179
+ </div>'.freeze
3180
+
3181
+
3182
+ # Entry form index each
3183
+ FormEntryIndexEach = '
3184
+ <div>
3185
+ <input type="hidden" name="ent-idx-%d" value="%d">%s
3186
+ <button class="form-del" type="button" onclick="delDiv(this)">X
3187
+ </button>
3188
+ </div>'.freeze
3189
+
3190
+
3191
+ # Entry form Perm each
3192
+ FormEntryPermEach = '
3193
+ <div>%s
3194
+ <input type="hidden" name="ent-perm-%d" value="%s">
3195
+ <button class="form-del" type="button" onclick="delDiv(this)">X
3196
+ </button>
3197
+ </div>'.freeze
3198
+
3199
+
3200
+ # Entry form Perm option
3201
+ FormEntryPermOpt = '
3202
+ <option value="%s">%s</option>'.freeze
3203
+
3204
+
3205
+ # Entry form file each
3206
+ FormEntryFileEach = '
3207
+ <div>
3208
+ <input class="form-file-name" type="text" name="ent-file-%d-name"
3209
+ value="%s">
3210
+ <input class="form-file-upl" type="file" name="ent-file-%d-file">
3211
+ <input type="hidden" name="ent-file-%d-num" value="%d-%d">
3212
+ <button class="form-del" type="button" onclick="delDiv(this)">X
3213
+ </button>
3214
+ </div>'.freeze
3215
+
3216
+
3217
+ # Entry form Stat option
3218
+ FormEntryStatOpt = '
3219
+ <option value="%s">%s</option>'.freeze
3220
+
3221
+
3222
+ # Entry form Stat select
3223
+ FormEntryStatSel = '
3224
+ <select class="stat-sel" id="ent-stat-sel" name="ent-stat-sel">%s
3225
+ </select>'.freeze
3226
+
3227
+
3228
+ # Entry form Stat each
3229
+ FormEntryStatEach = '
3230
+ <div class="list-row">
3231
+ <input type="hidden" name="ent-stat-%d" value="%d">
3232
+ <input type="hidden" name="ent-stat-%d-name" value="%s">
3233
+ <div class="list-stat">%s</div>
3234
+ <input class="form-float" type="text" name="ent-stat-%d-value"
3235
+ value="%s">
3236
+ <div class="list-vert">%s
3237
+ </div>
3238
+ <button class="add-claim" type="button"
3239
+ onClick="entAddClaim(this)">+
3240
+ </button>
3241
+ </div>'.freeze
3242
+
3243
+
3244
+ # Entry form Stat Claim
3245
+ FormEntryClaim = '
3246
+ <div>
3247
+ <input class="form-usergrp" type="text" name="ent-stat-%d-%d"
3248
+ value="%s">
3249
+ </div>'.freeze
3250
+
3251
+
3252
+ ###############################################
3253
+ # Action form
3254
+ #
3255
+ def _form_action(env, cid, act = nil, opt={})
3256
+ api = env['icfs']
3257
+
3258
+ # new action
3259
+ if !act
3260
+ ta = [{
3261
+ 'assigned' => ICFS::UserCase,
3262
+ 'title' => ''.freeze,
3263
+ 'status' => true,
3264
+ 'flag' => true,
3265
+ 'time' => nil,
3266
+ 'tags' => [ ICFS::TagNone ],
3267
+ }]
3268
+ else
3269
+ ta = act['tasks']
3270
+ end
3271
+
3272
+ # get perms
3273
+ al = api.access_list(cid)
3274
+ perm_act = al.include?(ICFS::PermAction)
3275
+ if !perm_act && !act
3276
+ raise(Error::Perms, 'Missing perm: %s'.freeze % ICFS::PermAction)
3277
+ end
3278
+
3279
+ # get user/group list
3280
+ ur = Set.new
3281
+ ur.add api.user
3282
+ ur.merge api.roles
3283
+
3284
+ # editing
3285
+ if opt[:edit]
3286
+ ena_val = 'true'.freeze
3287
+ ena_class_add = ''.freeze
3288
+ ena_class_tasks = ''.freeze
3289
+ else
3290
+ ena_val = 'false'.freeze
3291
+ ena_class_add = ' invisible'.freeze
3292
+ ena_class_tasks = ' hidden'.freeze
3293
+ end
3294
+
3295
+ # each task
3296
+ tasks = []
3297
+ ta.each_index do |ixr|
3298
+ ix = ixr + 1
3299
+ tk = ta[ixr]
3300
+
3301
+ # figure out if we can edit
3302
+ if ixr == 0
3303
+ edit = perm_act
3304
+ else
3305
+ edit = ur.include?(tk['assigned'])
3306
+ end
3307
+
3308
+ # never edit the tasked
3309
+ esc = Rack::Utils.escape_html(tk['assigned'])
3310
+ ug = FormActionTaskedRo % [ ix, esc, esc ]
3311
+
3312
+ # can edit it
3313
+ if edit
3314
+ title = FormActionTitleEd % [
3315
+ ix, Rack::Utils.escape_html(tk['title']) ]
3316
+ status = FormActionStatusEd % [
3317
+ ix, tk['status'] ? ' checked'.freeze : ''.freeze ]
3318
+ flag = FormActionFlagEd % [
3319
+ ix, tk['flag'] ? ' checked'.freeze : ''.freeze ]
3320
+ if tk['time']
3321
+ time = FormActionTimeEd % [ ix, _util_time(env, tk['time']) ]
3322
+ else
3323
+ time = FormActionTimeEd % [ix, ''.freeze]
3324
+ end
3325
+
3326
+ if tk['tags'][0] == ICFS::TagNone
3327
+ tags_cnt = 1
3328
+ tags = FormActionTagEd % [ix, 1, ''.freeze]
3329
+ else
3330
+ tags_cnt = 0
3331
+ tags = tk['tags'].map do |tg|
3332
+ tags_cnt = tags_cnt + 1
3333
+ FormActionTagEd % [
3334
+ ix, tags_cnt, Rack::Utils.escape_html(tg) ]
3335
+ end
3336
+ tags = tags.join(''.freeze)
3337
+ end
3338
+
3339
+ tag_list = 'act-%d-tag-list' % ix
3340
+ tag_add = FormActionTagButton % tag_list
3341
+
3342
+ # can't edit
3343
+ else
3344
+ esc = Rack::Utils.escape_html(tk['title'])
3345
+ title = FormActionTitleRo % [ ix, esc, esc ]
3346
+ status = FormActionStatusRo % [ ix,
3347
+ tk['status'] ? 'true'.freeze : 'false'.freeze,
3348
+ tk['status'] ? 'Open'.freeze : 'Closed'.freeze,
3349
+ ]
3350
+ if tk['flag']
3351
+ flag = FormActionFlagRo % ix
3352
+ else
3353
+ flag = FormActionFlagEd % [ ix, ''.freeze ]
3354
+ end
3355
+ esc = _util_time(env, tk['time'])
3356
+ time = FormActionTimeRo % [ ix, esc, esc ]
3357
+
3358
+ tags_cnt = 0
3359
+ if tk['tags'][0] != ICFS::TagNone
3360
+ tags = tk['tags'].map do |tg|
3361
+ tags_cnt = tags_cnt + 1
3362
+ esc = Rack::Utils.escape_html(tg)
3363
+ FormActionTagRo % [ ix, tags_cnt, esc, esc ]
3364
+ end
3365
+ tags = tags.join(''.freeze)
3366
+ else
3367
+ tags = ''.freeze
3368
+ end
3369
+
3370
+ tag_add = ''.freeze
3371
+
3372
+ end
3373
+
3374
+ tasks << FormActionTask % [
3375
+ edit ? 'ed'.freeze : 'ro'.freeze,
3376
+ ug, title, status, flag, time, ix, ix, tags_cnt, tags, tag_add
3377
+ ]
3378
+ end
3379
+
3380
+ return FormAction % [
3381
+ act ? act['action'] : 0,
3382
+ ena_class_add,
3383
+ ena_val,
3384
+ tasks.size,
3385
+ act ? act['action'] : 0,
3386
+ ena_class_tasks,
3387
+ tasks.join(''.freeze)
3388
+ ]
3389
+ end # def _form_action()
3390
+
3391
+
3392
+ # action edit form
3393
+ FormAction = '
3394
+ <div class="sect" id="act_section">
3395
+ <div class="sect-main">
3396
+ <div class="sect-label">Action</div>
3397
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3398
+ A unit of work, accomplished in a set of related real-world
3399
+ and administrative activitires.
3400
+ </div></div>
3401
+ <div class="sect-fill"> </div>
3402
+ <input name="act-num" type="hidden" value="%d">
3403
+ <div class="sect-right">
3404
+ <button id="act-ena-button" class="act-ena" type="button"
3405
+ onclick="actEnable()">Toggle Edit
3406
+ </button>
3407
+ </div>
3408
+ <div id="act-task-add" class="sect-right%s">
3409
+ <button class="tsk-add" type="button" onclick="actAddTask()">+
3410
+ </button>
3411
+ </div>
3412
+ <input id="act-ena" name="act-ena" type="hidden" value="%s">
3413
+ <input id="act-cnt" name="act-cnt" type="hidden" value="%d">
3414
+ <input type="hidden" name="act-num" value="%d">
3415
+ </div>
3416
+ <div id="act-tasks" class="%s">%s
3417
+ </div>
3418
+ </div>'.freeze
3419
+
3420
+
3421
+ # action task
3422
+ FormActionTask = '
3423
+ <div class="task %s">
3424
+ <div class="form-row">
3425
+ <div class="list-label">Tasked:</div>
3426
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3427
+ A user or role tasked with working on the action.
3428
+ </div></div>%s
3429
+ </div>
3430
+ <div class="form-row">
3431
+ <div class="list-label">Title:</div>
3432
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3433
+ One line summary of the action from the point of view of the
3434
+ tasked user or role.
3435
+ </div></div>%s
3436
+ </div>
3437
+ <div class="form-row">
3438
+ <div class="list-label">Open:</div>
3439
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3440
+ Action open for the tasked user or role.
3441
+ </div></div>%s
3442
+ </div>
3443
+ <div class="form-row">
3444
+ <div class="list-label">Flag:</div>
3445
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3446
+ Flag the action for attention by the user or role.
3447
+ </div></div>%s
3448
+ </div>
3449
+ <div class="form-row">
3450
+ <div class="list-label">Time:</div>
3451
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3452
+ Time associated with the action for the user or role. This can
3453
+ be a due date, an assigned date, or any other useful date or
3454
+ time.
3455
+ </div></div>%s
3456
+ </div>
3457
+ <div class="form-row">
3458
+ <div class="list-label">Tags:</div>
3459
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3460
+ A way to group of actions together.
3461
+ </div></div>
3462
+ <div class="tags-list" id="act-%d-tag-list">
3463
+ <input type="hidden" name="act-%d-tag" value="%d">%s
3464
+ </div>%s
3465
+ </div>
3466
+ </div>'.freeze
3467
+
3468
+
3469
+ # action tasked editable
3470
+ FormActionTaskedEd = '
3471
+ <input class="form-usergrp" name="act-%d-task" type="text"
3472
+ value="%s">'.freeze
3473
+
3474
+
3475
+ # action tasked read only
3476
+ FormActionTaskedRo = '
3477
+ <input name="act-%d-task" type="hidden" value="%s">
3478
+ <div class="list-usergrp">%s</div>'.freeze
3479
+
3480
+
3481
+ # action title editable
3482
+ FormActionTitleEd = '
3483
+ <input class="form-title" name="act-%d-title" type="text"
3484
+ value="%s">'.freeze
3485
+
3486
+
3487
+ # action title read only
3488
+ FormActionTitleRo = '
3489
+ <input name="act-%d-title" type="hidden" value="%s">
3490
+ <div class="list-title">%s</div>'.freeze
3491
+
3492
+
3493
+ # action open editable
3494
+ FormActionStatusEd = '
3495
+ <input class="form-check" name="act-%d-status" type="checkbox"
3496
+ value="true"%s>'.freeze
3497
+
3498
+ # action open readonly
3499
+ FormActionStatusRo = '
3500
+ <input name="act-%d-status" type="hidden" value="%s">
3501
+ <div class="item-boolean">%s</div>'.freeze
3502
+
3503
+
3504
+ # action flag editable
3505
+ FormActionFlagEd = '
3506
+ <input class="form-check" name="act-%d-flag" type="checkbox"
3507
+ value="true"%s>'.freeze
3508
+
3509
+
3510
+ # action flag read-only
3511
+ FormActionFlagRo = '
3512
+ <input name="act-%d-flag" type="hidden" value="true">
3513
+ <div class="item-boolean">flagged</div>'.freeze
3514
+
3515
+
3516
+ # action time editable
3517
+ FormActionTimeEd = '
3518
+ <input class="form-time" name="act-%d-time" type="text"
3519
+ value="%s">'.freeze
3520
+
3521
+
3522
+ # action time read-only
3523
+ FormActionTimeRo = '
3524
+ <input name="act-%d-time" type="hidden" value="%s">
3525
+ <div class="item-time">%s</div>'.freeze
3526
+
3527
+
3528
+ # action tag editable
3529
+ FormActionTagEd = '
3530
+ <div>
3531
+ <input class="form-tag" type="text" name="act-%d-tag-%d"
3532
+ value="%s"><button class="form-del" type="button"
3533
+ onclick="delDiv(this)">X</button>
3534
+ </div>'.freeze
3535
+
3536
+
3537
+ # action tag read-only
3538
+ FormActionTagRo = '
3539
+ <div>
3540
+ <input type="hidden" name="act-%d-tag-%d" value="%s">%s
3541
+ </div>'.freeze
3542
+
3543
+
3544
+ # action tag button
3545
+ FormActionTagButton = '
3546
+ <button class="tag-add" type="button"
3547
+ onClick="addTag(&quot;%s&quot;)">+</button>'.freeze
3548
+
3549
+
3550
+ ###############################################
3551
+ # Index form
3552
+ def _form_index(env, cid, idx=nil)
3553
+
3554
+ # title
3555
+ if idx && idx['title']
3556
+ title = Rack::Utils.escape_html(idx['title'])
3557
+ else
3558
+ title = ''.freeze
3559
+ end
3560
+
3561
+ # content
3562
+ if idx && idx['content']
3563
+ content = Rack::Utils.escape_html(idx['content'])
3564
+ else
3565
+ content = ''.freeze
3566
+ end
3567
+
3568
+ # tags
3569
+ tags_cnt = 0
3570
+ if idx && idx['tags'][0] != ICFS::TagNone
3571
+ tags_list = idx['tags'].map do |tg|
3572
+ tags_cnt += 1
3573
+ FormIndexTagEach % [tags_cnt, Rack::Utils.escape_html(tg)]
3574
+ end
3575
+ tags = tags_list.join(''.freeze)
3576
+ else
3577
+ tags = ''.freeze
3578
+ end
3579
+
3580
+ return FormIndex % [
3581
+ idx ? idx['index'] : 0,
3582
+ title, content,
3583
+ tags_cnt, tags
3584
+ ]
3585
+
3586
+ end # def _form_index()
3587
+
3588
+
3589
+ # Index form
3590
+ FormIndex = '
3591
+ <div class="sect">
3592
+ <div class="sect-main">
3593
+ <div class="sect-label">Index</div>
3594
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3595
+ A real-world factor that may appear in a case multiple times.
3596
+ </div></div>
3597
+ <div class="sect-fill"> </div>
3598
+ <input name="idx-num" type="hidden" value="%d">
3599
+ </div>
3600
+ <div class="form-row">
3601
+ <div class="list-label">Title:</div>
3602
+ <input class="form-title" name="idx-title" type="text"
3603
+ spellcheck="true" value="%s">
3604
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3605
+ One line description of the index.
3606
+ </div></div>
3607
+ </div>
3608
+ <div class="form-row">
3609
+ <div class="list-label">Content:</div>
3610
+ <textarea class="form-content" name="idx-content">%s</textarea>
3611
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3612
+ A complete description of the index.
3613
+ </div></div>
3614
+ </div>
3615
+ </div>
3616
+ <div class="sect">
3617
+ <div class="sect-head">
3618
+ <div class="sect-label">Tags</div>
3619
+ <div class="tip"><div class="tip-disp"></div><div class="tip-info">
3620
+ A way to group related indexes together.
3621
+ </div></div>
3622
+ <div class="sect-fill"> </div>
3623
+ <div class="sect-right">
3624
+ <button class="tag-add" type="button"
3625
+ onClick="addTag(&quot;idx-tag-list&quot;)">+</button>
3626
+ </div>
3627
+ </div>
3628
+ <div class="tag-list" id="idx-tag-list">
3629
+ <input type="hidden" name="idx-tag" value="%d">%s
3630
+ </div>
3631
+ </div> '.freeze
3632
+
3633
+
3634
+ # Index form tag
3635
+ FormIndexTagEach = '
3636
+ <div>
3637
+ <input class="form-tag" type="text" name="idx-tag-%d" value="%s">
3638
+ </div>'.freeze
3639
+
3640
+
3641
+ ###########################################################
3642
+ # Post
3643
+ ###########################################################
3644
+
3645
+
3646
+ ###############################################
3647
+ # Case edit
3648
+ #
3649
+ def _post_case(env, para)
3650
+
3651
+ # case object
3652
+ cse = {}
3653
+
3654
+ # title
3655
+ cse['title'] = para['cse-title']
3656
+
3657
+ # status
3658
+ cse['status'] = (para['cse-status'] == 'true'.freeze) ? true : false
3659
+
3660
+ # tags
3661
+ tags = []
3662
+ tcnt = para['cse-tag'].to_i
3663
+ if tcnt > 100
3664
+ raise(Error::Interface, 'Tag count too large'.freeze)
3665
+ end
3666
+ tcnt.times do |ix|
3667
+ tx = 'cse-tag-%d'.freeze % [ix + 1]
3668
+ tag = para[tx]
3669
+ next if !tag || tag.empty?
3670
+ tags << tag
3671
+ end
3672
+ if tags.empty?
3673
+ cse['tags'] = [ ICFS::TagNone ]
3674
+ else
3675
+ cse['tags'] = tags.uniq.sort
3676
+ end
3677
+
3678
+ # access
3679
+ acc = []
3680
+ acnt = para['cse-acc-cnt'].to_i
3681
+ if acnt > 100
3682
+ raise(Error::Interface, 'Access count too large'.freeze)
3683
+ end
3684
+ acnt.times do |ix|
3685
+ ixr = ix + 1
3686
+
3687
+ pnam = para['cse-acc-%d-perm'.freeze % ixr]
3688
+ gcnt = para['cse-acc-%d'.freeze % ixr].to_i
3689
+ next if gcnt == 0 || !pnam || pnam.empty?
3690
+
3691
+ grant = []
3692
+ if gcnt > 100
3693
+ raise(Error::Interface, 'Grant count too large'.freeze)
3694
+ end
3695
+ gcnt.times do |gx|
3696
+ sug = para['cse-acc-%d-%d'.freeze % [ixr, gx+1]]
3697
+ next if !sug || sug.empty?
3698
+ grant << sug
3699
+ end
3700
+
3701
+ next if grant.empty?
3702
+ acc << {
3703
+ 'perm' => pnam,
3704
+ 'grant' => grant
3705
+ }
3706
+ end
3707
+ cse['access'] = acc
3708
+
3709
+ # stats
3710
+ stats = []
3711
+ scnt = para['cse-stat'].to_i
3712
+ if scnt > 100
3713
+ raise(Error::Interface, 'Stat count too large')
3714
+ end
3715
+ scnt.times do |ix|
3716
+ sx = 'cse-stat-%d' % [ix + 1]
3717
+ stat = para[sx]
3718
+ next if !stat || stat.empty?
3719
+ stats << stat
3720
+ end
3721
+ cse['stats'] = stats unless stats.empty?
3722
+
3723
+ return cse
3724
+ end # def _post_case()
3725
+
3726
+
3727
+ ###############################################
3728
+ # Entry edit
3729
+ #
3730
+ def _post_entry(env, para)
3731
+ api = env['icfs']
3732
+
3733
+ # entry object
3734
+ ent = {}
3735
+
3736
+ # entry
3737
+ enum = para['ent-num'].to_i
3738
+ ent['entry'] = enum if enum != 0
3739
+
3740
+ # action
3741
+ anum = para['ent-act'].to_i
3742
+ ent['action'] = anum if anum != 0
3743
+
3744
+ # time
3745
+ tstr = para['ent-time']
3746
+ time = _util_time_parse(env, tstr)
3747
+ ent['time'] = time if time
3748
+
3749
+ # title & content
3750
+ ent['title'] = para['ent-title']
3751
+ ent['content'] = para['ent-content']
3752
+
3753
+ # tags
3754
+ tags = []
3755
+ tcnt = para['ent-tag'].to_i
3756
+ raise(Error::Interface, 'too many tags'.freeze) if(tcnt > 100)
3757
+ tcnt.times do |ix|
3758
+ tx = 'ent-tag-%d'.freeze % [ix + 1]
3759
+ tag = para[tx]
3760
+ tags << tag unless( !tag || tag.empty? )
3761
+ end
3762
+ ent['tags'] = tags.uniq.sort unless tags.empty?
3763
+
3764
+ # indexes
3765
+ index = []
3766
+ icnt = para['ent-idx-cnt'].to_i
3767
+ raise(Error::Interface, 'Too many indexes'.freeze) if(icnt > 100)
3768
+ icnt.times do |ix|
3769
+ tx = 'ent-idx-%d'.freeze % (ix + 1)
3770
+ xnum = para[tx].to_i
3771
+ index << xnum unless xnum == 0
3772
+ end
3773
+ ent['index'] = index.uniq.sort unless index.empty?
3774
+
3775
+ # perms
3776
+ perms = []
3777
+ pcnt = para['ent-perm-cnt'].to_i
3778
+ raise(Error::Interface, 'Too many perms'.freeze) if(pcnt > 100)
3779
+ pcnt.times do |ix|
3780
+ px = 'ent-perm-%d'.freeze % [ix + 1]
3781
+ pm = para[px]
3782
+ next if !pm || pm.empty?
3783
+ perms << pm
3784
+ end
3785
+ ent['perms'] = perms unless perms.empty?
3786
+
3787
+ # stats
3788
+ stats = []
3789
+ scnt = para['ent-stats-cnt'].to_i
3790
+ raise(Error::Interface, 'Too many stats'.freeze) if(scnt > 100)
3791
+ scnt.times do |ix|
3792
+ ixr = ix + 1
3793
+ sname = para['ent-stat-%d-name'.freeze % ixr]
3794
+ sval = para['ent-stat-%d-value'.freeze % ixr]
3795
+ next if !sname || !sval || sname.empty? || sval.empty?
3796
+
3797
+ sval = sval.to_f
3798
+
3799
+ scred = para['ent-stat-%d'.freeze % ixr].to_i
3800
+ sugs = []
3801
+ raise(Error::Interface, 'Too many credits'.freeze) if(scred > 100)
3802
+ scred.times do |cx|
3803
+ sug = para['ent-stat-%d-%d'.freeze % [ixr, cx + 1]]
3804
+ next if !sug || sug.empty?
3805
+ sugs << sug
3806
+ end
3807
+
3808
+ next if sugs.empty?
3809
+ stats << {
3810
+ 'name' => sname,
3811
+ 'value' => sval,
3812
+ 'credit' => sugs
3813
+ }
3814
+ end
3815
+ ent['stats'] = stats unless stats.empty?
3816
+
3817
+ # files
3818
+ files = []
3819
+ fcnt = para['ent-file-cnt'].to_i
3820
+ raise(Error::Interface, 'Too many files'.freeze) if(fcnt > 100)
3821
+ fcnt.times do |ix|
3822
+ ixr = ix + 1
3823
+ fnam = para['ent-file-%d-name' % ixr]
3824
+ fupl = para['ent-file-%d-file' % ixr]
3825
+ fnum = para['ent-file-%d-num' % ixr]
3826
+
3827
+ if fnum
3828
+ fnum, flog = fnum.split('-'.freeze).map do |xx|
3829
+ y = xx.to_i
3830
+ (y == 0) ? nil : y
3831
+ end
3832
+ else
3833
+ fnum = nil
3834
+ flog = nil
3835
+ end
3836
+
3837
+ if fupl && !fupl.empty?
3838
+ ftmp = api.tempfile
3839
+ IO::copy_stream(fupl[:tempfile], ftmp)
3840
+ fnam = fupl[:filename] if fnam.empty?
3841
+ fupl[:tempfile].close!
3842
+ files << {
3843
+ 'temp' => ftmp,
3844
+ 'name' => fnam
3845
+ }
3846
+ elsif fnam && !fnam.empty? && fnum && flog
3847
+ files << {
3848
+ 'num' => fnum,
3849
+ 'log' => flog,
3850
+ 'name' => fnam
3851
+ }
3852
+ end
3853
+ end
3854
+ ent['files'] = files unless files.empty?
3855
+
3856
+ return ent
3857
+ end # def _post_entry()
3858
+
3859
+
3860
+ ###############################################
3861
+ # Action edit
3862
+ #
3863
+ def _post_action(env, para)
3864
+
3865
+ # action object
3866
+ act = {}
3867
+
3868
+ # action
3869
+ anum = para['act-num'].to_i
3870
+ act['action'] = anum if anum != 0
3871
+
3872
+ # any edit?
3873
+ return anum unless para['act-ena'] == 'true'.freeze
3874
+
3875
+ # tasks
3876
+ tasks = []
3877
+ acnt = para['act-cnt'].to_i
3878
+ raise(Error::Interface, 'Too many tasks'.freeze) if(acnt > 100)
3879
+ acnt.times do |ix|
3880
+ tx = 'act-%d'.freeze % [ix + 1]
3881
+
3882
+ ug = para[tx + '-task'.freeze]
3883
+ title = para[tx + '-title'.freeze]
3884
+ status = (para[tx + '-status'] == 'true'.freeze) ? true : false
3885
+ flag = (para[tx + '-flag'] == 'true'.freeze) ? true : false
3886
+
3887
+ tstr = para[tx + '-time']
3888
+ time = _util_time_parse(env, tstr)
3889
+
3890
+ tags = []
3891
+ tcnt = para[tx + '-tag'.freeze].to_i
3892
+ raise(Error::Interface, 'Too many tags'.freeze) if (tcnt > 100)
3893
+ tcnt.times do |gx|
3894
+ tag = para[tx + '-tag-%d'.freeze % [gx + 1]]
3895
+ next if !tag || tag.empty?
3896
+ tags << tag
3897
+ end
3898
+ if tags.empty?
3899
+ tags = [ ICFS::TagNone ]
3900
+ else
3901
+ tags = tags.uniq.sort
3902
+ end
3903
+
3904
+ tk = {
3905
+ 'assigned' => ug,
3906
+ 'title' => title,
3907
+ 'time' => time,
3908
+ 'status' => status,
3909
+ 'flag' => flag,
3910
+ 'tags' => tags
3911
+ }
3912
+ tasks << tk
3913
+ end
3914
+ act['tasks'] = tasks
3915
+
3916
+ return act
3917
+ end # def _post_action()
3918
+
3919
+
3920
+ ###############################################
3921
+ # Index edit
3922
+ #
3923
+ def _post_index(env, para)
3924
+
3925
+ # index object
3926
+ idx = {}
3927
+
3928
+ # number
3929
+ xnum = para['idx-num'].to_i
3930
+ idx['index'] = xnum if xnum != 0
3931
+
3932
+ # title & content
3933
+ idx['title'] = para['idx-title']
3934
+ idx['content'] = para['idx-content']
3935
+
3936
+ # tags
3937
+ tags = []
3938
+ tcnt = para['idx-tag'].to_i
3939
+ raise(Error::Interface, 'Too many tags'.freeze) if(tcnt > 100)
3940
+ tcnt.times do |ix|
3941
+ tx = 'idx-tag-%d'.freeze % [ix + 1]
3942
+ tag = para[tx]
3943
+ tags << tag unless( !tag | tag.empty? )
3944
+ end
3945
+ idx['tags'] = tags.uniq.sort unless tags.empty?
3946
+
3947
+ return idx
3948
+ end # def _post_index()
3949
+
3950
+
3951
+ ###########################################################
3952
+ # Links
3953
+ ###########################################################
3954
+
3955
+ ###############################################
3956
+ # Link to info page
3957
+ #
3958
+ def _a_info(env, txt)
3959
+ '<a href="%s/info">%s</a>'.freeze % [
3960
+ env['SCRIPT_NAME'],
3961
+ Rack::Utils.escape_html(txt)
3962
+ ]
3963
+ end
3964
+
3965
+
3966
+ ###############################################
3967
+ # Link to Case search
3968
+ def _a_case_search(env, query, txt)
3969
+ '<a href="%s/case_search%s">%s</a>'.freeze % [
3970
+ env['SCRIPT_NAME'],
3971
+ _util_query(query),
3972
+ Rack::Utils.escape_html(txt)
3973
+ ]
3974
+ end
3975
+
3976
+
3977
+ ###############################################
3978
+ # Link to Entry search
3979
+ #
3980
+ def _a_entry_search(env, query, txt)
3981
+ '<a href="%s/entry_search%s">%s</a>'.freeze % [
3982
+ env['SCRIPT_NAME'],
3983
+ _util_query(query),
3984
+ Rack::Utils.escape_html(txt)
3985
+ ]
3986
+ end
3987
+
3988
+
3989
+ ###############################################
3990
+ # Link to Log search
3991
+ #
3992
+ def _a_log_search(env, query, txt)
3993
+ '<a href="%s/log_search%s">%s</a>'.freeze % [
3994
+ env['SCRIPT_NAME'],
3995
+ _util_query(query),
3996
+ Rack::Utils.escape_html(txt)
3997
+ ]
3998
+ end
3999
+
4000
+
4001
+ ###############################################
4002
+ # Link to Action search
4003
+ #
4004
+ def _a_action_search(env, query, txt)
4005
+ '<a href="%s/action_search%s">%s</a>'.freeze % [
4006
+ env['SCRIPT_NAME'],
4007
+ _util_query(query),
4008
+ Rack::Utils.escape_html(txt)
4009
+ ]
4010
+ end
4011
+
4012
+
4013
+ ###############################################
4014
+ # Link to Index search
4015
+ #
4016
+ def _a_index_search(env, query, txt)
4017
+ '<a href="%s/index_search%s">%s</a>'.freeze % [
4018
+ env['SCRIPT_NAME'],
4019
+ _util_query(query),
4020
+ Rack::Utils.escape_html(txt)
4021
+ ]
4022
+ end
4023
+
4024
+
4025
+ ###############################################
4026
+ # Link to stats search
4027
+ #
4028
+ def _a_stats(env, query, txt)
4029
+ '<a href="%s/stats%s">%s</a>'.freeze % [
4030
+ env['SCRIPT_NAME'],
4031
+ _util_query(query),
4032
+ Rack::Utils.escape_html(txt)
4033
+ ]
4034
+ end
4035
+
4036
+
4037
+ ###############################################
4038
+ # Link to case tags
4039
+ def _a_case_tags(env, query, txt)
4040
+ '<a href="%s/case_tags%s">%s</a>'.freeze % [
4041
+ env['SCRIPT_NAME'],
4042
+ _util_query(query),
4043
+ Rack::Utils.escape_html(txt)
4044
+ ]
4045
+ end
4046
+
4047
+
4048
+ ###############################################
4049
+ # Link to entry tags
4050
+ #
4051
+ def _a_entry_tags(env, query, txt)
4052
+ '<a href="%s/entry_tags/%s">%s</a>'.freeze % [
4053
+ env['SCRIPT_NAME'],
4054
+ _util_query(query),
4055
+ Rack::Utils.escape_html(txt),
4056
+ ]
4057
+ end # def _a_entry_tags()
4058
+
4059
+
4060
+ ###############################################
4061
+ # Link to action tags
4062
+ def _a_action_tags(env, query, txt)
4063
+ '<a href="%s/action_tags%s">%s</a>'.freeze % [
4064
+ env['SCRIPT_NAME'],
4065
+ _util_query(query),
4066
+ Rack::Utils.escape_html(txt)
4067
+ ]
4068
+ end # def _a_action_tags()
4069
+
4070
+
4071
+ ###############################################
4072
+ # Link to action tags
4073
+ def _a_index_tags(env, query, txt)
4074
+ '<a href="%s/index_tags/%s">%s</a>'.freeze % [
4075
+ env['SCRIPT_NAME'],
4076
+ _util_query(query),
4077
+ Rack::Utils.escape_html(txt)
4078
+ ]
4079
+ end # def _a_index_tags()
4080
+
4081
+
4082
+ ###############################################
4083
+ # Link to create a case
4084
+ #
4085
+ def _a_case_create(env, tid, txt)
4086
+ '<a href="%s/case_create/%s">%s</a>'.freeze % [
4087
+ env['SCRIPT_NAME'],
4088
+ Rack::Utils.escape(tid),
4089
+ Rack::Utils.escape_html(txt),
4090
+ ]
4091
+ end
4092
+
4093
+
4094
+ ###############################################
4095
+ # Link to Case edit
4096
+ #
4097
+ def _a_case_edit(env, cid, txt)
4098
+ '<a href="%s/case_edit/%s">%s</a>'.freeze % [
4099
+ env['SCRIPT_NAME'],
4100
+ Rack::Utils.escape(cid),
4101
+ Rack::Utils.escape_html(txt)
4102
+ ]
4103
+ end
4104
+
4105
+
4106
+ ###############################################
4107
+ # Link to Entry edit
4108
+ #
4109
+ def _a_entry_edit(env, cid, enum, anum, txt)
4110
+ '<a href="%s/entry_edit/%s/%d/%d">%s</a>'.freeze % [
4111
+ env['SCRIPT_NAME'],
4112
+ Rack::Utils.escape(cid),
4113
+ enum, anum,
4114
+ Rack::Utils.escape_html(txt)
4115
+ ]
4116
+ end
4117
+
4118
+
4119
+ ###############################################
4120
+ # Link to Index edit
4121
+ #
4122
+ def _a_index_edit(env, cid, xnum, txt)
4123
+ '<a href="%s/index_edit/%s/%d">%s</a>'.freeze % [
4124
+ env['SCRIPT_NAME'],
4125
+ Rack::Utils.escape(cid),
4126
+ xnum,
4127
+ Rack::Utils.escape_html(txt)
4128
+ ]
4129
+ end
4130
+
4131
+
4132
+ ###############################################
4133
+ # Link to Home
4134
+ #
4135
+ def _a_home(env, txt)
4136
+ '<a href="%s/home">%s</a>'.freeze % [
4137
+ env['SCRIPT_NAME'],
4138
+ Rack::Utils.escape_html(txt)
4139
+ ]
4140
+ end
4141
+
4142
+
4143
+ ###############################################
4144
+ # Link to Case
4145
+ #
4146
+ def _a_case(env, cid, lnum, txt)
4147
+ '<a href="%s/case/%s/%d">%s</a>'.freeze % [
4148
+ env['SCRIPT_NAME'],
4149
+ Rack::Utils.escape(cid),
4150
+ lnum,
4151
+ Rack::Utils.escape_html(txt)
4152
+ ]
4153
+ end
4154
+
4155
+
4156
+ ###############################################
4157
+ # Link to an entry
4158
+ def _a_entry(env, cid, enum, lnum, txt)
4159
+ '<a href="%s/entry/%s/%d/%d">%s</a>'.freeze % [
4160
+ env['SCRIPT_NAME'],
4161
+ Rack::Utils.escape(cid),
4162
+ enum,
4163
+ lnum,
4164
+ Rack::Utils.escape_html(txt)
4165
+ ]
4166
+ end
4167
+
4168
+
4169
+ ###############################################
4170
+ # Link to a Log
4171
+ #
4172
+ def _a_log(env, cid, lnum, txt)
4173
+ '<a href="%s/log/%s/%d">%s</a>'.freeze % [
4174
+ env['SCRIPT_NAME'],
4175
+ Rack::Utils.escape(cid),
4176
+ lnum,
4177
+ Rack::Utils.escape_html(txt)
4178
+ ]
4179
+ end
4180
+
4181
+
4182
+ ###############################################
4183
+ # Link to an Action
4184
+ #
4185
+ def _a_action(env, cid, anum, lnum, txt)
4186
+ '<a href="%s/action/%s/%d/%d">%s</a>'.freeze % [
4187
+ env['SCRIPT_NAME'],
4188
+ Rack::Utils.escape(cid),
4189
+ anum,
4190
+ lnum,
4191
+ Rack::Utils.escape_html(txt)
4192
+ ]
4193
+ end
4194
+
4195
+
4196
+ ###############################################
4197
+ # Link to an Index
4198
+ #
4199
+ def _a_index(env, cid, xnum, lnum, txt)
4200
+ '<a href="%s/index/%s/%d/%d">%s</a>'.freeze % [
4201
+ env['SCRIPT_NAME'],
4202
+ Rack::Utils.escape(cid),
4203
+ xnum,
4204
+ lnum,
4205
+ Rack::Utils.escape_html(txt)
4206
+ ]
4207
+ end
4208
+
4209
+
4210
+ ###############################################
4211
+ # Link to a File
4212
+ #
4213
+ def _a_file(env, cid, enum, lnum, fnum, fname, txt)
4214
+ '<a href="%s/file/%s/%d-%d-%d-%s">%s</a>'.freeze % [
4215
+ env['SCRIPT_NAME'],
4216
+ Rack::Utils.escape(cid),
4217
+ enum, lnum, fnum, Rack::Utils.escape(fname),
4218
+ Rack::Utils.escape_html(txt)
4219
+ ]
4220
+ end
4221
+
4222
+
4223
+ ###########################################################
4224
+ # Helper methods
4225
+ ###########################################################
4226
+
4227
+
4228
+ ###############################################
4229
+ # Require a GET HTTP method
4230
+ #
4231
+ def _verb_get(env)
4232
+ if env['REQUEST_METHOD'] != 'GET'.freeze
4233
+ raise(Error::Interface, 'Only GET method allowed'.freeze)
4234
+ end
4235
+ end # def _verb_get()
4236
+
4237
+
4238
+ ###############################################
4239
+ # Require a GET or POST method
4240
+ #
4241
+ def _verb_getpost(env)
4242
+ if env['REQUEST_METHOD'] != 'GET'.freeze &&
4243
+ env['REQUEST_METHOD'] != 'POST'.freeze
4244
+ raise(Error::Interface, 'Only GET or POST method allowed'.freeze)
4245
+ end
4246
+ end # def _verb_getpost()
4247
+
4248
+
4249
+ ###############################################
4250
+ # Process the POST
4251
+ #
4252
+ def _util_post(env)
4253
+ rck = Rack::Request.new(env)
4254
+ para = rck.POST
4255
+ para.each do |key, val|
4256
+ val.force_encoding('utf-8'.freeze) if val.is_a?(String)
4257
+ end
4258
+ return para
4259
+ end # def _util_post()
4260
+
4261
+
4262
+ ###############################################
4263
+ # Get the case
4264
+ #
4265
+ def _util_case(env)
4266
+ cmps = env['icfs.cmps']
4267
+ if cmps.size < 2 || cmps[1].empty?
4268
+ raise(Error::NotFound, 'No case specified in the URL'.freeze)
4269
+ end
4270
+ cid = Rack::Utils.unescape(cmps[1])
4271
+ Validate.validate(cid, 'case'.freeze, Items::FieldCaseid)
4272
+ env['icfs.cid'] = cid
4273
+ return cid
4274
+ end # def _util_case()
4275
+
4276
+
4277
+ ###############################################
4278
+ # Get a number from the URL
4279
+ #
4280
+ def _util_num(env, loc)
4281
+ cmps = env['icfs.cmps']
4282
+ (cmps.size < (loc+1) || cmps[loc].empty?) ? 0 : cmps[loc].to_i
4283
+ end # def _util_num()
4284
+
4285
+
4286
+ ###############################################
4287
+ # Epoch time as local
4288
+ #
4289
+ def _util_time(env, time)
4290
+ Time.at(time).getlocal(env['icfs.tz']).strftime('%F %T'.freeze)
4291
+ end
4292
+
4293
+
4294
+ ###############################################
4295
+ # Parse a provided time string
4296
+ #
4297
+ def _util_time_parse(env, str)
4298
+ return nil if !str || !str.is_a?(String)
4299
+ val = str.strip
4300
+ now = Time.now.to_i
4301
+
4302
+ # empty string defaults to now
4303
+ return now if val.empty?
4304
+
4305
+ # default use parse
4306
+ ma = /[+-]\d{2}:\d{2}$/.match str
4307
+ tstr = ma ? str : str + env['icfs.tz']
4308
+ time = Time.parse(tstr).to_i
4309
+ rescue ArgumentError
4310
+ return nil
4311
+ end
4312
+
4313
+
4314
+ # Generate query string
4315
+ #
4316
+ def _util_query(query)
4317
+ if query
4318
+ qa = query.map do |key, val|
4319
+ '%s=%s'.freeze % [Rack::Utils.escape(key), Rack::Utils.escape(val)]
4320
+ end
4321
+ return '?'.freeze + qa.join('&amp;'.freeze)
4322
+ else
4323
+ return ''.freeze
4324
+ end
4325
+ end # def _util_query()
4326
+
4327
+
4328
+ ###############################################
4329
+ # Parse a query string
4330
+ #
4331
+ def _util_get_query(env, sup)
4332
+ rck = Rack::Request.new(env)
4333
+ para = rck.GET
4334
+ query = {}
4335
+
4336
+ # supported parameters
4337
+ sup.each do |txt, sym, proc|
4338
+ val = para[txt]
4339
+ next if !val || val.empty?
4340
+ case proc
4341
+ when :string
4342
+ query[sym] = val
4343
+ when :array
4344
+ query[sym] = val.split(','.freeze).map{|aa| aa.strip}
4345
+ when :boolean
4346
+ if val == 'true'
4347
+ query[sym] = true
4348
+ elsif val == 'false'
4349
+ query[sym] = false
4350
+ end
4351
+ when :integer
4352
+ query[sym] = val.to_i
4353
+ when :time
4354
+ if /^\s*\d+\s*$/.match(val)
4355
+ time = val.to_i
4356
+ else
4357
+ time = _util_time_parse(env, val)
4358
+ end
4359
+ query[sym] = time
4360
+ else
4361
+ raise NotImplementedError
4362
+ end
4363
+ end
4364
+
4365
+ return query
4366
+ end # def _util_get_query()
4367
+
4368
+
4369
+ ###########################################################
4370
+ # Rack HTTP responses
4371
+ ###########################################################
4372
+
4373
+ ###############################################
4374
+ # A Rack HTTP response
4375
+ #
4376
+ # @param env [Hash] Rack environment
4377
+ # @param res [Integer] the HTTP result
4378
+ # @param body [Sting] the HTML page body
4379
+ #
4380
+ def _resp(env, res, body)
4381
+ html = Page % [
4382
+ env['icfs.page'],
4383
+ @css,
4384
+ @js,
4385
+ body
4386
+ ]
4387
+ head = {
4388
+ 'Content-Type' => 'text/html; charset=utf-8'.freeze,
4389
+ 'Content-Length' => html.bytesize.to_s
4390
+ }
4391
+ return [res, head, [html]]
4392
+ end # def _resp()
4393
+
4394
+
4395
+ # HTML page
4396
+ Page = '<!DOCTYPE html>
4397
+ <html>
4398
+ <head>
4399
+ <title>%s</title>
4400
+ <link rel="stylesheet" type="text/css" href="%s">
4401
+ <script src="%s"></script>
4402
+ </head>
4403
+ <body>%s
4404
+ </body>
4405
+ </html>
4406
+ '.freeze
4407
+
4408
+
4409
+ ###############################################
4410
+ # Success
4411
+ def _resp_success(env, body)
4412
+ return _resp(env, 200, body)
4413
+ end # def _resp_success()
4414
+
4415
+
4416
+ ###############################################
4417
+ # Bad Request
4418
+ #
4419
+ def _resp_badreq(env, msg)
4420
+ body = _div_nav(env) + _div_msg(env, msg)
4421
+ return _resp(env, 400, body)
4422
+ end # def _resp_badreq()
4423
+
4424
+
4425
+ ###############################################
4426
+ # Conflict
4427
+ #
4428
+ def _resp_conflict(env, msg)
4429
+ body = _div_nav(env) + _div_msg(env, msg)
4430
+ return _resp(env, 409, body)
4431
+ end # def _resp_conflict()
4432
+
4433
+
4434
+ ###############################################
4435
+ # Not Found
4436
+ #
4437
+ def _resp_notfound(env, msg)
4438
+ body = _div_nav(env) + _div_msg(env, msg)
4439
+ return _resp(env, 404, body)
4440
+ end # def _resp_notfound()
4441
+
4442
+
4443
+ ###############################################
4444
+ # Forbidden
4445
+ #
4446
+ def _resp_forbidden(env, msg)
4447
+ body = _div_nav(env) + _div_msg(env, msg)
4448
+ return _resp(env, 403, body)
4449
+ end # def _resp_forbidden()
4450
+
4451
+
4452
+ end # module ICFS::Web::Client
4453
+
4454
+
4455
+ ##########################################################################
4456
+ # A file response object to use in Rack
4457
+ #
4458
+ class FileResp
4459
+
4460
+ ###############################################
4461
+ # New response
4462
+ #
4463
+ def initialize(file)
4464
+ @file = file
4465
+ end
4466
+
4467
+
4468
+ # Chunk size of 64 kB
4469
+ #
4470
+ ChunkSize = 1024 * 64
4471
+
4472
+
4473
+ ###############################################
4474
+ # Provide body of the file in chunks
4475
+ #
4476
+ def each
4477
+ while str = @file.read(ChunkSize)
4478
+ yield str
4479
+ end
4480
+ end
4481
+
4482
+
4483
+ ###############################################
4484
+ # Close the file
4485
+ #
4486
+ def close
4487
+ if @file.respond_to?(:close!)
4488
+ @file.close!
4489
+ else
4490
+ @file.close
4491
+ end
4492
+ end
4493
+
4494
+ end # class ICFS::Web::FileResp
4495
+
4496
+ end # module ICFS::Web
4497
+
4498
+ end # module ICFS