weft-qda 0.9.6 → 0.9.8

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.
Files changed (86) hide show
  1. data/lib/weft.rb +16 -1
  2. data/lib/weft/WEFT-VERSION-STRING.rb +1 -1
  3. data/lib/weft/application.rb +17 -74
  4. data/lib/weft/backend.rb +6 -32
  5. data/lib/weft/backend/sqlite.rb +222 -164
  6. data/lib/weft/backend/sqlite/category_tree.rb +52 -48
  7. data/lib/weft/backend/sqlite/database.rb +57 -0
  8. data/lib/weft/backend/sqlite/upgradeable.rb +7 -0
  9. data/lib/weft/broadcaster.rb +90 -0
  10. data/lib/weft/category.rb +139 -47
  11. data/lib/weft/codereview.rb +160 -0
  12. data/lib/weft/coding.rb +74 -23
  13. data/lib/weft/document.rb +23 -10
  14. data/lib/weft/exceptions.rb +10 -0
  15. data/lib/weft/filters.rb +47 -224
  16. data/lib/weft/filters/indexers.rb +137 -0
  17. data/lib/weft/filters/input.rb +118 -0
  18. data/lib/weft/filters/output.rb +101 -0
  19. data/lib/weft/filters/templates.rb +80 -0
  20. data/lib/weft/filters/win32backtick.rb +246 -0
  21. data/lib/weft/query.rb +169 -0
  22. data/lib/weft/wxgui.rb +349 -294
  23. data/lib/weft/wxgui/constants.rb +43 -0
  24. data/lib/weft/wxgui/controls.rb +6 -0
  25. data/lib/weft/wxgui/controls/category_dropdown.rb +192 -0
  26. data/lib/weft/wxgui/controls/category_tree.rb +314 -0
  27. data/lib/weft/wxgui/controls/document_list.rb +97 -0
  28. data/lib/weft/wxgui/controls/multitype_control.rb +37 -0
  29. data/lib/weft/wxgui/{inspectors → controls}/textcontrols.rb +235 -64
  30. data/lib/weft/wxgui/dialogs.rb +144 -41
  31. data/lib/weft/wxgui/error_handler.rb +116 -36
  32. data/lib/weft/wxgui/exceptions.rb +7 -0
  33. data/lib/weft/wxgui/inspectors.rb +61 -208
  34. data/lib/weft/wxgui/inspectors/category.rb +19 -16
  35. data/lib/weft/wxgui/inspectors/codereview.rb +90 -132
  36. data/lib/weft/wxgui/inspectors/document.rb +12 -8
  37. data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -56
  38. data/lib/weft/wxgui/inspectors/query.rb +284 -0
  39. data/lib/weft/wxgui/inspectors/script.rb +147 -23
  40. data/lib/weft/wxgui/lang/en.rb +69 -0
  41. data/lib/weft/wxgui/sidebar.rb +90 -432
  42. data/lib/weft/wxgui/utilities.rb +70 -91
  43. data/lib/weft/wxgui/workarea.rb +150 -43
  44. data/share/icons/category.ico +0 -0
  45. data/share/icons/category.xpm +109 -0
  46. data/share/icons/codereview.ico +0 -0
  47. data/share/icons/codereview.xpm +54 -0
  48. data/share/icons/d_and_c.xpm +126 -0
  49. data/share/icons/document.ico +0 -0
  50. data/share/icons/document.xpm +70 -0
  51. data/share/icons/project.ico +0 -0
  52. data/share/icons/query.ico +0 -0
  53. data/share/icons/query.xpm +56 -0
  54. data/{lib/weft/wxgui → share/icons}/search.xpm +0 -0
  55. data/share/icons/weft.ico +0 -0
  56. data/share/icons/weft.xpm +62 -0
  57. data/share/icons/weft16.ico +0 -0
  58. data/share/icons/weft32.ico +0 -0
  59. data/share/templates/category_plain.html +18 -0
  60. data/share/templates/codereview_plain.html +18 -0
  61. data/share/templates/document_plain.html +13 -0
  62. data/share/templates/document_plain.txt +7 -0
  63. data/test/001-document.rb +55 -36
  64. data/test/002-category.rb +81 -6
  65. data/test/003-code.rb +8 -4
  66. data/test/004-application.rb +13 -34
  67. data/test/005-query_review.rb +139 -0
  68. data/test/006-filters.rb +54 -42
  69. data/test/007-output_filters.rb +113 -0
  70. data/test/009a-backend_sqlite_basic.rb +95 -24
  71. data/test/009b-backend_sqlite_complex.rb +43 -62
  72. data/test/009c_backend_sqlite_bench.rb +5 -10
  73. data/test/053-doc_inspector.rb +46 -0
  74. data/test/055-query_window.rb +50 -0
  75. data/test/all-tests.rb +1 -0
  76. data/test/test-common.rb +19 -0
  77. data/test/testdata/empty.qdp +0 -0
  78. data/test/testdata/simple with space.pdf +0 -0
  79. data/test/testdata/simple.pdf +0 -0
  80. data/weft-qda.rb +40 -7
  81. metadata +74 -14
  82. data/lib/weft/wxgui/category.xpm +0 -26
  83. data/lib/weft/wxgui/document.xpm +0 -25
  84. data/lib/weft/wxgui/inspectors/search.rb +0 -265
  85. data/lib/weft/wxgui/mondrian.xpm +0 -44
  86. data/lib/weft/wxgui/weft16.xpm +0 -31
