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.
Files changed (119) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +209 -30
  3. data/README.txt +16 -20
  4. data/Rakefile +8 -8
  5. data/TODO +6 -7
  6. data/bin/clevic +12 -73
  7. data/lib/clevic/action_builder.rb +168 -0
  8. data/lib/clevic/ar_methods.rb +120 -0
  9. data/lib/clevic/attribute_list.rb +56 -0
  10. data/lib/clevic/cache_table.rb +60 -37
  11. data/lib/clevic/default_view.rb +3 -16
  12. data/lib/clevic/delegate.rb +46 -0
  13. data/lib/clevic/emitter.rb +38 -0
  14. data/lib/clevic/extensions.rb +61 -114
  15. data/lib/clevic/field.rb +159 -228
  16. data/lib/clevic/field_valuer.rb +165 -0
  17. data/lib/clevic/filter_command.rb +2 -6
  18. data/lib/clevic/generic_format.rb +52 -0
  19. data/lib/clevic/{ui → icons}/icon.png +0 -0
  20. data/lib/clevic/many_field.rb +7 -0
  21. data/lib/clevic/model_builder.rb +234 -146
  22. data/lib/clevic/model_column.rb +61 -13
  23. data/lib/clevic/order_attribute.rb +10 -0
  24. data/lib/clevic/qt.rb +35 -0
  25. data/lib/clevic/qt/action_builder.rb +47 -0
  26. data/lib/clevic/qt/boolean_delegate.rb +8 -0
  27. data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
  28. data/lib/clevic/qt/clipboard.rb +35 -0
  29. data/lib/clevic/qt/combo_delegate.rb +198 -0
  30. data/lib/clevic/qt/delegates.rb +1 -0
  31. data/lib/clevic/qt/distinct_delegate.rb +35 -0
  32. data/lib/clevic/qt/extensions.rb +52 -0
  33. data/lib/clevic/qt/field.rb +18 -0
  34. data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
  35. data/lib/clevic/qt/relational_delegate.rb +87 -0
  36. data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
  37. data/lib/clevic/qt/set_delegate.rb +44 -0
  38. data/lib/clevic/qt/table_model.rb +331 -0
  39. data/lib/clevic/qt/table_view.rb +344 -0
  40. data/lib/clevic/qt/text_area_delegate.rb +8 -0
  41. data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
  42. data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
  43. data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
  44. data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
  45. data/lib/clevic/rails_models_loaders.rb +56 -0
  46. data/lib/clevic/record.rb +2 -17
  47. data/lib/clevic/sampler.rb +81 -0
  48. data/lib/clevic/sequel_ar_adapter.rb +215 -0
  49. data/lib/clevic/sequel_length_validation.rb +23 -0
  50. data/lib/clevic/sequel_meta.rb +65 -0
  51. data/lib/clevic/sequel_naked.rb +30 -0
  52. data/lib/clevic/swing.rb +38 -0
  53. data/lib/clevic/swing/action.rb +125 -0
  54. data/lib/clevic/swing/action_builder.rb +47 -0
  55. data/lib/clevic/swing/boolean_delegate.rb +26 -0
  56. data/lib/clevic/swing/browser.rb +282 -0
  57. data/lib/clevic/swing/cell_editor.rb +95 -0
  58. data/lib/clevic/swing/cell_renderer.rb +44 -0
  59. data/lib/clevic/swing/clipboard.rb +135 -0
  60. data/lib/clevic/swing/combo_delegate.rb +336 -0
  61. data/lib/clevic/swing/confirm_dialog.rb +57 -0
  62. data/lib/clevic/swing/delegate.rb +40 -0
  63. data/lib/clevic/swing/distinct_delegate.rb +30 -0
  64. data/lib/clevic/swing/extensions.rb +274 -0
  65. data/lib/clevic/swing/field.rb +35 -0
  66. data/lib/clevic/swing/relational_delegate.rb +48 -0
  67. data/lib/clevic/swing/row_header.rb +210 -0
  68. data/lib/clevic/swing/search_dialog.rb +230 -0
  69. data/lib/clevic/swing/selection_model.rb +90 -0
  70. data/lib/clevic/swing/set_delegate.rb +41 -0
  71. data/lib/clevic/swing/swing_table_index.rb +43 -0
  72. data/lib/clevic/swing/table_model.rb +200 -0
  73. data/lib/clevic/swing/table_view.rb +385 -0
  74. data/lib/clevic/swing/table_view_focus.rb +47 -0
  75. data/lib/clevic/swing/tag_delegate.rb +127 -0
  76. data/lib/clevic/swing/tag_editor.rb +101 -0
  77. data/lib/clevic/swing/text_area_delegate.rb +46 -0
  78. data/lib/clevic/swing/text_delegate.rb +31 -0
  79. data/lib/clevic/swing/ui/build.xml +74 -0
  80. data/lib/clevic/swing/ui/dist/README.TXT +33 -0
  81. data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
  82. data/lib/clevic/swing/ui/manifest.mf +3 -0
  83. data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
  84. data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
  85. data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
  86. data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
  87. data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
  88. data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
  89. data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
  90. data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
  91. data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
  92. data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
  93. data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
  94. data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
  95. data/lib/clevic/table_index.rb +100 -0
  96. data/lib/clevic/table_model.rb +54 -425
  97. data/lib/clevic/table_searcher.rb +113 -116
  98. data/lib/clevic/table_view.rb +171 -399
  99. data/lib/clevic/table_view_paste.rb +199 -0
  100. data/lib/clevic/version.rb +3 -2
  101. data/lib/clevic/view.rb +94 -43
  102. data/models/accounts_models.rb +13 -13
  103. data/models/minimal_models.rb +5 -9
  104. data/models/times_models.rb +19 -14
  105. data/models/times_psql_models.rb +10 -0
  106. data/models/times_sqlite_models.rb +1 -8
  107. data/models/values_models.rb +2 -8
  108. data/tasks/clevic.rake +1 -1
  109. data/tasks/rdoc.rake +1 -5
  110. data/tasks/website.rake +1 -1
  111. data/test/test_cache_table.rb +15 -29
  112. data/test/test_helper.rb +14 -83
  113. data/test/test_order_attribute.rb +1 -1
  114. data/test/test_table_model.rb +0 -21
  115. data/test/test_table_searcher.rb +67 -61
  116. metadata +262 -78
  117. data/lib/clevic.rb +0 -4
  118. data/lib/clevic/db_options.rb +0 -112
  119. data/lib/clevic/delegates.rb +0 -386
