weft-qda 0.9.8 → 1.0.0

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.
@@ -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