weft-qda 0.9.6 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
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