weft-qda 0.9.6 → 0.9.8
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 +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() }
|