weft-qda 0.9.8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,7 @@
1
+ # $Header: /var/cvs/weft-qda/weft-qda/lib/weft.rb,v 1.11 2006/01/08 20:27:19 brokentoy Exp $
2
+
1
3
  # maybe if running under rubygems
2
- if not defined?(WEFT_SHARE_DIR)
4
+ if not defined?(WEFT_SHAREDIR)
3
5
  WEFT_SHAREDIR = File.join( File.dirname(__FILE__), '..', 'share')
4
6
  end
5
7
 
@@ -1 +1 @@
1
- WEFT_VERSION_STRING = '0.9.8a'
1
+ WEFT_VERSION_STRING = '1.0.0'
@@ -1,4 +1,5 @@
1
1
  module QDA::Backend::SQLite::Schema
2
+ # The tables
2
3
  SCHEMA_TABLES = <<'SCHEMA_TABLES'
3
4
  CREATE TABLE category (
4
5
  catid INTEGER PRIMARY KEY,
@@ -36,6 +37,9 @@ CREATE TABLE app_preference (
36
37
  value TEXT);
37
38
  SCHEMA_TABLES
38
39
 
40
+ # Triggers- these currently just ensure that coding is cleaned up when a
41
+ # document or category is deleted. In the future they also trigger entries
42
+ # into the undo/redo table.
39
43
  SCHEMA_TRIGGERS = <<'SCHEMA_TRIGGERS'
40
44
  CREATE TRIGGER insert_category
41
45
  INSERT ON category
@@ -62,6 +66,10 @@ END;
62
66
  SCHEMA_TRIGGERS
63
67
 
64
68
  # This is here because it's written, but it's not in use yet.
69
+ #
70
+ # This is an outline of adding undo/redo facility using SQLite triggers, writing
71
+ # stepped actions to a undo action table, and recording SQL to restore the database
72
+ # to its prior state.
65
73
  SCHEMA_UNDO = <<'SCHEMA_UNDO'
66
74
  CREATE TABLE undoable (
67
75
  actionid INTEGER PRIMARY KEY,
@@ -117,6 +125,8 @@ BEGIN
117
125
  END;
118
126
  SCHEMA_UNDO
119
127
 
128
+ # Indexes - those on the coding table make a big difference to speed of
129
+ # retrieving marked text.
120
130
  SCHEMA_INDEXES = <<'SCHEMA_INDEXES'
121
131
 
122
132
  CREATE INDEX document_idx
@@ -130,6 +140,8 @@ ON docmeta(metaname, docid);
130
140
 
131
141
  SCHEMA_INDEXES
132
142
 
143
+ # model query for doing fast text searches from a reverse index
144
+ # (stored as categories).
133
145
  RINDEX_SEARCH_MODEL_QUERY = <<'RINDEX_SEARCH_MODEL_QUERY'
134
146
  SELECT document.docid AS docid, document.doctitle AS doctitle,
135
147
  MAX( 0, code.offset - ?)
@@ -7,4 +7,8 @@ module QDA
7
7
  end
8
8
  class NotFoundError < StandardError
9
9
  end
10
- end
10
+ class FilterError < StandardError
11
+ end
12
+ class CalculationError < StandardError
13
+ end
14
+ end
@@ -21,8 +21,11 @@ module QDA
21
21
  # +filename+, which should be a string.
22
22
  def import_file(klass, filename, opts = {}, &block)
23
23
  ext = filename[-3,3]
24
- filter = Filters.find_import_filter(klass, ext).new()
25
- import(filter, filename, &block)
24
+ filter_class = Filters.find_import_filter(klass, ext)
25
+ if not filter_class
26
+ raise FilterError.new("Cannot import a file of type '#{ext}'")
27
+ end
28
+ import(filter_class.new(), filename, &block)
26
29
  end
27
30
 
28
31
  def import(filter, content)
@@ -1,4 +1,4 @@
1
- # A stub class modelling a boolean query expression
1
+ # A class modelling an arbitrarily complexe boolean query expression
2
2
  module QDA
3
3
  class Query
4
4
  attr_accessor :dbid, :root
@@ -76,11 +76,11 @@ class Query
76
76
  super(app)
77
77
  case identifier
78
78
  when Category # a category specified directly
79
- @identifier = identifier.path
79
+ @identifier = identifier.dbid
80
80
  when String # a path to a category
81
- @identifier = identifier
81
+ @identifier = app.get_category(identifier).dbid
82
82
  when Fixnum # a category id
83
- @identifier = @app.get_category(identifier, false).path
83
+ @identifier = identifier
84
84
  when NilClass
85
85
  raise ArgumentError.new("Unspecified category for CODED BY expression")
86
86
  else
@@ -89,14 +89,19 @@ class Query
89
89
  end
90
90
 
91
91
  def to_s()
92
- "( CODED BY(#{@identifier.inspect}) )"
92
+ path = @app.get_category(@identifier).path
93
+ "( CODED BY(#{path}) )"
93
94
  end
94
95
 
95
96
  def calculate()
96
- cat = @app.get_category(@identifier, false)
97
+ cat = @app.get_category(@identifier, false) # Will raise NotFoundError
97
98
  @app.get_text_at_category( cat )
99
+ rescue NotFoundError => err
100
+ raise CalculationError,
101
+ "Could not retrieve category identified by #{@identifier}"
98
102
  end
99
103
  end
104
+
100
105
 
101
106
  class WordSearchFunction < Function
102
107
  attr_reader :word
@@ -166,4 +171,4 @@ class Query
166
171
  end
167
172
  end
168
173
  end
169
- end
174
+ end
@@ -10,7 +10,7 @@ require 'weft/wxgui/dialogs'
10
10
  require 'weft/wxgui/inspectors'
11
11
  require 'weft/wxgui/sidebar'
12
12
  require 'weft/wxgui/workarea'
13
- require 'weft/wxgui/error_handler.rb'
13
+ require 'weft/wxgui/error_handler'
14
14
 
15
15
 
16
16
  module QDA
@@ -285,22 +285,21 @@ module QDA
285
285
  end
286
286
  prog.step('Saving document')
287
287
  @app.save_document(doc, true)
288
- prog.step('Indexing document')
288
+ prog.title = "Importing document '#{doc.title}'"
289
+ prog.step("Indexing document '#{doc.title}'")
289
290
  indexer = QDA::WordIndexer.new()
290
291
  indexer.feed(doc)
291
- prog.step('Saving indexes')
292
+ prog.step("Saving word indexes for '#{doc.title}'")
292
293
  wordcount = indexer.words.keys.length
293
294
  if wordcount > 0
294
295
  prog.retarget(wordcount)
295
296
  @app.save_reverse_index(doc.dbid, indexer.words, prog)
296
297
  end
297
298
  prog.finish()
298
- rescue UserAbortedException, IOError => err
299
+ rescue FilterError, UserAbortedException, IOError => err
299
300
  prog.finish() if prog
300
301
  app.delete_document(doc) if doc && doc.dbid
301
- MessageDialog.new(nil, err.to_s(),
302
- "Document not imported",
303
- OK|ICON_ERROR).show_modal()
302
+ ErrorDialog.display( "Document not imported", err.to_s() )
304
303
  @workarea.cursor = Wx::Cursor.new(Wx::CURSOR_ARROW)
305
304
  end
306
305
  end
@@ -455,8 +454,9 @@ module QDA
455
454
  # returns true if a new project was started, false if not
456
455
  def on_open_project(e)
457
456
  return false unless on_close_project(e)
458
- file_dialog = FileDialog.new(@workarea, "Open Project",
459
- "", "", "Project Files (*.qdp)|*.qdp" )
457
+ file_dialog = FileDialog.new( @workarea, "Open Project",
458
+ "", "",
459
+ "Weft QDA Project Files (*.qdp)|*.qdp" )
460
460
  return false unless file_dialog.show_modal() == Wx::ID_OK
461
461
 
462
462
  open_project( file_dialog.get_path() )
@@ -528,7 +528,7 @@ module QDA
528
528
  end
529
529
 
530
530
  def on_saveas_project(e)
531
- wildcard = "QDA Project Files (*.qdp)|*.qdp"
531
+ wildcard = "Weft QDA Project Files (*.qdp)|*.qdp"
532
532
  file_dialog = Wx::FileDialog.new(@workarea, "Save Project As",
533
533
  "", "", wildcard, Wx::SAVE)
534
534
 
@@ -565,7 +565,7 @@ module QDA
565
565
  end
566
566
 
567
567
  def on_import_n6(e)
568
- wildcard = "Project Files (*.stp)|*.stp"
568
+ wildcard = "N6 Project Files (*.stp)|*.stp"
569
569
  file_dialog = Wx::FileDialog.new(@workarea, "Import N6 Project", "",
570
570
  "", wildcard )
571
571
  if file_dialog.show_modal() == Wx::ID_OK
@@ -54,7 +54,7 @@ module QDA::GUI
54
54
  def delete(id)
55
55
  super(id)
56
56
  del_cat = data.delete(id)
57
- if del_cat.parent and old_parent_id = value_to_ident(del_cat.parent)
57
+ if del_cat.parent && old_parent_id = value_to_ident(del_cat.parent)
58
58
  set_item_data( old_parent_id,
59
59
  @client.app.get_category(del_cat.parent.dbid) )
60
60
  end
@@ -194,9 +194,11 @@ module QDA::GUI
194
194
 
195
195
  movee = get_item_data( from )
196
196
  destination = get_item_data( to )
197
+ # if for some reason these tree ids didn't correspond to categories...
198
+ return dont_move(from) unless movee and destination
197
199
  # don't move root nodes
198
200
  return dont_move(from) if not movee.parent
199
- # ignore if no move
201
+ # ignore if no move - target is same as current parent
200
202
  return dont_move(from) if movee.parent == destination
201
203
  # don't attach to descendants
202
204
  return dont_move(from) if destination.is_descendant_of?(movee)
@@ -218,14 +220,27 @@ module QDA::GUI
218
220
  # of the node.
219
221
  def move_item(from, to)
220
222
  # don't alter if unchanged
221
- return from if get_item_parent(from) == to
222
- new_child = append_item(to, get_item_text(from), get_item_data(from))
223
+ from_parent = get_item_parent(from)
224
+ return from if from_parent == to
225
+
226
+ # remember expanded-or-collapsed state
227
+ reexpand = expanded?(from_parent)
228
+
229
+ # create a new tree item under the new parent
230
+ new_child = append_item(to, get_item_text(from),
231
+ get_item_data(from))
232
+
233
+ # recursively move all children underneath to the new node
223
234
  child = get_first_child(from)[0]
224
235
  while child != 0
225
236
  move_item(child, new_child)
226
237
  child = get_first_child(from)[0]
227
238
  end
239
+
240
+ # restore my visibility settings
241
+ expand(to) if reexpand
228
242
  delete( from )
243
+
229
244
  new_child
230
245
  end
231
246
 
@@ -311,4 +326,4 @@ module QDA::GUI
311
326
  select_item(id)
312
327
  end
313
328
  end
314
- end
329
+ end
@@ -172,7 +172,7 @@ module QDA::GUI
172
172
  filt::EXTENSIONS,
173
173
  filt::EXTENSIONS ] }.join('|')
