Updated Rake tasks and test, spec to reflect change in how Merb environments
[merb_mart.git] / app / models / product.rb
blob3b2699e9848589e9c11f715b2c16e24e02bc56f2
1 class Product < StoreItem
2   # Conditions that the product is in stock, or available 
3   # and just out of stock.
4   CONDITIONS_AVAILABLE = %Q/
5       CURRENT_DATE() >= DATE(items.date_available)
6       AND items.is_discontinued = 0
7       OR (items.is_discontinued = 1 AND (items.quantity > 0 OR items.variation_quantity > 0))
8   /
9   
10   has_many :product_images
11   has_many   :variations, :dependent => :destroy, :order => 'name ASC'
12   has_many :images, 
13     :through => :product_images, :order => "-product_images.rank DESC",
14     :dependent => :destroy
15   
16   # Join with related items...
17   has_and_belongs_to_many :related_products,
18     :class_name => 'Product',
19     :join_table => 'related_products',
20     :association_foreign_key => 'related_id',
21     :foreign_key => 'product_id',
22     :after_add => :add_return_relation,
23     :after_remove => :remove_return_relation
24   
25   def add_return_relation(relative)
26     relative.related_products << self unless relative.related_products.include?(self)
27   end
28   def remove_return_relation(relative)
29     sql  = "DELETE FROM related_products "
30     sql << "WHERE product_id = #{relative.id} AND related_id = #{self.id}"
31     
32     database.query(sql)
33   end
34   
35   has_and_belongs_to_many :tags
36   
37   #############################################################################
38   # CALLBACKS
39   #############################################################################
40   
41   after_create :add_cached_related_products
42   
43   # Related products (associations) freak out if this
44   # object isn't saved before adding relatives.
45   #
46   # If there's a cached @related_list we try to add em.
47   #
48   def add_cached_related_products
49     self.related_product_ids=@cached_related_list if @cached_related_list
50   end
51   
52   #############################################################################
53   # CLASS METHODS
54   #############################################################################
56   # Admin search for products
57   # Uses product name, code, or description
58   def self.search(search_term, count=false, limit_sql=nil)
59     if (count == true) then
60       sql = "SELECT COUNT(*) "
61     else
62       sql = "SELECT DISTINCT * "
63     end
64     sql << "FROM items "
65     sql << "WHERE ("
66     sql << "  name LIKE ? "
67     sql << "  OR items.description LIKE ? "
68     sql << "  OR items.code LIKE ? "
69     sql << ") AND items.type = 'Product' "
70     sql << "ORDER BY date_available DESC "
71     sql << "LIMIT #{limit_sql}" if limit_sql
72     arg_arr = [sql, "%#{search_term}%", "%#{search_term}%", "%#{search_term}%"]
73     if (count == true) then
74       count_by_sql(arg_arr)
75     else
76       find_by_sql(arg_arr)
77     end
78   end
80   # Finds products by list of tag ids passed in
81   #
82   # We could JOIN multiple times, but selecting IN grabs us the products
83   # and using GROUP BY & COUNT with the number of tag id's given
84   # is a faster approach according to freenode #mysql
85   def self.find_by_tags(tag_ids, find_available=false, order_by="items.date_available DESC")
86     sql =  "SELECT * "
87     sql << "FROM items "
88     sql << "JOIN products_tags on items.id = products_tags.product_id "
89     sql << "WHERE products_tags.tag_id IN (#{tag_ids.join(",")}) "
90     sql << "AND #{CONDITIONS_AVAILABLE}" if find_available==true
91     sql << "GROUP BY items.id HAVING COUNT(*)=#{tag_ids.length} "
92     sql << "ORDER BY #{order_by};"
93     find_by_sql(sql)
94   end
95   
96   #############################################################################
97   # INSTANCE METHODS
98   #############################################################################
100   # Defined to save tags from product edit view
101   def tags=(list)
102     tags.clear
103     for id in list
104       tags << Tag[id] if !id.empty?
105     end
106   end
107   
108   # Calculated based on variations
109   #
110   def display_price
111     variations = self.variations.all # :order => "price ASC")
112     if variations.size == 0
113       return self.price
114     else
115       low_price = variations[0].price
116       high_price = variations[variations.size-1].price
117       if low_price == high_price
118         return low_price
119       else
120         return [low_price, high_price]
121       end
122     end
123   end
124   
125   def quantity
126     if self.variations.count == 0
127       return self.attributes['quantity']
128     else
129       return self.variation_quantity
130     end
131   end
132   
133   # Is the item active on the site? Is it listed in the store?
134   #
135   def is_published?
136     !self.is_discontinued? || self.quantity > 0
137   end
138   
139   # Is this product new?
140   #
141   def is_new?
142     @cached_is_new ||=
143       begin
144         self.date_available >= 2.weeks.ago
145       end
146   end
147   
148   # Is this product on sale?
149   #
150   def is_on_sale?
151     @cached_on_sale ||=
152       begin
153         !self.tags.find_by_name('On Sale').nil?
154       end
155   end
156   
157   # Adds related products from list.
158   # Used in UI for auto-completion
159   #
160   # See callback for why we return if this is a new record.
161   #
162   def related_product_ids=(list)    
163     if self.new_record?
164       @cached_related_list = list
165       return
166     end
167     
168     # If self is a new record shit fails 
169     self.related_products.clear
170     for name in list do
171       p = Product.find_by_code(name.split(':')[0])
172       next if !p || p == self
173       self.related_products << p
174     end
175   end
176