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.
- data/lib/weft.rb +3 -1
- data/lib/weft/WEFT-VERSION-STRING.rb +1 -1
- data/lib/weft/backend/sqlite/schema.rb +12 -0
- data/lib/weft/exceptions.rb +5 -1
- data/lib/weft/filters.rb +5 -2
- data/lib/weft/query.rb +12 -7
- data/lib/weft/wxgui.rb +11 -11
- data/lib/weft/wxgui/controls/category_tree.rb +20 -5
- data/lib/weft/wxgui/dialogs.rb +1 -1
- data/lib/weft/wxgui/error_handler.rb +9 -7
- data/lib/weft/wxgui/inspectors.rb +8 -2
- data/lib/weft/wxgui/inspectors/query.rb +8 -2
- data/lib/weft/wxgui/lang/en.rb +2 -1
- data/test/003-code.rb +7 -0
- data/test/005-query_review.rb +18 -1
- data/test/007-output_filters.rb +18 -2
- data/test/009c_backend_sqlite_bench.rb +2 -2
- metadata +2 -2
data/lib/weft.rb
CHANGED
@@ -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?(
|
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.
|
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 - ?)
|
data/lib/weft/exceptions.rb
CHANGED
data/lib/weft/filters.rb
CHANGED
@@ -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
|
-
|
25
|
-
|
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)
|
data/lib/weft/query.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# A
|
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.
|
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 =
|
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
|
-
|
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
|
data/lib/weft/wxgui.rb
CHANGED
@@ -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
|
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.
|
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(
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
222
|
-
|
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
|
data/lib/weft/wxgui/dialogs.rb
CHANGED
@@ -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'
|
44
|
-
'os'
|
45
|
-
'config'
|
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'
|
50
|
-
'backtrace'
|
51
|
-
|
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
|
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,
|
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
|
-
|
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
|
data/lib/weft/wxgui/lang/en.rb
CHANGED
@@ -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
|
|
data/test/003-code.rb
CHANGED
@@ -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
|
data/test/005-query_review.rb
CHANGED
@@ -101,16 +101,26 @@ class TestQuery < Test::Unit::TestCase
|
|
101
101
|
|
102
102
|
|
103
103
|
|
104
|
-
def
|
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()
|
data/test/007-output_filters.rb
CHANGED
@@ -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.
|
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
|
-
|
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',
|
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',
|
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