@@ -1,10 +1,11 @@
1
1
  module Clevic
2
2
 
3
3
  # A subclass of Clevic::DefaultView is created by Clevic::Record
4
- # when the latter is included in an ActiveRecord::Base subclass.
4
+ # when the latter is included in a Sequel::Model
5
+ # subclass.
5
6
  #
6
7
  # The Clevic::DefaultView subclass knows how to:
7
- # - build a fairly sensible UI from the the ActiveRecord::Base metadata.
8
+ # - build a fairly sensible UI from the the ORM model metadata.
8
9
  # - create a UI definition using a class method called define_ui.
9
10
  #
10
11
  # See Clevic::ModelBuilder for an example.
@@ -17,20 +18,6 @@ module Clevic
17
18
  end
18
19
  end
19
20
 
20
- def self.define_ui_block( &block )
21
- @define_ui_block ||= block
22
- end
23
-
24
- def define_ui
25
- if self.class.define_ui_block.nil?
26
- # use the define_ui from Clevic::View to build a default UI
27
- super
28
- else
29
- # use the provided block
30
- model_builder( &self.class.define_ui_block )
31
- end
32
- end
33
-
34
21
  def title
35
22
  @title ||= entity_class.name.demodulize.tableize.humanize
36
23
  end
@@ -0,0 +1,46 @@
1
+ require 'clevic/field_valuer.rb'
2
+
3
+ module Clevic
4
+
5
+ # This has both a field and an entity, so it's a perfect candidate
6
+ # for including FieldValuer, which it does.
7
+ class Delegate
8
+ include FieldValuer
9
+
10
+ def initialize( field )
11
+ @field = field
12
+ end
13
+
14
+ attr_accessor :entity, :parent
15
+
16
+ attr_reader :field
17
+ def attribute
18
+ field.attribute
19
+ end
20
+
21
+ def entity_class
22
+ field.entity_class
23
+ end
24
+
25
+ def find_options
26
+ field.find_options
27
+ end
28
+
29
+ def is_combo?
30
+ false
31
+ end
32
+
33
+ # change the visual state of the editor to the biggest / most
34
+ # space-consuming it can be. This grew out of combo boxes having
35
+ # a drop-down that can show or hide.
36
+ def full_edit
37
+ end
38
+
39
+ # change the visual state of the editor to the smallest / least
40
+ # space-consuming it can be. This grew out of combo boxes having
41
+ # a drop-down that can show or hide.
42
+ def minimal_edit
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,38 @@
1
+ require 'set'
2
+
3
+ module Clevic
4
+ # This is a quickie emitter for Swing mostly. Qt has this built in.
5
+ module Emitter
6
+ def self.included( base )
7
+ base.extend( ClassMethods )
8
+ end
9
+
10
+ module ClassMethods
11
+ def emitter( emitter_name )
12
+ line, st = __LINE__, <<-EOF
13
+ def #{emitter_name}_listeners
14
+ @#{emitter_name}_listeners ||= Set.new
15
+ end
16
+
17
+ # If msg is provided, yield to stored block.
18
+ # If block is provided, store it for later.
19
+ def emit_#{emitter_name}( *args, &notifier_block )
20
+ if block_given?
21
+ #{emitter_name}_listeners << notifier_block
22
+ else
23
+ puts "emit_#{emitter_name} called with " + args.inspect
24
+ #{emitter_name}_listeners.each do |notify|
25
+ notify.call( *args )
26
+ end
27
+ end
28
+ end
29
+
30
+ def remove_#{emitter_name}( &notifier_block )
31
+ #{emitter_name}_listeners.delete( notifier_block )
32
+ end
33
+ EOF
34
+ class_eval st, __FILE__, line + 1
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,8 +1,5 @@
1
1
  # extensions specific to clevic
