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.
@@ -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'