lib/clevic/field.rb in clevic-0.12.0 vs lib/clevic/field.rb in clevic-0.13.0.b1
- old
+ new
@@ -1,42 +1,60 @@
-require 'gather.rb'
+require 'gather'
+require 'clevic/sampler.rb'
+require 'clevic/generic_format.rb'
module Clevic
=begin rdoc
This defines a field in the UI, and how it hooks up to a field in the DB.
-Attributes marked PROPERTY are DSL-style accessors, where the value can be
-set with either an assignment or by passing a parameter. For example
+Many attributes are DSL-style accessors, where the value can be
+set with either an assignment or by passing a parameter. Unfortunately
+rdoc seems to have lost the ability to display these nicely. Anyway, here's
+an example
+
property :ixnay
will allow
# reader
instance.ixnay
- #writer
+ # writer
instance.ixnay = 'nix, baby'
- #writer
+ # writer
instance.ixnay 'nix baby'
+ # store the block for later
+ instance.ixnay do |*args|
+ # block stuff here
+ end
+
Generally properties are for options that can be passed to the field creation
method in ModelBuilder, whereas ruby attributes are for the internal workings.
#--
TODO decide whether value_for type methods take an entity and do_something methods
take a value.
+TODO the xxx_for methods are in here because their return values don't change
+by entity. Well, maybe sometimes they do. Anyway, need to find a better location
+for these and a better caching strategy.
+
TODO this class is a bit confused about whether it handles metadata or record data, or both.
-TODO meta needs to handle virtual fields better. Also is_date_time?
+TODO meta needs to handle virtual fields better.
=end
class Field
# For defining properties
include Gather
+ # for formatting values
+ include GenericFormat
+
+ ##
# The value to be displayed after being optionally format-ed
#
# Takes a String, a Symbol, or a Proc.
#
# A String will be a dot-separated path of attributes starting on the object returned by attribute.
@@ -48,100 +66,115 @@
# fields from related tables, or calculated fields.
#
# Defaults to nil, in other words the value of the attribute for this field.
property :display
+ ##
# The label to be displayed in the column headings. Defaults to the humanised field name.
property :label
- # For relational fields, this is the class_name for the related AR entity.
- # TODO not used anymore?
- property :class_name
-
+ ##
# One of the alignment specifiers - :left, :centre, :right or :justified.
# Defaults to right for numeric fields, centre for boolean, and left for
# other values.
property :alignment
+ ##
# something to do with the icon that Qt displays. Not implemented yet.
property :decoration
+ ##
# This defines how to format the value returned by :display. It takes a string or a Proc.
# Generally the string is something
# that can be understood by strftime (for time and date fields) or understood
# by % (for everything else). It can also be a Proc that has one parameter -
# the current entity. There are sensible defaults for common field types.
property :format
+ ##
# This is just like format, except that it's used to format the value just
# before it's edited. A good use of this is to display dates with a 2-digit year
# but edit them with a 4 digit year.
# Defaults to a sensible value for some fields, for others it will default to the value of :format.
property :edit_format
+ ##
# Whether the field is currently visible or not.
property :visible
+ ##
# Sample is used if the programmer wishes to provide a value (that will be converted
# using to_s) that can be used
# as the basis for calculating the width of the field. By default this will be
# calculated from the database, but this may be an expensive operation, and
# doesn't always work properly. So we
# have the option to override that if we wish.
property :sample
+ ##
# Takes a boolean. Set the field to read-only.
property :read_only
+ ##
# The foreground and background colors.
# Can take a Proc, a string, or a symbol.
# - A Proc is called with an entity
# - A String is treated as a constant which may be one of the string constants understood by Qt::Color
# - A symbol is treated as a method to be call on an entity
#
# The result can be a Qt::Color, or one of the strings in
# http://www.w3.org/TR/SVG/types.html#ColorKeywords.
property :foreground, :background
+ ##
# Can take a Proc, a string, or a symbol.
# - A Proc is called with an entity
# - A String is treated as a constant
# - A symbol is treated as a method to be call on an entity
property :tooltip
+ ##
# An Enumerable of allowed values for restricted fields. If each yields
# two values (like it does for a Hash), the
# first will be stored in the db, and the second displayed in the UI.
# If it's a proc, it must return an Enumerable as above.
property :set
+ ##
# When this is true, only the values in the combo may be entered.
# Otherwise the text-entry part of the combo can be used to enter
# non-listed values. Default is true if a set is explicitly specified.
# Otherwise depends on the field type.
property :restricted
+ ##
# Only for the distinct field type. The values will be sorted either with the
- # most used values first (:frequency => true) or in alphabetical order (:description => true).
+ # most used values first (:frequency => true) or in
+ # alphabetical order (:description => true).
property :frequency, :description
+ ##
# Default value for this field for new records.
# Can be a Proc or a value. A value will just be
# set, a proc will be executed with the entity as a parameter.
property :default
- # the property used for finding the field, ie by TableModel#field_column
- # defaults to the attribute.
+ ##
+ # The property used for finding the field, ie by TableModel#field_column.
+ # Defaults to the attribute.
property :id
- # called when the data in this field changes. Either a proc( clevic_view, table_view, model_index ) or a symbol
- # for a method( view, model_index ) on the Clevic::View object. Both will take
+ ##
+ # Called when the data in this field changes.
+ # Either a proc( clevic_view, table_view, model_index ) or a symbol
+ # for a method( view, model_index ) on the Clevic::View object.
property :notify_data_changed
- # properties for ActiveRecord options
- # There are actually from ActiveRecord::Base.VALID_FIND_OPTIONS, but it's protected
- # each element becomes a property.
+ # The list of properties for ActiveRecord options.
+ # There are actually from ActiveRecord::Base.VALID_FIND_OPTIONS, but it's protected.
+ # Each element becomes a property.
+ # TODO remove these? That will destroy the migration path.
AR_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :from, :lock ]
AR_FIND_OPTIONS.each{|x| property x}
# Return a list of find options and their values, but only
# if the values are not nil
@@ -153,49 +186,46 @@
end
ha
end
end
- # The UI delegate class for the field. In Qt, this is a subclass of AbstractItemDelegate.
+ # The UI delegate class for the field. The delegate class knows how to create a UI
+ # for this field using whatever GUI toolkit is selected
attr_accessor :delegate
# The attribute on the AR entity that forms the basis for this field.
# Accessing the returned attribute (using send, or the [] method on an entity)
# will give a simple value, or another AR entity in the case of relational fields.
# In other words, this is *not* the same as the name of the field in the DB, which
# would normally have an _id suffix for relationships.
attr_accessor :attribute
- # The ActiveRecord::Base subclass this field uses to get data from.
+ # The Object Relational Model this field uses to get data from.
attr_reader :entity_class
# Create a new Field object that displays the contents of a database field in
# the UI using the given parameters.
# - attribute is the symbol for the attribute on the entity_class.
- # - entity_class is the ActiveRecord::Base subclass which this Field talks to.
+ # - entity_class is the Object Relational Model which this Field talks to.
# - options is a hash of writable attributes in Field, which can be any of the properties defined in this class.
def initialize( attribute, entity_class, options, &block )
# sanity checking
unless attribute.is_a?( Symbol )
raise "attribute #{attribute.inspect} must be a symbol"
end
- unless entity_class.ancestors.include?( ActiveRecord::Base )
- raise "entity_class must be a descendant of ActiveRecord::Base"
- end
-
- unless entity_class.has_attribute?( attribute ) or entity_class.instance_methods.include?( attribute.to_s )
+ unless ( entity_class.is_a?( Clevic.base_entity_class ) and entity_class.has_attribute?( attribute ) ) or entity_class.instance_methods.include?( attribute.to_s )
msg = <<EOF
#{attribute} not found in #{entity_class.name}. Possibilities are:
#{entity_class.attribute_names.join("\n")}
EOF
raise msg
end
# instance variables
@attribute = attribute
- # default to attribute
+ # default to attribute, can be overwritten later
@id = attribute
@entity_class = entity_class
@visible = true
# initialise
@@ -208,13 +238,44 @@
# they might stay nil, and we don't want to keep evaluating them.
default_label!
default_format!
default_edit_format!
default_alignment!
+ default_display! if association?
end
- # Return the attribute value for the given ActiveRecord entity, or nil
+ # x_to_many fields are by definition collections of other entities
+ def many( &block )
+ if block
+ many_view( &block )
+ else
+ many_view do |mb|
+ # TODO should fetch this from one of the field definitions
+ mb.plain related_attribute
+ end
+ end
+ end
+
+ def many_builder
+ @many_view.builder
+ end
+
+ def many_fields
+ many_builder.fields
+ end
+
+ # return an instance of Clevic::View that represents the many items
+ # for this field
+ def many_view( &block )
+ @many_view ||= View.new( :entity_class => related_class, &block )
+ end
+
+ # The model object (eg TableModel) this field is part of.
+ # Set to TableModel by ModelBuilder#build
+ attr_accessor :model
+
+ # Return the attribute value for the given Object Relational Model instance, or nil
# if entity is nil. Will call transform_attribute.
def value_for( entity )
begin
return nil if entity.nil?
transform_attribute( entity.send( attribute ) )
@@ -222,11 +283,11 @@
puts "error for #{entity}.#{entity.send( attribute ).inspect} in value_for: #{e.message}"
puts e.backtrace
end
end
- # Apply display, to the given
+ # Apply the value of the display property to the given
# attribute value. Otherwise just return the
# attribute_value itself.
def transform_attribute( attribute_value )
return nil if attribute_value.nil?
case display
@@ -243,72 +304,45 @@
attribute_value
end
end
# return true if this is a field for a related table, false otherwise.
- def is_association?
- meta.type == ActiveRecord::Reflection::AssociationReflection
+ def association?
+ meta.andand.association?
end
- # Return true if the field is a date, a time or a datetime.
- # If display is nil, the value is calculated, so we need
- # to check the value. Otherwise use the field metadata.
- # Cache the result for the first non-nil value.
- def is_date_time?( value )
- if value.nil?
- false
- else
- @is_date_time ||=
- if display.nil?
- [:time, :date, :datetime, :timestamp].include?( meta.type )
- else
- # it's a virtual field, so we need to use the value
- value.is_a?( Date ) || value.is_a?( Time )
- end
- end
- end
-
- # return ActiveRecord::Base.columns_hash[attribute]
- # in other words an ActiveRecord::ConnectionAdapters::Column object,
- # or an ActiveRecord::Reflection::AssociationReflection object
+ # ModelColumn object
def meta
- @meta ||= @entity_class.columns_hash[attribute.to_s] || @entity_class.reflections[attribute]
+ entity_class.meta[attribute]
end
# return the type of this attribute. Usually one of :string, :integer, :float
- # or some entity class (ActiveRecord::Base subclass)
+ # or some entity class
+ # TODO remove
def attribute_type
- @attribute_type ||=
- if meta.kind_of?( ActiveRecord::Reflection::MacroReflection )
- meta.klass
- else
- meta.type
- end
+ meta.type
end
# return true if this field can be used in a filter
# virtual fields (ie those that don't exist in this field's
# table) can't be used to filter on.
def filterable?
!meta.nil?
end
- # Return the name of the database field for this Field, quoted for the dbms.
- def quoted_field
- quote_field( meta.name )
- end
-
- # Quote the given string as a field name for SQL.
- def quote_field( field_name )
- @entity_class.connection.quote_column_name( field_name )
- end
-
# return the result of the attribute + the path
def column
[attribute.to_s, path].compact.join('.')
end
+ # return the class object of a related class if this is a relational
+ # field, otherwise nil
+ def related_class
+ return nil unless entity_class.meta.has_key?( attribute )
+ @related_class ||= eval( entity_class.meta[attribute].class_name || attribute.to_s.classify )
+ end
+
# return an array of the various attribute parts
def attribute_path
pieces = [ attribute.to_s ]
pieces.concat( display.to_s.split( '.' ) ) unless display.is_a? Proc
pieces.map{|x| x.to_sym}
@@ -317,81 +351,44 @@
# Return true if the field is read-only. Defaults to false.
def read_only?
@read_only || false
end
- # apply format to value. Use strftime for date_time types, or % for everything else.
- # If format is a proc, pass value to it.
- def do_generic_format( format, value )
- begin
- unless format.nil?
- if format.is_a? Proc
- format.call( value )
- else
- if is_date_time?( value )
- value.strftime( format )
- else
- format % value
- end
- end
- else
- value
- end
- rescue Exception => e
- puts "format: #{format.inspect}"
- puts "value.class: #{value.class.inspect}"
- puts "value: #{value.inspect}"
- puts e.message
- puts e.backtrace
- nil
- end
- end
-
+ # Called by Clevic::Model to format the display value.
def do_format( value )
do_generic_format( format, value )
end
+ # Called by Clevic::Model to format the edit value.
def do_edit_format( value )
do_generic_format( edit_format, value )
end
- # return a sample for the field which can be used to size the UI field widget
+ # Return a sample for the field which can be used to size the UI field widget.
def sample( *args )
if !args.empty?
- self.sample = *args
- return
- end
-
- if @sample.nil?
- self.sample =
- case meta.type
- # max width of 40 chars
- when :string, :text
- string_sample( 'n'*40 )
-
- when :date, :time, :datetime, :timestamp
- date_time_sample
-
- when :numeric, :decimal, :integer, :float
- numeric_sample
-
- # TODO return a width, or something like that
- when :boolean; 'W'
-
- when ActiveRecord::Reflection::AssociationReflection.class
- related_sample
-
+ @sample = args.first
+ self
+ else
+ if @sample.nil?
+ if meta.type == :boolean
+ @sample = self.label
else
- puts "#{@entity_class.name}.#{attribute} is a #{meta.type.inspect}"
+ begin
+ @sample ||= Sampler.new( entity_class, attribute, display ) do |value|
+ do_format( value )
+ end.compute
+ rescue
+ puts $!
+ ensure
+ # if we don't know how to figure it out from the data, just return the label size
+ @sample ||= self.label
+ end
+ end
end
-
- #~ if $options && $options[:debug]
- #~ puts "@sample for #{@entity_class.name}.#{attribute} #{meta.type}: #{@sample.inspect}"
- #~ end
+ @sample
end
- # if we don't know how to figure it out from the data, just return the label size
- @sample || self.label
end
# Called by Clevic::TableModel to get the tooltip value
def tooltip_for( entity )
cache_value_for( :tooltip, entity )
@@ -399,21 +396,10 @@
# TODO Doesn't do anything useful yet.
def decoration_for( entity )
nil
end
-
- # Convert something that responds to to_s to a Qt::Color,
- # or just return the argument if it's already a Qt::Color
- def string_or_color( s_or_c )
- case s_or_c
- when Qt::Color
- s_or_c
- else
- Qt::Color.new( s_or_c.to_s )
- end
- end
# Called by Clevic::TableModel to get the foreground color value
def foreground_for( entity )
cache_value_for( :foreground, entity ) {|x| string_or_color(x)}
end
@@ -421,10 +407,12 @@
# Called by Clevic::TableModel to get the background color value
def background_for( entity )
cache_value_for( :background, entity ) {|x| string_or_color(x)}
end
+ # called when a new entity object is created to set default values
+ # specified by the default property.
def set_default_for( entity )
begin
entity[attribute] =
case default
when String
@@ -436,10 +424,11 @@
puts e.message
puts e.backtrace
end
end
+ # fetch the permitted set of values for a restricted field.
def set_for( entity )
case set
when Proc
# the Proc should return an enumerable
set.call( entity )
@@ -451,10 +440,14 @@
# assume its an Enumerable
set
end
end
+ def inspect
+ "#<Clevic::Field id=#{id.inspect}>"
+ end
+
protected
# call the conversion_block with the value, or just return the
# value if conversion_block is nil
def convert_or_identity( value, &conversion_block )
@@ -479,113 +472,51 @@
when Symbol; convert_or_identity( entity.send( value ), &conversion_block ) unless entity.nil?
else; @value_cache[symbol] ||=convert_or_identity( value, &conversion_block )
end
end
+ # the label if it's not defined. Based on the attribute.
def default_label!
@label ||= attribute.to_s.humanize
end
+ # sensible display format defaults if they're not defined.
def default_format!
- if @format.nil?
- @format =
- case meta.type
- when :time; '%H:%M'
- when :date; '%d-%h-%y'
- when :datetime; '%d-%h-%y %H:%M:%S'
- when :decimal, :float; "%.2f"
- end
+ @format ||=
+ case meta.type
+ when :time; '%H:%M'
+ when :date; '%d-%h-%y'
+ when :datetime; '%d-%h-%y %H:%M:%S'
+ when :decimal, :float; "%.2f"
end
- @format
end
+ # sensible edit format defaults if they're not defined.
def default_edit_format!
- if @edit_format.nil?
- @edit_format =
- case meta.type
- when :date; '%d-%h-%Y'
- when :datetime; '%d-%h-%Y %H:%M:%S'
- end || default_format!
- end
- @edit_format
+ @edit_format ||=
+ case meta.type
+ when :date; '%d-%h-%Y'
+ when :datetime; '%d-%h-%Y %H:%M:%S'
+ end || default_format!
end
+ # sensible alignment defaults if they're not defined.
def default_alignment!
- if @alignment.nil?
- @alignment =
- case meta.type
- when :decimal, :integer, :float; :right
- when :boolean; :centre
- end
+ @alignment ||=
+ case meta.type
+ when :decimal, :integer, :float; :right
+ when :boolean; :centre
+ else :left
end
end
-private
-
- def format_result( result_set )
- unless result_set.size == 0
- obj = result_set[0][attribute]
- do_format( obj ) unless obj.nil?
- end
+ # try to find a sensible display method
+ def default_display!
+ candidates = %W{#{entity_class.name.downcase} name title username to_s}
+ @display ||= candidates.find do |m|
+ related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
+ end || raise( "Can't find one of #{candidates.inspect} in #{related_class.name}" )
end
-
- def string_sample( max_sample = nil, entity_class = @entity_class, field_name = meta.name )
- statement = <<-EOF
- select distinct #{quote_field field_name}
- from #{entity_class.table_name}
- where
- length( #{quote_field field_name} ) = (
- select max( length( #{quote_field field_name} ) )
- from #{entity_class.table_name}
- )
- EOF
- result_set = @entity_class.connection.execute statement
- unless result_set.entries.size == 0
- row = result_set[0]
- result =
- case row
- when Array
- row[0]
- when Hash
- row.values[0]
- end
-
- if max_sample.nil?
- result
- else
- result.length < max_sample.length ? result : max_sample
- end
- end
- end
-
- def date_time_sample
- result_set = @entity_class.find_by_sql <<-EOF
- select #{quoted_field}
- from #{@entity_class.table_name}
- where #{quoted_field} is not null
- limit 1
- EOF
- format_result( result_set )
- end
-
- def numeric_sample
- # TODO Use precision from metadata, not for integers
- # returns nil for floats. So it's probably not useful
- #~ puts "meta.precision: #{meta.precision.inspect}"
- result_set = @entity_class.find_by_sql <<-EOF
- select max( #{quoted_field} )
- from #{@entity_class.table_name}
- EOF
- format_result( result_set )
- end
-
- def related_sample
- # TODO this isn't really the right way to do this
- return nil if meta.nil?
- if meta.klass.attribute_names.include?( attribute_path[1].to_s )
- string_sample( nil, meta.klass, attribute_path[1] )
- end
- end
-
+
end
end