2
2
 
3
- require 'qtext/flags.rb'
4
- require 'qtext/hash_collector.rb'
5
-
6
3
  class Object
7
4
  # recursively calls each entry in path_ary
8
5
  # will return nil if any entry in path_ary
@@ -12,128 +9,78 @@ class Object
12
9
  value.nil? ? nil : value.send( att )
13
10
  end
14
11
  end
12
+
13
+ # pass self to the block and return the results of the block.
14
+ def with( &block )
15
+ yield( self )
16
+ end
15
17
  end
16
18
 
17
- module ActiveRecord
18
- class Base
19
- # checks to see if attribute_sym is either in the column
20
- # name list, or in the set of reflections.
21
- def self.has_attribute?( attribute_sym )
22
- if column_names.include?( attribute_sym.to_s )
23
- true
24
- elsif reflections.has_key?( attribute_sym )
25
- true
26
- else
27
- false
28
- end
29
- end
30
-
31
- def self.attribute_names
32
- ( column_names + reflections.keys.map {|sym| sym.to_s} ).sort
19
+ class String
20
+ # just grab the character code of the last character in the string
21
+ # TODO this won't work in unicode or utf-8
22
+ def to_char
23
+ if RUBY_VERSION <= '1.8.6'
24
+ self[0]
25
+ else
26
+ bytes.first
33
27
  end
34
28
  end
35
29
  end
36
30
 
37
- # convenience methods
38
- module Qt
31
+ class Array
32
+ def sparse_hash
33
+ Hash[ *(first..last).map do |index|
34
+ [index, include?( index ) ]
35
+ end.flatten ]
36
+ end
39
37
 
40
- PasteRole = UserRole + 1
41
-
42
- class AbstractItemDelegate
43
- # overridden in EntryDelegate subclasses
44
- def full_edit
38
+ def sparse
39
+ (first..last).map do |index|
40
+ index if include?( index )
45
41
  end
46
42
  end
47
43
 
48
- # This provides a bunch of methods to get easy access to the entity
49
- # and it's values directly from the index without having to keep
50
- # asking the model and jumping through other unncessary hoops
51
- class ModelIndex
52
- # the value to be displayed in the gui for this index
53
- def gui_value
54
- field.value_for( entity )
55
- end
56
-
57
- # return the Clevic::Field for this index
58
- def field
59
- @field ||= model.field_for_index( self )
60
- end
61
-
62
- def dump
63
- if valid?
64
- <<-EOF
65
- field: #{field_name} => #{field_value}
66
- attribute: #{attribute.inspect} => #{attribute_value.inspect}
67
- metadata: #{metadata.inspect}
68
- EOF
69
- else
70
- 'invalid'
71
- end
72
- end
73
-
74
- # return the attribute of the underlying entity corresponding
75
- # to the column of this index
76
- def attribute
77
- model.attributes[column]
78
- end
79
-
80
- # fetch the value of the attribute, without following
81
- # the full path. This will return a related entity for
82
- # belongs_to or has_one relationships, or a plain value
83
- # for model attributes
84
- def attribute_value
85
- entity.send( attribute )
86
- end
87
-
88
- # set the value of the attribute, without following the
89
- # full path.
90
- # TODO remove need to constantly recalculate the attribute writer
91
- def attribute_value=( obj )
92
- entity.send( "#{attribute.to_s}=", obj )
93
- end
94
-
95
- # returns the ActiveRecord column_for_attribute
96
- def metadata
97
- # use the optimised version
98
- model.metadata( column )
99
- end
100
-
101
- # return the table's field name. For associations, this would
102
- # be suffixed with _id
103
- def field_name
104
- metadata.name
105
- end
106
-
107
- # return the value of the field, it may be the _id value
108
- def field_value
109
- entity.send( field_name )
110
- end
111
-
112
- # the underlying entity
113
- def entity
114
- return nil if model.nil?
115
- @entity ||= model.collection[row]
116
- end
117
-
118
- attr_writer :entity
119
-
120
- # return true if validation failed for this indexes field
121
- def has_errors?
122
- # virtual fields don't have metadata
123
- if metadata.nil?
124
- false
125
- else
126
- entity.errors.invalid?( field_name.to_sym )
127
- end
128
- end
129
-
130
- # return a collection of errors. Unlike AR, this
131
- # will always return an array that will have zero, one
132
- # or many elements.
133
- def errors
134
- [ entity.errors[field_name.to_sym] ].flatten
44
+ def section
45
+ return [] if empty?
46
+ rv = [first]
47
+ self[1..-1].each_with_index do |next_value, index|
48
+ break if rv.last.succ != next_value
49
+ rv << next_value
50
+ end
51
+ rv
52
+ end
53
+
54
+ # group by ascending values
55
+ def group
56
+ parts = []
57
+ next_section = section
58
+ if next_section.empty?
59
+ parts
60
+ else
61
+ parts << section
62
+ parts + self[section.size..-1].group
135
63
  end