174
174
  super(parent, Lang::IMPORT_DOC_DIALOGUE_TITLE,
175
- "", "", wildcard, Wx::MULTIPLE)
175
+ "", "", wildcard, Wx::MULTIPLE|Wx::FILE_MUST_EXIST)
176
176
  end
177
177
 
178
178
  # return each selected file in turn into a block, passing in +path+ and
@@ -40,19 +40,21 @@ class CrashReportDialog < Wx::Dialog
40
40
  end
41
41
 
42
42
  def crash_details()
43
- @crash ||= { 'when' => Time.now.to_s(),
44
- 'os' => Config::CONFIG['target_os'],
45
- 'config' => Config::CONFIG['build'],
43
+ @crash ||= { 'when' => Time.now.to_s(),
44
+ 'os' => Config::CONFIG['target_os'],
45
+ 'config' => Config::CONFIG['build'],
46
+ 'weft_version' => WEFT_VERSION_STRING,
46
47
  'ruby_version' => [ Config::CONFIG['MAJOR'],
47
48
  Config::CONFIG['MINOR'],
48
49
  Config::CONFIG['TEENY'] ].join('.'),
49
- 'rs2exe' => rs2exe,
50
- 'backtrace' => ( [ err.inspect ] +
51
- err.backtrace ).join("\n") }
50
+ 'rs2exe' => rs2exe,
51
+ 'backtrace' => ( [ err.inspect ] +
52
+ err.backtrace ).join("\n") }
52
53
  end
