Skip to content

Commit

Permalink
Initial ideas for user segment system.
Browse files Browse the repository at this point in the history
  • Loading branch information
Doug Youch committed May 20, 2010
1 parent fd8e250 commit 7952a25
Show file tree
Hide file tree
Showing 15 changed files with 2,129 additions and 1 deletion.
5 changes: 4 additions & 1 deletion app/controllers/content_controller.rb
Expand Up @@ -25,7 +25,10 @@ class ContentController < ModuleController #:nodoc: all
register_handler :content, :feature, "Content::CoreFeature::KeywordGenerator"

register_handler :trigger, :actions, "Trigger::CoreTrigger"


register_handler :user_segment, :fields, 'EndUserSegmentField'
register_handler :user_segment, :fields, 'EndUserActionSegmentField'

def index
@content_models,@content_actions = CmsController.get_content_models_and_actions

Expand Down
16 changes: 16 additions & 0 deletions app/models/end_user_action_segment_field.rb
@@ -0,0 +1,16 @@

class EndUserActionSegmentField < UserSegment::FieldHandler

def self.user_segment_fields_handler_info
{
:name => 'End User Action Segment Fields',
:domain_model_class => EndUserAction
}
end

register_field :renderer, UserSegment::CoreType::StringType
register_field :action, UserSegment::CoreType::StringType
register_field :created, UserSegment::CoreType::DateTimeType, :field => :created_at
register_field :occurred, UserSegment::CoreType::DateTimeType, :field => :action_at

end
19 changes: 19 additions & 0 deletions app/models/end_user_segment_field.rb
@@ -0,0 +1,19 @@

class EndUserSegmentField < UserSegment::FieldHandler

def self.user_segment_fields_handler_info
{
:name => 'End User Segment Fields',
:domain_model_class => EndUser,
:end_user_field => :id
}
end

register_field :email, UserSegment::CoreType::StringType
register_field :gender, UserSegment::CoreType::StringType
register_field :created, UserSegment::CoreType::DateTimeType, :field => :created_at
register_field :registered, UserSegment::CoreType::BooleanType
register_field :activated, UserSegment::CoreType::BooleanType
register_field :id, UserSegment::CoreType::NumberType

end
27 changes: 27 additions & 0 deletions app/models/user_segment.rb
@@ -0,0 +1,27 @@

class UserSegment < DomainModel

serialize :segment_options
serialize :fields

def operations
return @operations if @operations
@operations = UserSegment::Operations.new
@operations.operations = self.segment_options if self.segment_options
@operations
end

def operations=(text)
@operations = UserSegment::Operations.new
@operations.parse text
end

def before_create
self.order_by = 'created_at DESC' unless self.order_by
end

def before_save
self.segment_options = self.operations.to_a if self.operations && self.operations.valid?
end
end

74 changes: 74 additions & 0 deletions app/models/user_segment/core_type.rb
@@ -0,0 +1,74 @@

class UserSegment::CoreType

@@datetime_format_options = ['day', 'days', 'week', 'weeks', 'month', 'months', 'year', 'years']
def self.datetime_format_options
@@datetime_format_options
end


class DateTimeType < UserSegment::FieldType
register_operation :before, [['Value', :integer], ['Format', :option, {:options => UserSegment::CoreType.datetime_format_options}]]

def self.before(cls, field, value, format)
time = value.send(format).ago
cls.scoped(:conditions => ["#{field} <= ?", time])
end

register_operation :since, [['Value', :integer], ['Format', :option, {:options => UserSegment::CoreType.datetime_format_options}]]

def self.since(cls, field, value, format)
time = value.send(format).ago
cls.scoped(:conditions => ["#{field} >= ?", time])
end

register_operation :between, [['From', :datetime], ['To', :datetime]]

def self.between(cls, field, from, to)
cls.scoped(:conditions => ["#{field} between ? and ?", from, to])
end
end


class NumberType < UserSegment::FieldType
register_operation :greater_than, [['Value', :integer]]

def self.greater_than(cls, field, value)
cls.scoped(:conditions => ["#{field} > ?", value])
end

register_operation :less_than, [['Value', :integer]]

def self.less_than(cls, field, value)
cls.scoped(:conditions => ["#{field} < ?", value])
end

register_operation :equals, [['Value', :integer]]

def self.equals(cls, field, value)
cls.scoped(:conditions => ["#{field} = ?", value])
end
end

class StringType < UserSegment::FieldType
register_operation :like, [['String', :string]]

def self.like(cls, field, string)
cls.scoped(:conditions => ["#{field} like ?", string])
end

register_operation :is, [['String', :string]]

def self.is(cls, field, string)
cls.scoped(:conditions => ["#{field} = ?", string])
end
end

class BooleanType < UserSegment::FieldType
register_operation :is, [['Boolean', :boolean]]

def self.is(cls, field, string)
cls.scoped(:conditions => ["#{field} = ?", string])
end
end
end
110 changes: 110 additions & 0 deletions app/models/user_segment/field.rb
@@ -0,0 +1,110 @@

class UserSegment::Field < HashModel
include HandlerActions

attributes :field => nil, :operation => nil, :arguments => [], :child => nil

validates_presence_of :field
validates_presence_of :operation

def strict?; true; end

def validate
unless self.field.blank?
self.errors.add(:field, 'invalid field') unless self.handler
end

unless self.operation.blank?
self.errors.add(:operation, 'invalid operation') unless self.type_class && self.type_class.has_operation?(self.operation)

if self.operation_info
self.errors.add(:arguments, 'are missing') if self.arguments.empty? && ! self.operation_arguments.empty?

unless self.arguments.empty?
self.errors.add(:arguments, 'are incorrect') unless self.arguments.size == self.operation_arguments.size
self.errors.add(:arguments, 'are invalid') if self.arguments.size == self.operation_arguments.size && ! self.valid_arguments?
end
end
end

