sgfa 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.create()
77
+ end # def self.create_raw()
64
78
 
65
79
 
66
80
  #####################################
@@ -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
- return txt.lines.map{|tg| tg.chomp }
202
- end # def list
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
- fn = File.join(@path, name)
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::NonExistent, 'Tag does not exist'
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
- thash[tag] = true
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.each_key{|tag| fi.puts tag }
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
@@ -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)
@@ -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
- _escape(name),
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='title'>Tag: %s</div>\n" +
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<br>Jacket: %s</div>\n"
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, _link_history(env, hnum, hnum.to_s), edit,
763
+ rnum, prev, curr, hist, edit,
674
764
  tags,
675
765
  att,
676
- ent.hash, ent.jacket
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
- [_link_tag(env, tag, _escape_html(tag)), size]
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
- [_link_tag(env, tag, _escape_html(tag)), size]
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
- "enctype='multipart/form-data'>\n" +
885
- "<input name='entry' type='hidden' value='%d'>\n" +
886
- "<input name='revision' type='hidden' value='%d'>\n" +
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
- "<div class='edit'>\n" +
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
- "<fieldset><legend>Basic Info</legend>\n" +
1079
+ var prow = pcol.parentNode;
1080
+ var ptab = prow.parentNode;
1081
+ ptab.appendChild(row);
891
1082
 
892
- "<label for='title'>Title:</label>" +
893
- "<input class='title' name='title' type='text' value='%s'><br>\n" +
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
- "<label for='body'>Body:</label>" +
899
- "<textarea class='body' name='body'>%s</textarea>\n" +
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
- "<input type='submit' name='save' value='Save Changes'>\n" +
906
- "</div></form>\n"
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
- EditFileEach =
913
- "<tr><td><input name='attname%d' type='text' value='%s'>" +
914
- "<input name='attnumb%d' type='hidden' value='%d'></td>" +
915
- "<td><input name='attfile%d' type='file'></td></tr>\n"
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
- EditTagOld =
921
- "<input name='tag%d' type='text' value='%s'><br>\n"
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
- EditTagNew =
924
- "<input name='tag%d' type='text'><br>\n"
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
- "<option value='%s: %s'>%s</option>"
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
- ent.tags.each do |tag|
974
- tags << EditTagOld % [cnt, _escape_html(tag)]
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
- 5.times do |ix|
1001
- atts << EditFileEach % [cnt, '', cnt, 0, cnt]
1002
- cnt += 1
1185
+ tcnt = 0
1186
+ ent.tags.sort.each do |tag|
1187
+ tags << EditTag % [tcnt, _escape_html(tag), '']
1188
+ tcnt += 1
1003
1189
  end
1004
- atts << EditFileCnt % cnt
1190
+ tags << EditTag % [tcnt, '', EditTagButton]
1005
1191
 
1192
+ # the page
1006
1193
  html = EditForm % [
1007
- env['SCRIPT_NAME'], env['sgfa.jacket.url'], enum, rnum,
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
- end
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'