53
54
 
54
55
  def crash_details_string
55
- %w[when os config ruby_version rs2exe backtrace].inject('') do | str, key |
56
+ %w[when weft_version os config
57
+ ruby_version rs2exe backtrace].inject('') do | str, key |
56
58
  str << "#{key}: #{crash_details[key]}\n"
57
59
  end
58
60
  end
@@ -177,9 +177,13 @@ module QDA::GUI
177
177
 
178
178
  # implements keyboard shortcuts for this window - these are activated by
179
179
  # interaction with the text-box.
180
- # currently, CTRL-k is mapped to "Code" and CTRL-l is mapped to "Uncode"
180
+ # currently,
181
+ # CTRL-m is mapped to "Mark"
182
+ # CTRL-, is mapped to "Unmark"
183
+ # CTRL-f is mapped to "Start find"
184
+
181
185
  def on_key_down(evt)
182
- return unless evt.control_down()
186
+ return evt.skip() unless evt.control_down()
183
187
  case evt.key_code()
184
188
  when 70 # "f"
185
189
  @text_box.start_find(self) if @text_box.respond_to?(:start_find)
@@ -188,6 +192,8 @@ module QDA::GUI
188
192
  when 44 # ","
189
193
  on_uncode(evt)
190
194
  end
195
+ # required so default shortcuts like CTRL-C, CTRL-V work
196
+ evt.skip()
191
197
  end
