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,9 +1,24 @@
1
- require 'weft/filters'
1
+ # maybe if running under rubygems
2
+ if not defined?(WEFT_SHARE_DIR)
3
+ WEFT_SHAREDIR = File.join( File.dirname(__FILE__), '..', 'share')
4
+ end
5
+
6
+ # core classes
7
+
8
+ require 'weft/exceptions'
2
9
  require 'weft/document'
3
10
  require 'weft/category'
11
+ require 'weft/query'
12
+ require 'weft/codereview'
13
+ require 'weft/broadcaster'
14
+
15
+ # backend
4
16
  require 'weft/backend'
5
17
  require 'weft/application'
6
18
 
19
+ # i/o
20
+ require 'weft/filters'
21
+
7
22
  begin
8
23
  require 'weft/WEFT-VERSION-STRING'
9
24
  WEFT_VERSION = QDA::Version.new(WEFT_VERSION_STRING)
@@ -1 +1 @@
1
- WEFT_VERSION_STRING = '0.9.6'
1
+ WEFT_VERSION_STRING = '0.9.8a'
@@ -1,5 +1,3 @@
1
- require 'observer'
2
-
3
1
  module QDA
4
2
  class Version
5
3
  attr_reader :major, :minor, :release
@@ -30,29 +28,24 @@ module QDA
30
28
  end
31
29
 
32
30
  class Application
33
- include Observable
34
- def initialize(observer = nil)
35
- add_observer(observer) if observer
36
- @dirty = false
37
- end
31
+ include Broadcaster
38
32
 
33
+ SUBSCRIBABLE_EVENTS = [ :saved, :started, :ended,
34
+ :document_added, :document_changed, :document_deleted,
35
+ :category_added, :category_changed, :category_deleted ]
36
+
39
37
  # creates a completely empty new application / project, using the
40
38
  # backend +backend+, to be intialized with args +args+
41
- def Application::new_virgin(backend, args, observer = nil)
42
- app = new()
43
- app.extend( backend )
44
- app.start(args)
45
- app.install_clean()
46
- app
39
+ def initialize(backend_class = nil)
40
+ self.extend(backend_class) if backend_class
47
41
  end
48
42
 
49
43
  # create some basic nodes
50
44
  def set_up()
51
- save_category( Category.new('CATEGORIES', nil) )
52
- save_category( Category.new('SEARCHES', nil) )
45
+ _save_category( Category.new('CATEGORIES', nil) )
46
+ _save_category( Category.new('SEARCHES', nil) )
53
47
  save_preference( 'CreateVersion', WEFT_VERSION )
54
48
  save_preference( 'CreateVersionString', WEFT_VERSION_STRING )
55
- undirty!
56
49
  end
57
50
 
58
51
  def each_doc()
@@ -64,67 +57,17 @@ module QDA
64
57
  @dirty
65
58
  end
66
59
 
67
- def dirty!()
68
- changed if not dirty?
69
- @dirty = true
70
- notify_observers(@dirty)
71
- end
72
-
73
- def undirty!()
74
- changed if dirty?
75
- @dirty = false
76
- notify_observers(@dirty)
77
- end
78
-
79
- # is it up and running
80
- def started?
81
- true
82
- end
83
-
84
- # signal to clear up - should this be the level at which an exception
85
- # is raised - no, should probably be at the gui level
86
- def finish(force = false)
87
- if ! consistent? and ! force
88
- raise "Not ready to be saved"
89
- end
90
- end
91
-
92
- def query_segment(function, *args)
93
- case function
94
- when "IS CODED BY"
95
- category = get_category(args[0])
96
- return get_text_at_category( category )
97
- when "CONTAINS WORD"
98
- return get_search_fragments( args[0], :wrap_both => 100 )
60
+ def broadcast(evt, content = nil)
61
+ super(evt, content)
62
+ if evt == :saved or evt == :started or evt == :ended
63
+ @dirty = false
99
64
  else
100
- raise RuntimeError.new("Unknown function '#{function}' in query")
65
+ @dirty = true
101
66
  end
102
67
  end
