From 7952a252faf86387f55beae91006b24847b3a22a Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Thu, 13 May 2010 20:33:01 +0000 Subject: [PATCH 001/139] Initial ideas for user segment system. --- app/controllers/content_controller.rb | 5 +- app/models/end_user_action_segment_field.rb | 16 + app/models/end_user_segment_field.rb | 19 + app/models/user_segment.rb | 27 + app/models/user_segment/core_type.rb | 74 + app/models/user_segment/field.rb | 110 ++ app/models/user_segment/field_handler.rb | 17 + app/models/user_segment/field_type.rb | 70 + app/models/user_segment/operation.rb | 41 + app/models/user_segment/operations.rb | 47 + app/models/user_segment/parser.rb | 4 + app/models/user_segment_cache.rb | 4 + app/models/user_segment_option_parser.rb | 1517 +++++++++++++++++ app/models/user_segment_option_parser.treetop | 140 ++ db/migrate/20100511165552_user_segments.rb | 39 + 15 files changed, 2129 insertions(+), 1 deletion(-) create mode 100644 app/models/end_user_action_segment_field.rb create mode 100644 app/models/end_user_segment_field.rb create mode 100644 app/models/user_segment.rb create mode 100644 app/models/user_segment/core_type.rb create mode 100644 app/models/user_segment/field.rb create mode 100644 app/models/user_segment/field_handler.rb create mode 100644 app/models/user_segment/field_type.rb create mode 100644 app/models/user_segment/operation.rb create mode 100644 app/models/user_segment/operations.rb create mode 100644 app/models/user_segment/parser.rb create mode 100644 app/models/user_segment_cache.rb create mode 100644 app/models/user_segment_option_parser.rb create mode 100644 app/models/user_segment_option_parser.treetop create mode 100644 db/migrate/20100511165552_user_segments.rb diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index 2975d01a..653e46f0 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -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 diff --git a/app/models/end_user_action_segment_field.rb b/app/models/end_user_action_segment_field.rb new file mode 100644 index 00000000..b6ef5df7 --- /dev/null +++ b/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 diff --git a/app/models/end_user_segment_field.rb b/app/models/end_user_segment_field.rb new file mode 100644 index 00000000..d4eb08ef --- /dev/null +++ b/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 diff --git a/app/models/user_segment.rb b/app/models/user_segment.rb new file mode 100644 index 00000000..13b6b85d --- /dev/null +++ b/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 + diff --git a/app/models/user_segment/core_type.rb b/app/models/user_segment/core_type.rb new file mode 100644 index 00000000..5ab25e4f --- /dev/null +++ b/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 diff --git a/app/models/user_segment/field.rb b/app/models/user_segment/field.rb new file mode 100644 index 00000000..c13685fb --- /dev/null +++ b/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 diff --git a/app/models/user_segment/field_handler.rb b/app/models/user_segment/field_handler.rb new file mode 100644 index 00000000..47b1bb30 --- /dev/null +++ b/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 diff --git a/app/models/user_segment/field_type.rb b/app/models/user_segment/field_type.rb new file mode 100644 index 00000000..b82402bd --- /dev/null +++ b/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 diff --git a/app/models/user_segment/operation.rb b/app/models/user_segment/operation.rb new file mode 100644 index 00000000..9727ef50 --- /dev/null +++ b/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 diff --git a/app/models/user_segment/operations.rb b/app/models/user_segment/operations.rb new file mode 100644 index 00000000..b7ee7531 --- /dev/null +++ b/app/models/user_segment/operations.rb @@ -0,0 +1,47 @@ +require 'treetop' + +class UserSegment::Operations + + def end_user_ids + return @end_user_ids if @end_user_ids + return [] unless self.valid? + + self.operations.sort_by { |op| op.count }.each do |op| + @end_user_ids = op.end_user_ids(@end_user_ids) + return [] if @end_user_ids.empty? + end + + @end_user_ids + end + + def parser + @parser ||= UserSegmentOptionParser.new + end + + def parse(text) + if options = self.parser.parse(text) + self.operations = options.eval + end + end + + def valid? + return false unless self.operations + self.operations.each { |op| return false unless op.valid? } + true + end + + def operations + @operations + end + + def operations=(options) + @operations = options.collect do |line| + UserSegment::Operation.new line[0], line[1..-1].collect { |op| UserSegment::Field.new op } + end + end + + def to_a + return [] unless @operations + @operations.collect { |op| op.to_a } + end +end diff --git a/app/models/user_segment/parser.rb b/app/models/user_segment/parser.rb new file mode 100644 index 00000000..16a21179 --- /dev/null +++ b/app/models/user_segment/parser.rb @@ -0,0 +1,4 @@ + +class UserSegment::Parser + +end diff --git a/app/models/user_segment_cache.rb b/app/models/user_segment_cache.rb new file mode 100644 index 00000000..6ed5c117 --- /dev/null +++ b/app/models/user_segment_cache.rb @@ -0,0 +1,4 @@ + +class UserSegmentCache < DomainModel + +end diff --git a/app/models/user_segment_option_parser.rb b/app/models/user_segment_option_parser.rb new file mode 100644 index 00000000..ca944e3b --- /dev/null +++ b/app/models/user_segment_option_parser.rb @@ -0,0 +1,1517 @@ +# Autogenerated from a Treetop grammar. Edits may be lost. + + + +module UserSegmentOption + include Treetop::Runtime + + def root + @root ||= :multiple_operations + end + + module MultipleOperations0 + def operations + elements[1] + end + end + + module MultipleOperations1 + def operations + elements[0] + end + + def more + elements[1] + end + + end + + module MultipleOperations2 + def eval(env={}) + [operations.eval(env)] + more.elements.collect { |e| e.operations.eval(env) } + end + end + + def _nt_multiple_operations + start_index = index + if node_cache[:multiple_operations].has_key?(index) + cached = node_cache[:multiple_operations][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_operations + s0 << r1 + if r1 + s2, i2 = [], index + loop do + i3, s3 = index, [] + s4, i4 = [], index + loop do + r5 = _nt_newline + if r5 + s4 << r5 + else + break + end + end + if s4.empty? + @index = i4 + r4 = nil + else + r4 = instantiate_node(SyntaxNode,input, i4...index, s4) + end + s3 << r4 + if r4 + r6 = _nt_operations + s3 << r6 + end + if s3.last + r3 = instantiate_node(SyntaxNode,input, i3...index, s3) + r3.extend(MultipleOperations0) + else + @index = i3 + r3 = nil + end + if r3 + s2 << r3 + else + break + end + end + r2 = instantiate_node(SyntaxNode,input, i2...index, s2) + s0 << r2 + if r2 + s7, i7 = [], index + loop do + r8 = _nt_newline + if r8 + s7 << r8 + else + break + end + end + r7 = instantiate_node(SyntaxNode,input, i7...index, s7) + s0 << r7 + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(MultipleOperations1) + r0.extend(MultipleOperations2) + else + @index = i0 + r0 = nil + end + + node_cache[:multiple_operations][start_index] = r0 + + r0 + end + + module Operations0 + def operation + elements[1] + end + end + + module Operations1 + def not_op + elements[0] + end + + def operation + elements[1] + end + + def more + elements[2] + end + end + + module Operations2 + def eval(env={}) + [not_op.eval(env), operation.eval(env)] + more.elements.collect { |e| e.operation.eval(env) } + end + end + + def _nt_operations + start_index = index + if node_cache[:operations].has_key?(index) + cached = node_cache[:operations][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_not + s0 << r1 + if r1 + r2 = _nt_operation + s0 << r2 + if r2 + s3, i3 = [], index + loop do + i4, s4 = index, [] + if has_terminal?('+', false, index) + r5 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('+') + r5 = nil + end + s4 << r5 + if r5 + r6 = _nt_operation + s4 << r6 + end + if s4.last + r4 = instantiate_node(SyntaxNode,input, i4...index, s4) + r4.extend(Operations0) + else + @index = i4 + r4 = nil + end + if r4 + s3 << r4 + else + break + end + end + r3 = instantiate_node(SyntaxNode,input, i3...index, s3) + s0 << r3 + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Operations1) + r0.extend(Operations2) + else + @index = i0 + r0 = nil + end + + node_cache[:operations][start_index] = r0 + + r0 + end + + module Operation0 + def space1 + elements[0] + end + + def field + elements[1] + end + + def operation + elements[3] + end + + def lparen + elements[4] + end + + def arguments + elements[5] + end + + def rparen + elements[6] + end + + def child + elements[7] + end + + def space2 + elements[8] + end + end + + module Operation1 + def eval(env={}) + {:field => field.eval(env), :operation => operation.eval(env), :arguments => arguments.eval(env), :child => child.eval(env)} + end + end + + def _nt_operation + start_index = index + if node_cache[:operation].has_key?(index) + cached = node_cache[:operation][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_space + s0 << r1 + if r1 + r2 = _nt_field + s0 << r2 + if r2 + if has_terminal?('.', false, index) + r3 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('.') + r3 = nil + end + s0 << r3 + if r3 + r4 = _nt_operation_name + s0 << r4 + if r4 + r5 = _nt_lparen + s0 << r5 + if r5 + r6 = _nt_arguments + s0 << r6 + if r6 + r7 = _nt_rparen + s0 << r7 + if r7 + r8 = _nt_sub_operations + s0 << r8 + if r8 + r9 = _nt_space + s0 << r9 + end + end + end + end + end + end + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Operation0) + r0.extend(Operation1) + else + @index = i0 + r0 = nil + end + + node_cache[:operation][start_index] = r0 + + r0 + end + + module SubOperations0 + def field + elements[1] + end + + def operation_name + elements[3] + end + + def lparen + elements[4] + end + + def arguments + elements[5] + end + + def rparen + elements[6] + end + end + + module SubOperations1 + def eval(env={}) + if empty? + nil + else + child = nil + elements.reverse.each do |e| + child = {:field => e.field.eval(env), :operation => e.operation_name.eval(env), :arguments => e.arguments.eval(env), :child => child} + end + child + end + end + end + + def _nt_sub_operations + start_index = index + if node_cache[:sub_operations].has_key?(index) + cached = node_cache[:sub_operations][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + s0, i0 = [], index + loop do + i1, s1 = index, [] + if has_terminal?('.', false, index) + r2 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('.') + r2 = nil + end + s1 << r2 + if r2 + r3 = _nt_field + s1 << r3 + if r3 + if has_terminal?('.', false, index) + r4 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('.') + r4 = nil + end + s1 << r4 + if r4 + r5 = _nt_operation_name + s1 << r5 + if r5 + r6 = _nt_lparen + s1 << r6 + if r6 + r7 = _nt_arguments + s1 << r7 + if r7 + r8 = _nt_rparen + s1 << r8 + end + end + end + end + end + end + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(SubOperations0) + else + @index = i1 + r1 = nil + end + if r1 + s0 << r1 + else + break + end + end + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(SubOperations1) + + node_cache[:sub_operations][start_index] = r0 + + r0 + end + + module Field0 + end + + module Field1 + def eval(env={}) + text_value + end + end + + def _nt_field + start_index = index + if node_cache[:field].has_key?(index) + cached = node_cache[:field][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + if has_terminal?('\G[a-zA-Z]', true, index) + r1 = true + @index += 1 + else + r1 = nil + end + s0 << r1 + if r1 + s2, i2 = [], index + loop do + if has_terminal?('\G[a-zA-Z0-9_]', true, index) + r3 = true + @index += 1 + else + r3 = nil + end + if r3 + s2 << r3 + else + break + end + end + r2 = instantiate_node(SyntaxNode,input, i2...index, s2) + s0 << r2 + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Field0) + r0.extend(Field1) + else + @index = i0 + r0 = nil + end + + node_cache[:field][start_index] = r0 + + r0 + end + + module OperationName0 + end + + module OperationName1 + def eval(env={}) + text_value + end + end + + def _nt_operation_name + start_index = index + if node_cache[:operation_name].has_key?(index) + cached = node_cache[:operation_name][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + if has_terminal?('\G[a-zA-Z]', true, index) + r1 = true + @index += 1 + else + r1 = nil + end + s0 << r1 + if r1 + s2, i2 = [], index + loop do + if has_terminal?('\G[a-zA-Z0-9_]', true, index) + r3 = true + @index += 1 + else + r3 = nil + end + if r3 + s2 << r3 + else + break + end + end + r2 = instantiate_node(SyntaxNode,input, i2...index, s2) + s0 << r2 + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(OperationName0) + r0.extend(OperationName1) + else + @index = i0 + r0 = nil + end + + node_cache[:operation_name][start_index] = r0 + + r0 + end + + module Arguments0 + def argument + elements[1] + end + end + + module Arguments1 + def arg + elements[0] + end + + def more + elements[1] + end + end + + module Arguments2 + def eval(env={}) + [arg.eval(env)] + more.elements.collect{ |e| e.argument.eval(env) } + end + end + + def _nt_arguments + start_index = index + if node_cache[:arguments].has_key?(index) + cached = node_cache[:arguments][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_argument + s0 << r1 + if r1 + s2, i2 = [], index + loop do + i3, s3 = index, [] + if has_terminal?(',', false, index) + r4 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure(',') + r4 = nil + end + s3 << r4 + if r4 + r5 = _nt_argument + s3 << r5 + end + if s3.last + r3 = instantiate_node(SyntaxNode,input, i3...index, s3) + r3.extend(Arguments0) + else + @index = i3 + r3 = nil + end + if r3 + s2 << r3 + else + break + end + end + r2 = instantiate_node(SyntaxNode,input, i2...index, s2) + s0 << r2 + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Arguments1) + r0.extend(Arguments2) + else + @index = i0 + r0 = nil + end + + node_cache[:arguments][start_index] = r0 + + r0 + end + + module Argument0 + def space1 + elements[0] + end + + def arg + elements[1] + end + + def space2 + elements[2] + end + end + + module Argument1 + def eval(env={}) + arg.eval(env) + end + end + + def _nt_argument + start_index = index + if node_cache[:argument].has_key?(index) + cached = node_cache[:argument][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_space + s0 << r1 + if r1 + i2 = index + r3 = _nt_string + if r3 + r2 = r3 + else + r4 = _nt_integer + if r4 + r2 = r4 + else + r5 = _nt_float + if r5 + r2 = r5 + else + r6 = _nt_boolean + if r6 + r2 = r6 + else + @index = i2 + r2 = nil + end + end + end + end + s0 << r2 + if r2 + r7 = _nt_space + s0 << r7 + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Argument0) + r0.extend(Argument1) + else + @index = i0 + r0 = nil + end + + node_cache[:argument][start_index] = r0 + + r0 + end + + module String0 + end + + module String1 + end + + module String2 + def eval(env={}) + text_value[1..-2] + end + end + + def _nt_string + start_index = index + if node_cache[:string].has_key?(index) + cached = node_cache[:string][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + if has_terminal?('"', false, index) + r1 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('"') + r1 = nil + end + s0 << r1 + if r1 + s2, i2 = [], index + loop do + i3 = index + i4, s4 = index, [] + i5 = index + if has_terminal?('"', false, index) + r6 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('"') + r6 = nil + end + if r6 + r5 = nil + else + @index = i5 + r5 = instantiate_node(SyntaxNode,input, index...index) + end + s4 << r5 + if r5 + if index < input_length + r7 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure("any character") + r7 = nil + end + s4 << r7 + end + if s4.last + r4 = instantiate_node(SyntaxNode,input, i4...index, s4) + r4.extend(String0) + else + @index = i4 + r4 = nil + end + if r4 + r3 = r4 + else + if has_terminal?('\"', false, index) + r8 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 + else + terminal_parse_failure('\"') + r8 = nil + end + if r8 + r3 = r8 + else + @index = i3 + r3 = nil + end + end + if r3 + s2 << r3 + else + break + end + end + r2 = instantiate_node(SyntaxNode,input, i2...index, s2) + s0 << r2 + if r2 + if has_terminal?('"', false, index) + r9 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('"') + r9 = nil + end + s0 << r9 + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(String1) + r0.extend(String2) + else + @index = i0 + r0 = nil + end + + node_cache[:string][start_index] = r0 + + r0 + end + + module Integer0 + end + + module Integer1 + def eval(env={}) + text_value.to_i + end + end + + def _nt_integer + start_index = index + if node_cache[:integer].has_key?(index) + cached = node_cache[:integer][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + i1, s1 = index, [] + if has_terminal?('\G[1-9]', true, index) + r2 = true + @index += 1 + else + r2 = nil + end + s1 << r2 + if r2 + s3, i3 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r4 = true + @index += 1 + else + r4 = nil + end + if r4 + s3 << r4 + else + break + end + end + r3 = instantiate_node(SyntaxNode,input, i3...index, s3) + s1 << r3 + end + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(Integer0) + else + @index = i1 + r1 = nil + end + if r1 + r0 = r1 + r0.extend(Integer1) + else + if has_terminal?('0', false, index) + r5 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('0') + r5 = nil + end + if r5 + r0 = r5 + r0.extend(Integer1) + else + @index = i0 + r0 = nil + end + end + + node_cache[:integer][start_index] = r0 + + r0 + end + + module Boolean0 + def eval(env={}) + if text_value.downcase == 'true' + true + else + false + end + end + end + + def _nt_boolean + start_index = index + if node_cache[:boolean].has_key?(index) + cached = node_cache[:boolean][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + if has_terminal?('true', false, index) + r1 = instantiate_node(SyntaxNode,input, index...(index + 4)) + @index += 4 + else + terminal_parse_failure('true') + r1 = nil + end + if r1 + r0 = r1 + r0.extend(Boolean0) + else + if has_terminal?('TRUE', false, index) + r2 = instantiate_node(SyntaxNode,input, index...(index + 4)) + @index += 4 + else + terminal_parse_failure('TRUE') + r2 = nil + end + if r2 + r0 = r2 + r0.extend(Boolean0) + else + if has_terminal?('True', false, index) + r3 = instantiate_node(SyntaxNode,input, index...(index + 4)) + @index += 4 + else + terminal_parse_failure('True') + r3 = nil + end + if r3 + r0 = r3 + r0.extend(Boolean0) + else + if has_terminal?('false', false, index) + r4 = instantiate_node(SyntaxNode,input, index...(index + 5)) + @index += 5 + else + terminal_parse_failure('false') + r4 = nil + end + if r4 + r0 = r4 + r0.extend(Boolean0) + else + if has_terminal?('FALSE', false, index) + r5 = instantiate_node(SyntaxNode,input, index...(index + 5)) + @index += 5 + else + terminal_parse_failure('FALSE') + r5 = nil + end + if r5 + r0 = r5 + r0.extend(Boolean0) + else + if has_terminal?('False', false, index) + r6 = instantiate_node(SyntaxNode,input, index...(index + 5)) + @index += 5 + else + terminal_parse_failure('False') + r6 = nil + end + if r6 + r0 = r6 + r0.extend(Boolean0) + else + @index = i0 + r0 = nil + end + end + end + end + end + end + + node_cache[:boolean][start_index] = r0 + + r0 + end + + module Float0 + end + + module Float1 + end + + module Float2 + end + + module Float3 + def eval(env={}) + text_value.to_f + end + end + + def _nt_float + start_index = index + if node_cache[:float].has_key?(index) + cached = node_cache[:float][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + i1, s1 = index, [] + if has_terminal?('\G[1-9]', true, index) + r2 = true + @index += 1 + else + r2 = nil + end + s1 << r2 + if r2 + s3, i3 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r4 = true + @index += 1 + else + r4 = nil + end + if r4 + s3 << r4 + else + break + end + end + r3 = instantiate_node(SyntaxNode,input, i3...index, s3) + s1 << r3 + end + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(Float0) + else + @index = i1 + r1 = nil + end + if r1 + r0 = r1 + else + i5, s5 = index, [] + if has_terminal?('.', false, index) + r6 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('.') + r6 = nil + end + s5 << r6 + if r6 + s7, i7 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r8 = true + @index += 1 + else + r8 = nil + end + if r8 + s7 << r8 + else + break + end + end + if s7.empty? + @index = i7 + r7 = nil + else + r7 = instantiate_node(SyntaxNode,input, i7...index, s7) + end + s5 << r7 + end + if s5.last + r5 = instantiate_node(SyntaxNode,input, i5...index, s5) + r5.extend(Float1) + else + @index = i5 + r5 = nil + end + if r5 + r0 = r5 + else + i9, s9 = index, [] + s10, i10 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r11 = true + @index += 1 + else + r11 = nil + end + if r11 + s10 << r11 + else + break + end + end + if s10.empty? + @index = i10 + r10 = nil + else + r10 = instantiate_node(SyntaxNode,input, i10...index, s10) + end + s9 << r10 + if r10 + if has_terminal?('.', false, index) + r12 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('.') + r12 = nil + end + s9 << r12 + if r12 + if has_terminal?('\G[0-9]', true, index) + r13 = true + @index += 1 + else + r13 = nil + end + s9 << r13 + end + end + if s9.last + r9 = instantiate_node(SyntaxNode,input, i9...index, s9) + r9.extend(Float2) + r9.extend(Float3) + else + @index = i9 + r9 = nil + end + if r9 + r0 = r9 + else + @index = i0 + r0 = nil + end + end + end + + node_cache[:float][start_index] = r0 + + r0 + end + + module Lparen0 + def space1 + elements[0] + end + + def space2 + elements[2] + end + end + + def _nt_lparen + start_index = index + if node_cache[:lparen].has_key?(index) + cached = node_cache[:lparen][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_space + s0 << r1 + if r1 + if has_terminal?('(', false, index) + r2 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('(') + r2 = nil + end + s0 << r2 + if r2 + r3 = _nt_space + s0 << r3 + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Lparen0) + else + @index = i0 + r0 = nil + end + + node_cache[:lparen][start_index] = r0 + + r0 + end + + module Rparen0 + def space1 + elements[0] + end + + def space2 + elements[2] + end + end + + def _nt_rparen + start_index = index + if node_cache[:rparen].has_key?(index) + cached = node_cache[:rparen][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_space + s0 << r1 + if r1 + if has_terminal?(')', false, index) + r2 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure(')') + r2 = nil + end + s0 << r2 + if r2 + r3 = _nt_space + s0 << r3 + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Rparen0) + else + @index = i0 + r0 = nil + end + + node_cache[:rparen][start_index] = r0 + + r0 + end + + module NonSpaceChar0 + end + + def _nt_non_space_char + start_index = index + if node_cache[:non_space_char].has_key?(index) + cached = node_cache[:non_space_char][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + i1 = index + if has_terminal?('\G[ ]', true, index) + r2 = true + @index += 1 + else + r2 = nil + end + if r2 + r1 = nil + else + @index = i1 + r1 = instantiate_node(SyntaxNode,input, index...index) + end + s0 << r1 + if r1 + if index < input_length + r3 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure("any character") + r3 = nil + end + s0 << r3 + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(NonSpaceChar0) + else + @index = i0 + r0 = nil + end + + node_cache[:non_space_char][start_index] = r0 + + r0 + end + + module Not0 + end + + module Not1 + def space + elements[1] + end + end + + module Not2 + def eval(env={}) + empty? ? nil : 'not' + end + end + + def _nt_not + start_index = index + if node_cache[:not].has_key?(index) + cached = node_cache[:not][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + i2 = index + i3, s3 = index, [] + if has_terminal?('\G[nN]', true, index) + r4 = true + @index += 1 + else + r4 = nil + end + s3 << r4 + if r4 + if has_terminal?('\G[oO]', true, index) + r5 = true + @index += 1 + else + r5 = nil + end + s3 << r5 + if r5 + if has_terminal?('\G[tT]', true, index) + r6 = true + @index += 1 + else + r6 = nil + end + s3 << r6 + if r6 + if has_terminal?(' ', false, index) + r7 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure(' ') + r7 = nil + end + s3 << r7 + end + end + end + if s3.last + r3 = instantiate_node(SyntaxNode,input, i3...index, s3) + r3.extend(Not0) + else + @index = i3 + r3 = nil + end + if r3 + r2 = r3 + else + if has_terminal?('!', false, index) + r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('!') + r8 = nil + end + if r8 + r2 = r8 + else + @index = i2 + r2 = nil + end + end + if r2 + r1 = r2 + else + r1 = instantiate_node(SyntaxNode,input, index...index) + end + s0 << r1 + if r1 + r9 = _nt_space + s0 << r9 + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Not1) + r0.extend(Not2) + else + @index = i0 + r0 = nil + end + + node_cache[:not][start_index] = r0 + + r0 + end + + module Newline0 + def space + elements[0] + end + + end + + def _nt_newline + start_index = index + if node_cache[:newline].has_key?(index) + cached = node_cache[:newline][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_space + s0 << r1 + if r1 + if has_terminal?("\n", false, index) + r2 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure("\n") + r2 = nil + end + s0 << r2 + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Newline0) + else + @index = i0 + r0 = nil + end + + node_cache[:newline][start_index] = r0 + + r0 + end + + def _nt_space + start_index = index + if node_cache[:space].has_key?(index) + cached = node_cache[:space][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + s0, i0 = [], index + loop do + if has_terminal?('\G[ ]', true, index) + r1 = true + @index += 1 + else + r1 = nil + end + if r1 + s0 << r1 + else + break + end + end + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + + node_cache[:space][start_index] = r0 + + r0 + end + +end + +class UserSegmentOptionParser < Treetop::Runtime::CompiledParser + include UserSegmentOption +end + + diff --git a/app/models/user_segment_option_parser.treetop b/app/models/user_segment_option_parser.treetop new file mode 100644 index 00000000..d4e4b9e3 --- /dev/null +++ b/app/models/user_segment_option_parser.treetop @@ -0,0 +1,140 @@ + +grammar UserSegmentOption + + rule multiple_operations + operations:operations more:(newline+ operations)* newline* { + def eval(env={}) + [operations.eval(env)] + more.elements.collect { |e| e.operations.eval(env) } + end + } + end + + rule operations + not_op:not operation:operation more:('+' operation)* { + def eval(env={}) + [not_op.eval(env), operation.eval(env)] + more.elements.collect { |e| e.operation.eval(env) } + end + } + end + + rule operation + space field:field '.' operation:operation_name lparen arguments:arguments rparen child:sub_operations space { + def eval(env={}) + {:field => field.eval(env), :operation => operation.eval(env), :arguments => arguments.eval(env), :child => child.eval(env)} + end + } + end + + rule sub_operations + ('.' field '.' operation_name lparen arguments rparen)* { + def eval(env={}) + if empty? + nil + else + child = nil + elements.reverse.each do |e| + child = {:field => e.field.eval(env), :operation => e.operation_name.eval(env), :arguments => e.arguments.eval(env), :child => child} + end + child + end + end + } + end + + rule field + [a-zA-Z] [a-zA-Z0-9_]* { + def eval(env={}) + text_value + end + } + end + + rule operation_name + [a-zA-Z] [a-zA-Z0-9_]* { + def eval(env={}) + text_value + end + } + end + + rule arguments + arg:argument more:(',' argument)* { + def eval(env={}) + [arg.eval(env)] + more.elements.collect{ |e| e.argument.eval(env) } + end + } + end + + rule argument + space arg:(string / integer / float / boolean) space { + def eval(env={}) + arg.eval(env) + end + } + end + + rule string + '"' (!'"' . / '\"')* '"' { + def eval(env={}) + text_value[1..-2] + end + } + end + + rule integer + ([1-9] [0-9]* / '0') { + def eval(env={}) + text_value.to_i + end + } + end + + rule boolean + ('true' / 'TRUE' / 'True' / 'false' / 'FALSE' / 'False') { + def eval(env={}) + if text_value.downcase == 'true' + true + else + false + end + end + } + end + + rule float + [1-9] [0-9]* / '.' [0-9]+ / [0-9]+ '.' [0-9] { + def eval(env={}) + text_value.to_f + end + } + end + + rule lparen + space '(' space + end + + rule rparen + space ')' space + end + + rule non_space_char + ![ ] . + end + + rule not + ([nN] [oO] [tT] ' ' / '!')? space { + def eval(env={}) + empty? ? nil : 'not' + end + } + end + + rule newline + space "\n" + end + + rule space + [ ]* + end +end + diff --git a/db/migrate/20100511165552_user_segments.rb b/db/migrate/20100511165552_user_segments.rb new file mode 100644 index 00000000..b6657ccd --- /dev/null +++ b/db/migrate/20100511165552_user_segments.rb @@ -0,0 +1,39 @@ +class UserSegments < ActiveRecord::Migration + def self.up + create_table :user_segments, :force => true do |t| + t.string :name + t.text :description + t.string :type + t.boolean :main_page + t.datetime :last_ran_at + t.integer :last_count + t.text :fields + t.text :segment_options + t.text :segment_options_text + t.string :order_by + t.timestamps + end + + create_table :user_segment_caches, :force => true do |t| + t.integer :user_segment_id + t.text :id_list + t.datetime :created_at + end + + create_table :user_segment_analytics, :force => true do |t| + t.integer :user_segment_id + t.text :fields + t.text :results + t.datetime :start_date + t.datetime :end_date + t.string :step + t.timestamps + end + end + + def self.down + drop_table :user_segments + drop_table :user_segment_caches + drop_table :user_segment_analytics + end +end From 185ce247234c103c21b5bb4abd11c6a557890f02 Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Thu, 13 May 2010 23:52:43 +0000 Subject: [PATCH 002/139] Fixes for user segment types. Added specs for most basic parts of user segments. --- app/models/user_segment/core_type.rb | 20 +++- app/models/user_segment/field_handler.rb | 17 ++- app/models/user_segment/field_type.rb | 24 ++-- app/models/user_segment/parser.rb | 4 - spec/models/user_segment/core_type_spec.rb | 111 ++++++++++++++++++ .../models/user_segment/field_handler_spec.rb | 18 +++ spec/models/user_segment/field_spec.rb | 62 ++++++++++ spec/models/user_segment/field_type_spec.rb | 90 ++++++++++++++ spec/models/user_segment/operation_spec.rb | 47 ++++++++ 9 files changed, 373 insertions(+), 20 deletions(-) delete mode 100644 app/models/user_segment/parser.rb create mode 100644 spec/models/user_segment/core_type_spec.rb create mode 100644 spec/models/user_segment/field_handler_spec.rb create mode 100644 spec/models/user_segment/field_spec.rb create mode 100644 spec/models/user_segment/field_type_spec.rb create mode 100644 spec/models/user_segment/operation_spec.rb diff --git a/app/models/user_segment/core_type.rb b/app/models/user_segment/core_type.rb index 5ab25e4f..28b83e60 100644 --- a/app/models/user_segment/core_type.rb +++ b/app/models/user_segment/core_type.rb @@ -1,7 +1,7 @@ class UserSegment::CoreType - @@datetime_format_options = ['day', 'days', 'week', 'weeks', 'month', 'months', 'year', 'years'] + @@datetime_format_options = ['second', 'seconds', 'minute', 'minutes', 'hour', 'hours', 'day', 'days', 'week', 'weeks', 'month', 'months', 'year', 'years'] def self.datetime_format_options @@datetime_format_options end @@ -37,17 +37,35 @@ def self.greater_than(cls, field, value) cls.scoped(:conditions => ["#{field} > ?", value]) end + register_operation :greater_than_or_equal_to, [['Value', :integer]] + + def self.greater_than_or_equal_to(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 :less_than_or_equal_to, [['Value', :integer]] + + def self.less_than_or_equal_to(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 + + register_operation :is, [['Value', :integer]] + + def self.is(cls, field, value) + self.equals(cls, field, value) + end end class StringType < UserSegment::FieldType diff --git a/app/models/user_segment/field_handler.rb b/app/models/user_segment/field_handler.rb index 47b1bb30..f8e7d126 100644 --- a/app/models/user_segment/field_handler.rb +++ b/app/models/user_segment/field_handler.rb @@ -1,17 +1,22 @@ class UserSegment::FieldHandler - def self.user_segment_fields - @user_segment_fields ||= {} - end + def self.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 + fields = self.user_segment_fields + + fields[field.to_sym] = options.merge(:type => type) + fields[field.to_sym][:name] ||= field.to_s.humanize + fields[field.to_sym][:field] ||= field.to_sym + + sing = class << self; self; end + sing.send :define_method, :user_segment_fields do + fields + end end end diff --git a/app/models/user_segment/field_type.rb b/app/models/user_segment/field_type.rb index b82402bd..bd722608 100644 --- a/app/models/user_segment/field_type.rb +++ b/app/models/user_segment/field_type.rb @@ -1,15 +1,15 @@ class UserSegment::FieldType - def self.user_segment_field_type_operations - @user_segment_field_type_operations ||= {} - end + def self.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={}) + operations = self.user_segment_field_type_operations + arguments = [] argument_names = [] argument_options = [] @@ -29,9 +29,14 @@ def self.register_operation(operation, args=[], options={}) argument_options << opts end - name = options[:name] || operation.to_s.humanize + + operations[operation.to_sym] = options.merge(:arguments => arguments, :argument_names => argument_names, :argument_options => argument_options) + operations[operation.to_sym][: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) + sing = class << self; self; end + sing.send :define_method, :user_segment_field_type_operations do + operations + end end # converts a string to the correct type @@ -43,10 +48,10 @@ def self.convert_to(value, type, opts={}) when :float, :double return value.to_f if value.is_a?(Numeric) || value =~ /^(\d+|\.\d+|\d+\.\d+)$/ when :string - return value + return value if value.is_a?(String) when :date, :datetime begin - return value if value.is_a?(::Time) + return value if value.is_a?(Time) return Time.parse(value) rescue end @@ -55,8 +60,9 @@ def self.convert_to(value, type, opts={}) 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' + value = value.downcase if value.is_a?(String) + return true if value == 1 || value == '1' || value == 'true' + return false if value == 0 || value == '0' || value == 'false' end nil diff --git a/app/models/user_segment/parser.rb b/app/models/user_segment/parser.rb deleted file mode 100644 index 16a21179..00000000 --- a/app/models/user_segment/parser.rb +++ /dev/null @@ -1,4 +0,0 @@ - -class UserSegment::Parser - -end diff --git a/spec/models/user_segment/core_type_spec.rb b/spec/models/user_segment/core_type_spec.rb new file mode 100644 index 00000000..4c645e69 --- /dev/null +++ b/spec/models/user_segment/core_type_spec.rb @@ -0,0 +1,111 @@ +require File.dirname(__FILE__) + "/../../spec_helper" + +describe UserSegment::CoreType do + + reset_domain_tables :end_users + + describe "DateTimeType" do + before(:each) do + EndUser.push_target('test1@test.dev', :created_at => 2.days.ago) + EndUser.push_target('test2@test.dev', :created_at => 5.days.ago) + EndUser.push_target('test3@test.dev') + + @type = UserSegment::CoreType::DateTimeType + end + + it "should return users using before" do + @type.before(EndUser, :created_at, 1, 'day').count.should == 2 + @type.before(EndUser, :created_at, 3, 'days').count.should == 1 + @type.before(EndUser, :created_at, 6, 'days').count.should == 0 + end + + it "should return users using since" do + @type.since(EndUser, :created_at, 1, 'day').count.should == 1 + @type.since(EndUser, :created_at, 3, 'days').count.should == 2 + @type.since(EndUser, :created_at, 6, 'days').count.should == 3 + end + + it "should return users using between" do + @type.between(EndUser, :created_at, 1.day.ago, Time.now).count.should == 1 + @type.between(EndUser, :created_at, 3.day.ago, Time.now).count.should == 2 + @type.between(EndUser, :created_at, 6.day.ago, Time.now).count.should == 3 + end + end + + describe "NumberType" do + before(:each) do + EndUser.push_target('test1@test.dev', :user_level => 1) + EndUser.push_target('test2@test.dev', :user_level => 2) + EndUser.push_target('test3@test.dev', :user_level => 3) + EndUser.push_target('test4@test.dev', :user_level => 3) + + @type = UserSegment::CoreType::NumberType + end + + it "should return users using greater_than" do + @type.greater_than(EndUser, :user_level, 2).count.should == 2 + @type.greater_than(EndUser, :user_level, 3).count.should == 0 + end + + it "should return users using greater_than_or_equal_to" do + @type.greater_than_or_equal_to(EndUser, :user_level, 1).count.should == 4 + @type.greater_than_or_equal_to(EndUser, :user_level, 2).count.should == 3 + @type.greater_than_or_equal_to(EndUser, :user_level, 4).count.should == 0 + end + + it "should return users using less_than" do + @type.less_than(EndUser, :user_level, 1).count.should == 0 + @type.less_than(EndUser, :user_level, 2).count.should == 1 + @type.less_than(EndUser, :user_level, 4).count.should == 4 + end + + it "should return users using less_than_or_equal_to" do + @type.less_than_or_equal_to(EndUser, :user_level, 1).count.should == 1 + @type.less_than_or_equal_to(EndUser, :user_level, 2).count.should == 2 + @type.less_than_or_equal_to(EndUser, :user_level, 3).count.should == 4 + @type.less_than_or_equal_to(EndUser, :user_level, 4).count.should == 4 + end + + it "should return users using equals" do + @type.equals(EndUser, :user_level, 1).count.should == 1 + @type.equals(EndUser, :user_level, 2).count.should == 1 + @type.equals(EndUser, :user_level, 3).count.should == 2 + @type.is(EndUser, :user_level, 3).count.should == 2 + end + end + + describe "StringType" do + before(:each) do + EndUser.push_target('test1@test.dev') + EndUser.push_target('test2@test.dev') + EndUser.push_target('test3@test.dev') + EndUser.push_target('test4@test.dev') + + @type = UserSegment::CoreType::StringType + end + + it "should return users using like" do + @type.like(EndUser, :email, 'test%@test.dev').count.should == 4 + end + + it "should return users using is" do + @type.is(EndUser, :email, 'test1@test.dev').count.should == 1 + end + end + + describe "BooleanType" do + before(:each) do + EndUser.push_target('test1@test.dev', :activated => true) + EndUser.push_target('test2@test.dev', :activated => true) + EndUser.push_target('test3@test.dev', :activated => true) + EndUser.push_target('test4@test.dev', :activated => false) + + @type = UserSegment::CoreType::BooleanType + end + + it "should return users using is" do + @type.is(EndUser, :activated, true).count.should == 3 + @type.is(EndUser, :activated, false).count.should == 1 + end + end +end diff --git a/spec/models/user_segment/field_handler_spec.rb b/spec/models/user_segment/field_handler_spec.rb new file mode 100644 index 00000000..bcd1682f --- /dev/null +++ b/spec/models/user_segment/field_handler_spec.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + "/../../spec_helper" + +describe UserSegment::FieldHandler do + + it "should be able to register a field" do + UserSegment::FieldHandler.register_field(:created, UserSegment::CoreType::DateTimeType, :field => :created_at) + UserSegment::FieldHandler.has_field?(:created).should be_true + UserSegment::FieldHandler.user_segment_fields[:created][:field].should == :created_at + UserSegment::FieldHandler.user_segment_fields[:created][:type].should == UserSegment::CoreType::DateTimeType + UserSegment::FieldHandler.user_segment_fields[:created][:name].should == 'Created' + + UserSegment::FieldHandler.register_field(:created, UserSegment::CoreType::DateTimeType, :field => :created_at, :name => 'EndUser.created') + UserSegment::FieldHandler.has_field?(:created).should be_true + UserSegment::FieldHandler.user_segment_fields[:created][:field].should == :created_at + UserSegment::FieldHandler.user_segment_fields[:created][:type].should == UserSegment::CoreType::DateTimeType + UserSegment::FieldHandler.user_segment_fields[:created][:name].should == 'EndUser.created' + end +end diff --git a/spec/models/user_segment/field_spec.rb b/spec/models/user_segment/field_spec.rb new file mode 100644 index 00000000..85ea21bb --- /dev/null +++ b/spec/models/user_segment/field_spec.rb @@ -0,0 +1,62 @@ +require File.dirname(__FILE__) + "/../../spec_helper" + +describe UserSegment::Field do + + reset_domain_tables :end_users + + @handler = EndUserSegmentField.user_segment_fields_handler_info + + before(:each) do + EndUser.push_target('test1@test.dev', :created_at => 2.days.ago, :activated => true) + EndUser.push_target('test2@test.dev', :created_at => 5.days.ago, :activated => false) + EndUser.push_target('test3@test.dev', :activated => false) + end + + it "should be able to use handler" do + @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] + @field.handler = @handler + + @field.handler_class.should == EndUserSegmentField + @field.domain_model_class.should == EndUser + @field.end_user_field.should == :id + @field.model_field.should == :created_at + @field.type_class.should == UserSegment::CoreType::DateTimeType + @field.operation_arguments.should == [:integer, :option] + @field.valid_arguments?.should == true + @field.valid?.should == true + end + + it "should return the count" do + @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] + @field.handler = @handler + @field.valid?.should == true + @field.count.should == 2 + @field.end_user_ids.length.should == 2 + + @field = UserSegment::Field.new :field => 'created', :operation => 'since', :arguments => [1, 'days'] + @field.handler = @handler + @field.valid?.should == true + @field.count.should == 1 + @field.end_user_ids.length.should == 1 + end + + it "should work with children" do + @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'], :child => {:field => 'activated', :operation => 'is', :arguments => [true]} + @field.handler = @handler + @field.valid?.should == true + @field.count.should == 1 + @field.end_user_ids.length.should == 1 + + @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'], :child => {:field => 'activated', :operation => 'is', :arguments => [true], :child => {:field => 'email', :operation => 'like', :arguments => ['test%@test.dev']}} + @field.handler = @handler + @field.valid?.should == true + @field.count.should == 1 + @field.end_user_ids.length.should == 1 + + @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'], :child => {:field => 'activated', :operation => 'is', :arguments => [false], :child => {:field => 'email', :operation => 'like', :arguments => ['test%@test.dev']}} + @field.handler = @handler + @field.valid?.should == true + @field.count.should == 1 + @field.end_user_ids.length.should == 1 + end +end diff --git a/spec/models/user_segment/field_type_spec.rb b/spec/models/user_segment/field_type_spec.rb new file mode 100644 index 00000000..54c9898a --- /dev/null +++ b/spec/models/user_segment/field_type_spec.rb @@ -0,0 +1,90 @@ +require File.dirname(__FILE__) + "/../../spec_helper" + +describe UserSegment::FieldType do + + it "should convert value to integer" do + UserSegment::FieldType.convert_to("0", :integer).should == 0 + UserSegment::FieldType.convert_to(0, :integer).should == 0 + UserSegment::FieldType.convert_to("1", :integer).should == 1 + UserSegment::FieldType.convert_to(99, :integer).should == 99 + UserSegment::FieldType.convert_to(99.0, :integer).should be_nil + UserSegment::FieldType.convert_to("99", :integer).should == 99 + UserSegment::FieldType.convert_to("1n", :integer).should be_nil + end + + it "should convert value to float" do + UserSegment::FieldType.convert_to("0", :float).should == 0.0 + UserSegment::FieldType.convert_to(0, :float).should == 0.0 + UserSegment::FieldType.convert_to("1", :float).should == 1.0 + UserSegment::FieldType.convert_to(99, :float).should == 99.0 + UserSegment::FieldType.convert_to(99.0, :float).should == 99.0 + UserSegment::FieldType.convert_to("99", :float).should == 99.0 + UserSegment::FieldType.convert_to("3.54", :float).should == 3.54 + UserSegment::FieldType.convert_to(3.54, :float).should == 3.54 + UserSegment::FieldType.convert_to("1n", :float).should be_nil + end + + it "should only return string values" do + UserSegment::FieldType.convert_to("hello world", :string).should == "hello world" + UserSegment::FieldType.convert_to(1, :string).should be_nil + UserSegment::FieldType.convert_to(true, :string).should be_nil + UserSegment::FieldType.convert_to(false, :string).should be_nil + UserSegment::FieldType.convert_to(nil, :string).should be_nil + UserSegment::FieldType.convert_to(0.1, :string).should be_nil + UserSegment::FieldType.convert_to(Time.now, :string).should be_nil + end + + it "should convert value to time" do + time = Time.parse "1/1/1990" + UserSegment::FieldType.convert_to(time.strftime("1/1/1990"), :datetime).should == time + time = Time.now + UserSegment::FieldType.convert_to(time, :datetime).should == time + end + + it "should convert value to boolean" do + UserSegment::FieldType.convert_to(true, :boolean).should == true + UserSegment::FieldType.convert_to('true', :boolean).should == true + UserSegment::FieldType.convert_to('TRUE', :boolean).should == true + UserSegment::FieldType.convert_to(1, :boolean).should == true + UserSegment::FieldType.convert_to('1', :boolean).should == true + UserSegment::FieldType.convert_to(false, :boolean).should == false + UserSegment::FieldType.convert_to('false', :boolean).should == false + UserSegment::FieldType.convert_to('FALSE', :boolean).should == false + UserSegment::FieldType.convert_to(0, :boolean).should == false + UserSegment::FieldType.convert_to('0', :boolean).should == false + end + + it "should convert value to option" do + UserSegment::FieldType.convert_to('day', :option, :options => ['day', 'days', 'week', 'weeks']).should == 'day' + UserSegment::FieldType.convert_to('Day', :option, :options => ['day', 'days', 'week', 'weeks']).should == 'day' + UserSegment::FieldType.convert_to('DAY', :option, :options => ['day', 'days', 'week', 'weeks']).should == 'day' + UserSegment::FieldType.convert_to('weeks', :option, :options => ['day', 'days', 'week', 'weeks']).should == 'weeks' + UserSegment::FieldType.convert_to('WeeKs', :option, :options => ['day', 'days', 'week', 'weeks']).should == 'weeks' + UserSegment::FieldType.convert_to('WeEk', :option, :options => ['day', 'days', 'week', 'weeks']).should == 'week' + UserSegment::FieldType.convert_to('now', :option, :options => ['day', 'days', 'week', 'weeks']).should be_nil + UserSegment::FieldType.convert_to('dayss', :option, :options => ['day', 'days', 'week', 'weeks']).should be_nil + end + + it "should covert all the arguments" do + arguments = ['1', 'test', 'true', '1/1/1990', 'Days', '1day'] + types = [:integer, :string, :boolean, :datetime, :option, :integer] + options = [{}, {}, {}, {}, {:options => ['day', 'days', 'week', 'weeks']}] + + converted = UserSegment::FieldType.convert_arguments(arguments, types, options) + converted[0].should == 1 + converted[1].should == 'test' + converted[2].should == true + converted[3].should == Time.parse('1/1/1990') + converted[4].should == 'days' + converted[5].should be_nil + end + + it "should be able to register and operation" do + UserSegment::FieldType.register_operation(:from, [['Value', :integer], ['Option', :option, {:options => ['day', 'days']}]]) + UserSegment::FieldType.has_operation?('from').should be_true + operation = UserSegment::FieldType.user_segment_field_type_operations[:from] + converted = UserSegment::FieldType.convert_arguments([1, 'Day'], operation[:arguments], operation[:argument_options]) + converted[0].should == 1 + converted[1].should == 'day' + end +end diff --git a/spec/models/user_segment/operation_spec.rb b/spec/models/user_segment/operation_spec.rb new file mode 100644 index 00000000..19ca163b --- /dev/null +++ b/spec/models/user_segment/operation_spec.rb @@ -0,0 +1,47 @@ +require File.dirname(__FILE__) + "/../../spec_helper" + +describe UserSegment::Operation do + + reset_domain_tables :end_users + + @handler = EndUserSegmentField.user_segment_fields_handler_info + + before(:each) do + EndUser.push_target('test1@test.dev', :created_at => 2.days.ago, :activated => true) + EndUser.push_target('test2@test.dev', :created_at => 5.days.ago, :activated => false) + EndUser.push_target('test3@test.dev', :activated => false) + end + + it "should be able to get the count" do + @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] + @field.handler = @handler + + @operation = UserSegment::Operation.new nil, [@field] + @operation.valid?.should be_true + @operation.count.should == 2 + @operation.end_user_ids.length.should == 2 + + @operation = UserSegment::Operation.new 'not', [@field] + @operation.valid?.should be_true + @operation.count.should == 1 + @operation.end_user_ids.length.should == 1 + end + + it "should be able to concat fields" do + @field1 = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] + @field1.handler = @handler + + @field2 = UserSegment::Field.new :field => 'activated', :operation => 'is', :arguments => [false] + @field2.handler = @handler + + @operation = UserSegment::Operation.new nil, [@field1, @field2] + @operation.valid?.should be_true + @operation.count.should == 4 + @operation.end_user_ids.length.should == 3 + + @operation = UserSegment::Operation.new 'not', [@field1, @field2] + @operation.valid?.should be_true + @operation.count.should == 2 + @operation.end_user_ids.length.should == 0 + end +end From d2fecdd2576f30e9505a7d25074612eee741ef5a Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Fri, 14 May 2010 14:07:07 +0000 Subject: [PATCH 003/139] Fixed the not condition in the parser. Add test for Operations. Added method in Field Handler called handlers. Makes sure end user fields are returned first. --- app/models/end_user_segment_field.rb | 1 + app/models/user_segment/field.rb | 2 +- app/models/user_segment/field_handler.rb | 7 ++ app/models/user_segment/operations.rb | 3 + app/models/user_segment_option_parser.rb | 112 ++++++++++-------- app/models/user_segment_option_parser.treetop | 6 +- .../models/user_segment/field_handler_spec.rb | 4 + spec/models/user_segment/field_spec.rb | 9 +- spec/models/user_segment/operation_spec.rb | 30 ++--- spec/models/user_segment/operations_spec.rb | 108 +++++++++++++++++ 10 files changed, 206 insertions(+), 76 deletions(-) create mode 100644 spec/models/user_segment/operations_spec.rb diff --git a/app/models/end_user_segment_field.rb b/app/models/end_user_segment_field.rb index d4eb08ef..b5442962 100644 --- a/app/models/end_user_segment_field.rb +++ b/app/models/end_user_segment_field.rb @@ -15,5 +15,6 @@ def self.user_segment_fields_handler_info register_field :registered, UserSegment::CoreType::BooleanType register_field :activated, UserSegment::CoreType::BooleanType register_field :id, UserSegment::CoreType::NumberType + register_field :user_level, UserSegment::CoreType::NumberType end diff --git a/app/models/user_segment/field.rb b/app/models/user_segment/field.rb index c13685fb..37d69904 100644 --- a/app/models/user_segment/field.rb +++ b/app/models/user_segment/field.rb @@ -80,7 +80,7 @@ def model_field end def handler - @handler ||= self.get_handler_info(:user_segment, :fields).find { |info| info[:class].has_field?(self.field) } + @handler ||= UserSegment::FieldHandler.handlers.find { |info| info[:class].has_field?(self.field) } end def handler=(handler) diff --git a/app/models/user_segment/field_handler.rb b/app/models/user_segment/field_handler.rb index f8e7d126..4d112600 100644 --- a/app/models/user_segment/field_handler.rb +++ b/app/models/user_segment/field_handler.rb @@ -1,5 +1,6 @@ class UserSegment::FieldHandler + include HandlerActions def self.user_segment_fields; {}; end @@ -19,4 +20,10 @@ def self.register_field(field, type, options={}) fields end end + + def self.handlers + ([ self.get_handler_info(:user_segment, :fields, 'end_user_segment_field'), + self.get_handler_info(:user_segment, :fields, 'end_user_action_segment_field')] + + self.get_handler_info(:user_segment, :fields)).uniq + end end diff --git a/app/models/user_segment/operations.rb b/app/models/user_segment/operations.rb index b7ee7531..e23561a4 100644 --- a/app/models/user_segment/operations.rb +++ b/app/models/user_segment/operations.rb @@ -21,6 +21,9 @@ def parser def parse(text) if options = self.parser.parse(text) self.operations = options.eval + true + else + false end end diff --git a/app/models/user_segment_option_parser.rb b/app/models/user_segment_option_parser.rb index ca944e3b..0c419dc0 100644 --- a/app/models/user_segment_option_parser.rb +++ b/app/models/user_segment_option_parser.rb @@ -1324,14 +1324,20 @@ module Not0 end module Not1 - def space - elements[1] + def space1 + elements[0] + end + + def space2 + elements[2] end end module Not2 def eval(env={}) - empty? ? nil : 'not' + return nil if empty? + return 'not' if text_value.strip.downcase == 'not' || text_value.strip.downcase == '!' + nil end end @@ -1347,76 +1353,80 @@ def _nt_not end i0, s0 = index, [] - i2 = index - i3, s3 = index, [] - if has_terminal?('\G[nN]', true, index) - r4 = true - @index += 1 - else - r4 = nil - end - s3 << r4 - if r4 - if has_terminal?('\G[oO]', true, index) + r1 = _nt_space + s0 << r1 + if r1 + i3 = index + i4, s4 = index, [] + if has_terminal?('\G[nN]', true, index) r5 = true @index += 1 else r5 = nil end - s3 << r5 + s4 << r5 if r5 - if has_terminal?('\G[tT]', true, index) + if has_terminal?('\G[oO]', true, index) r6 = true @index += 1 else r6 = nil end - s3 << r6 + s4 << r6 if r6 - if has_terminal?(' ', false, index) - r7 = instantiate_node(SyntaxNode,input, index...(index + 1)) + if has_terminal?('\G[tT]', true, index) + r7 = true @index += 1 else - terminal_parse_failure(' ') r7 = nil end - s3 << r7 + s4 << r7 + if r7 + if has_terminal?(' ', false, index) + r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure(' ') + r8 = nil + end + s4 << r8 + end end end - end - if s3.last - r3 = instantiate_node(SyntaxNode,input, i3...index, s3) - r3.extend(Not0) - else - @index = i3 - r3 = nil - end - if r3 - r2 = r3 - else - if has_terminal?('!', false, index) - r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) - @index += 1 + if s4.last + r4 = instantiate_node(SyntaxNode,input, i4...index, s4) + r4.extend(Not0) else - terminal_parse_failure('!') - r8 = nil + @index = i4 + r4 = nil end - if r8 - r2 = r8 + if r4 + r3 = r4 else - @index = i2 - r2 = nil + if has_terminal?('!', false, index) + r9 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('!') + r9 = nil + end + if r9 + r3 = r9 + else + @index = i3 + r3 = nil + end + end + if r3 + r2 = r3 + else + r2 = instantiate_node(SyntaxNode,input, index...index) + end + s0 << r2 + if r2 + r10 = _nt_space + s0 << r10 end - end - if r2 - r1 = r2 - else - r1 = instantiate_node(SyntaxNode,input, index...index) - end - s0 << r1 - if r1 - r9 = _nt_space - s0 << r9 end if s0.last r0 = instantiate_node(SyntaxNode,input, i0...index, s0) diff --git a/app/models/user_segment_option_parser.treetop b/app/models/user_segment_option_parser.treetop index d4e4b9e3..3d83bb8d 100644 --- a/app/models/user_segment_option_parser.treetop +++ b/app/models/user_segment_option_parser.treetop @@ -122,9 +122,11 @@ grammar UserSegmentOption end rule not - ([nN] [oO] [tT] ' ' / '!')? space { + space ([nN] [oO] [tT] ' ' / '!')? space { def eval(env={}) - empty? ? nil : 'not' + return nil if empty? + return 'not' if text_value.strip.downcase == 'not' || text_value.strip.downcase == '!' + nil end } end diff --git a/spec/models/user_segment/field_handler_spec.rb b/spec/models/user_segment/field_handler_spec.rb index bcd1682f..fd82ab29 100644 --- a/spec/models/user_segment/field_handler_spec.rb +++ b/spec/models/user_segment/field_handler_spec.rb @@ -15,4 +15,8 @@ UserSegment::FieldHandler.user_segment_fields[:created][:type].should == UserSegment::CoreType::DateTimeType UserSegment::FieldHandler.user_segment_fields[:created][:name].should == 'EndUser.created' end + + it "should always return the EndUserSegmentField as the first handler" do + UserSegment::FieldHandler.handlers[0][:class] == EndUserSegmentField + end end diff --git a/spec/models/user_segment/field_spec.rb b/spec/models/user_segment/field_spec.rb index 85ea21bb..81264a01 100644 --- a/spec/models/user_segment/field_spec.rb +++ b/spec/models/user_segment/field_spec.rb @@ -4,8 +4,6 @@ reset_domain_tables :end_users - @handler = EndUserSegmentField.user_segment_fields_handler_info - before(:each) do EndUser.push_target('test1@test.dev', :created_at => 2.days.ago, :activated => true) EndUser.push_target('test2@test.dev', :created_at => 5.days.ago, :activated => false) @@ -14,7 +12,6 @@ it "should be able to use handler" do @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] - @field.handler = @handler @field.handler_class.should == EndUserSegmentField @field.domain_model_class.should == EndUser @@ -28,13 +25,12 @@ it "should return the count" do @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] - @field.handler = @handler + @field.valid?.should == true @field.count.should == 2 @field.end_user_ids.length.should == 2 @field = UserSegment::Field.new :field => 'created', :operation => 'since', :arguments => [1, 'days'] - @field.handler = @handler @field.valid?.should == true @field.count.should == 1 @field.end_user_ids.length.should == 1 @@ -42,19 +38,16 @@ it "should work with children" do @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'], :child => {:field => 'activated', :operation => 'is', :arguments => [true]} - @field.handler = @handler @field.valid?.should == true @field.count.should == 1 @field.end_user_ids.length.should == 1 @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'], :child => {:field => 'activated', :operation => 'is', :arguments => [true], :child => {:field => 'email', :operation => 'like', :arguments => ['test%@test.dev']}} - @field.handler = @handler @field.valid?.should == true @field.count.should == 1 @field.end_user_ids.length.should == 1 @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'], :child => {:field => 'activated', :operation => 'is', :arguments => [false], :child => {:field => 'email', :operation => 'like', :arguments => ['test%@test.dev']}} - @field.handler = @handler @field.valid?.should == true @field.count.should == 1 @field.end_user_ids.length.should == 1 diff --git a/spec/models/user_segment/operation_spec.rb b/spec/models/user_segment/operation_spec.rb index 19ca163b..3690aac1 100644 --- a/spec/models/user_segment/operation_spec.rb +++ b/spec/models/user_segment/operation_spec.rb @@ -4,44 +4,46 @@ reset_domain_tables :end_users - @handler = EndUserSegmentField.user_segment_fields_handler_info - before(:each) do EndUser.push_target('test1@test.dev', :created_at => 2.days.ago, :activated => true) EndUser.push_target('test2@test.dev', :created_at => 5.days.ago, :activated => false) EndUser.push_target('test3@test.dev', :activated => false) + EndUser.push_target('test4@test.dev', :activated => true) + EndUser.push_target('test5@test.dev', :activated => false) + EndUser.push_target('test6@test.dev', :created_at => 10.days.ago, :activated => true) + EndUser.push_target('test7@test.dev', :activated => false) end it "should be able to get the count" do @field = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] - @field.handler = @handler @operation = UserSegment::Operation.new nil, [@field] @operation.valid?.should be_true - @operation.count.should == 2 - @operation.end_user_ids.length.should == 2 + @operation.count.should == 3 + @operation.end_user_ids.length.should == 3 + @operation.to_a.should == [nil, @field.to_h] @operation = UserSegment::Operation.new 'not', [@field] @operation.valid?.should be_true - @operation.count.should == 1 - @operation.end_user_ids.length.should == 1 + @operation.count.should == 4 + @operation.end_user_ids.length.should == 4 + @operation.to_a.should == ['not', @field.to_h] end it "should be able to concat fields" do @field1 = UserSegment::Field.new :field => 'created', :operation => 'before', :arguments => [1, 'days'] - @field1.handler = @handler - @field2 = UserSegment::Field.new :field => 'activated', :operation => 'is', :arguments => [false] - @field2.handler = @handler @operation = UserSegment::Operation.new nil, [@field1, @field2] @operation.valid?.should be_true - @operation.count.should == 4 - @operation.end_user_ids.length.should == 3 + @operation.count.should == 7 + @operation.end_user_ids.length.should == 6 + @operation.to_a.should == [nil, @field1.to_h, @field2.to_h] @operation = UserSegment::Operation.new 'not', [@field1, @field2] @operation.valid?.should be_true - @operation.count.should == 2 - @operation.end_user_ids.length.should == 0 + @operation.count.should == 7 + @operation.end_user_ids.length.should == 1 + @operation.to_a.should == ['not', @field1.to_h, @field2.to_h] end end diff --git a/spec/models/user_segment/operations_spec.rb b/spec/models/user_segment/operations_spec.rb new file mode 100644 index 00000000..ce27e177 --- /dev/null +++ b/spec/models/user_segment/operations_spec.rb @@ -0,0 +1,108 @@ +require File.dirname(__FILE__) + "/../../spec_helper" + +describe UserSegment::Operations do + + reset_domain_tables :end_users + + before(:each) do + @user1 = EndUser.push_target('test1@test.dev', :created_at => 2.days.ago, :activated => true, :user_level => 1) + @user2 = EndUser.push_target('test2@test.dev', :created_at => 5.days.ago, :activated => false, :user_level => 2) + @user3 = EndUser.push_target('test3@test.dev', :activated => false, :user_level => 3) + @user4 = EndUser.push_target('test4@test.dev', :activated => true, :user_level => 1) + @user5 = EndUser.push_target('test5@test.dev', :activated => false, :user_level => 2) + @user6 = EndUser.push_target('test6@test.dev', :created_at => 10.days.ago, :activated => true, :user_level => 3) + @user7 = EndUser.push_target('test7@test.dev', :activated => false, :user_level => 1) + end + + it "should return the end user ids from a given set of options" do + @operations = UserSegment::Operations.new + options = [[nil, {:field => 'created', :operation => 'since', :arguments => [3, 'days'], :child => nil}]] + @operations.operations = options + @operations.valid?.should == true + @operations.to_a.should == options + @operations.end_user_ids.length.should == 5 + end + + it "should be able to refine end user ids based on multiple operations" do + @operations = UserSegment::Operations.new + options = [ + [nil, {:field => 'created', :operation => 'since', :arguments => [3, 'days'], :child => nil}], + [nil, {:field => 'activated', :operation => 'is', :arguments => [true], :child => nil}] + ] + @operations.operations = options + @operations.valid?.should == true + @operations.to_a.should == options + @operations.end_user_ids.length.should == 2 + end + + it "should return the end user ids from a given set operations syntax" do + @operations = UserSegment::Operations.new + options = [[nil, {:field => 'created', :operation => 'since', :arguments => [3, 'days'], :child => nil}]] + code = <<-CODE + created.since(3, "days") + CODE + @operations.parse(code).should be_true + @operations.valid?.should == true + @operations.to_a.should == options + @operations.end_user_ids.length.should == 5 + + @operations = UserSegment::Operations.new + options = [['not', {:field => 'created', :operation => 'since', :arguments => [3, 'days'], :child => nil}]] + code = <<-CODE + not created.since(3, "days") + CODE + @operations.parse(code).should be_true + @operations.valid?.should == true + @operations.to_a.should == options + @operations.end_user_ids.length.should == 2 + end + + it "should be able to refine end user ids with multiple operations" do + @operations = UserSegment::Operations.new + options = [ + [nil, {:field => 'created', :operation => 'since', :arguments => [3, 'days'], :child => nil}], + [nil, {:field => 'activated', :operation => 'is', :arguments => [true], :child => nil}] + ] + code = <<-CODE + created.since(3, "days") + activated.is(true) + CODE + @operations.parse(code).should be_true + @operations.valid?.should == true + @operations.to_a.should == options + @operations.end_user_ids.length.should == 2 + @operations.end_user_ids.include?(@user1.id).should be_true + @operations.end_user_ids.include?(@user4.id).should be_true + + @operations = UserSegment::Operations.new + options = [ + ['not', {:field => 'created', :operation => 'since', :arguments => [3, 'days'], :child => nil}], + [nil, {:field => 'activated', :operation => 'is', :arguments => [true], :child => nil}] + ] + code = <<-CODE + not created.since(3, "days") + activated.is(true) + CODE + @operations.parse(code).should be_true + @operations.valid?.should == true + @operations.to_a.should == options + @operations.end_user_ids.length.should == 1 + @operations.end_user_ids.include?(@user6.id).should be_true + + @operations = UserSegment::Operations.new + options = [ + ['not', {:field => 'created', :operation => 'since', :arguments => [3, 'days'], :child => nil}, {:field => 'user_level', :operation => 'is', :arguments => [1], :child => nil}], + [nil, {:field => 'activated', :operation => 'is', :arguments => [true], :child => nil}] + ] + code = <<-CODE + not created.since(3, "days") + user_level.is(1) + activated.is(true) + CODE + @operations.parse(code).should be_true + @operations.valid?.should == true + @operations.to_a.should == options + @operations.end_user_ids.length.should == 1 + @operations.end_user_ids.include?(@user6.id).should be_true + end + +end From d3d80e4bd2fb5ce37d901a17478f335225c48419 Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Fri, 14 May 2010 16:26:49 +0000 Subject: [PATCH 004/139] Fix for strings. Test the parser --- app/models/user_segment_option_parser.rb | 340 ++++++++++-------- app/models/user_segment_option_parser.treetop | 8 +- .../models/user_segment_option_parser_spec.rb | 92 +++++ 3 files changed, 277 insertions(+), 163 deletions(-) create mode 100644 spec/models/user_segment_option_parser_spec.rb diff --git a/app/models/user_segment_option_parser.rb b/app/models/user_segment_option_parser.rb index 0c419dc0..e6cbb866 100644 --- a/app/models/user_segment_option_parser.rb +++ b/app/models/user_segment_option_parser.rb @@ -660,11 +660,11 @@ def _nt_argument if r3 r2 = r3 else - r4 = _nt_integer + r4 = _nt_float if r4 r2 = r4 else - r5 = _nt_float + r5 = _nt_integer if r5 r2 = r5 else @@ -706,7 +706,7 @@ module String1 module String2 def eval(env={}) - text_value[1..-2] + text_value[1..-2].gsub('\"', '"').gsub('\\\'', "'") end end @@ -721,95 +721,142 @@ def _nt_string return cached end - i0, s0 = index, [] + i0 = index + i1, s1 = index, [] if has_terminal?('"', false, index) - r1 = instantiate_node(SyntaxNode,input, index...(index + 1)) + r2 = instantiate_node(SyntaxNode,input, index...(index + 1)) @index += 1 else terminal_parse_failure('"') - r1 = nil + r2 = nil end - s0 << r1 - if r1 - s2, i2 = [], index + s1 << r2 + if r2 + s3, i3 = [], index loop do - i3 = index - i4, s4 = index, [] - i5 = index - if has_terminal?('"', false, index) - r6 = instantiate_node(SyntaxNode,input, index...(index + 1)) - @index += 1 + i4 = index + if has_terminal?('\"', false, index) + r5 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 else - terminal_parse_failure('"') - r6 = nil - end - if r6 + terminal_parse_failure('\"') r5 = nil - else - @index = i5 - r5 = instantiate_node(SyntaxNode,input, index...index) end - s4 << r5 if r5 - if index < input_length - r7 = instantiate_node(SyntaxNode,input, index...(index + 1)) - @index += 1 - else - terminal_parse_failure("any character") - r7 = nil - end - s4 << r7 - end - if s4.last - r4 = instantiate_node(SyntaxNode,input, i4...index, s4) - r4.extend(String0) - else - @index = i4 - r4 = nil - end - if r4 - r3 = r4 + r4 = r5 else - if has_terminal?('\"', false, index) - r8 = instantiate_node(SyntaxNode,input, index...(index + 2)) - @index += 2 + if has_terminal?('\G[^"]', true, index) + r6 = true + @index += 1 else - terminal_parse_failure('\"') - r8 = nil + r6 = nil end - if r8 - r3 = r8 + if r6 + r4 = r6 else - @index = i3 - r3 = nil + @index = i4 + r4 = nil end end - if r3 - s2 << r3 + if r4 + s3 << r4 else break end end - r2 = instantiate_node(SyntaxNode,input, i2...index, s2) - s0 << r2 - if r2 + r3 = instantiate_node(SyntaxNode,input, i3...index, s3) + s1 << r3 + if r3 if has_terminal?('"', false, index) - r9 = instantiate_node(SyntaxNode,input, index...(index + 1)) + r7 = instantiate_node(SyntaxNode,input, index...(index + 1)) @index += 1 else terminal_parse_failure('"') - r9 = nil + r7 = nil end - s0 << r9 + s1 << r7 end end - if s0.last - r0 = instantiate_node(SyntaxNode,input, i0...index, s0) - r0.extend(String1) + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(String0) + else + @index = i1 + r1 = nil + end + if r1 + r0 = r1 r0.extend(String2) else - @index = i0 - r0 = nil + i8, s8 = index, [] + if has_terminal?("'", false, index) + r9 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure("'") + r9 = nil + end + s8 << r9 + if r9 + s10, i10 = [], index + loop do + i11 = index + if has_terminal?("\\'", false, index) + r12 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 + else + terminal_parse_failure("\\'") + r12 = nil + end + if r12 + r11 = r12 + else + if has_terminal?('\G[^\']', true, index) + r13 = true + @index += 1 + else + r13 = nil + end + if r13 + r11 = r13 + else + @index = i11 + r11 = nil + end + end + if r11 + s10 << r11 + else + break + end + end + r10 = instantiate_node(SyntaxNode,input, i10...index, s10) + s8 << r10 + if r10 + if has_terminal?("'", false, index) + r14 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure("'") + r14 = nil + end + s8 << r14 + end + end + if s8.last + r8 = instantiate_node(SyntaxNode,input, i8...index, s8) + r8.extend(String1) + else + @index = i8 + r8 = nil + end + if r8 + r0 = r8 + r0.extend(String2) + else + @index = i0 + r0 = nil + end end node_cache[:string][start_index] = r0 @@ -1005,9 +1052,6 @@ module Float1 end module Float2 - end - - module Float3 def eval(env={}) text_value.to_f end @@ -1026,30 +1070,59 @@ def _nt_float i0 = index i1, s1 = index, [] - if has_terminal?('\G[1-9]', true, index) - r2 = true - @index += 1 - else + s2, i2 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r3 = true + @index += 1 + else + r3 = nil + end + if r3 + s2 << r3 + else + break + end + end + if s2.empty? + @index = i2 r2 = nil + else + r2 = instantiate_node(SyntaxNode,input, i2...index, s2) end s1 << r2 if r2 - s3, i3 = [], index - loop do - if has_terminal?('\G[0-9]', true, index) - r4 = true - @index += 1 - else - r4 = nil + if has_terminal?('.', false, index) + r4 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('.') + r4 = nil + end + s1 << r4 + if r4 + s5, i5 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r6 = true + @index += 1 + else + r6 = nil + end + if r6 + s5 << r6 + else + break + end end - if r4 - s3 << r4 + if s5.empty? + @index = i5 + r5 = nil else - break + r5 = instantiate_node(SyntaxNode,input, i5...index, s5) end + s1 << r5 end - r3 = instantiate_node(SyntaxNode,input, i3...index, s3) - s1 << r3 end if s1.last r1 = instantiate_node(SyntaxNode,input, i1...index, s1) @@ -1060,104 +1133,53 @@ def _nt_float end if r1 r0 = r1 + r0.extend(Float2) else - i5, s5 = index, [] + i7, s7 = index, [] if has_terminal?('.', false, index) - r6 = instantiate_node(SyntaxNode,input, index...(index + 1)) + r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) @index += 1 else terminal_parse_failure('.') - r6 = nil + r8 = nil end - s5 << r6 - if r6 - s7, i7 = [], index + s7 << r8 + if r8 + s9, i9 = [], index loop do if has_terminal?('\G[0-9]', true, index) - r8 = true + r10 = true @index += 1 else - r8 = nil + r10 = nil end - if r8 - s7 << r8 + if r10 + s9 << r10 else break end end - if s7.empty? - @index = i7 - r7 = nil + if s9.empty? + @index = i9 + r9 = nil else - r7 = instantiate_node(SyntaxNode,input, i7...index, s7) + r9 = instantiate_node(SyntaxNode,input, i9...index, s9) end - s5 << r7 + s7 << r9 end - if s5.last - r5 = instantiate_node(SyntaxNode,input, i5...index, s5) - r5.extend(Float1) + if s7.last + r7 = instantiate_node(SyntaxNode,input, i7...index, s7) + r7.extend(Float1) else - @index = i5 - r5 = nil + @index = i7 + r7 = nil end - if r5 - r0 = r5 + if r7 + r0 = r7 + r0.extend(Float2) else - i9, s9 = index, [] - s10, i10 = [], index - loop do - if has_terminal?('\G[0-9]', true, index) - r11 = true - @index += 1 - else - r11 = nil - end - if r11 - s10 << r11 - else - break - end - end - if s10.empty? - @index = i10 - r10 = nil - else - r10 = instantiate_node(SyntaxNode,input, i10...index, s10) - end - s9 << r10 - if r10 - if has_terminal?('.', false, index) - r12 = instantiate_node(SyntaxNode,input, index...(index + 1)) - @index += 1 - else - terminal_parse_failure('.') - r12 = nil - end - s9 << r12 - if r12 - if has_terminal?('\G[0-9]', true, index) - r13 = true - @index += 1 - else - r13 = nil - end - s9 << r13 - end - end - if s9.last - r9 = instantiate_node(SyntaxNode,input, i9...index, s9) - r9.extend(Float2) - r9.extend(Float3) - else - @index = i9 - r9 = nil - end - if r9 - r0 = r9 - else - @index = i0 - r0 = nil - end + @index = i0 + r0 = nil end end diff --git a/app/models/user_segment_option_parser.treetop b/app/models/user_segment_option_parser.treetop index 3d83bb8d..db139bec 100644 --- a/app/models/user_segment_option_parser.treetop +++ b/app/models/user_segment_option_parser.treetop @@ -66,7 +66,7 @@ grammar UserSegmentOption end rule argument - space arg:(string / integer / float / boolean) space { + space arg:(string / float / integer / boolean) space { def eval(env={}) arg.eval(env) end @@ -74,9 +74,9 @@ grammar UserSegmentOption end rule string - '"' (!'"' . / '\"')* '"' { + ('"' ('\"' / [^"])* '"' / "'" ("\\'" / [^'])* "'") { def eval(env={}) - text_value[1..-2] + text_value[1..-2].gsub('\"', '"').gsub('\\\'', "'") end } end @@ -102,7 +102,7 @@ grammar UserSegmentOption end rule float - [1-9] [0-9]* / '.' [0-9]+ / [0-9]+ '.' [0-9] { + ([0-9]+ '.' [0-9]+ / '.' [0-9]+) { def eval(env={}) text_value.to_f end diff --git a/spec/models/user_segment_option_parser_spec.rb b/spec/models/user_segment_option_parser_spec.rb new file mode 100644 index 00000000..43d541c3 --- /dev/null +++ b/spec/models/user_segment_option_parser_spec.rb @@ -0,0 +1,92 @@ +require File.dirname(__FILE__) + "/../spec_helper" +require 'treetop' + +describe UserSegmentOptionParser do + + def parse(text) + parser = UserSegmentOptionParser.new + if data = parser.parse(text) + data.eval + else + raise parser.inspect + nil + end + end + + it "should be able to parse operations to and array" do + code = <<-CODE + created.since(1, "days") + CODE + parse(code).should == [[nil, {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => nil}]] + + code = <<-CODE + created_at.since_ago(1, "days") + CODE + parse(code).should == [[nil, {:field => 'created_at', :operation => 'since_ago', :arguments => [1, "days"], :child => nil}]] + + code = <<-CODE + created_at2.since_ago8(1, "days") + CODE + parse(code).should == [[nil, {:field => 'created_at2', :operation => 'since_ago8', :arguments => [1, "days"], :child => nil}]] + + code = <<-CODE + test.contains("") + CODE + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => [''], :child => nil}]] + + code = <<-CODE + test.contains(true, false) + CODE + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => [true, false], :child => nil}]] + + code = 'test.contains("days\"", 1)' + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => ['days"', 1], :child => nil}]] + + code = <<-CODE + test.contains('1.1') + CODE + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => ['1.1'], :child => nil}]] + + code = "test.contains('1.1\\'')" + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => ["1.1'"], :child => nil}]] + + code = <<-CODE + test.contains(1.1) + CODE + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => [1.1], :child => nil}]] + + code = <<-CODE + test.contains(1.0) + CODE + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => [1.0], :child => nil}]] + + code = <<-CODE + test.contains(.1) + CODE + parse(code).should == [[nil, {:field => 'test', :operation => 'contains', :arguments => [0.1], :child => nil}]] + end + + it "should be able to not an operation" do + code = <<-CODE + Not created.since(1, "days") + CODE + parse(code).should == [['not', {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => nil}]] + end + + it "should be able to chain operations" do + code = <<-CODE + created.since(1, "days").registered.is(true) + CODE + parse(code).should == [[nil, {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => {:field => 'registered', :operation => 'is', :arguments => [true], :child => nil}}]] + + code = <<-CODE + created.since(1, "days").registered.is(true).logged_in.is(false) + CODE + parse(code).should == [[nil, {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => {:field => 'registered', :operation => 'is', :arguments => [true], :child => {:field => 'logged_in', :operation => 'is', :arguments => [false], :child => nil}}}]] + + code = <<-CODE + NOT created.since(1, "days").registered.is(true).logged_in.is(false) + CODE + parse(code).should == [['not', {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => {:field => 'registered', :operation => 'is', :arguments => [true], :child => {:field => 'logged_in', :operation => 'is', :arguments => [false], :child => nil}}}]] + end +end From a47d5d6f4943526aec4f0496c14473272127243c Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Fri, 14 May 2010 17:41:30 +0000 Subject: [PATCH 005/139] Testing and/or coditions for option parser. --- .../models/user_segment_option_parser_spec.rb | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/models/user_segment_option_parser_spec.rb b/spec/models/user_segment_option_parser_spec.rb index 43d541c3..9b76782e 100644 --- a/spec/models/user_segment_option_parser_spec.rb +++ b/spec/models/user_segment_option_parser_spec.rb @@ -89,4 +89,27 @@ def parse(text) CODE parse(code).should == [['not', {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => {:field => 'registered', :operation => 'is', :arguments => [true], :child => {:field => 'logged_in', :operation => 'is', :arguments => [false], :child => nil}}}]] end + + it "should be able to or operations" do + code = <<-CODE + created.since(1, "days") + registered.is(true) + CODE + parse(code).should == [[nil, {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => nil}, {:field => 'registered', :operation => 'is', :arguments => [true], :child => nil}]] + + code = <<-CODE + created.since(1, "days") + registered.is(true) + test.is('good') + CODE + parse(code).should == [[nil, {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => nil}, {:field => 'registered', :operation => 'is', :arguments => [true], :child => nil}, {:field => 'test', :operation => 'is', :arguments => ['good'], :child => nil}]] + end + + it "should be able to and operations" do + code = <<-CODE + created.since(1, "days") + registered.is(true) + CODE + parse(code).should == [ + [nil, {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => nil}], + [nil, {:field => 'registered', :operation => 'is', :arguments => [true], :child => nil}] + ] + end end From 5de548e270ca9430f70d00680657d75b04cdb441 Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Fri, 14 May 2010 17:49:28 +0000 Subject: [PATCH 006/139] Added a test to parse a complicated set of operations. --- spec/models/user_segment_option_parser_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/models/user_segment_option_parser_spec.rb b/spec/models/user_segment_option_parser_spec.rb index 9b76782e..2376782f 100644 --- a/spec/models/user_segment_option_parser_spec.rb +++ b/spec/models/user_segment_option_parser_spec.rb @@ -112,4 +112,17 @@ def parse(text) [nil, {:field => 'registered', :operation => 'is', :arguments => [true], :child => nil}] ] end + + it "should parse a complaticated set of operations" do + code = <<-CODE + created.since(1 , "days").born.before('yesterday')+ registered.is( true ) + registered.is( true ) + not test.is( false ) + product.purchased( 'computer' ).created.since( 1, 'day' ) + CODE + parse(code).should == [ + [nil, {:field => 'created', :operation => 'since', :arguments => [1, "days"], :child => {:field => 'born', :operation => 'before', :arguments => ['yesterday'], :child => nil}}, {:field => 'registered', :operation => 'is', :arguments => [true], :child => nil}], + [nil, {:field => 'registered', :operation => 'is', :arguments => [true], :child => nil}], + ['not', {:field => 'test', :operation => 'is', :arguments => [false], :child => nil}, {:field => 'product', :operation => 'purchased', :arguments => ['computer'], :child => {:field => 'created', :operation => 'since', :arguments => [1, 'day'], :child => nil}}] + ] + end end From 97dfafbefe8cdf3e7ed2bced6de2302bf4df5048 Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Fri, 14 May 2010 19:38:58 +0000 Subject: [PATCH 007/139] Added each, each_with_index and find to perform actions with segments. --- app/models/user_segment.rb | 54 ++++++++++++++++++++-- app/models/user_segment_cache.rb | 25 ++++++++++ db/migrate/20100511165552_user_segments.rb | 4 ++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/app/models/user_segment.rb b/app/models/user_segment.rb index 13b6b85d..23a1726c 100644 --- a/app/models/user_segment.rb +++ b/app/models/user_segment.rb @@ -1,9 +1,12 @@ class UserSegment < DomainModel + has_many :user_segment_caches, :order => 'created_at, id', :dependent => :destroy, :class_name => 'UserSegmentCache' serialize :segment_options serialize :fields + validates_presence_of :name + def operations return @operations if @operations @operations = UserSegment::Operations.new @@ -11,17 +14,60 @@ def operations @operations end - def operations=(text) + def segment_options_text=(text) + self.write_attribute :segment_options_text, text @operations = UserSegment::Operations.new @operations.parse text + self.segment_options = @operations.valid? ? self.operations.to_a : nil + text end - def before_create + def cache_ids + self.user_segment_caches.delete_all + self.order_by = 'created_at DESC' unless self.order_by + ids = EndUser.find(:all, :select => 'id', :conditions => {:id => self.operations.end_user_ids}, :order => self.order_by) + + num_segements = (self.operations.end_user_ids.length / 1000) + num_segements = num_segements + 1 if (self.operations.end_user_ids.length % 1000) > 0 + + (0..num_segements-1).each do |idx| + start = idx * 1000 + self.user_segment_caches.create :id_list => ids[start..start+999] + end + + self.last_ran_at = Time.now + self.last_count = ids.length + self.save + end + + def end_user_ids + return @end_user_ids if @end_user_ids + @end_user_ids = [] + self.user_segment_caches.each do |segement| + @end_user_ids = @end_user_ids + segement.id_list + end + @end_user_ids + end + + def each(&block) + self.user_segment_caches.each do |segement| + segement.each &block + end + end + + def each_with_index(&block) + idx = 0 + self.user_segment_caches.each do |segement| + idx = segement.each_with_index idx, &block + end end - def before_save - self.segment_options = self.operations.to_a if self.operations && self.operations.valid? + def find(&block) + self.user_segment_caches.each do |segement| + user = segement.find &block + return user if user + end end end diff --git a/app/models/user_segment_cache.rb b/app/models/user_segment_cache.rb index 6ed5c117..9edd9629 100644 --- a/app/models/user_segment_cache.rb +++ b/app/models/user_segment_cache.rb @@ -1,4 +1,29 @@ class UserSegmentCache < DomainModel + belongs_to :user_segment + serialize :id_list + + validates_presence_of :user_segment_id + + def before_create + self.created_at = Time.now unless self.created_at + end + + def end_users + @end_users ||= EndUser.find(:all, :conditions => {:id => self.id_list}) + end + + def each + self.end_users.each { |user| yield user } + end + + def each_with_index(idx=0) + self.end_users.each { |user| yield user, idx; idx = idx.succ } + idx + end + + def find + self.end_users.find { |user| yield user } + end end diff --git a/db/migrate/20100511165552_user_segments.rb b/db/migrate/20100511165552_user_segments.rb index b6657ccd..dfcec4b3 100644 --- a/db/migrate/20100511165552_user_segments.rb +++ b/db/migrate/20100511165552_user_segments.rb @@ -20,6 +20,8 @@ def self.up t.datetime :created_at end + add_index :user_segment_caches, [:user_segment_id], :name => 'user_segment_cache_segment_idx' + create_table :user_segment_analytics, :force => true do |t| t.integer :user_segment_id t.text :fields @@ -29,6 +31,8 @@ def self.up t.string :step t.timestamps end + + add_index :user_segment_analytics, [:user_segment_id], :name => 'user_segment_analytics_segment_idx' end def self.down From afa05c5f95ac8de2207be3928ac0c8087db9723d Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Fri, 14 May 2010 19:58:56 +0000 Subject: [PATCH 008/139] Can pass options to enumerable methods. --- app/models/user_segment.rb | 12 ++++++------ app/models/user_segment_cache.rb | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/models/user_segment.rb b/app/models/user_segment.rb index 23a1726c..1a532ab1 100644 --- a/app/models/user_segment.rb +++ b/app/models/user_segment.rb @@ -50,22 +50,22 @@ def end_user_ids @end_user_ids end - def each(&block) + def each(opts={}, &block) self.user_segment_caches.each do |segement| - segement.each &block + segement.each opts, &block end end - def each_with_index(&block) + def each_with_index(opts={}, &block) idx = 0 self.user_segment_caches.each do |segement| - idx = segement.each_with_index idx, &block + idx = segement.each_with_index idx, opts, &block end end - def find(&block) + def find(opts={}, &block) self.user_segment_caches.each do |segement| - user = segement.find &block + user = segement.find opts, &block return user if user end end diff --git a/app/models/user_segment_cache.rb b/app/models/user_segment_cache.rb index 9edd9629..73f917ee 100644 --- a/app/models/user_segment_cache.rb +++ b/app/models/user_segment_cache.rb @@ -10,20 +10,22 @@ def before_create self.created_at = Time.now unless self.created_at end - def end_users - @end_users ||= EndUser.find(:all, :conditions => {:id => self.id_list}) + def end_users(opts={}) + return @end_users if @end_users + users_by_id = EndUser.find(:all, :conditions => {:id => self.id_list}).index_by(&:id) + @end_users = self.id_list.map { |id| users_by_id[id] }.compact end - def each - self.end_users.each { |user| yield user } + def each(opts={}) + self.end_users(opts).each { |user| yield user } end - def each_with_index(idx=0) - self.end_users.each { |user| yield user, idx; idx = idx.succ } + def each_with_index(idx=0, opts={}) + self.end_users(opts).each { |user| yield user, idx; idx = idx.succ } idx end - def find - self.end_users.find { |user| yield user } + def find(opts={}) + self.end_users(opts).find { |user| yield user } end end From 8558a233397d555daea8f22680128fe5a3fd1327 Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Mon, 17 May 2010 14:31:56 +0000 Subject: [PATCH 009/139] Don't store user objects in user segment cache. Batch fetch users. --- app/models/user_segment.rb | 50 +++++++++++++++++++--- app/models/user_segment_cache.rb | 39 ++++++++++++++--- db/migrate/20100511165552_user_segments.rb | 3 +- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/app/models/user_segment.rb b/app/models/user_segment.rb index 1a532ab1..2a399416 100644 --- a/app/models/user_segment.rb +++ b/app/models/user_segment.rb @@ -1,7 +1,7 @@ class UserSegment < DomainModel - has_many :user_segment_caches, :order => 'created_at, id', :dependent => :destroy, :class_name => 'UserSegmentCache' + has_many :user_segment_caches, :order => 'position', :dependent => :destroy, :class_name => 'UserSegmentCache' serialize :segment_options serialize :fields @@ -26,14 +26,14 @@ def cache_ids self.user_segment_caches.delete_all self.order_by = 'created_at DESC' unless self.order_by - ids = EndUser.find(:all, :select => 'id', :conditions => {:id => self.operations.end_user_ids}, :order => self.order_by) + ids = EndUser.find(:all, :select => 'id', :conditions => {:id => self.operations.end_user_ids}, :order => self.order_by).collect &:id - num_segements = (self.operations.end_user_ids.length / 1000) - num_segements = num_segements + 1 if (self.operations.end_user_ids.length % 1000) > 0 + num_segements = (self.operations.end_user_ids.length / UserSegmentCache::SIZE) + num_segements = num_segements + 1 if (self.operations.end_user_ids.length % UserSegmentCache::SIZE) > 0 (0..num_segements-1).each do |idx| - start = idx * 1000 - self.user_segment_caches.create :id_list => ids[start..start+999] + start = idx * UserSegmentCache::SIZE + self.user_segment_caches.create :id_list => ids[start..start+UserSegmentCache::SIZE-1], :position => idx end self.last_ran_at = Time.now @@ -56,6 +56,14 @@ def each(opts={}, &block) end end + def collect(opts={}, &block) + data = [] + self.user_segment_caches.each do |segement| + data = data + segement.collect(opts, &block) + end + data + end + def each_with_index(opts={}, &block) idx = 0 self.user_segment_caches.each do |segement| @@ -69,5 +77,35 @@ def find(opts={}, &block) return user if user end end + + def paginate(page,args = {}) + args = args.clone.symbolize_keys! + window_size =args.delete(:window) || 2 + + page_size = args.delete(:per_page).to_i + page_size = 20 if page_size <= 0 + + total_count = self.last_count + pages = (total_count.to_f / (page_size || 10)).ceil + pages = 1 if pages < 1 + page = (page ? page.to_i : 1).clamp(1,pages) + + offset = (page-1) * page_size + + position = (offset / UserSegmentCache::SIZE).to_i + + cache = self.user_segment_caches.find_by_position(position) + items = cache ? cache.fetch_users(:offset => (offset % UserSegmentCache::SIZE), :limit => page_size) : [] + + [ { :pages => pages, + :page => page, + :window_size => window_size, + :total => total_count, + :per_page => page_size, + :first => offset+1, + :last => offset + items.length, + :count => items.length + }, items ] + end end diff --git a/app/models/user_segment_cache.rb b/app/models/user_segment_cache.rb index 73f917ee..d207a807 100644 --- a/app/models/user_segment_cache.rb +++ b/app/models/user_segment_cache.rb @@ -1,5 +1,8 @@ class UserSegmentCache < DomainModel + SIZE = 25000 + DEFAULT_BATCH_SIZE = 1000 + belongs_to :user_segment serialize :id_list @@ -10,22 +13,44 @@ def before_create self.created_at = Time.now unless self.created_at end - def end_users(opts={}) - return @end_users if @end_users - users_by_id = EndUser.find(:all, :conditions => {:id => self.id_list}).index_by(&:id) - @end_users = self.id_list.map { |id| users_by_id[id] }.compact + def fetch_users(opts={}) + ids = opts[:offset] && opts[:limit] ? self.id_list[opts[:offset]..opts[:offset]+opts[:limit]-1] : self.id_list + users_by_id = EndUser.find(:all, :conditions => {:id => ids}).index_by(&:id) + ids.map { |id| users_by_id[id] }.compact + end + + def find_in_batches(opts={}) + limit = opts[:batch_size] || DEFAULT_BATCH_SIZE + offset = 0 + num_chunks = (self.id_list.size / limit).to_i + num_chunks = num_chunks.succ if (self.id_list.length % limit) > 0 + (1..num_chunks).each do |chunk| + users = self.fetch_users(:offset => offset, :limit => limit) + offset = offset + limit + yield users + end end def each(opts={}) - self.end_users(opts).each { |user| yield user } + self.find_in_batches(opts) { |users| users.each { |user| yield user } } + end + + def collect(opts={}) + data = [] + self.find_in_batches(opts) { |users| data = data + users.collect { |user| yield user } } + data end def each_with_index(idx=0, opts={}) - self.end_users(opts).each { |user| yield user, idx; idx = idx.succ } + self.find_in_batches(opts) { |users| users.each { |user| yield user, idx; idx = idx.succ } } idx end def find(opts={}) - self.end_users(opts).find { |user| yield user } + self.find_in_batches(opts) do |users| + user = users.find { |user| yield user } + return user if user + end + nil end end diff --git a/db/migrate/20100511165552_user_segments.rb b/db/migrate/20100511165552_user_segments.rb index dfcec4b3..88655354 100644 --- a/db/migrate/20100511165552_user_segments.rb +++ b/db/migrate/20100511165552_user_segments.rb @@ -16,7 +16,8 @@ def self.up create_table :user_segment_caches, :force => true do |t| t.integer :user_segment_id - t.text :id_list + t.integer :position + t.text :id_list, :limit => 16777215 t.datetime :created_at end From d12fa762397b5691a466271d775f20d6030f2b46 Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Mon, 17 May 2010 19:18:03 +0000 Subject: [PATCH 010/139] Can search through a segment. --- app/models/user_segment.rb | 24 ++++++++++++++++++++++++ app/models/user_segment_cache.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/app/models/user_segment.rb b/app/models/user_segment.rb index 2a399416..1e4a5dca 100644 --- a/app/models/user_segment.rb +++ b/app/models/user_segment.rb @@ -107,5 +107,29 @@ def paginate(page,args = {}) :count => items.length }, items ] end + + def search(offset=0, args={}) + args = args.clone.symbolize_keys! + + page_size = args.delete(:per_page).to_i + page_size = 20 if page_size <= 0 + + cache_offset = offset % UserSegmentCache::SIZE + + ids = [] + ((offset / UserSegmentCache::SIZE).to_i..self.user_segment_caches.length-1).each do |position| + cache = self.user_segment_caches.find_by_position(position) + cache_offset, cache_ids = cache.search(cache_offset, args.merge(:limit => page_size-ids.length)) + ids = ids + cache_ids + offset = UserSegmentCache::SIZE * position + cache_offset + cache_offset = 0 + break if ids.length >= page_size + end + + args.delete(:conditions) + args.delete(:joins) + users = EndUser.find(:all, args.merge(:conditions => {:id => ids})) + return [offset, users] + end end diff --git a/app/models/user_segment_cache.rb b/app/models/user_segment_cache.rb index d207a807..4a8e594e 100644 --- a/app/models/user_segment_cache.rb +++ b/app/models/user_segment_cache.rb @@ -19,6 +19,33 @@ def fetch_users(opts={}) ids.map { |id| users_by_id[id] }.compact end + def search(offset=0, opts={}) + opts = opts.clone.symbolize_keys! + opts.delete(:offset) + opts.delete(:include) + limit = opts.delete(:limit) || 20 + batch_size = opts.delete(:batch_size) || DEFAULT_BATCH_SIZE + num_chunks = (self.id_list.size / batch_size).to_i + num_chunks = num_chunks.succ if (self.id_list.length % batch_size) > 0 + + base_scope = EndUser.scoped(opts) + + ids = [] + ((offset/batch_size).to_i..num_chunks-1).each do |chunk| + sub_list = self.id_list[offset..(offset+batch_size-1)] + scope = base_scope.scoped(:select => 'id', :conditions => {:id => sub_list}) + users_by_id = scope.find(:all).index_by(&:id) + ids = ids + sub_list.map { |id| users_by_id[id] ? id : nil }.compact unless users_by_id.empty? + offset = offset + batch_size + break if ids.length > limit + end + + has_more = ids.length > limit + ids = ids[0..limit-1] if has_more + offset = ids.size > 0 ? self.id_list.index(ids[-1]) : 0 + [offset, ids] + end + def find_in_batches(opts={}) limit = opts[:batch_size] || DEFAULT_BATCH_SIZE offset = 0 From 4f8b85b84ed398bb6bbaf61b778d306228d86a05 Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Mon, 17 May 2010 20:15:50 +0000 Subject: [PATCH 011/139] Added a end user caches table. Used to store a full text index on all the end user fields. --- app/models/end_user.rb | 7 +++++- app/models/end_user_cache.rb | 25 ++++++++++++++++++++++ db/migrate/20100511165552_user_segments.rb | 9 ++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/models/end_user_cache.rb diff --git a/app/models/end_user.rb b/app/models/end_user.rb index 4b49bd76..196b85f8 100644 --- a/app/models/end_user.rb +++ b/app/models/end_user.rb @@ -74,7 +74,8 @@ class EndUser < DomainModel has_many :addresses, :class_name => 'EndUserAddress', :dependent => :destroy has_one :tag_cache, :dependent => :destroy - + has_one :end_user_cache, :dependent => :destroy, :class_name => 'EndUserCache' + has_many :end_user_cookies, :dependent => :delete_all, :class_name => 'EndUserCookie' has_many :end_user_actions, :dependent => :delete_all @@ -128,6 +129,10 @@ def after_create #:nodoc: end end + def after_save #:nodoc: + self.end_user_cache ? self.end_user_cache.save : EndUserCache.create(:end_user_id => self.id) + end + ## Validation Fucntions diff --git a/app/models/end_user_cache.rb b/app/models/end_user_cache.rb new file mode 100644 index 00000000..6810b5b9 --- /dev/null +++ b/app/models/end_user_cache.rb @@ -0,0 +1,25 @@ + +class EndUserCache < DomainModel + belongs_to :end_user + validates_presence_of :end_user_id + + def before_save + self.data = self.get_end_user_data.delete_if { |v| v.blank? }.join(' ') + end + + def get_end_user_data + data = [ + self.end_user.email, + self.end_user.name, + self.end_user.user_class.name, + self.end_user.source, + self.end_user.registered? ? 'registered' : nil + ] + end + + def self.reindex + EndUser.find_in_batches do |users| + users.each { |user| user.save } + end + end +end diff --git a/db/migrate/20100511165552_user_segments.rb b/db/migrate/20100511165552_user_segments.rb index 88655354..3174b94b 100644 --- a/db/migrate/20100511165552_user_segments.rb +++ b/db/migrate/20100511165552_user_segments.rb @@ -34,11 +34,20 @@ def self.up end add_index :user_segment_analytics, [:user_segment_id], :name => 'user_segment_analytics_segment_idx' + + create_table :end_user_caches, :force => true, :options => "ENGINE=MyISAM" do |t| + t.integer :end_user_id + t.text :data + end + + add_index :end_user_caches, [:end_user_id], :name => 'end_user_caches_idx', :unique => true + execute "CREATE FULLTEXT INDEX end_user_caches_data_index ON end_user_caches (data)" end def self.down drop_table :user_segments drop_table :user_segment_caches drop_table :user_segment_analytics + drop_table :end_user_caches end end From 5edc34f991917b1d29c88a18059b83ff7c1f5d4b Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Mon, 17 May 2010 20:58:11 +0000 Subject: [PATCH 012/139] can use a scope to search for end user ids within end user segments. --- app/models/end_user_cache.rb | 3 ++- app/models/user_segment.rb | 20 +++++++++++++------- app/models/user_segment_cache.rb | 7 ++++--- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/models/end_user_cache.rb b/app/models/end_user_cache.rb index 6810b5b9..d984e4ed 100644 --- a/app/models/end_user_cache.rb +++ b/app/models/end_user_cache.rb @@ -13,7 +13,8 @@ def get_end_user_data self.end_user.name, self.end_user.user_class.name, self.end_user.source, - self.end_user.registered? ? 'registered' : nil + self.end_user.registered? ? 'registered' : nil, + self.end_user.tag_names.join(" ") ] end diff --git a/app/models/user_segment.rb b/app/models/user_segment.rb index 1e4a5dca..4d41489c 100644 --- a/app/models/user_segment.rb +++ b/app/models/user_segment.rb @@ -108,26 +108,32 @@ def paginate(page,args = {}) }, items ] end - def search(offset=0, args={}) + def search(args={}) args = args.clone.symbolize_keys! - page_size = args.delete(:per_page).to_i - page_size = 20 if page_size <= 0 + offset = args.delete(:offset).to_i + limit = args.delete(:limit).to_i + limit = 20 if limit <= 0 cache_offset = offset % UserSegmentCache::SIZE + if args[:scope].nil? + args[:scope] = EndUser.scoped(:conditions => args.delete(:conditions), :joins => args.delete(:joins)) + args[:end_user_field] = :id + end + ids = [] ((offset / UserSegmentCache::SIZE).to_i..self.user_segment_caches.length-1).each do |position| cache = self.user_segment_caches.find_by_position(position) - cache_offset, cache_ids = cache.search(cache_offset, args.merge(:limit => page_size-ids.length)) + cache_offset, cache_ids = cache.search(cache_offset, args.merge(:limit => limit-ids.length)) ids = ids + cache_ids offset = UserSegmentCache::SIZE * position + cache_offset cache_offset = 0 - break if ids.length >= page_size + break if ids.length >= limit end - args.delete(:conditions) - args.delete(:joins) + args.delete(:scope) + args.delete(:end_user_field) users = EndUser.find(:all, args.merge(:conditions => {:id => ids})) return [offset, users] end diff --git a/app/models/user_segment_cache.rb b/app/models/user_segment_cache.rb index 4a8e594e..ba53782e 100644 --- a/app/models/user_segment_cache.rb +++ b/app/models/user_segment_cache.rb @@ -28,13 +28,14 @@ def search(offset=0, opts={}) num_chunks = (self.id_list.size / batch_size).to_i num_chunks = num_chunks.succ if (self.id_list.length % batch_size) > 0 - base_scope = EndUser.scoped(opts) + base_scope = opts.delete(:scope) || EndUser + end_user_field = opts.delete(:end_user_field) || :end_user_id ids = [] ((offset/batch_size).to_i..num_chunks-1).each do |chunk| sub_list = self.id_list[offset..(offset+batch_size-1)] - scope = base_scope.scoped(:select => 'id', :conditions => {:id => sub_list}) - users_by_id = scope.find(:all).index_by(&:id) + scope = base_scope.scoped(:select => end_user_field.to_s, :conditions => {end_user_field.to_sym => sub_list}) + users_by_id = scope.find(:all).index_by(&end_user_field.to_sym) ids = ids + sub_list.map { |id| users_by_id[id] ? id : nil }.compact unless users_by_id.empty? offset = offset + batch_size break if ids.length > limit From 143a62379d510df50de61715990cd6a9bc7134dc Mon Sep 17 00:00:00 2001 From: Doug Youch Date: Tue, 18 May 2010 18:17:03 +0000 Subject: [PATCH 013/139] Added a search box to members page. Can set the text for active next and previous links. Added EndUserSearch to handle search through EndUserCache with and without user segments. --- app/controllers/members_controller.rb | 139 +++++++++---------------- app/helpers/cms_helper.rb | 7 +- app/models/end_user_cache.rb | 2 + app/models/end_user_search.rb | 22 ++++ app/models/user_segment.rb | 4 +- app/views/members/_targets_table.rhtml | 44 +++----- app/views/members/index.rhtml | 20 ++-- lib/active_table.rb | 7 +- 8 files changed, 103 insertions(+), 142 deletions(-) create mode 100644 app/models/end_user_search.rb diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index 486bab33..82a2f712 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -14,18 +14,15 @@ class MembersController < CmsController # :nodoc: all include ActiveTable::Controller active_table :email_targets_table, EndUser, - [ hdr(:icon, 'check'), - hdr(:static, 'edit', :width => '16'), - hdr(:option, 'user_level',:options => EndUser.user_level_select_options,:label => 'Lvl', :width=> 40), - hdr(:string, 'email'), - hdr(:string, 'first_name',:label => 'First'), - hdr(:string, 'last_name',:label => 'Last'), - hdr(:date_range, 'created_at',:label => 'Created'), - hdr(:date_range, 'registered_at',:label => 'Reg.'), - hdr(:option, 'user_class_id',:label=> 'Profile',:options => :get_user_classes), - hdr(:option, 'source',:options => EndUser.source_select_options,:label => 'Src. Type',:width=>90), - hdr(:string, 'lead_source',:label => 'Src.' ), - 'tags' + [ hdr(:icon, 'check', :width => '16'), + hdr(:static, 'Profile', :width => '70'), + hdr(:static, 'Image', :width => '70'), + 'Name', + 'Email', + hdr(:static, 'Src. Type', :label => 'Src. Type'), + hdr(:static, 'Src.', :label => 'Src.'), + hdr(:static, 'Created', :width => '120'), + hdr(:static, 'Reg.', :label => 'Reg.', :width => '120') ], :count_callback => 'count_end_users', :find_callback => 'find_end_users' @@ -35,47 +32,56 @@ class MembersController < CmsController # :nodoc: all def segmentations - @segment= session[:et] - @segmentations = MarketSegment.find(:all,:conditions => 'segment_type = "members" AND market_campaign_id IS NULL',:order => :name).collect { |seg| - [ seg.name, seg.id ] - } - - @loaded_segment_id = session[:et_segment] - @loaded_segment_name = session[:et_segment_name] + @segmentations ||= UserSegment.find(:all, :conditions => {:main_page => true}, :order => 'name') + end + def segment + @segment ||= UserSegment.find_by_id params[:path][0] end def count_end_users(opts) - - @seg = MarketSegment.new(:segment_type => 'members',:options => { :tags => @segment[:tags], :tags_select => @segment[:tags_select], - :conditions => opts[:conditions], :order => opts[:order], - :search => session[:active_table][:email_targets_table] }) - @seg.target_count + if self.search_results + pages, users = self.search_results + if self.search.user_segment + self.search.offsets.length * self.search.per_page - (self.search.per_page - users.length) + else + pages[:total] + end + elsif self.segment + self.segment.last_count + else + EndUser.count :conditions => 'client_user_id IS NULL' + end end def find_end_users(opts) + if self.search_results + pages, users = self.search_results + users + elsif self.segment + pages, users = self.segment.paginate self.search.page, :per_page => 25 + users + else + EndUser.find(:all, :conditions => 'client_user_id IS NULL', :include => [:user_class, :domain_file], :offset => opts[:offset], :limit => opts[:limit], :order => opts[:order]) + end + end + + def search + return @search if @search + + @search = EndUserSearch.new + @search.terms = params[:terms] + @search.per_page = 25 + @search.offset = (params[:offset] || 0).to_i + @search.offsets = (params[:offsets] || [0]).collect { |n| n.to_i } + @search.user_segment = self.segment + @search.page = (params[:page] || 1).to_i + @search.offset = @search.offsets[@search.page-1] if @search.user_segment && @search.offsets.length >= @search.page + @search + end - # (set in handle_table_actions) - if we are saving this segment, find one with the same name, - # or create a new one - if @save_segment - @seg = MarketSegment.find_by_name(@save_segment,:conditions => 'segment_type="members"') || - MarketSegment.new(:segment_type => 'members', :name => @save_segment) - else - @seg = MarketSegment.new(:segment_type => 'members') - end - - @seg.options = { :tags => @segment[:tags], :tags_select => @segment[:tags_select], - :conditions => opts[:conditions], :order => opts[:order], - :search => session[:active_table][:email_targets_table] } - - session[:members_table_segment] = @seg.options.clone - if @save_segment - @seg.save - session[:et_segment] = @seg.id - session[:et_segment_name] = @seg.name - end - - @seg.target_find(:offset => opts[:offset], :limit => opts[:limit], :order => opts[:order]) + def search_results + @search_results ||= self.search.search if self.search.terms end def get_user_classes @@ -104,30 +110,6 @@ def handle_table_actions end end end - elsif request.post? && params[:save_segment] - @save_segment = params[:save_segment] - @update_segments = true - elsif request.post? && params[:load_segment] - load_segment = MarketSegment.find_by_id(params[:load_segment].to_i) - - if load_segment - params.delete(:email_targets_table) - session[:active_table][:email_targets_table] = load_segment.options[:search] - session[:et] = { :tags => load_segment.options[:tags], :tags_select => load_segment.options[:tags_select] } - session[:et_segment] = load_segment.id - session[:et_segment_name] = load_segment.name - else - session[:et_segment] = nil - session[:et_segment_name] = nil - end - @update_segments = true - elsif request.post? && params[:delete_segment] - load_segment = MarketSegment.find_by_id(params[:delete_segment].to_i) - load_segment.destroy - session[:et_segment] = nil - session[:et_segment_name] = nil - @update_segments = true - end end @@ -162,25 +144,6 @@ def display_targets_table(display = true) if params[:table_action] || params[:segment_action] handle_table_actions end - - default_options = { :tags => [], - :tags_select => 'any' - } - - session[:et] ||= default_options.clone - session[:et_segment] ||= nil - - if(params[:tag]) - session[:et][:tags] << params[:tag] - session[:et][:tags].uniq! - elsif params[:remove_tag] - session[:et][:tags].delete(params[:remove_tag]) - elsif params[:tags_select] - session[:et][:tags_select] = params[:tags_select] - elsif params[:clear_tag] - session[:et][:tags] = [] - end - @segment= session[:et] @active_table_output = email_targets_table_generate params, :per_page => 25, :include => :tag_cache, :conditions => 'client_user_id IS NULL', :order => 'created_at DESC' diff --git a/app/helpers/cms_helper.rb b/app/helpers/cms_helper.rb index 8fa220aa..c000a4c5 100644 --- a/app/helpers/cms_helper.rb +++ b/app/helpers/cms_helper.rb @@ -520,6 +520,9 @@ def active_table_for(name,active_table_output,options={},&block) table_actions = options.delete(:actions) more_actions = options.delete(:more_actions) + next_link_text = options.delete(:next) || '>' + previous_link_text = options.delete(:previous) || '<' + options[:class] ||= "active_table" form_elements = options.delete(:form_elements) @@ -694,7 +697,7 @@ def active_table_for(name,active_table_output,options={},&block) initial = true if(pagination[:page] > 1) initial = false - concat( "
  • <
  • ") + concat( "
  • #{previous_link_text}
  • ") end concat(pagination[:pages].collect { |number| first = true if initial @@ -709,7 +712,7 @@ def active_table_for(name,active_table_output,options={},&block) }.to_s ) if(pagination[:page] < pagination[:pages_count]) - concat( "
  • >
  • ") + concat( "
  • #{next_link_text}
  • ") end concat('') end diff --git a/app/models/end_user_cache.rb b/app/models/end_user_cache.rb index d984e4ed..5f8eefc6 100644 --- a/app/models/end_user_cache.rb +++ b/app/models/end_user_cache.rb @@ -3,6 +3,8 @@ class EndUserCache < DomainModel belongs_to :end_user validates_presence_of :end_user_id + named_scope :search, lambda { |query| {:conditions => ['MATCH (data) AGAINST (?)', query], :order => "MATCH (data) AGAINST (#{self.quote_value(query)}) DESC"} } + def before_save self.data = self.get_end_user_data.delete_if { |v| v.blank? }.join(' ') end diff --git a/app/models/end_user_search.rb b/app/models/end_user_search.rb new file mode 100644 index 00000000..18bdb624 --- /dev/null +++ b/app/models/end_user_search.rb @@ -0,0 +1,22 @@ + +class EndUserSearch + attr_accessor :terms, :offset, :page, :per_page, :user_segment, :offsets + + def search(opts={}) + scope = EndUserCache.search self.terms + if self.user_segment + self.offsets << self.offset unless self.offsets.include?(self.offset) + new_offset, users = self.user_segment.search opts.merge(:scope => scope, :offset => self.offset, :limit => self.per_page) + if users.length >= self.per_page + self.offsets << (new_offset+1) unless self.offsets.include?(new_offset+1) + end + [new_offset, users] + else + pages, ids = scope.paginate(self.page, :select => 'end_user_id') + ids = ids.collect { |cache| cache[:end_user_id] } + users_by_id = EndUser.find(:all, opts.merge(:conditions => {:id => ids})).index_by(&:id) + users = ids.map { |id| users_by_id[id] } + [pages, users] + end + end +end diff --git a/app/models/user_segment.rb b/app/models/user_segment.rb index 4d41489c..f95afec0 100644 --- a/app/models/user_segment.rb +++ b/app/models/user_segment.rb @@ -26,7 +26,7 @@ def cache_ids self.user_segment_caches.delete_all self.order_by = 'created_at DESC' unless self.order_by - ids = EndUser.find(:all, :select => 'id', :conditions => {:id => self.operations.end_user_ids}, :order => self.order_by).collect &:id + ids = EndUser.find(:all, :select => 'id', :conditions => {:id => self.operations.end_user_ids, :client_user_id => nil}, :order => self.order_by).collect &:id num_segements = (self.operations.end_user_ids.length / UserSegmentCache::SIZE) num_segements = num_segements + 1 if (self.operations.end_user_ids.length % UserSegmentCache::SIZE) > 0 @@ -78,7 +78,7 @@ def find(opts={}, &block) end end - def paginate(page,args = {}) + def paginate(page=1, args={}) args = args.clone.symbolize_keys! window_size =args.delete(:window) || 2 diff --git a/app/views/members/_targets_table.rhtml b/app/views/members/_targets_table.rhtml index bc8f43ea..5e13d9a9 100644 --- a/app/views/members/_targets_table.rhtml +++ b/app/views/members/_targets_table.rhtml @@ -1,15 +1,3 @@ -<% if @update_tags -%> - -<% end -%> - -<% if @update_segments -%> - -<% end -%> - <% @user_classes = [] @user_level_options = EndUser.user_level_select_options @source_options = EndUser.source_options_hash @@ -21,9 +9,10 @@ @user_class_objs.each { |cls| @user_classes[cls.id] = h(cls.name.t) } %> <% active_table_for :email_targets_table, @active_table_output, - :refresh_url => url_for(:action => 'display_targets_table'), + :refresh_url => url_for(:action => 'display_targets_table', :path => @segment ? @segment.id : nil, :terms => @search.terms, :offsets => @search.offsets.empty? ? nil : @search.offsets), :class => 'active_table', :width => '100%', + :next => @search.user_segment && @search.terms ? 'more' : '>', :actions => [ ['Add Tags','js','MemberEditor.addTags();'], ['Remove Tags','js','MemberEditor.removeTags();'] ], @@ -33,33 +22,28 @@ <%= entry_checkbox 'user',t.id %> + <%= t.user_class ? t.user_class.name : '-' %> - <% unless @user_class_index[t.user_class_id].editor? %> - <%= theme_image_tag 'icons/table_actions/edit.gif' %> - <% end -%> + <%= image = t.image ? t.image.image_tag(:thumb) : 'No Image'.t + t.editor? ? image : content_tag(:a, image, {:href => url_for(:action => 'edit', :path => t.id)}) + %> - <%= t.user_level %> - <%= h t.email.blank? ? 'No Email'.t : t.email %> - <%= h(truncate(t.first_name,:length => 30)) %> + <%= t.editor? ? h(t.name) : content_tag(:a, t.name, {:href => url_for(:action => 'edit', :path => t.id)}) %> - <%= h(truncate(t.last_name ,:llength => 30)) %> + <%= email = t.email.blank? ? 'No Email'.t : t.email + t.editor? ? h(email) : content_tag(:a, email, {:href => url_for(:action => 'edit', :path => t.id)}) + %> - + <%= h @source_options[t.source] %> + <%= h t.lead_source.blank? ? '-' : t.lead_source %> + <%= t.created_at ? t.created_at.strftime(DEFAULT_DATE_FORMAT.t) : '-' %> - + <%= t.registered_at ? t.registered_at.strftime(DEFAULT_DATE_FORMAT.t) : '-' %> - - <%= @user_classes[t.user_class_id] if t.user_class_id %> - <%= h @source_options[t.source] %> - <%= h t.lead_source.blank? ? '-' : t.lead_source %> - <% user_tags = t.tag_cache_tags.to_s.split(",") %> - <%= user_tags[0..5].collect {|tag| "#{tag}" }.join(", ") %> - <%= "+(#{user_tags.length-6})" if user_tags.length > 6 %> - <% end -%> diff --git a/app/views/members/index.rhtml b/app/views/members/index.rhtml index 96999d39..9b582418 100644 --- a/app/views/members/index.rhtml +++ b/app/views/members/index.rhtml @@ -106,22 +106,14 @@
    -
    -<% ajax_tabs [ 'Tags','Segmentations'], 'Tags' do |t| %> - <% t.tab do -%> -
    - <%= render :partial => 'tags' %> -
    - <% end -%> - <% t.tab do -%> -
    - <%= render :partial => 'segmentations' %> -
    - <% end -%> -<% end -%> +
    -
    @@ -102,18 +105,34 @@ <%= p.link "Download Visible Targets",:icon => 'download.gif', :controller => :member_export %> <%= p.link "Tag List", :icon => 'show.gif', :action => 'tags' %> <%= p.link "Access Tokens", :icon => 'access.gif', :controller => '/access_token' if myself.has_role?('editor_access_tokens') %> + <%= p.link "Segments", :icon => 'access.gif', :action => 'segments' %> <% end -%>
    -