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.
- data/lib/weft.rb +16 -1
- data/lib/weft/WEFT-VERSION-STRING.rb +1 -1
- data/lib/weft/application.rb +17 -74
- data/lib/weft/backend.rb +6 -32
- data/lib/weft/backend/sqlite.rb +222 -164
- data/lib/weft/backend/sqlite/category_tree.rb +52 -48
- data/lib/weft/backend/sqlite/database.rb +57 -0
- data/lib/weft/backend/sqlite/upgradeable.rb +7 -0
- data/lib/weft/broadcaster.rb +90 -0
- data/lib/weft/category.rb +139 -47
- data/lib/weft/codereview.rb +160 -0
- data/lib/weft/coding.rb +74 -23
- data/lib/weft/document.rb +23 -10
- data/lib/weft/exceptions.rb +10 -0
- data/lib/weft/filters.rb +47 -224
- data/lib/weft/filters/indexers.rb +137 -0
- data/lib/weft/filters/input.rb +118 -0
- data/lib/weft/filters/output.rb +101 -0
- data/lib/weft/filters/templates.rb +80 -0
- data/lib/weft/filters/win32backtick.rb +246 -0
- data/lib/weft/query.rb +169 -0
- data/lib/weft/wxgui.rb +349 -294
- data/lib/weft/wxgui/constants.rb +43 -0
- data/lib/weft/wxgui/controls.rb +6 -0
- data/lib/weft/wxgui/controls/category_dropdown.rb +192 -0
- data/lib/weft/wxgui/controls/category_tree.rb +314 -0
- data/lib/weft/wxgui/controls/document_list.rb +97 -0
- data/lib/weft/wxgui/controls/multitype_control.rb +37 -0
- data/lib/weft/wxgui/{inspectors → controls}/textcontrols.rb +235 -64
- data/lib/weft/wxgui/dialogs.rb +144 -41
- data/lib/weft/wxgui/error_handler.rb +116 -36
- data/lib/weft/wxgui/exceptions.rb +7 -0
- data/lib/weft/wxgui/inspectors.rb +61 -208
- data/lib/weft/wxgui/inspectors/category.rb +19 -16
- data/lib/weft/wxgui/inspectors/codereview.rb +90 -132
- data/lib/weft/wxgui/inspectors/document.rb +12 -8
- data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -56
- data/lib/weft/wxgui/inspectors/query.rb +284 -0
- data/lib/weft/wxgui/inspectors/script.rb +147 -23
- data/lib/weft/wxgui/lang/en.rb +69 -0
- data/lib/weft/wxgui/sidebar.rb +90 -432
- data/lib/weft/wxgui/utilities.rb +70 -91
- data/lib/weft/wxgui/workarea.rb +150 -43
- data/share/icons/category.ico +0 -0
- data/share/icons/category.xpm +109 -0
- data/share/icons/codereview.ico +0 -0
- data/share/icons/codereview.xpm +54 -0
- data/share/icons/d_and_c.xpm +126 -0
- data/share/icons/document.ico +0 -0
- data/share/icons/document.xpm +70 -0
- data/share/icons/project.ico +0 -0
- data/share/icons/query.ico +0 -0
- data/share/icons/query.xpm +56 -0
- data/{lib/weft/wxgui → share/icons}/search.xpm +0 -0
- data/share/icons/weft.ico +0 -0
- data/share/icons/weft.xpm +62 -0
- data/share/icons/weft16.ico +0 -0
- data/share/icons/weft32.ico +0 -0
- data/share/templates/category_plain.html +18 -0
- data/share/templates/codereview_plain.html +18 -0
- data/share/templates/document_plain.html +13 -0
- data/share/templates/document_plain.txt +7 -0
- data/test/001-document.rb +55 -36
- data/test/002-category.rb +81 -6
- data/test/003-code.rb +8 -4
- data/test/004-application.rb +13 -34
- data/test/005-query_review.rb +139 -0
- data/test/006-filters.rb +54 -42
- data/test/007-output_filters.rb +113 -0
- data/test/009a-backend_sqlite_basic.rb +95 -24
- data/test/009b-backend_sqlite_complex.rb +43 -62
- data/test/009c_backend_sqlite_bench.rb +5 -10
- data/test/053-doc_inspector.rb +46 -0
- data/test/055-query_window.rb +50 -0
- data/test/all-tests.rb +1 -0
- data/test/test-common.rb +19 -0
- data/test/testdata/empty.qdp +0 -0
- data/test/testdata/simple with space.pdf +0 -0
- data/test/testdata/simple.pdf +0 -0
- data/weft-qda.rb +40 -7
- metadata +74 -14
- data/lib/weft/wxgui/category.xpm +0 -26
- data/lib/weft/wxgui/document.xpm +0 -25
- data/lib/weft/wxgui/inspectors/search.rb +0 -265
- data/lib/weft/wxgui/mondrian.xpm +0 -44
- data/lib/weft/wxgui/weft16.xpm +0 -31
data/lib/weft.rb
CHANGED
@@ -1,9 +1,24 @@
|
|
1
|
-
|
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.
|
1
|
+
WEFT_VERSION_STRING = '0.9.8a'
|
data/lib/weft/application.rb
CHANGED
@@ -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
|
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
|
42
|
-
|
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
|
-
|
52
|
-
|
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
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
65
|
+
@dirty = true
|
101
66
|
end
|
102
67
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
data/lib/weft/backend.rb
CHANGED
@@ -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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
data/lib/weft/backend/sqlite.rb
CHANGED
@@ -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
|
-
|
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
|
-
#
|
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
|
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
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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(
|
67
|
+
def end()
|
115
68
|
@cat_tree = nil
|
116
|
-
@dbh
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
125
|
-
@
|
97
|
+
|
98
|
+
@dbh.close() if not @dbh.closed?
|
126
99
|
FileUtils.copy(@tmpfile, @dbfile)
|
127
|
-
@dbh =
|
128
|
-
|
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
|
-
@
|
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 =
|
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
|
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 :
|
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
|
-
|
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
|
-
|
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
|
-
#
|
241
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
274
|
-
|
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
|
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(
|
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.
|
371
|
+
elem.each_child { | c | append_f.call(cat, c) }
|
329
372
|
end
|
330
|
-
|
331
|
-
|
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
|
-
|
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
|
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
|
-
|
408
|
-
|
409
|
-
|
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
|
-
|
415
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
449
|
-
|
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
|
-
|
471
|
-
|
472
|
-
|
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
|
-
|
477
|
-
|
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
|
-
|
483
|
-
|
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
|
-
|
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.
|
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() }
|