lib/maily_herald/context.rb in maily_herald-0.0.1 vs lib/maily_herald/context.rb in maily_herald-0.8.0

- old
+ new

@@ -1,79 +1,225 @@ module MailyHerald - class Context - class Drop < Liquid::Drop - def initialize attributes, item - @attributes = attributes - @item = item - end - def has_key?(name) - name = name.to_sym + # Abstraction layer for accessing collections of Entities and their attributes. + # Information provided by scope is used while sending {MailyHerald::Mailing mailings}. + # + # {Context} defines following: + # + # * Entity scope - +ActiveRecord::Relation+, list of Entities that will be returned + # by {Context}. + # * Entity model name - deducted automatically from scope. + # * Entity attributes - Defined as procs that can be evaluated for every item of + # the scope (single Entity). + # + # * Entity email - defined as proc or string/symbol (email method name). + # + class Context - @attributes.has_key? name - end + # Context Attributes drop definition for Liquid + class Drop < Liquid::Drop + def initialize attrs + @attrs = attrs + end - def invoke_drop name - name = name.to_sym + def has_key?(name) + name = name.to_s - #@attributes[name].try(:call, @item) - @attributes[name].call(@item) - end + @attrs.has_key? name + end - alias :[] :invoke_drop - end + def invoke_drop name + name = name.to_s - attr_accessor :entity + if @attrs.has_key? name + if @attrs[name].is_a? Hash + Drop.new(@attrs[name]) + else + @attrs[name].call + end + else + nil + end + end - def scope &block - if block_given? - @scope = block - else - @scope.call - end - end + alias :[] :invoke_drop + end - def destination &block - if block_given? - @destination = block - else - @destination - end - end + class Attributes + def initialize block + @attrs = {} + @node = @parent_node = @attrs + @block = block + end - def attribute name, &block - name = name.to_sym + def setup entity = nil, subscription = nil + if entity + @attrs["subscription"] = Proc.new{ subscription } if subscription + instance_exec entity, &@block + else + instance_eval &@block + end + end - @attributes ||= {} - if block_given? - @attributes[name] = block - else - @attributes[name] - end - end + def attribute_group name, &block + @parent_node = @node + @parent_node[name.to_s] ||= {} + @node = @parent_node[name.to_s] + yield + @node = @parent_node + end - def attribute_names - @attributes.keys - end + def attribute name, &block + @node[name.to_s] = block + end - def model - @scope.call.table.engine - end + def for_drop + @attrs + end - def extract_attributes hash = @attributes, collect = false - hash.map do |k, v| - v.is_a?(Hash) ? extract_attributes(v, (k == "items_attributes")) : (collect ? v : nil) - end.compact.flatten - end + def method_missing(m, *args, &block) + true + end + end - def each &block - @scope.call.each do |item| - drop = Drop.new(@attributes, item) - block.call(item, drop) - end - end + # Friendly name of the {Context}. + # + # Displayed ie. in the Web UI. + attr_accessor :title - def drop_for item - Drop.new(@attributes, item) - end - end + # Identification name of the {Context}. + # + # This can be then used in {MailyHerald.context} method to fetch the {Context}. + # + # @see MailyHerald.context + attr_reader :name + + attr_writer :destination + + # Creates {Context} and sets its name. + def initialize name + @name = name + end + + # Defines or returns Entity scope - collection of Entities. + # + # If block passed, it is saved as scope proc. Block has to return + # +ActiveRecord::Relation+ containing Entity objects that will belong to scope. + # + # If no block given, scope proc is called and Entity collection returned. + def scope &block + if block_given? + @scope = block + else + @scope.call + end + end + + # Fetches the Entity model class based on scope. + def model + @model ||= @scope.call.klass + end + + # Entity email address. + # + # Can be eitner +Proc+ or attribute name (string, symbol). + # + # If block passed, it is saved as destination proc. Block has to: + # + # * accept single Entity object, + # * return Entity email. + # + # If no block given, +destination+ attribute is returned (a string, symbol or proc). + def destination &block + if block_given? + @destination = block + else + @destination + end + end + + # Returns Entity email attribute name only if it is not defined as a proc. + def destination_attribute + @destination unless @destination.respond_to?(:call) + end + + # Fetches Entity's email address based on {Context} destination definition. + def destination_for entity + destination_attribute ? entity.send(@destination) : @destination.call(entity) + end + + # Simply filter Entity scope by email. + # + # If destination is provided in form of Entity attribute name (not the proc), + # this method creates the scope filtered by `query` email using SQL LIKE. + # + # @param query [String] email address which is being searched. + # @return [ActiveRecord::Relation] collection filtered by email address + def scope_like query + if destination_attribute + scope.where("#{model.table_name}.#{destination_attribute} LIKE (?)", "%#{query}%") + end + end + + # Returns Entity collection scope with joined {MailyHerald::Subscription}. + # + # @param list [List, Fixnum, String] {MailyHerald::List} reference + # @param mode [:inner, :outer] SQL JOIN mode + def scope_with_subscription list, mode = :inner + list_id = case list + when List + list.id + when Fixnum + list + when String + list.to_i + else + raise ArgumentError + end + + join_mode = case mode + when :outer + "LEFT OUTER JOIN" + else + "INNER JOIN" + end + + subscription_fields_select = Subscription.columns.collect{|c| "#{Subscription.table_name}.#{c.name} AS maily_subscription_#{c.name}"}.join(", ") + + scope.select("#{model.table_name}.*, #{subscription_fields_select}").joins( + "#{join_mode} #{Subscription.table_name} ON #{Subscription.table_name}.entity_id = #{model.table_name}.id AND #{Subscription.table_name}.entity_type = '#{model.base_class.to_s}' AND #{Subscription.table_name}.list_id = '#{list_id}'" + ) + end + + # Sepcify or return {Context} attributes. + # + # Defines Entity attributes that can be accessed using this Context. + # Attributes defined this way are then accesible in Liquid templates + # in Generic Mailer ({MailyHerald::Mailer#generic}). + # + # If block passed, it is used to create Context Attributes. + # + # If no block given, current attributes are returned. + def attributes &block + if block_given? + @attributes = Attributes.new block + else + @attributes + end + end + + # Obtains {Context} attributes in a form of (nested) +Hash+ which + # values are procs each returning single Entity attribute value. + def attributes_list + attributes = @attributes.dup + attributes.setup + attributes.for_drop + end + + # Returns Liquid drop created from Context attributes. + def drop_for entity, subscription + attributes = @attributes.dup + attributes.setup entity, subscription + Drop.new(attributes.for_drop) + end + end end