103
- private :query_segment
104
-
105
- # executes a query, which is a series of descriptions of text
106
- # functions (eg 'CODED BY "category x"') and operators describing
107
- # how to combine them('AND', 'NOT', 'Or')
108
- def do_query(*query)
109
- text_1 = query_segment(query.shift, query.shift)
110
-
111
- # work rightwards through the query, doing various kinds of
112
- # combination of the result sets retrieved
113
- while op = query.shift
114
- return text_1 if op.empty?
115
- text_2 = query_segment(query.shift, query.shift)
116
- if op == 'AND'
117
- text_1.join(text_2)
118
- elsif op == 'OR'
119
- text_1.merge(text_2)
120
- elsif op =~ /(AND )?NOT/
121
- text_1.remove(text_2)
122
- else
123
- raise RuntimeError.new("Unknown operator '#{op}' in query")
124
- end
125
- end
126
-
127
- return text_1
68
+
69
+ def inspect()
70
+ "<QDA::Application>"
128
71
  end
129
72
  end
130
73
  end
@@ -1,39 +1,13 @@
1
1
  module QDA
2
2
  module Backend
3
- autoload :MySQL, 'weft/backend/mysql'
4
- # autoload :SQLite, 'backend/sqlite'
5
3
  require 'weft/backend/sqlite'
6
- # autoload :N6, 'backend/n6'
7
4
  require 'weft/backend/n6'
8
- autoload :RubyNative, 'weft/backend/marshal'
9
-
10
- module Abstract
11
- # receive arguments and make any connection required to the
12
- # storage source
13
- def start(args = {})
14
- # raise "virtual"
15
- end
16
-
17
- # load a specific document
18
- def get_doc(doctitle)
19
- raise "virtual"
20
- end
21
-
22
- # an array of all the documents - should this include TEXT
23
- def get_all_docs()
24
- []
25
- end
26
5
 
27
- # all categories in a tree structure, the root nodes are returned
28
- def get_all_categories()
29
- [ Category.new('') ]
30
- end
31
-
32
- # save changes
33
- def save()
34
- raise "virtual"
35
- end
36
-
37
- end
6
+ # autoload :MySQL, 'weft/backend/mysql'
7
+ # autoload :SQLite, 'backend/sqlite'
8
+ # autoload :N6, 'backend/n6'
9
+ # autoload :RubyNative, 'weft/backend/marshal'
10
+ class BackendError < StandardError
11
+ end
38
12
  end
39
13
  end
@@ -12,130 +12,106 @@ module QDA
12
12
  # currently problems with SQLite 3 and non-ASCII characters. Will pick
13
13
  # up whether sqlite or sqlite3 is available.
14
14
  module Backend::SQLite
15
-
15
+ include Backend
16
+ require 'weft/backend/sqlite/database.rb'
16
17
  require 'weft/backend/sqlite/schema.rb'
17
18
  require 'weft/backend/sqlite/upgradeable.rb'
18
19
  require 'weft/backend/sqlite/category_tree.rb'