136
-
137
64
  end
65
+
66
+ def range
67
+ first..last
68
+ end
69
+ end
70
+
71
+ def Range
72
+ def distance
73
+ last - first
74
+ end
75
+ end
138
76
 
77
+ # workaround for the date freeze issue, if it exists
78
+ begin
79
+ Date.new.freeze.to_s
80
+ rescue TypeError
81
+ class Date
82
+ def freeze
83
+ self
84
+ end
85
+ end
139
86
  end
data/lib/clevic/field.rb CHANGED
@@ -1,12 +1,17 @@
1
- require 'gather.rb'
1
+ require 'gather'
2
+ require 'clevic/sampler.rb'
3
+ require 'clevic/generic_format.rb'
2
4
 
3
5
  module Clevic
4
6
 
5
7
  =begin rdoc
6
8
  This defines a field in the UI, and how it hooks up to a field in the DB.
7
9
 
8
- Attributes marked PROPERTY are DSL-style accessors, where the value can be
9
- set with either an assignment or by passing a parameter. For example
10
+ Many attributes are DSL-style accessors, where the value can be
11
+ set with either an assignment or by passing a parameter. Unfortunately
12
+ rdoc seems to have lost the ability to display these nicely. Anyway, here's
13
+ an example
14
+
10
15
  property :ixnay
11
16
 
12
17
  will allow
@@ -14,12 +19,17 @@ will allow
14
19
  # reader
15
20
  instance.ixnay
16
21
 
17
- #writer
22
+ # writer
18
23
  instance.ixnay = 'nix, baby'
19
24
 
20
- #writer
25
+ # writer
21
26
  instance.ixnay 'nix baby'
22
27
 
28
+ # store the block for later
29
+ instance.ixnay do |*args|
30
+ # block stuff here
31
+ end
32
+
23
33
  Generally properties are for options that can be passed to the field creation
24
34
  method in ModelBuilder, whereas ruby attributes are for the internal workings.
25
35
 
@@ -27,14 +37,22 @@ method in ModelBuilder, whereas ruby attributes are for the internal workings.
27
37
  TODO decide whether value_for type methods take an entity and do_something methods
28
38
  take a value.
29
39
 
40
+ TODO the xxx_for methods are in here because their return values don't change
41
+ by entity. Well, maybe sometimes they do. Anyway, need to find a better location
42
+ for these and a better caching strategy.
43
+
30
44
  TODO this class is a bit confused about whether it handles metadata or record data, or both.
31
45
 
32
- TODO meta needs to handle virtual fields better. Also is_date_time?
46
+ TODO meta needs to handle virtual fields better.
33
47
  =end
34
48
  class Field
35
49
  # For defining properties
36
50
  include Gather
37
51
 
52
+ # for formatting values
53
+ include GenericFormat
54
+
55
+ ##
38
56
  # The value to be displayed after being optionally format-ed
39
57
  #
40
58
  # Takes a String, a Symbol, or a Proc.
@@ -50,21 +68,21 @@ class Field
50
68
  # Defaults to nil, in other words the value of the attribute for this field.
51
69
  property :display
52
70
 
71
+ ##
53
72
  # The label to be displayed in the column headings. Defaults to the humanised field name.
54
73
  property :label
55
74
 
56
- # For relational fields, this is the class_name for the related AR entity.
57
- # TODO not used anymore?
58
- property :class_name
59
-
75
+ ##
60
76
  # One of the alignment specifiers - :left, :centre, :right or :justified.
61
77
  # Defaults to right for numeric fields, centre for boolean, and left for
62
78
  # other values.
63
79
  property :alignment
64
80
 
81
+ ##
65
82
  # something to do with the icon that Qt displays. Not implemented yet.
66
83
  property :decoration
67
84
 
85
+ ##
68
86
  # This defines how to format the value returned by :display. It takes a string or a Proc.
69
87
  # Generally the string is something
70
88
  # that can be understood by strftime (for time and date fields) or understood
@@ -72,15 +90,18 @@ class Field
72
90
  # the current entity. There are sensible defaults for common field types.
73
91
  property :format
74
92
 
93
+ ##
75
94
  # This is just like format, except that it's used to format the value just
76
95
  # before it's edited. A good use of this is to display dates with a 2-digit year
77
96
  # but edit them with a 4 digit year.
78
97
  # Defaults to a sensible value for some fields, for others it will default to the value of :format.
