clevic 0.12.0 → 0.13.0.b1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +10 -0
- data/Manifest.txt +209 -30
- data/README.txt +16 -20
- data/Rakefile +8 -8
- data/TODO +6 -7
- data/bin/clevic +12 -73
- data/lib/clevic/action_builder.rb +168 -0
- data/lib/clevic/ar_methods.rb +120 -0
- data/lib/clevic/attribute_list.rb +56 -0
- data/lib/clevic/cache_table.rb +60 -37
- data/lib/clevic/default_view.rb +3 -16
- data/lib/clevic/delegate.rb +46 -0
- data/lib/clevic/emitter.rb +38 -0
- data/lib/clevic/extensions.rb +61 -114
- data/lib/clevic/field.rb +159 -228
- data/lib/clevic/field_valuer.rb +165 -0
- data/lib/clevic/filter_command.rb +2 -6
- data/lib/clevic/generic_format.rb +52 -0
- data/lib/clevic/{ui → icons}/icon.png +0 -0
- data/lib/clevic/many_field.rb +7 -0
- data/lib/clevic/model_builder.rb +234 -146
- data/lib/clevic/model_column.rb +61 -13
- data/lib/clevic/order_attribute.rb +10 -0
- data/lib/clevic/qt.rb +35 -0
- data/lib/clevic/qt/action_builder.rb +47 -0
- data/lib/clevic/qt/boolean_delegate.rb +8 -0
- data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
- data/lib/clevic/qt/clipboard.rb +35 -0
- data/lib/clevic/qt/combo_delegate.rb +198 -0
- data/lib/clevic/qt/delegates.rb +1 -0
- data/lib/clevic/qt/distinct_delegate.rb +35 -0
- data/lib/clevic/qt/extensions.rb +52 -0
- data/lib/clevic/qt/field.rb +18 -0
- data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
- data/lib/clevic/qt/relational_delegate.rb +87 -0
- data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
- data/lib/clevic/qt/set_delegate.rb +44 -0
- data/lib/clevic/qt/table_model.rb +331 -0
- data/lib/clevic/qt/table_view.rb +344 -0
- data/lib/clevic/qt/text_area_delegate.rb +8 -0
- data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
- data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
- data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
- data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
- data/lib/clevic/rails_models_loaders.rb +56 -0
- data/lib/clevic/record.rb +2 -17
- data/lib/clevic/sampler.rb +81 -0
- data/lib/clevic/sequel_ar_adapter.rb +215 -0
- data/lib/clevic/sequel_length_validation.rb +23 -0
- data/lib/clevic/sequel_meta.rb +65 -0
- data/lib/clevic/sequel_naked.rb +30 -0
- data/lib/clevic/swing.rb +38 -0
- data/lib/clevic/swing/action.rb +125 -0
- data/lib/clevic/swing/action_builder.rb +47 -0
- data/lib/clevic/swing/boolean_delegate.rb +26 -0
- data/lib/clevic/swing/browser.rb +282 -0
- data/lib/clevic/swing/cell_editor.rb +95 -0
- data/lib/clevic/swing/cell_renderer.rb +44 -0
- data/lib/clevic/swing/clipboard.rb +135 -0
- data/lib/clevic/swing/combo_delegate.rb +336 -0
- data/lib/clevic/swing/confirm_dialog.rb +57 -0
- data/lib/clevic/swing/delegate.rb +40 -0
- data/lib/clevic/swing/distinct_delegate.rb +30 -0
- data/lib/clevic/swing/extensions.rb +274 -0
- data/lib/clevic/swing/field.rb +35 -0
- data/lib/clevic/swing/relational_delegate.rb +48 -0
- data/lib/clevic/swing/row_header.rb +210 -0
- data/lib/clevic/swing/search_dialog.rb +230 -0
- data/lib/clevic/swing/selection_model.rb +90 -0
- data/lib/clevic/swing/set_delegate.rb +41 -0
- data/lib/clevic/swing/swing_table_index.rb +43 -0
- data/lib/clevic/swing/table_model.rb +200 -0
- data/lib/clevic/swing/table_view.rb +385 -0
- data/lib/clevic/swing/table_view_focus.rb +47 -0
- data/lib/clevic/swing/tag_delegate.rb +127 -0
- data/lib/clevic/swing/tag_editor.rb +101 -0
- data/lib/clevic/swing/text_area_delegate.rb +46 -0
- data/lib/clevic/swing/text_delegate.rb +31 -0
- data/lib/clevic/swing/ui/build.xml +74 -0
- data/lib/clevic/swing/ui/dist/README.TXT +33 -0
- data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
- data/lib/clevic/swing/ui/manifest.mf +3 -0
- data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
- data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
- data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
- data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
- data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
- data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
- data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
- data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
- data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
- data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
- data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
- data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
- data/lib/clevic/table_index.rb +100 -0
- data/lib/clevic/table_model.rb +54 -425
- data/lib/clevic/table_searcher.rb +113 -116
- data/lib/clevic/table_view.rb +171 -399
- data/lib/clevic/table_view_paste.rb +199 -0
- data/lib/clevic/version.rb +3 -2
- data/lib/clevic/view.rb +94 -43
- data/models/accounts_models.rb +13 -13
- data/models/minimal_models.rb +5 -9
- data/models/times_models.rb +19 -14
- data/models/times_psql_models.rb +10 -0
- data/models/times_sqlite_models.rb +1 -8
- data/models/values_models.rb +2 -8
- data/tasks/clevic.rake +1 -1
- data/tasks/rdoc.rake +1 -5
- data/tasks/website.rake +1 -1
- data/test/test_cache_table.rb +15 -29
- data/test/test_helper.rb +14 -83
- data/test/test_order_attribute.rb +1 -1
- data/test/test_table_model.rb +0 -21
- data/test/test_table_searcher.rb +67 -61
- metadata +262 -78
- data/lib/clevic.rb +0 -4
- data/lib/clevic/db_options.rb +0 -112
- data/lib/clevic/delegates.rb +0 -386
@@ -0,0 +1,165 @@
|
|
1
|
+
module Clevic
|
2
|
+
# to be included in something that responds to entity and field
|
3
|
+
# used for getting values from the entity based on the definitions
|
4
|
+
# in the field.
|
5
|
+
module FieldValuer
|
6
|
+
|
7
|
+
# the value for this index
|
8
|
+
# used to be gui_value, but that wasn't right
|
9
|
+
def raw_value
|
10
|
+
field.value_for( entity )
|
11
|
+
end
|
12
|
+
|
13
|
+
def display_value
|
14
|
+
field.do_format( raw_value ) unless raw_value.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def edit_value
|
18
|
+
field.do_edit_format( raw_value ) unless raw_value.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the value from an editable text representation
|
22
|
+
# of the value
|
23
|
+
def edit_value=( value )
|
24
|
+
# translate the value from the ui to something that
|
25
|
+
# the model will understand
|
26
|
+
self.attribute_value =
|
27
|
+
case
|
28
|
+
# allow flexibility in entering dates. For example
|
29
|
+
# 16jun, 16-jun, 16 jun, 16 jun 2007 would be accepted here
|
30
|
+
# TODO need to be cleverer about which year to use
|
31
|
+
# for when you're entering 16dec and you're in the next
|
32
|
+
# year
|
33
|
+
when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{1,2})[ /-]?(\w{3})$}
|
34
|
+
Date.parse( "#$1 #$2 #{Time.now.year.to_s}" )
|
35
|
+
|
36
|
+
# if a digit only is entered, fetch month and year from
|
37
|
+
# previous row
|
38
|
+
when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{1,2})$}
|
39
|
+
# year,month,day
|
40
|
+
prev_date = prev.attribute_value
|
41
|
+
if prev_date
|
42
|
+
Date.new( prev_date.year, prev_date.month, $1.to_i )
|
43
|
+
else
|
44
|
+
value
|
45
|
+
end
|
46
|
+
|
47
|
+
# this one is mostly to fix date strings that have come
|
48
|
+
# out of the db and been formatted
|
49
|
+
when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{2})[ /-](\w{3})[ /-](\d{2})$}
|
50
|
+
Date.parse( "#$1 #$2 20#$3" )
|
51
|
+
|
52
|
+
# allow lots of flexibility in entering times
|
53
|
+
# 01:17, 0117, 117, 1 17, are all accepted
|
54
|
+
when field.meta.type == :time && value =~ %r{^(\d{1,2}).?(\d{2})$}
|
55
|
+
Time.parse( "#$1:#$2" )
|
56
|
+
|
57
|
+
# remove thousand separators, allow for space and comma
|
58
|
+
# instead of . as a decimal separator
|
59
|
+
when field.meta.type == :decimal
|
60
|
+
# do various transforms
|
61
|
+
case
|
62
|
+
# accept a space or a comma instead of a . for floats
|
63
|
+
when value =~ /(.*?)(\d)[ ,](\d{2})$/
|
64
|
+
"#$1#$2.#$3"
|
65
|
+
else
|
66
|
+
value
|
67
|
+
end.gsub( ',', '' ) # strip remaining commas
|
68
|
+
|
69
|
+
else
|
70
|
+
value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_related( attribute, value )
|
75
|
+
candidates = field.related_class.adaptor.find( :all, :conditions => {attribute => value} )
|
76
|
+
if candidates.size != 1
|
77
|
+
raise "#{candidates.size} != 1 candidates for #{value}: #{candidates.inspect}"
|
78
|
+
end
|
79
|
+
candidates.first
|
80
|
+
end
|
81
|
+
|
82
|
+
# set the field value from a value that could be
|
83
|
+
# a text representation a-la edit_value, or possible
|
84
|
+
# a name from a related entity
|
85
|
+
def text_value=( value )
|
86
|
+
case field.display
|
87
|
+
when Symbol
|
88
|
+
# we have a related class of some kind,
|
89
|
+
self.attribute_value = find_related( field.display, value )
|
90
|
+
|
91
|
+
when NilClass
|
92
|
+
self.edit_value = value
|
93
|
+
|
94
|
+
when String
|
95
|
+
# allow plain strings, but not dotted paths
|
96
|
+
if field.display['.']
|
97
|
+
raise "display (#{field.display}) is a dotted path"
|
98
|
+
else
|
99
|
+
self.attribute_value = find_related( field.display.to_sym, value )
|
100
|
+
end
|
101
|
+
|
102
|
+
else
|
103
|
+
raise "display is a not a symbol or nil: #{display.inspect}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# fetch the value of the attribute, without following
|
108
|
+
# the full path. This will return a related entity for
|
109
|
+
# belongs_to or has_one relationships, or a plain value
|
110
|
+
# for model attributes
|
111
|
+
def attribute_value
|
112
|
+
entity.send( field.attribute )
|
113
|
+
end
|
114
|
+
|
115
|
+
# cache the writer method name since it's not likely to change
|
116
|
+
def writer
|
117
|
+
@writer ||= "#{field.attribute.to_s}="
|
118
|
+
end
|
119
|
+
|
120
|
+
# set the value of the attribute, without following the
|
121
|
+
# full path.
|
122
|
+
# TODO remove need to constantly recalculate the attribute writer
|
123
|
+
def attribute_value=( value )
|
124
|
+
entity.send( writer, value )
|
125
|
+
end
|
126
|
+
|
127
|
+
def tooltip
|
128
|
+
case
|
129
|
+
# show validation errors
|
130
|
+
when has_errors?
|
131
|
+
errors.join("\n")
|
132
|
+
|
133
|
+
# provide a tooltip when an empty relational field is encountered
|
134
|
+
# TODO should be part of field definition
|
135
|
+
when field.meta.type == :association
|
136
|
+
field.delegate.if_empty_message
|
137
|
+
|
138
|
+
# read-only field
|
139
|
+
when field.read_only?
|
140
|
+
field.tooltip_for( entity ) || 'Read-only'
|
141
|
+
|
142
|
+
else
|
143
|
+
field.tooltip_for( entity )
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class Valuer
|
148
|
+
include FieldValuer
|
149
|
+
attr_accessor :field, :entity
|
150
|
+
def initialize( field, entity )
|
151
|
+
@field, @entity = field, entity
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.valuer( field, entity )
|
156
|
+
Valuer.new(field,entity)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Provides the minimum necessary for valuing, ie for irb testing
|
161
|
+
module SimpleFieldValuer
|
162
|
+
include FieldValuer
|
163
|
+
attr_accessor :field, :entity
|
164
|
+
end
|
165
|
+
end
|
@@ -7,18 +7,14 @@ module Clevic
|
|
7
7
|
|
8
8
|
# Better make the status message now, before the indexes become invalid
|
9
9
|
@status_message =
|
10
|
-
|
11
|
-
"Filtered on #{filter_indexes.first.field.label} = #{filter_indexes.first.gui_value}"
|
12
|
-
rescue
|
13
|
-
"Filtered"
|
14
|
-
end
|
10
|
+
"Filtered on #{filter_indexes.first.field.label} = #{filter_indexes.first.display_value}"
|
15
11
|
end
|
16
12
|
|
17
13
|
# Do the filtering. Return true if successful, false otherwise.
|
18
14
|
def doit
|
19
15
|
begin
|
20
16
|
# store current AR conditions
|
21
|
-
@stored_conditions = @table_view.model.cache_table.
|
17
|
+
@stored_conditions = @table_view.model.cache_table.find_options
|
22
18
|
|
23
19
|
# store auto_new
|
24
20
|
@auto_new = @table_view.model.auto_new
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Clevic
|
2
|
+
|
3
|
+
#includers must provide meta and display
|
4
|
+
module GenericFormat
|
5
|
+
# Return true if the field is a date, datetime, time or timestamp.
|
6
|
+
# If display is nil, the value is calculated, so we need
|
7
|
+
# to check the value. Otherwise use the field metadata.
|
8
|
+
# Cache the result for the first non-nil value.
|
9
|
+
def is_date_time?( value )
|
10
|
+
if value.nil?
|
11
|
+
false
|
12
|
+
else
|
13
|
+
@is_date_time ||=
|
14
|
+
if display.nil?
|
15
|
+
[:time, :date, :datetime, :timestamp].include?( meta.type )
|
16
|
+
else
|
17
|
+
# it's a virtual field, so we need to use the value
|
18
|
+
value.is_a?( Date ) || value.is_a?( Time )
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# apply format to value. Use strftime for date_time types, or % for everything else.
|
24
|
+
# If format is a proc, pass value to it.
|
25
|
+
def do_generic_format( format, value )
|
26
|
+
begin
|
27
|
+
unless format.nil?
|
28
|
+
if format.is_a? Proc
|
29
|
+
format.call( value )
|
30
|
+
else
|
31
|
+
if is_date_time?( value )
|
32
|
+
value.strftime( format )
|
33
|
+
else
|
34
|
+
format % value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
else
|
38
|
+
value
|
39
|
+
end
|
40
|
+
rescue Exception => e
|
41
|
+
puts "format: #{format.inspect}"
|
42
|
+
puts "value.class: #{value.class.inspect}"
|
43
|
+
puts "value: #{value.inspect}"
|
44
|
+
puts e.message
|
45
|
+
puts e.backtrace
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
File without changes
|
data/lib/clevic/model_builder.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
# TODO not needed for 1.9
|
2
|
+
require 'hashery/orderedhash'
|
3
3
|
|
4
4
|
require 'clevic/table_model.rb'
|
5
|
-
require 'clevic/delegates.rb'
|
6
|
-
require 'clevic/text_delegate.rb'
|
7
5
|
require 'clevic/cache_table.rb'
|
8
6
|
require 'clevic/field.rb'
|
9
7
|
|
@@ -21,99 +19,187 @@ you need it.
|
|
21
19
|
|
22
20
|
To that end, there are 2 ways to define UIs:
|
23
21
|
|
24
|
-
- an Embedded View as part of the
|
22
|
+
- an Embedded View as part of the model (Sequel::Model) object (which is useful if you
|
25
23
|
want minimal framework overhead). Just show me the data, dammit.
|
26
24
|
|
27
25
|
- a Separate View in a separate class (which is useful when you want several
|
28
26
|
diffent views of the same underlying table). I want a neato-nifty UI that does
|
29
27
|
(relatively) complex things.
|
30
28
|
|
31
|
-
I've tried to consistently refer to an instance of an
|
32
|
-
as an 'entity'.
|
29
|
+
I've tried to consistently refer to an instance of an Sequel::Model subclass as an 'entity'.
|
33
30
|
|
34
31
|
==Embedded View
|
35
32
|
Minimal embedded definition is
|
36
33
|
|
37
|
-
class Position <
|
34
|
+
class Position < Sequel::Model
|
38
35
|
include Clevic::Record
|
39
36
|
end
|
40
37
|
|
41
38
|
which will build a fairly sensible default UI from the
|
42
39
|
entity's metadata. Obviously you can use open classes to do
|
43
40
|
|
41
|
+
class Position < Sequel::Model
|
42
|
+
one_to_many :transactions
|
43
|
+
many_to_one :account
|
44
|
+
end
|
45
|
+
|
44
46
|
class Position
|
45
47
|
include Clevic::Record
|
46
48
|
end
|
47
49
|
|
48
|
-
where Position is previously defined to inherit from ActiveRecord::Base.
|
49
|
-
|
50
50
|
A full-featured UI for an entity called Entry (part of an accounting database)
|
51
51
|
could be defined like this:
|
52
52
|
|
53
|
-
class Entry <
|
53
|
+
class Entry < Sequel::Model
|
54
|
+
belongs_to :invoice
|
55
|
+
belongs_to :activity
|
56
|
+
belongs_to :project
|
57
|
+
|
54
58
|
include Clevic::Record
|
55
59
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
60
|
+
# spans of time more than 8 ours are coloured violet
|
61
|
+
# because they're often the result of typos.
|
62
|
+
def time_color
|
63
|
+
return if self.end.nil? || start.nil?
|
64
|
+
'darkviolet' if self.end - start > 8.hours
|
65
|
+
end
|
66
|
+
|
67
|
+
# tooltip for spans of time > 8 hours
|
68
|
+
def time_tooltip
|
69
|
+
return if self.end.nil? || start.nil?
|
70
|
+
'Time interval greater than 8 hours' if self.end - start > 8.hours
|
71
|
+
end
|
72
|
+
|
61
73
|
define_ui do
|
62
|
-
|
63
|
-
plain :date, :format => '%d-%h-%y', :edit_format => '%d-%h-%Y'
|
64
|
-
plain :start, :format => '%H:%M'
|
65
|
-
plain :amount, :format => '%.2f'
|
66
|
-
# :set is mandatory for a restricted field
|
67
|
-
restricted :vat, :label => 'VAT', :set => %w{ yes no all }, :tooltip => 'Is VAT included?'
|
74
|
+
plain :date, :sample => '28-Dec-08'
|
68
75
|
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
76
|
+
# The project field
|
77
|
+
relational :project do |field|
|
78
|
+
field.display = 'project'
|
79
|
+
field.conditions = 'active = true'
|
80
|
+
field.order = 'lower(project)'
|
81
|
+
|
82
|
+
# handle data changed events. In this case,
|
83
|
+
# auto-fill-in the invoice field.
|
84
|
+
field.notify_data_changed do |entity_view, table_view, model_index|
|
85
|
+
if model_index.entity.invoice.nil?
|
86
|
+
entity_view.invoice_from_project( table_view, model_index ) do
|
87
|
+
# move here next if the invoice was changed
|
88
|
+
table_view.override_next_index model_index.choppy( :column => :start )
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
74
92
|
end
|
75
93
|
|
76
|
-
|
77
|
-
# and display them in the combo.
|
78
|
-
distinct :description, :conditions => 'now() - date <= interval( 1 year )'
|
79
|
-
|
80
|
-
# this is a read-only field
|
81
|
-
plain :origin, :read_only => true
|
94
|
+
relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
|
82
95
|
|
83
|
-
#
|
84
|
-
|
85
|
-
# :order is an ActiveRecord option to find, defining the order in which related entries will be displayed.
|
86
|
-
# :conditions is an ActiveRecord option to find, defining the subset of related entries to be displayed.
|
87
|
-
relational :debit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
|
88
|
-
relational :credit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
|
96
|
+
# call time_color method for foreground color value
|
97
|
+
plain :start, :foreground => :time_color, :tooltip => :time_tooltip
|
89
98
|
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
99
|
+
# another way to call time_color method for foreground color value
|
100
|
+
plain :end, :foreground => lambda{|x| x.time_color}, :tooltip => :time_tooltip
|
101
|
+
|
102
|
+
# multiline text
|
103
|
+
text :description, :sample => 'This is a long string designed to hold lots of data and description'
|
104
|
+
|
105
|
+
relational :activity do
|
106
|
+
display 'activity'
|
107
|
+
order 'lower(activity)'
|
108
|
+
sample 'Troubleshooting'
|
109
|
+
conditions 'active = true'
|
96
110
|
end
|
97
111
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
# You can also use a Proc for :display
|
102
|
-
plain :currency, :display => 'short', :label => 'Currency'
|
112
|
+
distinct :module, :tooltip => 'Module or sub-project'
|
113
|
+
plain :charge, :tooltip => 'Is this time billable?'
|
114
|
+
distinct :person, :default => 'John', :tooltip => 'The person who did the work'
|
103
115
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
116
|
+
records :order => 'date, start, id'
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.define_actions( view, action_builder )
|
120
|
+
action_builder.action :smart_copy, 'Smart Copy', :shortcut => 'Ctrl+"' do
|
121
|
+
smart_copy( view )
|
122
|
+
end
|
110
123
|
|
111
|
-
|
112
|
-
|
124
|
+
action_builder.action :invoice_from_project, 'Invoice from Project', :shortcut => 'Ctrl+Shift+I' do
|
125
|
+
invoice_from_project( view, view.current_index ) do
|
126
|
+
# execute the block if the invoice is changed
|
127
|
+
|
128
|
+
# save this before selection model is cleared
|
129
|
+
current_index = view.current_index
|
130
|
+
view.selection_model.clear
|
131
|
+
view.current_index = current_index.choppy( :column => :start )
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# do a smart copy from the previous line
|
137
|
+
def self.smart_copy( view )
|
138
|
+
view.sanity_check_read_only
|
139
|
+
view.sanity_check_ditto
|
113
140
|
|
114
|
-
#
|
115
|
-
|
141
|
+
# need a reference to current_index here, because selection_model.clear will
|
142
|
+
# invalidate view.current_index. And anyway, its shorter and easier to read.
|
143
|
+
current_index = view.current_index
|
144
|
+
if current_index.row >= 1
|
145
|
+
# fetch previous item
|
146
|
+
previous_item = view.model.collection[current_index.row - 1]
|
147
|
+
|
148
|
+
# copy the relevant fields
|
149
|
+
current_index.entity.date = previous_item.date if current_index.entity.date.blank?
|
150
|
+
# depends on previous line
|
151
|
+
current_index.entity.start = previous_item.end if current_index.entity.date == previous_item.date
|
152
|
+
|
153
|
+
# copy rest of fields
|
154
|
+
[:project, :invoice, :activity, :module, :charge, :person].each do |attr|
|
155
|
+
current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
|
156
|
+
end
|
157
|
+
|
158
|
+
# tell view to update
|
159
|
+
view.model.data_changed do |change|
|
160
|
+
change.top_left = current_index.choppy( :column => 0 )
|
161
|
+
change.bottom_right = current_index.choppy( :column => view.model.fields.size - 1 )
|
162
|
+
end
|
163
|
+
|
164
|
+
# move to the first empty time field
|
165
|
+
next_field =
|
166
|
+
if current_index.entity.start.blank?
|
167
|
+
:start
|
168
|
+
else
|
169
|
+
:end
|
170
|
+
end
|
171
|
+
|
172
|
+
# next cursor location
|
173
|
+
view.selection_model.clear
|
174
|
+
view.current_index = current_index.choppy( :column => next_field )
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Auto-complete invoice number field from project.
|
179
|
+
# &block will be executed if an invoice was assigned
|
180
|
+
# If block takes one parameter, pass the new invoice.
|
181
|
+
def self.invoice_from_project( table_view, current_index, &block )
|
182
|
+
if current_index.entity.project != nil
|
183
|
+
# most recent entry, ordered in reverse
|
184
|
+
invoice = current_index.entity.project.latest_invoice
|
185
|
+
unless invoice.nil?
|
186
|
+
# make a reference to the invoice
|
187
|
+
current_index.entity.invoice = invoice
|
188
|
+
|
189
|
+
# update view from top_left to bottom_right
|
190
|
+
table_view.model.data_changed( current_index.choppy( :column => :invoice ) )
|
191
|
+
|
192
|
+
unless block.nil?
|
193
|
+
if block.arity == 1
|
194
|
+
block.call( invoice )
|
195
|
+
else
|
196
|
+
block.call
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
116
201
|
end
|
202
|
+
|
117
203
|
end
|
118
204
|
|
119
205
|
== Separate View
|
@@ -121,7 +207,7 @@ could be defined like this:
|
|
121
207
|
To define a separate ui class, do something like this:
|
122
208
|
class Prospect < Clevic::View
|
123
209
|
|
124
|
-
# This is the
|
210
|
+
# This is the Sequel::Model descendant
|
125
211
|
entity_class Position
|
126
212
|
|
127
213
|
# This must return a ModelBuilder instance, which is made easier
|
@@ -136,6 +222,8 @@ To define a separate ui class, do something like this:
|
|
136
222
|
# use the define_ui block from Position
|
137
223
|
mb.exec_ui_block( Position )
|
138
224
|
|
225
|
+
# any other ModelBuilder code can go here too
|
226
|
+
|
139
227
|
# use a different recordset
|
140
228
|
mb.records :conditions => "status in ('prospect','open')", :order => 'date desc,code'
|
141
229
|
end
|
@@ -174,6 +262,9 @@ can use DSL-style acessors (no assignment =) or assignment statement.
|
|
174
262
|
plain
|
175
263
|
is an ordinary editable field. Boolean values are displayed as checkboxes.
|
176
264
|
|
265
|
+
text
|
266
|
+
is a multiline editable field.
|
267
|
+
|
177
268
|
relational
|
178
269
|
displays a set of values pulled from a belongs_to (many-to-one) relationship.
|
179
270
|
In other words all the possible related entities that this one could belong_to. Some
|
@@ -198,7 +289,7 @@ definition from somewhere else. It may go away and be replaced by remove.
|
|
198
289
|
The attribute symbol is required, and is the first parameter after the field type. It must refer
|
199
290
|
to a method already defined in the entity. In other words any of:
|
200
291
|
- a db column
|
201
|
-
- a relationship (
|
292
|
+
- a relationship (one_to_many, etc)
|
202
293
|
- a plain method that takes no parameters.
|
203
294
|
|
204
295
|
will work. Named scopes might also work, but I haven't tried them yet.
|
@@ -223,7 +314,7 @@ See Clevic::Field properties for available options.
|
|
223
314
|
|
224
315
|
=== Menu Items
|
225
316
|
|
226
|
-
You can define view/model specific
|
317
|
+
You can define view/model specific menu items.
|
227
318
|
These will be added to the Edit menu, show up on context-click in the table
|
228
319
|
display, and can have optional keyboard shortcuts:
|
229
320
|
|
@@ -242,17 +333,20 @@ display, and can have optional keyboard shortcuts:
|
|
242
333
|
|
243
334
|
=== Notifications
|
244
335
|
|
245
|
-
The following method will be called whenever data is changed, ie a field edit is completed:
|
246
|
-
|
247
|
-
def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
|
248
|
-
end
|
249
|
-
|
250
336
|
Key presses will be sent here:
|
251
337
|
|
338
|
+
# may also be defined as class methods on an entity class.
|
252
339
|
def notify_key_press( table_view, key_press_event, current_model_index )
|
253
340
|
end
|
254
341
|
|
255
|
-
|
342
|
+
Fields have a property called notify_data_changed, which is called whenever
|
343
|
+
the field value changes. There is also an view method:
|
344
|
+
|
345
|
+
def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
|
346
|
+
end
|
347
|
+
|
348
|
+
But note that this will override the delegation to the field notify_data_changed
|
349
|
+
unless super is called.
|
256
350
|
|
257
351
|
=== Tab Order
|
258
352
|
|
@@ -271,11 +365,14 @@ class ModelBuilder
|
|
271
365
|
|
272
366
|
# Create a definition for entity_view (subclass of Clevic::View).
|
273
367
|
# Then execute block using self.instance_eval.
|
368
|
+
# entity_view must respond to entity_class, and if title is called, it
|
369
|
+
# must respond to title.
|
274
370
|
def initialize( entity_view, &block )
|
275
371
|
@entity_view = entity_view
|
276
372
|
@auto_new = true
|
277
373
|
@read_only = false
|
278
|
-
|
374
|
+
# TODO not needed for 1.9
|
375
|
+
@fields = OrderedHash.new
|
279
376
|
exec_ui_block( &block )
|
280
377
|
end
|
281
378
|
|
@@ -304,7 +401,8 @@ class ModelBuilder
|
|
304
401
|
# The collection of Clevic::Field instances where visible == true.
|
305
402
|
# the visible may go away.
|
306
403
|
def fields
|
307
|
-
@fields.reject{|id,field| !field.visible}
|
404
|
+
#~ @fields.reject{|id,field| !field.visible}
|
405
|
+
@fields
|
308
406
|
end
|
309
407
|
|
310
408
|
# return the index of the named field in the collection of fields.
|
@@ -314,7 +412,7 @@ class ModelBuilder
|
|
314
412
|
retval
|
315
413
|
end
|
316
414
|
|
317
|
-
# The
|
415
|
+
# The ORM class
|
318
416
|
def entity_class
|
319
417
|
@entity_view.entity_class
|
320
418
|
end
|
@@ -331,18 +429,29 @@ class ModelBuilder
|
|
331
429
|
|
332
430
|
# should this table automatically show a new blank record?
|
333
431
|
def auto_new?; @auto_new; end
|
432
|
+
|
433
|
+
# DSL for changing the title
|
434
|
+
def title( value )
|
435
|
+
entity_view.title = value
|
436
|
+
end
|
334
437
|
|
335
438
|
# an ordinary field, edited in place with a text box
|
336
439
|
def plain( attribute, options = {}, &block )
|
337
440
|
read_only_default!( attribute, options )
|
338
|
-
@fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
441
|
+
field = @fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
442
|
+
field.delegate =
|
443
|
+
if field.meta.type == :boolean
|
444
|
+
BooleanDelegate.new( field )
|
445
|
+
else
|
446
|
+
TextDelegate.new( field )
|
447
|
+
end
|
339
448
|
end
|
340
449
|
|
341
450
|
# an ordinary field like plain, except that a larger edit area can be used
|
342
451
|
def text( attribute, options = {}, &block )
|
343
452
|
read_only_default!( attribute, options )
|
344
453
|
field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
345
|
-
field.delegate =
|
454
|
+
field.delegate = TextAreaDelegate.new( field )
|
346
455
|
@fields[attribute] = field
|
347
456
|
end
|
348
457
|
|
@@ -350,7 +459,7 @@ class ModelBuilder
|
|
350
459
|
# a combo box containing all values for this field from the table.
|
351
460
|
def distinct( attribute, options = {}, &block )
|
352
461
|
field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
353
|
-
field.delegate = DistinctDelegate.new(
|
462
|
+
field.delegate = DistinctDelegate.new( field )
|
354
463
|
@fields[attribute] = field
|
355
464
|
end
|
356
465
|
|
@@ -364,7 +473,7 @@ class ModelBuilder
|
|
364
473
|
field.format ||= lambda{|x| field.set[x]}
|
365
474
|
end
|
366
475
|
|
367
|
-
field.delegate = SetDelegate.new(
|
476
|
+
field.delegate = SetDelegate.new( field )
|
368
477
|
@fields[attribute] = field
|
369
478
|
end
|
370
479
|
|
@@ -373,23 +482,37 @@ class ModelBuilder
|
|
373
482
|
options[:restricted] = true
|
374
483
|
combo( attribute, options, &block )
|
375
484
|
end
|
376
|
-
|
377
|
-
#
|
485
|
+
|
486
|
+
# For many_to_one relationships.
|
487
|
+
# Edited with a combo box using values from the specified
|
378
488
|
# path on the foreign key model object
|
379
489
|
# if options[:format] has a value, it's used either as a block
|
380
490
|
# or as a dotted path
|
381
491
|
def relational( attribute, options = {}, &block )
|
382
492
|
field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
383
|
-
|
384
|
-
|
385
|
-
|
493
|
+
field.delegate = RelationalDelegate.new( field )
|
494
|
+
@fields[attribute] = field
|
495
|
+
end
|
496
|
+
|
497
|
+
def tags( attribute, options = {}, &block )
|
498
|
+
field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
386
499
|
|
387
|
-
#
|
388
|
-
|
389
|
-
|
500
|
+
# build a collection setter if necessary
|
501
|
+
unless entity_class.instance_methods.include? "#{attribute}="
|
502
|
+
raise NotImplementedError, "Need to build a collection setter for '#{attribute}='"
|
503
|
+
end
|
504
|
+
|
505
|
+
field.delegate = TagDelegate.new( field )
|
390
506
|
@fields[attribute] = field
|
391
507
|
end
|
392
508
|
|
509
|
+
# force a checkbox
|
510
|
+
def check( attribute, options = {}, &block )
|
511
|
+
read_only_default!( attribute, options )
|
512
|
+
field = @fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
513
|
+
field.delegate = BooleanDelegate.new( field )
|
514
|
+
end
|
515
|
+
|
393
516
|
# mostly used in the new block to define the set of records
|
394
517
|
# for the TableModel, but may also be
|
395
518
|
# used as an accessor for records.
|
@@ -411,7 +534,7 @@ class ModelBuilder
|
|
411
534
|
# as editable in the table. Any belongs_to relations are used to build
|
412
535
|
# combo boxes. Default ordering is the primary key.
|
413
536
|
# Subscriber is already defined elsewhere as a subclass
|
414
|
-
# of
|
537
|
+
# of an ORM class ie Sequel::Model:
|
415
538
|
# class Subscriber
|
416
539
|
# include Clevic::Record
|
417
540
|
# define_ui do
|
@@ -427,51 +550,26 @@ class ModelBuilder
|
|
427
550
|
# * :name
|
428
551
|
# * :title
|
429
552
|
# * :username
|
553
|
+
# * :to_s
|
430
554
|
def default_ui
|
431
|
-
# combine reflections and attributes into one set
|
432
|
-
reflections = entity_class.reflections.keys.map{|x| x.to_s}
|
433
|
-
ui_columns = entity_class.columns.reject{|x| x.name == entity_class.primary_key }.map do |column|
|
434
|
-
# TODO there must be a better way to do this
|
435
|
-
att = column.name.gsub( /_id$/, '' )
|
436
|
-
if reflections.include?( att )
|
437
|
-
att
|
438
|
-
else
|
439
|
-
column.name
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
555
|
# don't create an empty record, because sometimes there are
|
444
556
|
# validations that will cause trouble
|
445
557
|
auto_new false
|
446
558
|
|
447
559
|
# build columns
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
# try to find a sensible display class. Default to to_s
|
456
|
-
display_method =
|
457
|
-
%w{#{entity_class.name} name title username}.find( lambda{ 'to_s' } ) do |m|
|
458
|
-
related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
|
459
|
-
end
|
460
|
-
|
461
|
-
# set the display method
|
462
|
-
relational column.to_sym, :display => display_method
|
463
|
-
else
|
464
|
-
plain column.to_sym
|
465
|
-
end
|
466
|
-
rescue
|
467
|
-
puts $!.message
|
468
|
-
puts $!.backtrace
|
469
|
-
# just do a plain
|
470
|
-
puts "Doing plain for #{entity_class}.#{column}"
|
471
|
-
plain column.to_sym
|
560
|
+
entity_class.attributes.each do |column,model_column|
|
561
|
+
begin
|
562
|
+
if model_column.association?
|
563
|
+
relational column
|
564
|
+
else
|
565
|
+
plain column
|
472
566
|
end
|
473
|
-
|
474
|
-
|
567
|
+
rescue
|
568
|
+
puts $!.message
|
569
|
+
puts $!.backtrace
|
570
|
+
# just do a plain
|
571
|
+
puts "Doing plain for #{entity_class}.#{column}"
|
572
|
+
plain column
|
475
573
|
end
|
476
574
|
end
|
477
575
|
records :order => entity_class.primary_key
|
@@ -484,22 +582,27 @@ class ModelBuilder
|
|
484
582
|
|
485
583
|
# This takes all the information collected
|
486
584
|
# by the other methods, and returns a new TableModel
|
487
|
-
# with the given
|
488
|
-
def build(
|
585
|
+
# with the given parent (usually a TableView) as its parent.
|
586
|
+
def build( parent )
|
489
587
|
# build the model with all it's collections
|
490
588
|
# using @model here because otherwise the view's
|
491
589
|
# reference to this very same model is garbage collected.
|
492
|
-
@model = Clevic::TableModel.new
|
590
|
+
@model = Clevic::TableModel.new
|
493
591
|
@model.builder = self
|
494
592
|
@model.entity_view = entity_view
|
495
593
|
@model.fields = @fields.values
|
496
594
|
@model.read_only = @read_only
|
497
595
|
@model.auto_new = auto_new?
|
498
596
|
|
499
|
-
#
|
500
|
-
|
501
|
-
|
502
|
-
|
597
|
+
# set view name
|
598
|
+
parent.object_name = @object_name if parent.respond_to? :object_name
|
599
|
+
|
600
|
+
# set UI parent for all delegates
|
601
|
+
# and model for each field
|
602
|
+
fields.each do |id,field|
|
603
|
+
field.delegate.parent = parent unless field.delegate.nil?
|
604
|
+
field.model = @model
|
605
|
+
end
|
503
606
|
|
504
607
|
# the data
|
505
608
|
@model.collection = records
|
@@ -509,19 +612,6 @@ class ModelBuilder
|
|
509
612
|
|
510
613
|
protected
|
511
614
|
|
512
|
-
# Add ActiveRecord :include options for foreign keys, but it takes up too much memory,
|
513
|
-
# and actually takes longer to load a data set.
|
514
|
-
#--
|
515
|
-
# TODO ActiveRecord-2.1 has smarter includes
|
516
|
-
def add_include_options
|
517
|
-
fields.each do |id,field|
|
518
|
-
if field.delegate.class == RelationalDelegate
|
519
|
-
@options[:include] ||= []
|
520
|
-
@options[:include] << field.attribute
|
521
|
-
end
|
522
|
-
end
|
523
|
-
end
|
524
|
-
|
525
615
|
# set a sensible read-only value if it isn't already specified in options
|
526
616
|
def read_only_default!( attribute, options )
|
527
617
|
# sensible defaults for read-only-ness
|
@@ -537,8 +627,7 @@ protected
|
|
537
627
|
|
538
628
|
when entity_class.reflections.include?( attribute )
|
539
629
|
# one-to-one relationships can be edited. many-to-one certainly can't
|
540
|
-
|
541
|
-
reflection.macro != :has_one
|
630
|
+
entity_class.meta[attribute].type != :many_to_one
|
542
631
|
|
543
632
|
when entity_class.instance_methods.include?( attribute.to_s )
|
544
633
|
# read-only if there's no setter for the attribute
|
@@ -566,7 +655,6 @@ protected
|
|
566
655
|
# Called by records( *args )
|
567
656
|
def get_records
|
568
657
|
if @records.nil?
|
569
|
-
#~ add_include_options
|
570
658
|
@records = CacheTable.new( entity_class, @find_options )
|
571
659
|
end
|
572
660
|
@records
|