192
198
 
193
199
  # the current category active in this window
@@ -80,8 +80,12 @@ module QDA::GUI
80
80
  @last_query = query
81
81
  set_cursor( Wx::BUSY_CURSOR )
82
82
  @last_results = @last_query.calculate()
83
- set_cursor( Wx::NORMAL_CURSOR )
84
83
  return @last_results
84
+ rescue QDA::CalculationError => err
85
+ ErrorDialog.display("Error computing query", err.to_s)
86
+ return @last_results = QDA::FragmentTable.new()
87
+ ensure
88
+ set_cursor( Wx::NORMAL_CURSOR )
85
89
  end
86
90
 
87
91
  def get_target_argument(offset, as_string = false)
@@ -143,7 +147,9 @@ module QDA::GUI
143
147
  def on_save_results(e)
144
148
  # check for saved results
145
149
  Wx::BusyCursor.busy do
146
- results = get_results( get_query )
150
+ query = get_query()
151
+ return unless query
152
+ results = get_results( query )
147
153
  return unless results
148
154
  @text_box.populate(results)
149
155
  end
@@ -52,7 +52,8 @@ Are you sure you want to proceed?"
52
52
  DELETE_CATEGORY_TITLE= "Confirm category deletion"
53
53
  DELETE_CATEGORY_WARNING =
54
54
  "The category '%s' will permanently deleted, along with its memo and
55
- any coding associated with it.
55
+ any coding associated with it. Any categories attached underneath this
56
+ category in the tree will also be deleted.
56
57
 
57
58
  Are you sure you want to proceed?"
58
59
 
@@ -336,6 +336,13 @@ class TestCode < Test::Unit::TestCase
336
336
 
337
337
  sets = tbl.sets
338
338
  assert_equal(1, sets.length)
339
+
340
+ tbl_2 = CodingTable.new()
341
+ result = tbl_2.merge(tbl)
342
+ assert( tbl_2.key?(3) )
343
+ assert_equal(1, tbl_2.num_of_docs)
344
+ assert_equal(6, tbl_2.num_of_chars)
345
+ assert_equal(1, tbl_2.num_of_codes)
339
346
  end
340
347
 
341
348
  def test_fragment_table
@@ -101,16 +101,26 @@ class TestQuery < Test::Unit::TestCase
101
101
 
102
102
 
103
103
 
104
- def test_or
104
+ def test_and
105
105
  cs_1 = CodingTable.new()
106
106
  cs_2 = CodingTable.new()
107
+
107
108
  cs_1.add( Code.new(3, 0, 4) )