79
98
  property :edit_format
80
99
 
100
+ ##
81
101
  # Whether the field is currently visible or not.
82
102
  property :visible
83
103
 
104
+ ##
84
105
  # Sample is used if the programmer wishes to provide a value (that will be converted
85
106
  # using to_s) that can be used
86
107
  # as the basis for calculating the width of the field. By default this will be
@@ -89,9 +110,11 @@ class Field
89
110
  # have the option to override that if we wish.
90
111
  property :sample
91
112
 
113
+ ##
92
114
  # Takes a boolean. Set the field to read-only.
93
115
  property :read_only
94
116
 
117
+ ##
95
118
  # The foreground and background colors.
96
119
  # Can take a Proc, a string, or a symbol.
97
120
  # - A Proc is called with an entity
@@ -102,44 +125,54 @@ class Field
102
125
  # http://www.w3.org/TR/SVG/types.html#ColorKeywords.
103
126
  property :foreground, :background
104
127
 
128
+ ##
105
129
  # Can take a Proc, a string, or a symbol.
106
130
  # - A Proc is called with an entity
107
131
  # - A String is treated as a constant
108
132
  # - A symbol is treated as a method to be call on an entity
109
133
  property :tooltip
110
134
 
135
+ ##
111
136
  # An Enumerable of allowed values for restricted fields. If each yields
112
137
  # two values (like it does for a Hash), the
113
138
  # first will be stored in the db, and the second displayed in the UI.
114
139
  # If it's a proc, it must return an Enumerable as above.
115
140
  property :set
116
141
 
142
+ ##
117
143
  # When this is true, only the values in the combo may be entered.
118
144
  # Otherwise the text-entry part of the combo can be used to enter
119
145
  # non-listed values. Default is true if a set is explicitly specified.
120
146
  # Otherwise depends on the field type.
121
147
  property :restricted
122
148
 
149
+ ##
123
150
  # Only for the distinct field type. The values will be sorted either with the
124
- # most used values first (:frequency => true) or in alphabetical order (:description => true).
151
+ # most used values first (:frequency => true) or in
152
+ # alphabetical order (:description => true).
125
153
  property :frequency, :description
126
154
 
155
+ ##
127
156
  # Default value for this field for new records.
128
157
  # Can be a Proc or a value. A value will just be
129
158
  # set, a proc will be executed with the entity as a parameter.
130
159
  property :default
131
160
 
132
- # the property used for finding the field, ie by TableModel#field_column
133
- # defaults to the attribute.
161
+ ##
162
+ # The property used for finding the field, ie by TableModel#field_column.
163
+ # Defaults to the attribute.
134
164
  property :id
135
165
 
136
- # called when the data in this field changes. Either a proc( clevic_view, table_view, model_index ) or a symbol
137
- # for a method( view, model_index ) on the Clevic::View object. Both will take
166
+ ##
167
+ # Called when the data in this field changes.
168
+ # Either a proc( clevic_view, table_view, model_index ) or a symbol
169
+ # for a method( view, model_index ) on the Clevic::View object.
138
170
  property :notify_data_changed
139
171
 
140
- # properties for ActiveRecord options
141
- # There are actually from ActiveRecord::Base.VALID_FIND_OPTIONS, but it's protected
142
- # each element becomes a property.
172
+ # The list of properties for ActiveRecord options.
173
+ # There are actually from ActiveRecord::Base.VALID_FIND_OPTIONS, but it's protected.
174
+ # Each element becomes a property.
175
+ # TODO remove these? That will destroy the migration path.
143
176
  AR_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :from, :lock ]
144
177
  AR_FIND_OPTIONS.each{|x| property x}
145
178
 
@@ -155,7 +188,8 @@ class Field
155
188
  end
156
189
  end
157
190
 
158
- # The UI delegate class for the field. In Qt, this is a subclass of AbstractItemDelegate.
191
+ # The UI delegate class for the field. The delegate class knows how to create a UI
192
+ # for this field using whatever GUI toolkit is selected
159
193
  attr_accessor :delegate
160
194
 
161
195
  # The attribute on the AR entity that forms the basis for this field.
@@ -165,13 +199,13 @@ class Field
165
199
  # would normally have an _id suffix for relationships.
166
200
  attr_accessor :attribute
167
201
 
168
- # The ActiveRecord::Base subclass this field uses to get data from.
202
+ # The Object Relational Model this field uses to get data from.
169
203
  attr_reader :entity_class
170
204
 
171
205
  # Create a new Field object that displays the contents of a database field in
172
206
  # the UI using the given parameters.
173
207
  # - attribute is the symbol for the attribute on the entity_class.
174
- # - entity_class is the ActiveRecord::Base subclass which this Field talks to.
208
+ # - entity_class is the Object Relational Model which this Field talks to.
175
209
  # - options is a hash of writable attributes in Field, which can be any of the properties defined in this class.
