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,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() }