@@ -1,40 +1,27 @@
1
1
  require 'base64'
2
2
 
3
3
  module QDA::Backend::SQLite
4
- class CategoryTreeNode
5
- attr_reader :dbid, :children
6
- attr_accessor :parent, :name
7
- protected :parent=
8
-
4
+ class CategoryTreeNode < QDA::Category
9
5
  def initialize(parent, dbid, name)
10
- @parent, @dbid, @name = parent, dbid, name
6
+ self.name = name
7
+ @parent = parent
8
+ @dbid = dbid
11
9
  @children = []
12
10
  end
13
-
11
+
14
12
  def add(dbid, name)
15
- append( CategoryTreeNode.new(@dbid, dbid, name) )
13
+ add_child( CategoryTreeNode.new(@dbid, dbid, name) )[-1]
16
14
  end
17
-
18
- def append(child)
19
- child.parent = @dbid
20
- @children.push(child)[-1]
21
- end
22
-
23
- def remove(target)
24
- @children.delete_if { | c | c.dbid == target.dbid }
15
+
16
+ def move(parent)
17
+ @parent = parent.dbid
18
+ parent.add_child(self)
25
19
  end
26
-
27
- def like(other)
28
- name =~ /^#{other}/i
29
- end
30
-
20
+
31
21
  def to_s()
32
- "<CategoryTreeNode #{dbid} '#{name}' parent=#{parent}>"
33
- end
34
-
35
- def descendants()
36
- @children.map() { | c | [ c.dbid, c.descendants ] }.flatten
22
+ "<CTNode '#{name}' [#{dbid}]>"
37
23
  end
24
+ alias :inspect :to_s
38
25
  end
39
26
 
40
27
  class CategoryTree
@@ -49,20 +36,9 @@ module QDA::Backend::SQLite
49
36
  end
50
37
 
51
38
  def [](id)
52
- @table[id] or raise "Unknown id #{id.inspect}"
53
- end
54
-
55
- def find(path)
56
- points = path.split('/')
57
- scope = points[0].empty? ? @roots : @table.values
58
- points.delete('')
59
- while elem = points.shift
60
- scope = scope.find_all { | x | x.like(elem) }
61
- scope.map! { | x | x.children }.flatten! unless points.empty?
62
- end
63
- scope
39
+ @table[id] or raise QDA::NotFoundError.new("Unknown id #{id.inspect}")
64
40
  end
65
-
41
+
66
42
  def add(parentid, dbid, name)
67
43
  if parentid
68
44
  @table[dbid] = @table[parentid].add(dbid, name)
@@ -73,19 +49,47 @@ module QDA::Backend::SQLite
73
49
  end
74
50
 
75
51
  def remove(dbid)