176
210
  def initialize( attribute, entity_class, options, &block )
177
211
  # sanity checking
@@ -179,11 +213,7 @@ class Field
179
213
  raise "attribute #{attribute.inspect} must be a symbol"
180
214
  end
181
215
 
182
- unless entity_class.ancestors.include?( ActiveRecord::Base )
183
- raise "entity_class must be a descendant of ActiveRecord::Base"
184
- end
185
-
186
- unless entity_class.has_attribute?( attribute ) or entity_class.instance_methods.include?( attribute.to_s )
216
+ unless ( entity_class.is_a?( Clevic.base_entity_class ) and entity_class.has_attribute?( attribute ) ) or entity_class.instance_methods.include?( attribute.to_s )
187
217
  msg = <<EOF
188
218
  #{attribute} not found in #{entity_class.name}. Possibilities are:
189
219
  #{entity_class.attribute_names.join("\n")}
@@ -193,7 +223,7 @@ EOF
193
223
 
194
224
  # instance variables
195
225
  @attribute = attribute
196
- # default to attribute
226
+ # default to attribute, can be overwritten later
197
227
  @id = attribute
198
228
  @entity_class = entity_class
199
229
  @visible = true
@@ -210,9 +240,40 @@ EOF
210
240
  default_format!
211
241
  default_edit_format!
212
242
  default_alignment!
243
+ default_display! if association?
244
+ end
245
+
246
+ # x_to_many fields are by definition collections of other entities
247
+ def many( &block )
248
+ if block
249
+ many_view( &block )
250
+ else
251
+ many_view do |mb|
252
+ # TODO should fetch this from one of the field definitions
253
+ mb.plain related_attribute
254
+ end
255
+ end
256
+ end
257
+
258
+ def many_builder
259
+ @many_view.builder
213
260
  end
214
261
 
215
- # Return the attribute value for the given ActiveRecord entity, or nil
262
+ def many_fields
263
+ many_builder.fields
264
+ end
265
+
266
+ # return an instance of Clevic::View that represents the many items
267
+ # for this field
268
+ def many_view( &block )
269
+ @many_view ||= View.new( :entity_class => related_class, &block )
270
+ end
271
+
272
+ # The model object (eg TableModel) this field is part of.
273
+ # Set to TableModel by ModelBuilder#build
274
+ attr_accessor :model
275
+
276
+ # Return the attribute value for the given Object Relational Model instance, or nil
216
277
  # if entity is nil. Will call transform_attribute.
217
278
  def value_for( entity )
218
279
  begin
@@ -224,7 +285,7 @@ EOF
224
285
  end
225
286
  end
226
287
 
227
- # Apply display, to the given
288
+ # Apply the value of the display property to the given
228
289
  # attribute value. Otherwise just return the
229
290
  # attribute_value itself.
230
291
  def transform_attribute( attribute_value )
@@ -245,44 +306,20 @@ EOF
245
306
  end
246
307
 
247
308
  # return true if this is a field for a related table, false otherwise.
248
- def is_association?
249
- meta.type == ActiveRecord::Reflection::AssociationReflection
309
+ def association?
310
+ meta.andand.association?
250
311
  end
251
312
 
252
- # Return true if the field is a date, a time or a datetime.
253
- # If display is nil, the value is calculated, so we need
254
- # to check the value. Otherwise use the field metadata.
255
- # Cache the result for the first non-nil value.
256
- def is_date_time?( value )
257
- if value.nil?
258
- false
259
- else
260
- @is_date_time ||=
261
- if display.nil?
262
- [:time, :date, :datetime, :timestamp].include?( meta.type )
263
- else
264
- # it's a virtual field, so we need to use the value
265
- value.is_a?( Date ) || value.is_a?( Time )
266
- end
267
- end
268
- end
269
-
270
- # return ActiveRecord::Base.columns_hash[attribute]
271
- # in other words an ActiveRecord::ConnectionAdapters::Column object,
272
- # or an ActiveRecord::Reflection::AssociationReflection object
313
+ # ModelColumn object
273
314
  def meta
274
- @meta ||= @entity_class.columns_hash[attribute.to_s] || @entity_class.reflections[attribute]
315
+ entity_class.meta[attribute]
275
316
  end
276
317
 
277
318
  # return the type of this attribute. Usually one of :string, :integer, :float
278
- # or some entity class (ActiveRecord::Base subclass)
319
+ # or some entity class
320
+ # TODO remove
279
321
  def attribute_type
280
- @attribute_type ||=
281
- if meta.kind_of?( ActiveRecord::Reflection::MacroReflection )
282
- meta.klass
283
- else
284
- meta.type
285
- end
322
+ meta.type
286
323
  end
287
324
 
288
325
  # return true if this field can be used in a filter
@@ -292,21 +329,18 @@ EOF
292
329
  !meta.nil?
293
330
  end
294
331
 
