sgfa 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/data/sgfa_web.css +37 -34
- data/lib/sgfa/binder.rb +205 -3
- data/lib/sgfa/binder_fs.rb +55 -20
- data/lib/sgfa/cli/binder.rb +248 -31
- data/lib/sgfa/cli/jacket.rb +206 -0
- data/lib/sgfa/entry.rb +96 -0
- data/lib/sgfa/error.rb +2 -2
- data/lib/sgfa/jacket.rb +314 -9
- data/lib/sgfa/jacket_fs.rb +18 -4
- data/lib/sgfa/state_fs.rb +33 -9
- data/lib/sgfa/store_s3.rb +119 -0
- data/lib/sgfa/web/base.rb +7 -0
- data/lib/sgfa/web/binder.rb +294 -85
- metadata +4 -3
data/lib/sgfa/jacket_fs.rb
CHANGED
@@ -31,14 +31,28 @@ class JacketFs < Jacket
|
|
31
31
|
#
|
32
32
|
# @param path [String] Path to create the jacket
|
33
33
|
# @param id_text [String] Text ID of the jacket
|
34
|
-
# @return [String] Hash ID of the jacket
|
35
34
|
# @raise [Error::Limits] if id_text exceeds allowed limits
|
36
35
|
# @raise [Error::Conflict] if path already exists
|
36
|
+
# @return [String] Hash ID of the jacket
|
37
37
|
def self.create(path, id_text)
|
38
|
+
id_hash = Digest::SHA256.new.update(id_text).hexdigest
|
39
|
+
JacketFs.create_raw(path, id_text, id_hash)
|
40
|
+
end # def self.create()
|
41
|
+
|
42
|
+
|
43
|
+
#####################################
|
44
|
+
# Create a jacket from backup
|
45
|
+
#
|
46
|
+
# @param path (see create)
|
47
|
+
# @param id_text (see create)
|
48
|
+
# @param id_hash [String] Hash ID of the jacket
|
49
|
+
# @return [String] Hash ID of the jacket
|
50
|
+
# @raise [Error::Limits] if id_text exceeds allowed limits
|
51
|
+
# @raise [Error::Conflict] if path already exists
|
52
|
+
# @note You probably want to use {create}, not this method
|
53
|
+
def self.create_raw(path, id_text, id_hash)
|
38
54
|
Jacket.limits_id(id_text)
|
39
55
|
|
40
|
-
# info
|
41
|
-
id_hash = Digest::SHA256.new.update(id_text).hexdigest
|
42
56
|
info = {
|
43
57
|
'sgfa_jacket_ver' => 1,
|
44
58
|
'id_hash' => id_hash,
|
@@ -60,7 +74,7 @@ class JacketFs < Jacket
|
|
60
74
|
StoreFs.create(File.join(path, 'store'))
|
61
75
|
|
62
76
|
return id_hash
|
63
|
-
end # def self.
|
77
|
+
end # def self.create_raw()
|
64
78
|
|
65
79
|
|
66
80
|
#####################################
|
data/lib/sgfa/state_fs.rb
CHANGED
@@ -190,6 +190,15 @@ class StateFs
|
|
190
190
|
# @raise [Error::Sanity] if state not open
|
191
191
|
# @raise [Error::Corrupt] if tag list is missing
|
192
192
|
def list
|
193
|
+
tagh, max = _list
|
194
|
+
return tagh.keys
|
195
|
+
end # def list
|
196
|
+
|
197
|
+
|
198
|
+
#####################################
|
199
|
+
# Read raw tag list
|
200
|
+
# return [Array] \[max_tagn, tag_hash\]
|
201
|
+
def _list
|
193
202
|
raise Error::Sanity, 'State not open' if !@path
|
194
203
|
|
195
204
|
ftn = File.join(@path, TagList)
|
@@ -198,8 +207,20 @@ class StateFs
|
|
198
207
|
rescue Errno::ENOENT
|
199
208
|
raise Error::Corrupt, 'Unable to read tag list'
|
200
209
|
end
|
201
|
-
|
202
|
-
|
210
|
+
|
211
|
+
tagh = {}
|
212
|
+
max = 0
|
213
|
+
txt.lines.each do |ln|
|
214
|
+
ma = /^(\d{9}) (.*)$/.match(ln)
|
215
|
+
raise Error::Corrupt, 'Tag list format incorrect' if !ma
|
216
|
+
num = ma[1].to_i
|
217
|
+
tagh[ma[2]] = num
|
218
|
+
max = num if num > max
|
219
|
+
end
|
220
|
+
|
221
|
+
return tagh, max
|
222
|
+
end # _list
|
223
|
+
private :_list
|
203
224
|
|
204
225
|
|
205
226
|
#####################################
|
@@ -216,11 +237,13 @@ class StateFs
|
|
216
237
|
def tag(name, offs, max)
|
217
238
|
raise Error::Sanity, 'State not open' if !@path
|
218
239
|
|
219
|
-
|
240
|
+
tagh, tagm = _list
|
241
|
+
raise Error::NonExistent, 'Tag does not exist' if !tagh[name]
|
242
|
+
fn = File.join(@path, tagh[name].to_s)
|
220
243
|
begin
|
221
244
|
fi = File.open(fn, 'rb')
|
222
245
|
rescue Errno::ENOENT
|
223
|
-
raise Error::
|
246
|
+
raise Error::Corrupt, 'Tag file missing'
|
224
247
|
end
|
225
248
|
|
226
249
|
ents = []
|
@@ -257,8 +280,7 @@ class StateFs
|
|
257
280
|
|
258
281
|
# read list of tags
|
259
282
|
changed = false
|
260
|
-
thash =
|
261
|
-
self.list.each{|tag| thash[tag] = true }
|
283
|
+
thash, max = _list
|
262
284
|
|
263
285
|
cng.each do |tag, hc|
|
264
286
|
cnt = 0
|
@@ -267,14 +289,15 @@ class StateFs
|
|
267
289
|
se = hc.to_a.select{|en, ti| ti }.sort{|aa, bb| aa[1] <=> bb[1] }
|
268
290
|
|
269
291
|
# files
|
270
|
-
fn = File.join(@path, tag)
|
271
292
|
if thash[tag]
|
293
|
+
fn = File.join(@path, thash[tag].to_s)
|
272
294
|
begin
|
273
295
|
oldf = File.open(fn, 'rb')
|
274
296
|
rescue Errno::ENOENT
|
275
297
|
raise Error::Corrupt, 'Existing tag is missing'
|
276
298
|
end
|
277
299
|
else
|
300
|
+
fn = File.join(@path, (max + 1).to_s)
|
278
301
|
oldf = nil
|
279
302
|
end
|
280
303
|
newf = Tempfile.new('state', @path, :encoding => 'ASCII-8BIT')
|
@@ -319,7 +342,8 @@ class StateFs
|
|
319
342
|
else
|
320
343
|
FileUtils.ln(newf.path, fn, :force => true)
|
321
344
|
if !oldf
|
322
|
-
|
345
|
+
max += 1
|
346
|
+
thash[tag] = max
|
323
347
|
changed = true
|
324
348
|
end
|
325
349
|
end
|
@@ -330,7 +354,7 @@ class StateFs
|
|
330
354
|
if changed
|
331
355
|
fnl = File.join(@path, TagList)
|
332
356
|
File.open(fnl, 'w', :encoding => 'utf-8') do |fi|
|
333
|
-
thash.
|
357
|
+
thash.each{|tag, num| fi.puts '%09d %s' % [num, tag] }
|
334
358
|
end
|
335
359
|
end
|
336
360
|
|
@@ -0,0 +1,119 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Jacket store using AWS S3.
|
4
|
+
#
|
5
|
+
# Copyright (C) 2015 by Graham A. Field.
|
6
|
+
#
|
7
|
+
# See LICENSE.txt for licensing information.
|
8
|
+
#
|
9
|
+
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
|
+
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
|
+
|
12
|
+
require 'aws-sdk'
|
13
|
+
require 'tempfile'
|
14
|
+
|
15
|
+
require_relative 'error'
|
16
|
+
|
17
|
+
module Sgfa
|
18
|
+
|
19
|
+
#####################################################################
|
20
|
+
# Stores copies of {History}, {Entry}, and attached files that are
|
21
|
+
# in a {Jacket} using AWS S3.
|
22
|
+
#
|
23
|
+
class StoreS3
|
24
|
+
|
25
|
+
#####################################
|
26
|
+
# Open the store
|
27
|
+
#
|
28
|
+
# @param client [AWS::S3::Client] The configured S3 client
|
29
|
+
# @param bucket [String] The bucket name
|
30
|
+
# @param prefix [String] Prefix to use for object keys
|
31
|
+
def open(client, bucket, prefix=nil)
|
32
|
+
@s3 = client
|
33
|
+
@bck = bucket
|
34
|
+
@pre = prefix || ''
|
35
|
+
end # def open()
|
36
|
+
|
37
|
+
|
38
|
+
#####################################
|
39
|
+
# Close the store
|
40
|
+
def close()
|
41
|
+
true
|
42
|
+
end # def close()
|
43
|
+
|
44
|
+
|
45
|
+
#####################################
|
46
|
+
# Get a temp file
|
47
|
+
def temp
|
48
|
+
Tempfile.new('blob', Dir.tmpdir, :encoding => 'utf-8')
|
49
|
+
end # def temp
|
50
|
+
|
51
|
+
|
52
|
+
#####################################
|
53
|
+
# Get key
|
54
|
+
def _key(type, item)
|
55
|
+
case type
|
56
|
+
when :entry then ext = '-e'
|
57
|
+
when :history then ext = '-h'
|
58
|
+
when :file then ext = '-f'
|
59
|
+
else raise NotImplementedError, 'Invalid item type'
|
60
|
+
end
|
61
|
+
key = @pre + item + ext
|
62
|
+
return key
|
63
|
+
end # def _key()
|
64
|
+
private :_key
|
65
|
+
|
66
|
+
|
67
|
+
#####################################
|
68
|
+
# Read an item from the store
|
69
|
+
def read(type, item)
|
70
|
+
key = _key(type, item)
|
71
|
+
fi = temp
|
72
|
+
fi.set_encoding(Encoding::ASCII_8BIT)
|
73
|
+
@s3.get_object( bucket: @bck, key: key, response_target: fi )
|
74
|
+
fi.rewind
|
75
|
+
return fi
|
76
|
+
rescue Aws::S3::Errors::NoSuchKey
|
77
|
+
return false
|
78
|
+
end # def read()
|
79
|
+
|
80
|
+
|
81
|
+
#####################################
|
82
|
+
# Store an item
|
83
|
+
def write(type, item, cont)
|
84
|
+
key = _key(type, item)
|
85
|
+
cont.rewind
|
86
|
+
@s3.put_object( bucket: @bck, key: key, body: cont )
|
87
|
+
|
88
|
+
if cont.respond_to?( :close! )
|
89
|
+
cont.close!
|
90
|
+
else
|
91
|
+
cont.close
|
92
|
+
end
|
93
|
+
end # def write()
|
94
|
+
|
95
|
+
|
96
|
+
#####################################
|
97
|
+
# Delete
|
98
|
+
def delete(type, item)
|
99
|
+
key = _key(type, item)
|
100
|
+
@s3.delete_object( bucket: @bck, key: key )
|
101
|
+
return true
|
102
|
+
rescue Aws::S3::Errors::NoSuchKey
|
103
|
+
return false
|
104
|
+
end # def delete()
|
105
|
+
|
106
|
+
|
107
|
+
#####################################
|
108
|
+
# Get size of an item
|
109
|
+
def size(type, item)
|
110
|
+
key = _key(type, item)
|
111
|
+
resp = @s3.head_object( bucket: @bck, key: key )
|
112
|
+
return resp.content_length
|
113
|
+
rescue Aws::S3::Errors::NotFound
|
114
|
+
return false
|
115
|
+
end # def size()
|
116
|
+
|
117
|
+
end # class StoreS3
|
118
|
+
|
119
|
+
end # module Sgfa
|
data/lib/sgfa/web/base.rb
CHANGED
@@ -169,6 +169,13 @@ class Base
|
|
169
169
|
end # def _escape()
|
170
170
|
|
171
171
|
|
172
|
+
#####################################
|
173
|
+
# Escape URL using only percent encoding
|
174
|
+
def _escape_path(txt)
|
175
|
+
Rack::Utils.escape_path(txt)
|
176
|
+
end # def _escape_path():
|
177
|
+
|
178
|
+
|
172
179
|
##########################################
|
173
180
|
# Unescape URL
|
174
181
|
def _escape_un(txt)
|
data/lib/sgfa/web/binder.rb
CHANGED
@@ -22,7 +22,6 @@ module Web
|
|
22
22
|
#####################################################################
|
23
23
|
# Binder web interface
|
24
24
|
#
|
25
|
-
# @todo Add a docket view
|
26
25
|
class Binder < Base
|
27
26
|
|
28
27
|
#####################################
|
@@ -139,6 +138,7 @@ class Binder < Base
|
|
139
138
|
when '_log'; return _get_log(env, path)
|
140
139
|
when '_list'; return _get_list(env, path)
|
141
140
|
when '_info'; return _get_info(env, path)
|
141
|
+
when '_docket'; return _get_docket(env, path)
|
142
142
|
else; return
|
143
143
|
end
|
144
144
|
|
@@ -170,6 +170,7 @@ class Binder < Base
|
|
170
170
|
|
171
171
|
NavBarJacket = [
|
172
172
|
['Tag', '_tag'],
|
173
|
+
['Docket', '_docket'],
|
173
174
|
['List', '_list'],
|
174
175
|
['Entry', nil],
|
175
176
|
['Edit', '_edit'],
|
@@ -262,6 +263,18 @@ class Binder < Base
|
|
262
263
|
end # def _link_tag()
|
263
264
|
|
264
265
|
|
266
|
+
#####################################
|
267
|
+
# Link to a docket
|
268
|
+
def _link_dock(env, tag, disp)
|
269
|
+
"<a href='%s/%s/_docket/%s'>%s</a>" % [
|
270
|
+
env['SCRIPT_NAME'],
|
271
|
+
env['sgfa.jacket.url'],
|
272
|
+
_escape(tag),
|
273
|
+
disp
|
274
|
+
]
|
275
|
+
end # def _link_dock()
|
276
|
+
|
277
|
+
|
265
278
|
#####################################
|
266
279
|
# Link to a tag prefix
|
267
280
|
def _link_prefix(env, pre, disp)
|
@@ -283,7 +296,7 @@ class Binder < Base
|
|
283
296
|
enum,
|
284
297
|
anum,
|
285
298
|
hnum,
|
286
|
-
|
299
|
+
_escape_path(name),
|
287
300
|
disp
|
288
301
|
]
|
289
302
|
end # def _link_attach()
|
@@ -300,7 +313,7 @@ class Binder < Base
|
|
300
313
|
|
301
314
|
JacketsForm =
|
302
315
|
"\n<hr>\n<form class='edit' method='post' action='%s' " +
|
303
|
-
"enctype='multipart/form-data'>\n" +
|
316
|
+
"enctype='multipart/form-data' accept-charset='utf-8'>\n" +
|
304
317
|
"<fieldset><legend>Create or Edit Jacket</legend>\n" +
|
305
318
|
"<label for='jacket'>Name:</label>" +
|
306
319
|
"<input class='jacket' name='jacket' type='text'><br>\n" +
|
@@ -389,7 +402,7 @@ class Binder < Base
|
|
389
402
|
|
390
403
|
UsersForm =
|
391
404
|
"\n<hr>\n<form class='edit' method='post' action='%s' " +
|
392
|
-
"enctype='multipart/form-data'>\n" +
|
405
|
+
"enctype='multipart/form-data' accept-charset='utf-8'>\n" +
|
393
406
|
"<fieldset><legend>Set User or Group Permissions</legend>\n" +
|
394
407
|
"<label for='user'>Name:</label>" +
|
395
408
|
"<input class='user' name='user' type='text'><br>\n" +
|
@@ -490,7 +503,7 @@ class Binder < Base
|
|
490
503
|
|
491
504
|
|
492
505
|
TagTable =
|
493
|
-
"<div class='
|
506
|
+
"<div class='tagname'>Tag: %s</div>\n" +
|
494
507
|
"<table class='list'>\n<tr>" +
|
495
508
|
"<th>Time</th><th>Title</th><th>Files</th><th>Tags</th><th>Edit</th>" +
|
496
509
|
"</tr>\n%s</table>\n"
|
@@ -528,7 +541,7 @@ class Binder < Base
|
|
528
541
|
html = 'No entries'
|
529
542
|
else
|
530
543
|
rows = ''
|
531
|
-
ents.reverse_each do |enum, rnum, time, title, tcnt, acnt|
|
544
|
+
ents.reverse_each do |enum, rnum, hnum, time, title, tcnt, acnt|
|
532
545
|
rows << TagRow % [
|
533
546
|
time.localtime.strftime("%F %T %z"),
|
534
547
|
_link_entry(env, enum, _escape_html(title)),
|
@@ -545,13 +558,87 @@ class Binder < Base
|
|
545
558
|
_escape(tag)
|
546
559
|
]
|
547
560
|
query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
|
548
|
-
pages = _link_pages(page, per, size, link, query)
|
561
|
+
pages = PageDiv % _link_pages(page, per, size, link, query)
|
549
562
|
|
550
563
|
env['sgfa.status'] = :ok
|
551
564
|
env['sgfa.html'] = html + pages
|
552
565
|
end # def _get_tag()
|
553
566
|
|
554
567
|
|
568
|
+
# each entry
|
569
|
+
DocketEach =
|
570
|
+
"<div class='title'>%s</div>
|
571
|
+
<div class='body'><em>Permission denied</em></div>
|
572
|
+
<div class='sidebar'>
|
573
|
+
<div class='time'>%s</div>
|
574
|
+
<div class='history'>Revision: %d %s<br>History: %s</div>
|
575
|
+
<div class='tags'>Number of tags: %d</div>
|
576
|
+
<div class='attach'>Number of attachments: %d</div>
|
577
|
+
</div>
|
578
|
+
<div class='hash'>Hash: Permission denied</div>
|
579
|
+
"
|
580
|
+
|
581
|
+
# Page division
|
582
|
+
PageDiv = "<div class='pages'>%s</div>\n"
|
583
|
+
|
584
|
+
|
585
|
+
#####################################
|
586
|
+
# Get a docket view
|
587
|
+
def _get_docket(env, path)
|
588
|
+
_navbar_jacket(env, 'Docket')
|
589
|
+
|
590
|
+
# tag, page, perpage
|
591
|
+
tag = path.empty? ? Jacket::TagAll : _escape_un(path.shift)
|
592
|
+
page = path.empty? ? 1 : path.shift.to_i
|
593
|
+
page = 1 if page == 0
|
594
|
+
rck = Rack::Request.new(env)
|
595
|
+
params = rck.GET
|
596
|
+
per = params['perpage'] ? params['perpage'].to_i : 0
|
597
|
+
per = PageSize if( per == 0 || per > PageSizeMax )
|
598
|
+
|
599
|
+
# get
|
600
|
+
tr = _trans(env)
|
601
|
+
size, ents = env['sgfa.binder'].read_tag(tr, tag, (page-1)*per, per,
|
602
|
+
raw: true)
|
603
|
+
html = "<div class='tagname'>Docket: %s</div>\n" % _escape_html(tag)
|
604
|
+
if ents.size == 0
|
605
|
+
html << 'No entries'
|
606
|
+
else
|
607
|
+
rows = ''
|
608
|
+
ents.reverse_each do |item|
|
609
|
+
if item.is_a?(Entry)
|
610
|
+
rows << _disp_entry(env, item, jacket: false, current: true)
|
611
|
+
else
|
612
|
+
enum, rnum, hnum, time, title, tcnt, acnt = item
|
613
|
+
if rnum == 1
|
614
|
+
prev = 'previous'
|
615
|
+
else
|
616
|
+
prev = _link_revision(env, enum, rnum-1, 'previous')
|
617
|
+
end
|
618
|
+
hist = _link_history(env, hnum, hnum.to_s)
|
619
|
+
rows << DocketEach % [
|
620
|
+
_escape_html(title),
|
621
|
+
time.localtime.strftime("%F %T %z"),
|
622
|
+
rnum, prev, hist, tcnt, acnt,
|
623
|
+
]
|
624
|
+
end
|
625
|
+
end
|
626
|
+
html << rows
|
627
|
+
end
|
628
|
+
|
629
|
+
link = '%s/%s/_docket/%s' % [
|
630
|
+
env['SCRIPT_NAME'],
|
631
|
+
env['sgfa.jacket.url'],
|
632
|
+
_escape(tag)
|
633
|
+
]
|
634
|
+
query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
|
635
|
+
pages = PageDiv % _link_pages(page, per, size, link, query)
|
636
|
+
|
637
|
+
env['sgfa.status'] = :ok
|
638
|
+
env['sgfa.html'] = html + pages
|
639
|
+
end # def _get_docket()
|
640
|
+
|
641
|
+
|
555
642
|
LogTable =
|
556
643
|
"<table class='list'>\n<tr>" +
|
557
644
|
"<th>History</th><th>Date/Time</th><th>User</th><th>Entries</th>" +
|
@@ -628,11 +715,11 @@ class Binder < Base
|
|
628
715
|
"<div class='tags'>%s</div>\n" +
|
629
716
|
"<div class='attach'>%s</div>\n" +
|
630
717
|
"</div>\n" +
|
631
|
-
"<div class='hash'>Hash: %s
|
718
|
+
"<div class='hash'>Hash: %s</div>\n"
|
632
719
|
|
633
720
|
#####################################
|
634
721
|
# Display an entry
|
635
|
-
def _disp_entry(env, ent)
|
722
|
+
def _disp_entry(env, ent, opts={})
|
636
723
|
|
637
724
|
enum = ent.entry
|
638
725
|
rnum = ent.revision
|
@@ -643,7 +730,7 @@ class Binder < Base
|
|
643
730
|
if tl.empty?
|
644
731
|
tags << "none\n"
|
645
732
|
else
|
646
|
-
tl.each do |tag|
|
733
|
+
tl.sort.each do |tag|
|
647
734
|
tags << _link_tag(env, tag, _escape_html(tag)) + "<br>\n"
|
648
735
|
end
|
649
736
|
end
|
@@ -663,17 +750,20 @@ class Binder < Base
|
|
663
750
|
else
|
664
751
|
prev = _link_revision(env, enum, rnum-1, 'previous')
|
665
752
|
end
|
666
|
-
curr = _link_entry(env, enum, 'current')
|
753
|
+
curr = opts[:current] ? '' : _link_entry(env, enum, 'current')
|
667
754
|
edit = _link_edit(env, enum, 'edit')
|
755
|
+
hist = _link_history(env, hnum, hnum.to_s)
|
756
|
+
hash = ent.hash
|
757
|
+
hash << ('<br>Jacket: %s' % ent.jacket) if opts[:jacket]
|
668
758
|
|
669
759
|
body = EntryDisp % [
|
670
760
|
_escape_html(ent.title),
|
671
761
|
_escape_html(ent.body),
|
672
762
|
ent.time.localtime.strftime('%F %T %z'),
|
673
|
-
rnum, prev, curr,
|
763
|
+
rnum, prev, curr, hist, edit,
|
674
764
|
tags,
|
675
765
|
att,
|
676
|
-
|
766
|
+
hash
|
677
767
|
]
|
678
768
|
|
679
769
|
return body
|
@@ -735,14 +825,14 @@ class Binder < Base
|
|
735
825
|
|
736
826
|
ListTable =
|
737
827
|
"<table class='list'>\n<tr>" +
|
738
|
-
"<th>Tag</th><th>Number</th></tr>\n" +
|
828
|
+
"<th>Tag</th><th>Number</th><th>View</th></tr>\n" +
|
739
829
|
"%s\n</table>\n"
|
740
830
|
|
741
831
|
ListPrefix =
|
742
|
-
"<tr><td class='prefix'>%s: prefix</td><td>%d tags</td></tr>\n"
|
832
|
+
"<tr><td class='prefix'>%s: prefix</td><td>%d tags</td><td></td></tr>\n"
|
743
833
|
|
744
834
|
ListTag =
|
745
|
-
"<tr><td class='tag'>%s</td><td>%d entries</td></tr>\n"
|
835
|
+
"<tr><td class='tag'>%s</td><td>%d entries</td><td>%s %s</td></tr>\n"
|
746
836
|
|
747
837
|
#####################################
|
748
838
|
# Get list of tags
|
@@ -780,8 +870,12 @@ class Binder < Base
|
|
780
870
|
end
|
781
871
|
regular.sort.each do |tag|
|
782
872
|
size, ents = bnd.read_tag(tr, tag, 0, 0)
|
783
|
-
rows << ListTag %
|
784
|
-
|
873
|
+
rows << ListTag % [
|
874
|
+
_link_tag(env, tag, _escape_html(tag)),
|
875
|
+
size,
|
876
|
+
_link_tag(env, tag, 'Tag'),
|
877
|
+
_link_dock(env, tag, 'Docket')
|
878
|
+
]
|
785
879
|
end
|
786
880
|
|
787
881
|
# list entire prefix
|
@@ -796,8 +890,12 @@ class Binder < Base
|
|
796
890
|
|
797
891
|
prefix[pre].sort.each do |tag|
|
798
892
|
size, ents = bnd.read_tag(tr, tag, 0, 0)
|
799
|
-
rows << ListTag %
|
800
|
-
|
893
|
+
rows << ListTag % [
|
894
|
+
_link_tag(env, tag, _escape_html(tag)),
|
895
|
+
size,
|
896
|
+
_link_tag(env, tag, 'Tag'),
|
897
|
+
_link_dock(env, tag, 'Docket')
|
898
|
+
]
|
801
899
|
end
|
802
900
|
end
|
803
901
|
|
@@ -880,58 +978,149 @@ class Binder < Base
|
|
880
978
|
|
881
979
|
|
882
980
|
EditForm =
|
883
|
-
"<form class='edit' method='post' action='%s/%s'
|
884
|
-
|
885
|
-
|
886
|
-
|
981
|
+
"<form class='edit' method='post' action='%s/%s' \
|
982
|
+
enctype='multipart/form-data' accept-charset='utf-8'>
|
983
|
+
<fieldset>
|
984
|
+
<legend>Basic Info</legend>
|
985
|
+
<label for='title'>Title:</label>
|
986
|
+
<input class='title' name='title' type='text' value='%s'><br>
|
987
|
+
<label for='time'>Time:</label>
|
988
|
+
<input name='time' type='text' value='%s'><br>
|
989
|
+
<label for='body'>Body:</label>
|
990
|
+
<textarea class='body' name='body'>%s</textarea>
|
991
|
+
</fieldset>
|
992
|
+
<fieldset>
|
993
|
+
<legend>Attachments</legend>
|
994
|
+
<table>
|
995
|
+
<tr><th>Name</th><th>Upload/Replace</th><th></th></tr>
|
996
|
+
%s</table>
|
997
|
+
</fieldset>
|
998
|
+
<fieldset>
|
999
|
+
<legend>Tags</legend>
|
1000
|
+
<table>
|
1001
|
+
%s</table>
|
1002
|
+
</fieldset>
|
1003
|
+
<input name='entry' type='hidden' value='%d'>
|
1004
|
+
<input name='revision' type='hidden' value='%d'>
|
1005
|
+
<input name='attcnt' id='attcnt' type='hidden' value='%d'>
|
1006
|
+
<input name='tagcnt' id='tagcnt' type='hidden' value='%d'>
|
1007
|
+
<input type='submit' name='save' value='Save Changes'>
|
1008
|
+
</form>
|
1009
|
+
<script>
|
1010
|
+
var addAttach = (function(){
|
1011
|
+
var count = %d;
|
1012
|
+
return function(elmt){
|
1013
|
+
count = count + 1;
|
1014
|
+
var row = document.createElement('tr');
|
1015
|
+
|
1016
|
+
var col1 = document.createElement('td');
|
1017
|
+
var inpt = document.createElement('input');
|
1018
|
+
inpt.setAttribute('type', 'text');
|
1019
|
+
inpt.setAttribute('name', 'attname' + count);
|
1020
|
+
inpt.setAttribute('class', 'attname');
|
1021
|
+
col1.appendChild(inpt);
|
1022
|
+
inpt = document.createElement('input');
|
1023
|
+
inpt.setAttribute('type', 'hidden');
|
1024
|
+
inpt.setAttribute('name', 'attnumb' + count);
|
1025
|
+
inpt.setAttribute('value', '0');
|
1026
|
+
col1.appendChild(inpt);
|
1027
|
+
row.appendChild(col1);
|
1028
|
+
|
1029
|
+
var col2 = document.createElement('td');
|
1030
|
+
inpt = document.createElement('input');
|
1031
|
+
inpt.setAttribute('type', 'file');
|
1032
|
+
inpt.setAttribute('name', 'attfile' + count);
|
1033
|
+
col2.appendChild(inpt);
|
1034
|
+
row.appendChild(col2);
|
1035
|
+
|
1036
|
+
var col3 = document.createElement('td');
|
1037
|
+
col3.appendChild(elmt.cloneNode(true));
|
1038
|
+
row.appendChild(col3);
|
1039
|
+
|
1040
|
+
var pcol = elmt.parentNode;
|
1041
|
+
pcol.removeChild(elmt);
|
1042
|
+
var prow = pcol.parentNode;
|
1043
|
+
var ptab = prow.parentNode;
|
1044
|
+
ptab.appendChild(row);
|
1045
|
+
|
1046
|
+
var acnt = document.getElementById('attcnt');
|
1047
|
+
acnt.setAttribute('value', count + 1);
|
1048
|
+
}
|
1049
|
+
})();
|
1050
|
+
var addTag = (function(){
|
1051
|
+
var count = %d;
|
1052
|
+
return function(elmt){
|
1053
|
+
count = count + 1;
|
1054
|
+
var row = document.createElement('tr');
|
1055
|
+
|
1056
|
+
var col1 = document.createElement('td');
|
1057
|
+
var pcol = elmt.parentNode;
|
1058
|
+
var prev = pcol.previousElementSibling.lastChild;
|
1059
|
+
var ptyp = prev.nodeName;
|
1060
|
+
if( ptyp == 'SELECT' ){
|
1061
|
+
var valu = prev.options[prev.selectedIndex].getAttribute('value');
|
1062
|
+
} else {
|
1063
|
+
var valu = '';
|
1064
|
+
}
|
1065
|
+
var inpt = document.createElement('input');
|
1066
|
+
inpt.setAttribute('name', 'tag' + count);
|
1067
|
+
inpt.setAttribute('class', 'tag');
|
1068
|
+
inpt.setAttribute('type', 'text');
|
1069
|
+
inpt.setAttribute('value', valu);
|
1070
|
+
col1.appendChild(inpt);
|
1071
|
+
row.appendChild(col1);
|
887
1072
|
|
888
|
-
|
1073
|
+
var col2 = document.createElement('td');
|
1074
|
+
var addb = document.getElementById('add_tag');
|
1075
|
+
addb.parentNode.removeChild(addb);
|
1076
|
+
col2.appendChild(addb);
|
1077
|
+
row.appendChild(col2);
|
889
1078
|
|
890
|
-
|
1079
|
+
var prow = pcol.parentNode;
|
1080
|
+
var ptab = prow.parentNode;
|
1081
|
+
ptab.appendChild(row);
|
891
1082
|
|
892
|
-
|
893
|
-
|
1083
|
+
var tcnt = document.getElementById('tagcnt');
|
1084
|
+
tcnt.setAttribute('value', count + 1);
|
1085
|
+
}
|
1086
|
+
})();
|
1087
|
+
</script>"
|
894
1088
|
|
895
|
-
"<label for='time'>Time:</label>" +
|
896
|
-
"<input name='time' type='text' value='%s'><br>\n" +
|
897
1089
|
|
898
|
-
|
899
|
-
|
1090
|
+
EditAttach =
|
1091
|
+
"<tr><td><input name='attname%d' class='attname' type='text' value='%s'>\
|
1092
|
+
<input name='attnumb%d' type='hidden' value='%d'>\
|
1093
|
+
</td><td><input name='attfile%d' type='file'></td><td>%s</td></tr>\n"
|
900
1094
|
|
901
|
-
"</fieldset>\n" +
|
902
|
-
"<fieldset><legend>Attachments</legend>\n%s</fieldset>\n" +
|
903
|
-
"<fieldset><legend>Tags</legend>\n%s</fieldset>\n" +
|
904
1095
|
|
905
|
-
|
906
|
-
|
1096
|
+
EditAttachButton =
|
1097
|
+
"<button type='button' class='add_row' id='add_attach' \
|
1098
|
+
onclick='addAttach(this)'>+</button>"
|
907
1099
|
|
908
|
-
EditFilePre =
|
909
|
-
"<table class='edit_file'>\n" +
|
910
|
-
"<tr><th>Name</th><th>Upload/Replace</th></tr>\n"
|
911
1100
|
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
"<td><input name='attfile%d' type='file'></td></tr>\n"
|
1101
|
+
EditTag =
|
1102
|
+
"<tr><td><input name='tag%d' class='tag' type='text' value='%s'></td>\
|
1103
|
+
<td>%s</td></tr>\n"
|
916
1104
|
|
917
|
-
EditFileCnt =
|
918
|
-
"</table>\n<input name='attcnt' type='hidden' value='%d'>\n"
|
919
1105
|
|
920
|
-
|
921
|
-
|
1106
|
+
EditTagSel =
|
1107
|
+
"<tr>
|
1108
|
+
<td>%s: <select name='pre%d'>
|
1109
|
+
<option value='%s: ' selected></option>
|
1110
|
+
%s</select></td>
|
1111
|
+
<td><button type='button' class='add_row' onclick='addTag(this)'>+\
|
1112
|
+
</button></td>
|
1113
|
+
</tr>\n"
|
922
1114
|
|
923
|
-
|
924
|
-
|
1115
|
+
|
1116
|
+
EditTagButton =
|
1117
|
+
"<button type='button' class='add_row' id='add_tag' onclick='addTag(this)'>+\
|
1118
|
+
</button>"
|
925
1119
|
|
926
|
-
EditTagSel =
|
927
|
-
"%s: <select name='tag%d'>" +
|
928
|
-
"<option value='' selected></option>%s</select><br>\n"
|
929
1120
|
|
930
1121
|
EditTagOpt =
|
931
|
-
|
1122
|
+
"<option value='%s: %s'>%s</option>\n"
|
932
1123
|
|
933
|
-
EditTagCnt =
|
934
|
-
"<input name='tagcnt' type='hidden' value='%d'>\n"
|
935
1124
|
|
936
1125
|
#####################################
|
937
1126
|
# Get edit form
|
@@ -940,6 +1129,7 @@ class Binder < Base
|
|
940
1129
|
|
941
1130
|
tr = _trans(env)
|
942
1131
|
|
1132
|
+
# get entry
|
943
1133
|
if path.empty?
|
944
1134
|
enum = 0
|
945
1135
|
rnum = 0
|
@@ -954,6 +1144,7 @@ class Binder < Base
|
|
954
1144
|
rnum = ent.revision
|
955
1145
|
end
|
956
1146
|
|
1147
|
+
# get tag list and prefixes
|
957
1148
|
lst = env['sgfa.binder'].read_list(tr)
|
958
1149
|
prefix = {}
|
959
1150
|
lst.each do |tag|
|
@@ -968,51 +1159,49 @@ class Binder < Base
|
|
968
1159
|
end
|
969
1160
|
end
|
970
1161
|
|
1162
|
+
# attachments
|
1163
|
+
atts = ''
|
1164
|
+
acnt = 0
|
1165
|
+
ent.attachments.each do |anum, hnum, name|
|
1166
|
+
atts << EditAttach % [acnt, _escape_html(name), acnt, anum, acnt, '']
|
1167
|
+
acnt += 1
|
1168
|
+
end
|
1169
|
+
atts << EditAttach % [acnt, '', acnt, 0, acnt, EditAttachButton]
|
1170
|
+
|
1171
|
+
# tags
|
971
1172
|
tags = ''
|
972
1173
|
cnt = 0
|
973
|
-
|
974
|
-
|
975
|
-
cnt += 1
|
976
|
-
end
|
977
|
-
prefix.each do |pre, lst|
|
1174
|
+
prefix.keys.sort.each do |pre|
|
1175
|
+
lst = prefix[pre]
|
978
1176
|
px = _escape_html(pre)
|
979
1177
|
opts = ''
|
980
|
-
lst.each do |post|
|
1178
|
+
lst.sort.each do |post|
|
981
1179
|
ex = _escape_html(post)
|
982
1180
|
opts << EditTagOpt % [px, ex, ex]
|
983
1181
|
end
|
984
|
-
tags << EditTagSel % [px, cnt, opts]
|
985
|
-
cnt += 1
|
986
|
-
end
|
987
|
-
5.times do |tg|
|
988
|
-
tags << EditTagNew % cnt
|
989
|
-
cnt += 1
|
990
|
-
end
|
991
|
-
tags << EditTagCnt % cnt
|
992
|
-
|
993
|
-
atts = "Attachments go here\n"
|
994
|
-
atts = EditFilePre.dup
|
995
|
-
cnt = 0
|
996
|
-
ent.attachments.each do |anum, hnum, name|
|
997
|
-
atts << EditFileEach % [cnt, _escape_html(name), cnt, anum, cnt]
|
1182
|
+
tags << EditTagSel % [px, cnt, px, opts]
|
998
1183
|
cnt += 1
|
999
1184
|
end
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1185
|
+
tcnt = 0
|
1186
|
+
ent.tags.sort.each do |tag|
|
1187
|
+
tags << EditTag % [tcnt, _escape_html(tag), '']
|
1188
|
+
tcnt += 1
|
1003
1189
|
end
|
1004
|
-
|
1190
|
+
tags << EditTag % [tcnt, '', EditTagButton]
|
1005
1191
|
|
1192
|
+
# the page
|
1006
1193
|
html = EditForm % [
|
1007
|
-
env['SCRIPT_NAME'], env['sgfa.jacket.url'],
|
1194
|
+
env['SCRIPT_NAME'], env['sgfa.jacket.url'],
|
1008
1195
|
_escape_html(ent.title), ent.time.localtime.strftime('%F %T %z'),
|
1009
|
-
_escape_html(ent.body), atts, tags,
|
1196
|
+
_escape_html(ent.body), atts, tags, enum, rnum, acnt+1, tcnt+1,
|
1197
|
+
acnt, tcnt
|
1010
1198
|
]
|
1011
1199
|
|
1012
1200
|
env['sgfa.status'] = :ok
|
1013
1201
|
env['sgfa.html'] = html
|
1014
1202
|
end # def _get_edit
|
1015
|
-
|
1203
|
+
|
1204
|
+
|
1016
1205
|
JacketPost = [
|
1017
1206
|
'entry',
|
1018
1207
|
'revision',
|
@@ -1029,6 +1218,13 @@ class Binder < Base
|
|
1029
1218
|
rck = Rack::Request.new(env)
|
1030
1219
|
params = rck.POST
|
1031
1220
|
|
1221
|
+
# stupid kludge to fix bad standards inplementation.
|
1222
|
+
# apparently browsers don't set the charset correctly so rack defaults
|
1223
|
+
# to ASCII-8BIT, and then everything dies when there's actually UTF-8
|
1224
|
+
# present. Since modern browsers actually send UTF-8 when we ask for
|
1225
|
+
# it, just force it to UTF-8 and hope for the best.
|
1226
|
+
params.each{ |key, val| val.force_encoding('utf-8') if val.is_a?(String) }
|
1227
|
+
|
1032
1228
|
# validate fields present
|
1033
1229
|
JacketPost.each do |fn|
|
1034
1230
|
next if params[fn]
|
@@ -1088,9 +1284,15 @@ class Binder < Base
|
|
1088
1284
|
|
1089
1285
|
# old file
|
1090
1286
|
else
|
1091
|
-
ent.rename(anum, name) if name != ''
|
1092
1287
|
ent.replace(anum, ftmp) if ftmp
|
1093
|
-
|
1288
|
+
if name != ''
|
1289
|
+
ent.rename(anum, name)
|
1290
|
+
elsif ftmp
|
1291
|
+
ent.rename(anum, file[:filename])
|
1292
|
+
else
|
1293
|
+
ent.delete(anum)
|
1294
|
+
end
|
1295
|
+
end
|
1094
1296
|
|
1095
1297
|
end
|
1096
1298
|
|
@@ -1121,6 +1323,13 @@ class Binder < Base
|
|
1121
1323
|
rck = Rack::Request.new(env)
|
1122
1324
|
params = rck.POST
|
1123
1325
|
|
1326
|
+
# stupid kludge to fix bad standards inplementation.
|
1327
|
+
# apparently browsers don't set the charset correctly so rack defaults
|
1328
|
+
# to ASCII-8BIT, and then everything dies when there's actually UTF-8
|
1329
|
+
# present. Since modern browsers actually send UTF-8 when we ask for
|
1330
|
+
# it, just force it to UTF-8 and hope for the best.
|
1331
|
+
params.each{ |key, val| val.force_encoding('utf-8') if val.is_a?(String) }
|
1332
|
+
|
1124
1333
|
tr = _trans(env)
|
1125
1334
|
tr[:title] = 'Test title'
|
1126
1335
|
tr[:body] = 'Test description of action'
|