19
- include Upgradeable
20
-
21
- # if working with sqlite v2 with the sqlite-ruby v2, we need a
22
- # couple of compatibility tweaks.
23
- if defined?(::SQLite)
24
- SQLITE_DB_CLASS = ::SQLite::Database
25
- # Ruby-SQLite3 statements have a close() method, but Ruby-SQLite
26
- # v 2 don't - so we supply a dummy method for when using v2
27
- class ::SQLite::Statement
28
- def close(); end
29
- end
30
- # SQLite3 introduced this more ruby-ish notation
31
- class ::SQLite::Database::FunctionProxy
32
- alias :result= :set_result
33
- end
34
- elsif defined?(::SQLite3)
35
- SQLITE_DB_CLASS = ::SQLite3::Database
36
- else
37
- raise LoadError, "No SQlite database class loaded"
38
- end
39
-
40
- class Database < SQLITE_DB_CLASS
41
- def initialize(file)
42
- # super(file, :driver => "Native")
43
- super(file)
44
- self.results_as_hash = true
45
- # self.type_translation = true
46
- end
47
-
48
- def undo_action()
49
- @dbh.transaction do
50
- @dbh.execute("SELECT * FROM undoable WHERE step = 1
51
- ORDER BY step, actionid DESC") do | task |
52
- @dbh.execute(task[0])
53
- end
54
- @dbh.execute("UPDATE undoable SET step = step -1")
55
- @dbh.execute("DELETE FROM undoable WHERE step = 0")
56
- end
57
- end
58
-
59
- def redo_action()
60
- transaction do
61
- execute("SELECT * FROM undoable WHERE step = -1
62
- ORDER BY step, actionid DESC") do | task |
63
- execute(task[0])
64
- end
65
- execute("DELETE FROM undoable WHERE step = -1")
66
- execute("UPDATE undoable SET step = step + 1")
67
- end
68
- end
69
-
70
- def date_freeze(date)
71
- date ? date.strftime('%Y-%m-%d %H:%M:%S') : ''
72
- end
73
20
 
74
- def date_thaw(str)
75
- return nil if str.empty?
76
- return Time.local( *str.split(/[- :]/) )
77
- end
78
- end
21
+ include Upgradeable
79
22
 
80
23
  attr_reader :dbh, :dbfile
81
24
 
82
- # load up the database connection. A hash argument containing the
25
+ # Makes the database connection. A hash argument containing the
83
26
  # key :dbfile should be supplied. If this is +nil+, then a
84
- # temporary storage will be used
27
+ # temporary storage will be used. Alternately, the key :in_memory may
28
+ # supplied with any true value, and a new in-memory database will be created.
29
+ # This is normally only useful for testing, as these databases can't currenly
30
+ # be saved. They are, however, fast and straightforward.
85
31
  def start(args)
86
- if ! args.key?(:dbfile)
32
+ return start_in_memory() if args.key?(:memory)
33
+ unless args.key?(:dbfile)
87
34
  raise ArgumentError, "Must specify SQLite dbfile to load from"
88
35
  end
89
-
36
+
90
37
  @dbfile = args[:dbfile]
91
38
  if @dbfile and ! File.exists?(@dbfile)
92
- raise RuntimeError, "Tried to open an non-existent database"
39
+ raise ArgumentError, "Tried to open an non-existent database"
93
40
  end
94
41
 
42
+ # create an empty temporary file
95
43
  tmp_fname = @dbfile ? File::basename(@dbfile) : 'Weft'
96
- tmpfile = Tempfile.new(tmp_fname || 'Weft')
97
- tmpfile.close(false) # don't delete
98
-
99
- @tmpfile = tmpfile.path
100
- if @dbfile
101
- FileUtils.copy(@dbfile, @tmpfile)
102
- end
103
- @dbh = Database.new(@tmpfile)
44
+ @tmp_tmp = Tempfile.new(tmp_fname)
45
+ @tmp_tmp.close(false) # don't delete
46
+
47
+ # copy the existing file being opened to the tempfile
48
+ @tmpfile = @tmp_tmp.path
49
+ FileUtils.copy(@dbfile, @tmpfile) if @dbfile
50
+ @dbh = FileDatabase.new( @tmpfile )
51
+
104
52
  # if opening from an existing file, check and do any upgrding
105
53
  # required from older versions
106
54
  do_version_format_upgrading() if @dbfile
107
- undirty!
55
+ broadcast(:started)
108
56
  end
109
-
57
+
58
+ def start_in_memory()
59
+ @dbh = InMemoryDatabase.new()
60
+ broadcast(:started)
61
+ end
62
+
110
63
  def connect(args)
111
64
  @dbh = args[:dbh]
112
65
  end
113
66
 
114
- def end(force = false)
67
+ def end()
115
68
  @cat_tree = nil
116
- @dbh.close()
117
- end
118
-
119
- def save(target = @dbfile)
120
- if target.nil?
121
- raise RuntimeError,
122
- "No previously saved file, and no named supplied for save"
69
+ if @dbh
70
+ @dbh.close()
71
+ @dbh = nil
72
+ end
73
+
74
+ # seem to be ending up with a lot of tempfiles left over .. but unlink
75
+ # gives 'Permission denied'
76
+ # File.unlink(@tmpfile) if @tmpfile
77
+ @tmp_tmp = @tmpfile = nil
78
+
79
+ broadcast(:ended)
80
+ end
81
+
82
+ # Saves all changes since the file was either newly opened or saved. May
83
+ # optionally specify +target+ as the file to save changes to. If no +target+
84
+ # is specified, changes are saved to the file from which the database was
85
+ # first opened. An error will be raised if this is a previously unsaved
86
+ # project and no save destination is specified.
87
+ def save(target = nil)
88
+ if @dbh.is_a?(InMemoryDatabase)
89
+ raise QDA::Backend::BackendError,
90
+ "Cannot save an in-memory database"
91
+ end
92
+
93
+ unless ( target && @dbfile = target ) or @dbfile
94
+ raise QDA::Backend::BackendError,
95
+ "No previously saved file, and no named supplied for save"
123
96
  end
124
- @dbh.close
125
- @dbfile = target
97
+
98
+ @dbh.close() if not @dbh.closed?
126
99
  FileUtils.copy(@tmpfile, @dbfile)
127
- @dbh = Database.new(@tmpfile)
128
- undirty!
100
+ @dbh = FileDatabase.new(@tmpfile)
101
+ broadcast(:saved)
129
102
  end
130
103
 
131
104
  # roll the current state back to the last-saved state.
132
105
  def revert()
133
- @dbh.close()
106
+ unless @dbfile
107
+ raise QDA::Backend::BackendError, "No previously saved file to revert to"
108
+ end
109
+ @dbh.close() if not @dbh.closed?
134
110
  FileUtils.copy(@dbfile, @tmpfile)
135
- @dbh = Database.new(@tmpfile)
111
+ @dbh = FileDatabase.new(@tmpfile)
112
+ broadcast(:saved)
136
113
  end
137
114
 
138
-
139
115
  # hint to do the next series of actions as a batch
140
116
  def batch
141
117
  @dbh.transaction { yield }
@@ -164,7 +140,7 @@ module Backend::SQLite
164
140
  end
165
141
 
166
142
  # fetch the document identified by the string ident
167
- def get_doc(ident)
143
+ def get_document(ident)
168
144
  doc = nil
169
145
  @dbh.transaction do
170
146
  stmt = nil
@@ -187,8 +163,19 @@ module Backend::SQLite
187
163
  end
188
164
  return doc
189
165
  end
190
- alias :get_document :get_doc
191
-
166
+ alias :get_doc :get_document
167
+
168
+ # Replaces +string+ (in place) with a numeric bracketed suffix, incrementing
169
+ # the integer each time.
170
+ # e.g.
171
+ # name
172
+ # name (1)
173
+ # name (2)
174
+ # name (3)
175
+ def magic_rename(str)
176
+ str.sub!(/(?:\s*\((\d+)\))?$/) { " (#{$1.to_i.succ})" }
177
+ end
178
+
192
179
  def save_preference(pref_name, pref_value)
193
180
  frozen_value = Base64.encode64( Marshal.dump( pref_value) )
194
181
  @dbh.transaction do
@@ -196,7 +183,6 @@ module Backend::SQLite
196
183
  VALUES (?, ?)",
197
184
  pref_name, frozen_value )
198
185
  end
199
- dirty!
200
186
  end
201
187
 
202
188
  def get_preference(pref_name)
@@ -211,14 +197,27 @@ module Backend::SQLite
211
197
  return Marshal.load( Base64.decode64(frozen_pref) )
212
198
  end
213
199
 
214
- def save_document(doc)
200
+ # saves the document +doc+ to the database; if the document already has a dbid
201
+ # it will be treated as an existing document
202
+ def save_document(doc, rename_magic = false)
215
203
  raise TypeError unless doc.kind_of? QDA::Document
204
+ new = doc.dbid ? false : true
216
205
  @dbh.transaction { _save_document(doc) }
217
- dirty!
206
+ if new
207
+ broadcast(:document_added, doc)
208
+ else
209
+ broadcast(:document_changed, doc)
210
+ end
218
211
  doc
212
+ rescue NotUniqueNameError
213
+ raise unless rename_magic
214
+ magic_rename(doc.title)
215
+ retry
219
216
  end
220
-
217
+ alias :save_doc :save_document
218
+
221
219
  def _save_document(doc)
220
+ # if we are saving changes to an already-saved document
222
221
  if doc.dbid
223
222
  @dbh.execute("UPDATE document
224
223
  SET doctitle = ?, doctext = ?,
@@ -227,7 +226,9 @@ module Backend::SQLite
227
226
  doc.title, doc.text, doc.memo,
228
227
  @dbh.date_freeze( Time.now() ),
229
228
  doc.dbid)
229
+ # a new document is being saved
230
230
  else
231
+ raise NotUniqueNameError if doc_exists(doc.title)
231
232
  @dbh.execute("INSERT INTO document
232
233
  VALUES(NULL, ?, ?, ?, ?, ?)",
233
234
  doc.title, doc.text, doc.memo,
@@ -236,21 +237,42 @@ module Backend::SQLite
236
237
  doc.dbid = @dbh.last_insert_row_id().to_i
237
238
  end
238
239
  end
239
-
240
- # delete teh document identified by +dbid+ from the database
241
- def delete_document(dbid)
240
+
241
+ # returns the dbid for the document titled +title+, or nil if no such
242
+ # document exists.
243
+ def doc_exists(title)
244
+ @dbh.execute("SELECT docid
245
+ FROM document
246
+ WHERE doctitle = ?", title) { | r | return r[0].to_i }
247
+ end
248
+
249
+ # delete the document identified by +dbid+ from the database
250
+ def delete_document(doc)
242
251
  @dbh.transaction do
243
- @dbh.execute("DELETE FROM document WHERE docid = ?", dbid)
252
+ @dbh.execute("DELETE FROM document WHERE docid = ?", doc.dbid)
244
253
  end
245
- dirty!
254
+ broadcast(:document_deleted, doc)
246
255
  end
247
256
 
248
257
  # retrieve the category with the internal id +catid+, along with
249
258
  # its codes. If +get_structure+ is set to a true value then the
250
259
  # category's children will also be retrieved from the database
251
- def get_category(catid, get_structure = false)
260
+
261
+ def get_category(identifier, get_struct = false)
262
+ case identifier
263
+ when Fixnum
264
+ return get_category_by_id(identifier, get_struct)
265
+ when String
266
+ return get_category_by_path(identifier, get_struct)
267
+ when QDA::Category
268
+ return get_category_by_id(identifier.dbid, get_struct)
269
+ else
270
+ raise "Invalid identifier #{catid.inspect}"
271
+ end
272
+ end
273
+
274
+ def get_category_by_id(catid, get_structure = false)
252
275
  catid = catid.to_i if catid =~ /^\d+$/
253
- raise "Invalid id #{catid.inspect}" unless catid.kind_of?(Fixnum)
254
276
 
255
277
  category = nil
256
278
  stmt = @dbh.prepare("SELECT * FROM category WHERE catid = ?")
@@ -259,28 +281,49 @@ module Backend::SQLite
259
281
  category = Category.new(r['catname'], parent, r['catdesc'])
260
282
  category.dbid = catid
261
283
  end
262
- raise "No category found matching id '#{catid}'" unless category
263
284
  stmt.close()
285
+
286
+ if not category
287
+ raise NotFoundError.new("No category found matching id '#{catid}'")
288
+ end
264
289
 
265
290
  get_codes_for_category(category)
266
291
  get_and_build_children(category) if get_structure
267
292
  category
268
293
  end
269
-
294
+
295
+ # returns the dbid of the category named +name+ attached to the parent
296
+ # category +parent+, or nil if no such category exists
297
+ def category_exists?(parent, name)
298
+ if parent
299
+ x = cat_tree[parent.dbid][name]
300
+ else
301
+ x = cat_tree.roots.find { | r | r.name == name }
302
+ end
303
+ x ? x.dbid : nil
304
+ end
305
+
270
306
  # gets the root category named +name+
271
- def get_root_category(name)
307
+ def get_root_category(name, get_structure = false)
272
308
  root = cat_tree.roots.find { | r | r.name == name }
273
- raise "Not found, root category #{name.inspect}" unless root
274
- return get_category(root.dbid)
309
+ unless root
310
+ raise NotFoundError.new("Not found, root category #{name.inspect}")
311
+ end
312
+ return get_category(root.dbid, get_structure)
275
313
  end
276
314
 
315
+
316
+ def get_category_by_path(path, get_struct)
317
+ get_categories_by_path(path, get_struct)[0]
318
+ end
319
+
277
320
  # fetch categories by relative or absolute paths. Returns an
278
321
  # array of categories
279
- def get_categories_by_path(path)
322
+ def get_categories_by_path(path, get_struct = false)
280
323
  # cos it should be quicker ...
281
324
  if path =~ /\//
282
325
  return cat_tree.find(path).map do | found |
283
- get_category(found.dbid)
326
+ get_category(found.dbid, get_struct)
284
327
  end
285
328
  else
286
329
  return get_categories_by_name(path)
@@ -296,17 +339,17 @@ module Backend::SQLite
296
339
  stmt = @dbh.prepare("SELECT catid FROM category
297
340
  WHERE UPPER(catname) LIKE ?
298
341
  AND parent >= 0" )
299
- namebit = namebit.upcase
342
+ namebit = namebit.upcase + "%"
300
343
  else
301
344
  stmt = @dbh.prepare("SELECT catid FROM category
302
345
  WHERE catname GLOB ?
303
346
  AND parent >= 0" )
304
-
347
+ namebit = namebit + "*"
305
348
  end
306
349
  categories = []
307
350
  @dbh.transaction do
308
- stmt.execute!(namebit + "%") do | r |
309
- categories.push( get_category( r['catid'] ) )
351
+ stmt.execute!(namebit) do | r |
352
+ categories.push( get_category( r['catid'].to_i, true ) )
310
353
  end
311
354
  stmt.close()
312
355
  end
@@ -320,22 +363,22 @@ module Backend::SQLite
320
363
  # builds the tree structure below +category+, modifying
321
364
  # +category+ in place. After this call, the retrieved structure
322
365
  # is available as the +children+ property of the category.
323
- def get_and_build_children(category)
366
+ def get_and_build_children(base)
324
367
  # this duplicates stuff below
325
368
  append_f = Proc.new do | parent, elem |
326
369
  cat = Category.new(elem.name, parent)
327
370
  cat.dbid = elem.dbid
328
- elem.children { | c | append_f.call(cat, c) }
371
+ elem.each_child { | c | append_f.call(cat, c) }
329
372
  end
330
-
331
- cat_tree[category.dbid].children do | first_child |
332
- append_f.call(category, first_child)
373
+ cat_tree[base.dbid].each_child do | kid |
374
+ append_f.call(base, kid)
333
375
  end
334
376
  end
335
377
  private :get_and_build_children
336
378
 
337
379
  # applies the codes to category +cat+
338
380
  def get_codes_for_category(cat)
381
+ cat.codetable = QDA::CodingTable.new
339
382
  @dbh.execute("SELECT docid, offset, length
340
383
  FROM code
341
384
  WHERE catid = ? ", cat.dbid) do | row |
@@ -391,31 +434,44 @@ module Backend::SQLite
391
434
  end
392
435
 
393
436
  # saves the category
394
- def save_category(cat)
437
+ def save_category(cat, rename_magic = false)
438
+ new = cat.dbid ? false : true
395
439
  @dbh.transaction { _save_category(cat) }
396
- dirty!
440
+ if new
441
+ broadcast(:category_added, cat)
442
+ else
443
+ broadcast(:category_changed, cat)
444
+ end
397
445
  cat
446
+ rescue NotUniqueNameError
447
+ raise unless rename_magic
448
+ magic_rename(cat.name)
449
+ retry
398
450
  end
399
451
 
400
452
  def _save_category(cat)
401
- # only resave the tree structure if nec,
453
+ # only resave the tree structure if necessary
402
454
  xml_needs_update = false
403
-
455
+
404
456
  # updating an existing category
405
457
  if cat.dbid
458
+ # fetch the corresponding node from the tree
459
+ node = cat_tree[cat.dbid]
460
+
406
461
  # check for re-parenting or renaming
407
- child = cat_tree[cat.dbid]
408
-
409
- if child.parent != cat.parent.dbid
410
- cat_tree.move(child.dbid, cat.parent.dbid)
462
+ if cat.parent and node.parent != cat.parent.dbid
463
+ raise NotUniqueNameError if category_exists?(cat.parent, cat.name)
464
+ cat_tree.move(node.dbid, cat.parent.dbid)
411
465
  xml_needs_update = true
412
466
  end
413
-
414
- if child.name != cat.name
415
- child.name = cat.name
467
+
468
+ # check for name change
469
+ if node.name != cat.name
470
+ raise NotUniqueNameError if category_exists?(cat.parent, cat.name)
471
+ node.name = cat.name
416
472
  xml_needs_update = true
417
473
  end
418
- @dbh.execute("DELETE FROM code WHERE catid = ?", cat.dbid)
474
+
419
475
  @dbh.execute("UPDATE category
420
476
  SET catname = ?,
421
477
  catdesc = ?,
@@ -429,6 +485,7 @@ module Backend::SQLite
429
485
  cat.dbid)
430
486
  # adding a new category
431
487
  else
488
+ raise NotUniqueNameError if category_exists?(cat.parent, cat.name)
432
489
  parentid = cat.parent ? cat.parent.dbid : nil
433
490
  @dbh.execute("INSERT INTO category
434
491
  VALUES(NULL, ?, ?, ?, ?, ?)",
@@ -436,56 +493,57 @@ module Backend::SQLite
436
493
  @dbh.date_freeze( Time.now ),
437
494
  @dbh.date_freeze( Time.now ) )
438
495
  cat.dbid = @dbh.last_insert_row_id().to_i
439
-
440
- if cat.parent
441
- cat_tree.add(cat.parent.dbid, cat.dbid, cat.name)
442
- else
443
- cat_tree.add(nil, cat.dbid, cat.name)
444
- end
496
+ cat_tree.add(parentid, cat.dbid, cat.name)
497
+
445
498
  xml_needs_update = true
446
499
  end
447
-
448
- stmt_code = @dbh.prepare("INSERT INTO code VALUES(?, ?, ?, ?)")
449
- cat.codes.each do | docid, vecs |
450
- vecs.each do | vec |
451
- stmt_code.execute( cat.dbid, vec.docid, vec.offset, vec.length )
452
- end
500
+
501
+ if cat.codes
502
+ _save_codingtable(cat.dbid, cat.codes)
453
503
  end
454
- stmt_code.close()
455
504
 
456
505
  if xml_needs_update
457
506
  @dbh.execute( "UPDATE category_structure SET xml = ? ",
458
507
  cat_tree.serialise())
459
508
  end
460
509
  end
461
-
510
+
511
+ # save all the codes in the table +codingtable+ to the database, associating
512
+ # them with the category id +dbid+. Deletes all previous coding associated
513
+ # with that category.
514
+ def _save_codingtable(dbid, codingtable)
515
+ @dbh.execute("DELETE FROM code WHERE catid = ?", dbid)
516
+ stmt_code = @dbh.prepare("INSERT INTO code VALUES(?, ?, ?, ?)")
517
+ codingtable.each do | docid, vecs |
518
+ vecs.each do | vec |
519
+ stmt_code.execute( dbid, vec.docid, vec.offset, vec.length )
520
+ end
521
+ end
522
+ stmt_code.close()
523
+ end
524
+
462
525
  # deletes the category +category+. If +recursive+ is false then
463
526
  # any children of +category+ will be reattached to the deleted
464
527
  # category's parent. If +recursive+ is true (default), then all
465
528
  # descendants will be deleted.
466
529
  # Returns a list of categories that were actually deleted.
467
530
  def delete_category(cat, recursive = true)
531
+ if not recursive
532
+ raise NotImplementedError.new('Non-recursive deletion not implemented')
533
+ end
468
534
  return unless cat.dbid
469
- deleted_items = []
470
- # TODO not all items being returned in list
471
- if recursive
472
- me = cat_tree[cat.dbid]
473
- me.children.each do | child |
474
- deleted_items += delete_category(child, true)
535
+ deleted_items = cat_tree.remove(cat.dbid)
536
+ @dbh.transaction do
537
+ deleted_items.each do | dbid |
538
+ @dbh.execute("DELETE FROM category WHERE catid = ? ", dbid)
475
539
  end
476
- cat_tree.remove(cat.dbid)
477
- deleted_items << cat
478
- else
479
- raise NotImplementedError,
480
- 'Non-recursive deletion not implemented'
540
+ @dbh.execute("UPDATE category_structure SET xml = ?",
541
+ cat_tree.serialise())
481
542
  end
482
- @dbh.transaction do
483
- @dbh.execute("DELETE FROM category WHERE catid = ? ", cat.dbid)
484
- xml = cat_tree.serialise()
485
- @dbh.execute("UPDATE category_structure SET xml = ?", xml)
543
+ [ cat, *cat.descendants ].each do | deletion |
544
+ broadcast(:category_deleted, deletion)
486
545
  end
487
- dirty!
488
- return deleted_items
546
+ return cat
489
547
  end
490
548
 
491
549
  MAGIC_REV_INDEX_ID = -2
@@ -514,7 +572,7 @@ module Backend::SQLite
514
572
  locations.each do | loc |
515
573
  stmt_code.execute(wordid, docid, loc, word.length)
516
574
  end
517
- prog_bar.next() if prog_bar
575
+ prog_bar.step() if prog_bar
518
576
  end
519
577
  end # transaction
520
578
  [stmt_wordid, stmt_insert, stmt_code].each { | s | s.close() }