295
- # Return the name of the database field for this Field, quoted for the dbms.
296
- def quoted_field
297
- quote_field( meta.name )
298
- end
299
-
300
- # Quote the given string as a field name for SQL.
301
- def quote_field( field_name )
302
- @entity_class.connection.quote_column_name( field_name )
303
- end
304
-
305
332
  # return the result of the attribute + the path
306
333
  def column
307
334
  [attribute.to_s, path].compact.join('.')
308
335
  end
309
336
 
337
+ # return the class object of a related class if this is a relational
338
+ # field, otherwise nil
339
+ def related_class
340
+ return nil unless entity_class.meta.has_key?( attribute )
341
+ @related_class ||= eval( entity_class.meta[attribute].class_name || attribute.to_s.classify )
342
+ end
343
+
310
344
  # return an array of the various attribute parts
311
345
  def attribute_path
312
346
  pieces = [ attribute.to_s ]
@@ -319,77 +353,40 @@ EOF
319
353
  @read_only || false
320
354
  end
321
355
 
322
- # apply format to value. Use strftime for date_time types, or % for everything else.
323
- # If format is a proc, pass value to it.
324
- def do_generic_format( format, value )
325
- begin
326
- unless format.nil?
327
- if format.is_a? Proc
328
- format.call( value )
329
- else
330
- if is_date_time?( value )
331
- value.strftime( format )
332
- else
333
- format % value
334
- end
335
- end
336
- else
337
- value
338
- end
339
- rescue Exception => e
340
- puts "format: #{format.inspect}"
341
- puts "value.class: #{value.class.inspect}"
342
- puts "value: #{value.inspect}"
343
- puts e.message
344
- puts e.backtrace
345
- nil
346
- end
347
- end
348
-
356
+ # Called by Clevic::Model to format the display value.
349
357
  def do_format( value )
350
358
  do_generic_format( format, value )
351
359
  end
352
360
 
361
+ # Called by Clevic::Model to format the edit value.
353
362
  def do_edit_format( value )
354
363
  do_generic_format( edit_format, value )
355
364
  end
356
365
 
357
- # return a sample for the field which can be used to size the UI field widget
366
+ # Return a sample for the field which can be used to size the UI field widget.
358
367
  def sample( *args )
359
368
  if !args.empty?
360
- self.sample = *args
361
- return
362
- end
363
-
364
- if @sample.nil?
365
- self.sample =
366
- case meta.type
367
- # max width of 40 chars
368
- when :string, :text
369
- string_sample( 'n'*40 )
370
-
371
- when :date, :time, :datetime, :timestamp
372
- date_time_sample
373
-
374
- when :numeric, :decimal, :integer, :float
375
- numeric_sample
376
-
377
- # TODO return a width, or something like that
378
- when :boolean; 'W'
379
-
380
- when ActiveRecord::Reflection::AssociationReflection.class
381
- related_sample
382
-
369
+ @sample = args.first
370
+ self
371
+ else
372
+ if @sample.nil?
373
+ if meta.type == :boolean
374
+ @sample = self.label
383
375
  else
384
- puts "#{@entity_class.name}.#{attribute} is a #{meta.type.inspect}"
376
+ begin
377
+ @sample ||= Sampler.new( entity_class, attribute, display ) do |value|
378
+ do_format( value )
379
+ end.compute
380
+ rescue
381
+ puts $!
382
+ ensure
383
+ # if we don't know how to figure it out from the data, just return the label size
384
+ @sample ||= self.label
385
+ end
386
+ end
385
387
  end
386
-
387
- #~ if $options && $options[:debug]
388
- #~ puts "@sample for #{@entity_class.name}.#{attribute} #{meta.type}: #{@sample.inspect}"
389
- #~ end
388
+ @sample
390
389
  end
391
- # if we don't know how to figure it out from the data, just return the label size
392
- @sample || self.label
393
390
  end
394
391
 
395
392
  # Called by Clevic::TableModel to get the tooltip value
@@ -401,17 +398,6 @@ EOF
401
398
  def decoration_for( entity )
402
399
  nil
403
400
  end
404
-
405
- # Convert something that responds to to_s to a Qt::Color,
406
- # or just return the argument if it's already a Qt::Color
407
- def string_or_color( s_or_c )
408
- case s_or_c
409
- when Qt::Color
410
- s_or_c
411
- else
412
- Qt::Color.new( s_or_c.to_s )
413
- end
414
- end
415
401
 
416
402
  # Called by Clevic::TableModel to get the foreground color value
417
403
  def foreground_for( entity )
@@ -423,6 +409,8 @@ EOF
423
409
  cache_value_for( :background, entity ) {|x| string_or_color(x)}
424
410
  end
425
411
 
412
+ # called when a new entity object is created to set default values
413
+ # specified by the default property.
426
414
  def set_default_for( entity )
427
415
  begin
428
416
  entity[attribute] =
@@ -438,6 +426,7 @@ EOF
438
426
  end
439
427
  end