self.errors.add(:child, 'is invalid') if self.child_field && ! self.child_field.valid?
end

def count
@count ||= self.get_scope.count
end

def end_user_ids(ids=nil)
return @end_user_ids if @end_user_ids
scope = self.get_scope
scope = scope.scoped(:conditions => {self.end_user_field => ids}) if ids
@end_user_ids = scope.find(:all, :select => self.end_user_field).collect &self.end_user_field
end

def get_scope(scope=nil)
return @scope if @scope
scope ||= self.domain_model_class
@scope = self.type_class.send(self.operation, scope, self.model_field, *self.converted_arguments)
@scope = self.child_field.get_scope(@scope) if self.child_field
@scope
end

def converted_arguments
@converted_arguments ||= UserSegment::FieldType.convert_arguments(self.arguments, self.operation_arguments, self.operation_argument_options) if self.operation_arguments
end

def valid_arguments?
return false unless self.converted_arguments
self.converted_arguments.each { |arg| return false if arg.nil? }
true
end

def operation_arguments
self.operation_info[:arguments] if self.operation_info
end

def operation_argument_options
self.operation_info[:argument_options] if self.operation_info
end

def operation_info
@operation_info ||= self.type_class.user_segment_field_type_operations[self.operation.to_sym] if self.type_class
end

def type_class
@type_class ||= self.handler_class.user_segment_fields[self.field.to_sym][:type] if self.handler_class
end

def model_field
self.handler_class.user_segment_fields[self.field.to_sym][:field] if self.handler_class
end

def handler
@handler ||= self.get_handler_info(:user_segment, :fields).find { |info| info[:class].has_field?(self.field) }
end

def handler=(handler)
@handler = handler
end

def handler_class
self.handler[:class] if self.handler
end

def domain_model_class
self.handler[:domain_model_class] if self.handler
end

def end_user_field
(self.handler[:end_user_field] || :end_user_id) if self.handler
end

def child_field
return @child_field if @child_field
return nil unless self.child
return nil unless self.handler
@child_field = UserSegment::Field.new self.child
@child_field.handler = self.handler
@child_field
end
end
17 changes: 17 additions & 0 deletions app/models/user_segment/field_handler.rb
@@ -0,0 +1,17 @@

class UserSegment::FieldHandler

def self.user_segment_fields
@user_segment_fields ||= {}
end

def self.has_field?(field)
self.user_segment_fields[field.to_sym] ? true : false
end

def self.register_field(field, type, options={})
self.user_segment_fields[field.to_sym] = options.merge(:type => type)
self.user_segment_fields[field.to_sym][:name] ||= field.to_s.humanize
self.user_segment_fields[field.to_sym][:field] ||= field.to_sym
end
end
70 changes: 70 additions & 0 deletions app/models/user_segment/field_type.rb
@@ -0,0 +1,70 @@

class UserSegment::FieldType

def self.user_segment_field_type_operations
@user_segment_field_type_operations ||= {}
end

def self.has_operation?(operation)
self.user_segment_field_type_operations[operation.to_sym] ? true : false
end

def self.register_operation(operation, args=[], options={})
arguments = []
argument_names = []
argument_options = []
args.each do |arg|
if arg.is_a?(Array)
name = arg[0]
type = arg[1].to_sym
opts = arg[2] || {}
else
type = arg
name = type.to_s.humanize
opts = {}
end

arguments << type.to_sym
argument_names << name
argument_options << opts
end

name = options[:name] || operation.to_s.humanize

self.user_segment_field_type_operations[operation.to_sym] = options.merge(:name => name, :arguments => arguments, :argument_names => argument_names, :argument_options => argument_options)
end

# converts a string to the correct type
# supported types are :integer, :float, :double, :date, :datetime, :option, :boolean
def self.convert_to(value, type, opts={})
case type
when :integer
return value.to_i if value.is_a?(Integer) || value =~ /^\d+$/
when :float, :double
return value.to_f if value.is_a?(Numeric) || value =~ /^(\d+|\.\d+|\d+\.\d+)$/
when :string
return value
when :date, :datetime
begin
return value if value.is_a?(::Time)
return Time.parse(value)
rescue
end
when :option
value = opts[:options].find { |o| o.downcase == value.downcase }
return value if value
when :boolean
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
return true if value == '1' || value.downcase == 'true'
return false if value == '0' || value.downcase == 'false'
end

nil
end

def self.convert_arguments(arguments, types, options)
(0..arguments.length-1).collect do |idx|
self.convert_to(arguments[idx], types[idx], options[idx])
end
end
end
41 changes: 41 additions & 0 deletions app/models/user_segment/operation.rb
@@ -0,0 +1,41 @@

class UserSegment::Operation

def initialize(operator, fields)
@operator = operator
@fields = fields
end

def valid?
@fields.each { |fld| return false unless fld.valid? }
true
end

def count
@fields.collect do |fld|
@operator == 'not' ? EndUser.count - fld.count : fld.count
end.inject(0) { |sum, num| sum + num }
end

def end_user_ids(ids=nil)
return @end_user_ids if @end_user_ids

@end_user_ids = []
@fields.each { |fld| @end_user_ids = @end_user_ids + fld.end_user_ids(ids) }
@end_user_ids.uniq!

if @operator == 'not'
if ids
@end_user_ids = ids - @end_user_ids
else
@end_user_ids = EndUser.find(:all, :select => 'id', :conditions => ['id not in(?)', @end_user_ids]).collect &:id
end
end

@end_user_ids
end

def to_a
[@operator] + @fields.collect { |fld| fld.to_h }
end
end

0 comments on commit 7952a25

Please sign in to comment.