76
- child = @table.delete(dbid)
77
- @table[child.parent].remove(child)
52
+ me = @table[dbid]
53
+ @table[me.parent].delete(me) if @table[me.parent]
54
+ dbids = []
55
+ me.children.each { | c | dbids += remove(c.dbid) }
56
+ @table.delete(dbid)
57
+ dbids.push(dbid)
58
+ dbids
59
+ end
60
+
61
+ def find(path)
62
+ paths = QDA::Category.parse_path(path)
63
+ # if it's a path with "/" at the beginning, search among roots
64
+ if paths[0].empty?
65
+ # maybe a named root e.g. '/CATEORIES'
66
+ fixed_roots = @roots.find_all { | x | x.name =~ /^#{paths[1]}/ }
67
+ # no root category matched, so search starting from all root nodes
68
+ if fixed_roots.empty?
69
+ scope = @roots
70
+ # root matched, and the path is so short it is the root category itself
71
+ elsif paths.length < 3
72
+ return fixed_roots
73
+ # restrict the search to only the matching root categories
74
+ else
75
+ path = paths[2 .. -1].map { | p | p.gsub(/\//, '//') }.join('/')
76
+ scope = fixed_roots
77
+ end
78
+ # if a relative path (not starting with '/'
79
+ else
80
+ scope = @table.values
81
+ end
82
+ scope.inject([]) { | a, x | a + x.find(path) }
78
83
  end
79
-
80
- def move(dbid, new_parent)
81
- child = @table[dbid]
82
- old_parent = child.parent
83
- @table[new_parent].append(child)
84
- @table[old_parent].remove(child)
84
+
85
+ def move(dbid, new_parentid)
86
+ movee = @table[dbid]
87
+ @table[movee.parent].delete(movee)
88
+ movee.move(@table[new_parentid])
85
89
  end
86
90
 
87
91
  def is_descendant?(ancestor, descendant)
88
- @table[ancestor].descendants.include?(descendant)
92
+ @table[ancestor].is_ancestor_of?(descendant)
89
93
  end
90
94
 
91
95
  def serialise()
@@ -0,0 +1,57 @@
1
+ # mainly just utility code to handle transparent choice of sqlite v2 or v3 -
2
+ # all deals diretly with ruby-sqlite module
3
+ module QDA::Backend::SQLite
4
+
5
+ # if working with sqlite v2 with the sqlite-ruby v2, we need a
6
+ # couple of compatibility tweaks.
7
+ if defined?(::SQLite)
8
+ SQLITE_DB_CLASS = ::SQLite::Database
9
+ # Ruby-SQLite3 statements have a close() method, but Ruby-SQLite
10
+ # v 2 don't - so we supply a dummy method for when using v2
11
+ class ::SQLite::Statement
12
+ def close(); end
13
+ end
14
+ # SQLite3 introduced this more ruby-ish notation
15
+ class ::SQLite::Database::FunctionProxy
16
+ alias :result= :set_result
17
+ end
18
+ elsif defined?(::SQLite3)
19
+ SQLITE_DB_CLASS = ::SQLite3::Database
20
+ else
21
+ raise LoadError, "No SQlite database class loaded"
22
+ end
23
+
24
+ class DatabaseBase < SQLITE_DB_CLASS
25
+ def initialize(*args)
26
+ super(*args)
27
+ self.results_as_hash = true
28
+ self.synchronous = 'OFF'
29
+ end
30
+
31
+ def date_freeze(date)
32
+ date ? date.strftime('%Y-%m-%d %H:%M:%S') : ''
33
+ end
34
+
35
+ def date_thaw(str)
36
+ return nil if str.empty?
37
+ return Time.local( *str.split(/[- :]/) )
38
+ end
39
+ end
40
+
41
+ # only for use in testing - this cannot be saved
42
+ class InMemoryDatabase < DatabaseBase
43
+ def initialize()
44
+ super(":memory:")
45
+ end
46
+ end
47
+
48
+ # A class derived from a standard SQLite database with the ability to save
49
+ # and revert changes. Under the hood, when a database file is requested to be
50
+ # opened, a temporary copy of that file is used, and all changes are only
51
+ # written on a call to save().
52
+ class FileDatabase < DatabaseBase
53
+ def initialize(file)
54
+ super(file)
55
+ end
56
+ end
57
+ end
@@ -27,6 +27,13 @@ module QDA::Backend::SQLite
27
27
  legacy_category_tree_storage()
28
28
  save_preference('LastModifiedVersion', WEFT_VERSION)
29
29
  end
30
+
31
+ begin
32
+ old_codes = get_root_category('CODES')
33
+ old_codes.name = 'CATEGORIES'
34
+ save_category(old_codes)
35
+ rescue QDA::NotFoundError
36
+ end
30
37
  end
31
38
 
32
39
  # This is a change from 0.9.5 -> 0.9.6; Category tree structure
@@ -0,0 +1,90 @@
1
+ module QDA
2
+
3
+ # Allows an object such as the global Wx App to broadcast messages
4
+ # about changes in the database and other application states that
5
+ # might require the child to update its appearance.
6
+ module Broadcaster
7
+ # intended to be module private variables to managed which widgets
8
+ # are subscribing to which events
9
+ attr_accessor :bc__subscribers, :bc__subscriptions
10
+
11
+
12
+ def event_types()
13
+ self.class.const_get('SUBSCRIBABLE_EVENTS')
14
+ end
15
+
16
+ # broadcast an event to all subscribers to event of type
17
+ # +event_type+, optionally including +content+
18
+ def broadcast(event_type, content = nil)
19
+ return unless bc__subscriptions
20
+ for subscriber in bc__subscriptions[event_type]
21
+ subscriber.notify(event_type, content)
22
+ end
23
+ end
24
+
25
+ # subscribe the widget +subscriber+ to receive notification of
26
+ # app event types +*events+ (which should be a set of symbols)
27
+ def add_subscriber(subscriber, *events)
28
+ reset_subscriptions if not bc__subscriptions
29
+ if events.length == 1 and events[0] == :all
30
+ events = self.event_types()
31
+ end
32
+
33
+ events.each do | e |
34
+ bc__subscriptions[e].push(subscriber)
35
+ end
36
+ end
37
+
38
+ # accepts a ruby item
39
+ def delete_subscriber(subscriber)
40
+ if subscriber.respond_to?(:associated_subscribers)
41
+ subscriber.associated_subscribers.each do | child |
42
+ delete_subscriber(child)
43
+ end
44
+ end
45
+ bc__subscribers.delete(subscriber)
46
+ bc__subscriptions.each_value do | subscriber_set |
47
+ subscriber_set.delete(subscriber)
48
+ end
49
+ end
50
+
51
+ def reset_subscriptions()
52
+ self.bc__subscribers = []
53
+ self.bc__subscriptions = Hash.new do | ev |
54
+ raise "Cannot subscribe to unknown event #{ev}"
55
+ end
56
+ event_types.each { | ev | self.bc__subscriptions[ev] = [] }
57
+ end
58
+ end
59
+
60
+ # Any gui element that may need to modify its appearance in response
61
+ # to application updates may include the subscriber module. Instances
62
+ # of the class may then call the +subscribe+ method to receive
63
+ # notification of changes.
64
+ module Subscriber
65
+ # receive notification that an event of type +ev+ has been called,
66
+ # optionally passing +content+ for additional information about the
67
+ # object the event concerned.
68
+ def notify(ev, content = nil)
69
+ receiver = "receive_#{ev}".intern
70
+ if respond_to?(receiver)
71
+ send(receiver, content)
72
+ else
73
+ warn "#{self} received unhandled event #{ev}"
74
+ end
75
+ end
76
+
77
+ # Subscribe this object to the events +events+, which should be a
78
+ # list of symbols. For example, to receive notification of category
79
+ # changes and additions, the recipient shoudl call
80
+ # subscribe(:category_changed, :category_added)
81
+ #
82
+ # For every event type to which a subscriber subscribes, it should
83
+ # implement a corresponding receive_xxx method, where xxx is the
84
+ # name of the event type. To act upon category changes, the
85
+ # subscriber should implement +receive_category_changed+
86
+ def subscribe(broadcaster, *events)
87
+ broadcaster.add_subscriber(self, *events)
88
+ end
89
+ end
90
+ end
@@ -2,39 +2,108 @@ require 'weft/coding'
2
2
 
3
3
  module QDA
4
4
  class Category
5
- attr_reader :children, :codes
6
- attr_accessor :dbid, :text, :name, :meta, :parent, :memo
7
-
8
- def initialize(name, parent = nil, memo = '')
9
- @name = name
10
- @memo = memo
11
- @parent = parent
5
+ attr_reader :children, :codes, :reader, :parent, :name
6
+ attr_accessor :dbid, :text, :meta, :memo
7
+
8
+ def Category.parse_path(path)
9
+ bits = path.scan(/(?:[^\/]|\/\/)+/)
10
+ bits.map! { | x | x.gsub(/\/\//, '/') }
11
+ bits.unshift('') if path =~ /\A\/(?!\/)/
12
+ bits
13
+ end
14
+
15
+ def initialize(a_name, a_parent = nil, a_memo = '')
12
16
  @children = []
13
- @codes = QDA::CodingTable.new()
14
- @parent.add_child(self) if @parent
15
- end
16
-
17
- def add_child(child)
18
- @children.push(child)
19
- child.parent = self
17
+ self.name = a_name
18
+ self.memo = a_memo
19
+ self.parent = a_parent
20
+ # @codes = QDA::CodingTable.new()
21
+ @codes = nil
20
22
  end
21
23
 
22
- def append_to(parent)
23
- parent.add_child(self)
24
- @parent = parent
25
- end
24
+ def name=(a_name)
25
+ case a_name
26
+ when /(\A\/|\/\z)/
27
+ raise BadNameError.new('Category name cannot have the "/" character' +
28
+ ' at beginning or end')
29
+ when /[\n\t]/
30
+ raise BadNameError.new('Category name cannot contain newline or tab' +
31
+ ' characters')
32
+ when String
33
+ @name = a_name
34
+ else
35
+ raise BadNameError.new('Category name must be a string')
36
+ end
37
+ end
26
38
 
39
+ def escape_name()
40
+ name.gsub(/\//, '//')
41
+ end
42
+
27
43
  def ==(other)
28
44
  if other.respond_to?(:dbid)
29
- return self.dbid == other.dbid
45
+ if self.dbid.nil? and other.dbid.nil?
46
+ return self.name == other.name && self.parent == other.parent
47
+ else
48
+ return self.dbid == other.dbid && self.parent == other.parent
49
+ end
30
50
  elsif other.nil?
31
51
  return false
32
52
  else
33
53
  raise "No comparison of Category with #{other.inspect}"
34
54
  end
35
55
  end
36
-
56
+
57
+ def add_child(child)
58
+ if self[ child.name ]
59
+ raise NotUniqueNameError.new("Child with the name '#{child.name}'" +
60
+ "is already attached to '#{self.name}'")
61
+ end
62
+ @children.push(child)
63
+ end
64
+ protected :add_child
65
+
66
+ # set the parent of this category to be +new_parent+, deleting from old
67
+ # parent if necessary.
68
+ def parent=(new_parent)
69
+ if is_ancestor_of?(new_parent)
70
+ raise BadStructureError.new("Cannot make category a child of " +
71
+ "#{new_parent.path} as it is an ancestor")
72
+ end
73
+ parent.delete(self) if parent
74
+ @parent = new_parent
75
+ new_parent.add_child(self) if new_parent
76
+ end
77
+
78
+ # returns all the children who match +idx+
79
+ def [](idx)
80
+ case idx
81
+ when Fixnum, Range
82
+ return @children[idx]
83
+ when Regexp
84
+ return @children.find { | c | c.name =~ idx}
85
+ when String
86
+ patt = /\A#{Regexp.escape(idx)}\z/i
87
+ return @children.find { | c | c.name =~ patt }
88
+ else
89
+ raise ArgumentError.new("Bad index #{idx}")
90
+ end
91
+ end
92
+
93
+ def delete(target)
94
+ @children.delete(target)
95
+ end
96
+
37
97
  # number of separate documents coded by this category
98
+ def unique_name(name)
99
+ while self[ name ]
100
+ puts "#{name} exists"
101
+ name.sub!(/(?:\s*\((\d+)\))?$/) { " (#{$1.to_i.succ})" }
102
+ puts "trying #{name}"
103
+ end
104
+ name
105
+ end
106
+
38
107
  def num_of_docs
39
108
  @codes.num_of_docs
40
109
  end
@@ -47,51 +116,74 @@ module QDA
47
116
  @codes.num_of_chars
48
117
  end
49
118
 
50
- def codetable=(codetable)
51
- @codes = codetable
119
+ def each_child
120
+ children.each { | c | yield c }
121
+ end
122
+ def path()
123
+ ancestors.inject("/" << escape_name) do | path, anc |
124
+ "/" << anc.escape_name << path
125
+ end
126
+ end
127
+
128
+ def ancestors()
129
+ ancestor, ancestors = self, []
130
+ ancestors.push(ancestor) while ancestor = ancestor.parent
131
+ ancestors
132
+ end
133
+
134
+ def descendants()
135
+ @children.map() { | c | [ c, c.descendants ] }.flatten
52
136
  end
53
137
 
54
- # returns a new category with codes representing all the text by
55
- # +self+ and +other+.
56
- def intersection(other, new_name = 'INTERSECTION',
57
- new_parent = nil, new_memo = '')
58
- result = Category.new(new_name, new_parent, new_memo)
59
- @codes.each do | docid, codes |
60
- if other.codes.include?[docid]
61
- result.codes[docid] = codes.intersect( other.codes[docid] )
62
- end
138
+ def find(path)
139
+ points = Category.parse_path(path)
140
+ scope = @children
141
+ points.delete('')
142
+ while point = points.shift
143
+ scope = scope.find_all { | x | x.name =~ /^#{point}/ }
144
+ scope.map! { | x | x.children }.flatten! unless points.empty?
63
145
  end
64
- return result
146
+ scope
147
+ end
148
+
149
+ # returns true if +cat+ is a Category located below self
150
+ def is_ancestor_of?(cat)
151
+ descendants.include?(cat)
152
+ end
153
+
154
+
155
+ def is_descendant_of?(cat)
156
+ ancestors.include?(cat)
157
+ end
158
+
159
+ def codetable=(codetable)
160
+ @codes = codetable
65
161
  end
66
162
 
163
+ def codetable_init()
164
+ @codes ||= QDA::CodingTable.new()
165
+ end
166
+
67
167
  # apply a code to a document; returns the new set of codes applied
68
168
  # to that document. +docid+ should be the database id of the
69
169
  # document to be retrieved (a string)
70
170
  def code(docid, offset, length)
71
- unless docid.nil? || docid.kind_of?(Fixnum)
72
- raise ArgumentError,
73
- "Docid should be an integer or nil, got #{docid.inspect}"
74
- end
75
- unless offset >= 0
76
- raise ArgumentError, "Offset should be an integer >= 0, got #{offset}"
77
- end
78
- unless length > 0
79
- raise ArgumentError, "Length should be an integer > 0, got #{length}"
80
- end
171
+ codetable_init()
81
172
  new_code = QDA::Code.new(docid, offset, length)
82
173
  @codes.add(new_code)
83
174
  end
84
175
 
85
176
  def uncode(docid, offset, length)
177
+ @codes ||= QDA::CodingTable.new()
86
178
  # raise "docid should be an integer > 0, is #{docid}" if docid == 0
87
179
  c = Code.new(docid, offset, length)
88
180
  @codes.subtract(c)
89
181
  end
90
-
91
- # return the vector set associated with +docid+
92
- def [](docid)
93
- @codes[docid]
182
+
183
+ def to_s()
184
+ "<Category '#{path}' [#{dbid}]>"
94
185
  end
186
+ alias :inspect :to_s
95
187
  end
96
188
 
97
189
  # object representing a particular application of a code to a
@@ -125,7 +217,7 @@ module QDA
125
217
  @length = length
126
218
  end
127
219
 
128
- # a Code is already it's own simplest representation, so never
220
+ # a Code is already its own simplest representation, so never
129
221
  # needs to be modified to work with another code-like object.
130
222
  def coerce(other)
131
223
  self