440
428
 
429
+ # fetch the permitted set of values for a restricted field.
441
430
  def set_for( entity )
442
431
  case set
443
432
  when Proc
@@ -453,6 +442,10 @@ EOF
453
442
  end
454
443
  end
455
444
 
445
+ def inspect
446
+ "#<Clevic::Field id=#{id.inspect}>"
447
+ end
448
+
456
449
  protected
457
450
 
458
451
  # call the conversion_block with the value, or just return the
@@ -481,111 +474,49 @@ protected
481
474
  end
482
475
  end
483
476
 
477
+ # the label if it's not defined. Based on the attribute.
484
478
  def default_label!
485
479
  @label ||= attribute.to_s.humanize
486
480
  end
487
481
 
482
+ # sensible display format defaults if they're not defined.
488
483
  def default_format!
489
- if @format.nil?
490
- @format =
491
- case meta.type
492
- when :time; '%H:%M'
493
- when :date; '%d-%h-%y'
494
- when :datetime; '%d-%h-%y %H:%M:%S'
495
- when :decimal, :float; "%.2f"
496
- end
484
+ @format ||=
485
+ case meta.type
486
+ when :time; '%H:%M'
487
+ when :date; '%d-%h-%y'
488
+ when :datetime; '%d-%h-%y %H:%M:%S'
489
+ when :decimal, :float; "%.2f"
497
490
  end
498
- @format
499
491
  end
500
492
 
493
+ # sensible edit format defaults if they're not defined.
501
494
  def default_edit_format!
502
- if @edit_format.nil?
503
- @edit_format =
504
- case meta.type
505
- when :date; '%d-%h-%Y'
506
- when :datetime; '%d-%h-%Y %H:%M:%S'
507
- end || default_format!
508
- end
509
- @edit_format
495
+ @edit_format ||=
496
+ case meta.type
497
+ when :date; '%d-%h-%Y'
498
+ when :datetime; '%d-%h-%Y %H:%M:%S'
499
+ end || default_format!
510
500
  end
511
501
 
502
+ # sensible alignment defaults if they're not defined.
512
503
  def default_alignment!
513
- if @alignment.nil?
514
- @alignment =
515
- case meta.type
516
- when :decimal, :integer, :float; :right
517
- when :boolean; :centre
518
- end
504
+ @alignment ||=
505
+ case meta.type
506
+ when :decimal, :integer, :float; :right
507
+ when :boolean; :centre
508
+ else :left
519
509
  end
520
510
  end
521
511
 
522
- private
523
-
524
- def format_result( result_set )
525
- unless result_set.size == 0
526
- obj = result_set[0][attribute]
527
- do_format( obj ) unless obj.nil?
528
- end
529
- end
530
-
531
- def string_sample( max_sample = nil, entity_class = @entity_class, field_name = meta.name )
532
- statement = <<-EOF
533
- select distinct #{quote_field field_name}
534
- from #{entity_class.table_name}
535
- where
536
- length( #{quote_field field_name} ) = (
537
- select max( length( #{quote_field field_name} ) )
538
- from #{entity_class.table_name}
539
- )
540
- EOF
541
- result_set = @entity_class.connection.execute statement
542
- unless result_set.entries.size == 0
543
- row = result_set[0]
544
- result =
545
- case row
546
- when Array
547
- row[0]
548
- when Hash
549
- row.values[0]
550
- end
551
-
552
- if max_sample.nil?
553
- result
554
- else
555
- result.length < max_sample.length ? result : max_sample
556
- end
557
- end
512
+ # try to find a sensible display method
513
+ def default_display!
514
+ candidates = %W{#{entity_class.name.downcase} name title username to_s}
515
+ @display ||= candidates.find do |m|
516
+ related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
517
+ end || raise( "Can't find one of #{candidates.inspect} in #{related_class.name}" )
558
518
  end
559
-
560
- def date_time_sample
561
- result_set = @entity_class.find_by_sql <<-EOF
562
- select #{quoted_field}
563
- from #{@entity_class.table_name}
564
- where #{quoted_field} is not null
565
- limit 1
566
- EOF
567
- format_result( result_set )
568
- end
569
-
570
- def numeric_sample
571
- # TODO Use precision from metadata, not for integers
572
- # returns nil for floats. So it's probably not useful
573
- #~ puts "meta.precision: #{meta.precision.inspect}"
574
- result_set = @entity_class.find_by_sql <<-EOF
575
- select max( #{quoted_field} )
576
- from #{@entity_class.table_name}
577
- EOF
578
- format_result( result_set )
579
- end
580
-
581
- def related_sample
582
- # TODO this isn't really the right way to do this
583
- return nil if meta.nil?
584
- if meta.klass.attribute_names.include?( attribute_path[1].to_s )
585
- string_sample( nil, meta.klass, attribute_path[1] )
586
- end
587
- end
588
-
519
+
589
520
  end
590
521
 
591
522
  end