109
+ expr = Query::AND.new(cs_1, cs_2)
110
+ result = expr.calculate()
111
+
112
+ assert_equal(0, result.num_of_docs)
113
+ assert_equal(0, result.num_of_chars)
114
+ assert_equal(0, result.num_of_passages)
115
+
108
116
  cs_2.add( Code.new(3, 3, 5) )
109
117
  expr = Query::AND.new(cs_1, cs_2)
110
118
  result = expr.calculate()
111
119
  assert_equal(1, result.num_of_docs)
112
120
  assert_equal(1, result.num_of_chars)
113
121
  assert_equal(1, result.num_of_passages)
122
+
123
+ # empty OR
114
124
  end
115
125
 
116
126
  def test_and_not
@@ -129,6 +139,13 @@ class TestQuery < Test::Unit::TestCase
129
139
  cs_1 = CodingTable.new()
130
140
  cs_2 = CodingTable.new()
131
141
  cs_1.add( Code.new(3, 0, 4) )
142
+
143
+ expr = Query::OR.new(cs_1, cs_2)
144
+ result = expr.calculate()
145
+ assert_equal(1, result.num_of_docs)
146
+ assert_equal(4, result.num_of_chars)
147
+ assert_equal(1, result.num_of_passages)
148
+
132
149
  cs_2.add( Code.new(3, 3, 5) )
133
150
  expr = Query::OR.new(cs_1, cs_2)
134
151
  result = expr.calculate()
@@ -37,7 +37,9 @@ class TestFilter < Test::Unit::TestCase
37
37
 
38
38
  def test_export_document()
39
39
  doc = Document.new('foo')
40
- doc.append('blah blah blah')
40
+ doc.memo = 'The memo'
41
+ doc << "First paragraph blah blah\n\n"
42
+ doc << "Second paragraph bar baz qux\n\n"
41
43
  filter = Filters::DocumentTextOutput.new()
42
44
 
43
45
  tmp = Tempfile.new('doc_text')
@@ -45,7 +47,21 @@ class TestFilter < Test::Unit::TestCase
45
47
  tmp.rewind()
46
48
  contents = tmp.read()
47
49
  # Proper template not written yet
48
- # assert_match(/blah/m, contents, 'Looks a bit like the document')
50
+ assert_match(/foo/m, contents, 'Looks a bit like the document with title')
51
+ assert_match(/blah/m, contents, 'Looks a bit like the document')
52
+ assert_match(/qux/m, contents, 'Looks a bit like the document')
53
+
54
+ filter = Filters::DocumentHTMLOutput.new()
55
+
56
+ tmp = Tempfile.new('doc_text')
57
+ filter.write(doc, tmp)
58
+ tmp.rewind()
59
+ contents = tmp.read()
60
+ puts contents
61
+ # Proper template not written yet
62
+ assert_match(/<h1>foo<\/h1>/m, contents, 'Looks a bit like the document with title')
63
+ assert_match(/blah/m, contents, 'Looks a bit like the document')
64
+ assert_match(/qux/m, contents, 'Looks a bit like the document')
49
65
  end
50
66
 
51
67
  def test_export_category()
@@ -43,7 +43,7 @@ class TestSQLiteBenchmark < Test::Unit::TestCase
43
43
  doc.append("This is various text that'll be used for testing")
44
44
  doc.append("Some more text with different letters?")
45
45
 
46
- measure('Save Document', 25) do
46
+ measure('Save Document', 100) do
47
47
  doc.dbid = nil
48
48
  @app.save_document(doc, true)
49
49
  end
@@ -67,7 +67,7 @@ class TestSQLiteBenchmark < Test::Unit::TestCase
67
67
  @app.install_clean()
68
68
 
69
69
  catparent = nil
70
- measure('Save Category', 25) do
70
+ measure('Save Category', 100) do
71
71
  cat = QDA::Category.new("About 'Something'", catparent, 'the "memo"')
72
72
  @app.save_category(cat)
73
73
  catparent = cat
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.10
3
3
  specification_version: 1
4
4
  name: weft-qda
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.9.8
7
- date: 2006-01-04
6
+ version: 1.0.0
7
+ date: 2006-02-17
8
8
  summary: GUI Qualitative Data Analysis Tool.
9
9
  require_paths:
10
10
  - lib