分析admin路径下的一系列模块
products
- routes.rb:
resources :products, except: :edit do
collection do
get :inventory
post :set
end
member do
put :update_published
post :duplicate
end
创建没有edit方法的products,新增
/products/inventory
/products/set
/products/1/update_published
/products/1/duplicate
四个路由
- 从点击/shopqi/app/views/layouts/admin.html.haml中的“商品”菜单开始,/shopqi/app/controllers/admin/products_controller.rb
def index
@products_json = products.to_json({include: [:variants, :options], except: [:created_at, :updated_at],methods:[:index_photo]})
end
跳转到index页面
由于没有商品,在页面中只有一个入口——new
现在回到products_controller来看new方法
def new
#保证至少有一个款式
product.variants.build if product.variants.empty?
end
说的很明白了 直接跳到页面,准备填写新的product
现在让我们来分析product这个model
# encoding: utf-8
class Product < ActiveRecord::Base
include Models::Handle
导入module 位置:lib/models/handle.rb,include首先执行self.included(base)。 里面的方法有模块方法self.handleize;类方法handle!,published_handle!;实例方法make_valid。一共五个方法。
belongs_to :shop
shop.rb中代码 has_many :products , dependent: :destroy , order: 'id desc' 两者关系为一对多,如果shop没了,product也会消失,排序按照id 倒序
has_many :photos , dependent: :destroy , order: 'position asc'
photos 图片类 belongs_to :product 一对多关系 需要注意的是用到了 order position,可以从/shopqi/db/migrate/20110422070212_create_products.rb中看出 create_table :photos do |t| t.references :product t.string :product_image_uid t.string :product_image_format t.integer :position , comment: '排序序号' t.timestamps end photo中有一个类型为integer的position字段
has_many :variants , dependent: :destroy , class_name: 'ProductVariant' , order: 'position asc'
ProductVariant,商品款式类 需要注意的是它的order:position并不是字段名,而是acts_as_list插件,在rails2中被剔除,然后已插件形式出现,该插件的目的是再model数据库存储中,在一对多关系中,将多端按照作为一个有顺序的列表来存储,并提供一些移动等方法来辅助,acts_as_list详细用法请移步这里 下面可以看到定义的variants类中的代码
class ProductVariant < ActiveRecord::Base
.......
acts_as_list scope: :product
.......
has_many :options , dependent: :destroy , class_name: 'ProductOption' , order: 'position asc'
options 产品选项类
has_many :custom_collection_products, dependent: :destroy , class_name: 'CustomCollectionProduct'
集合关联的商品 中间表 在/shopqi/app/models/custom_collection.rb中
has_many :custom_collections , class_name: 'CustomCollection', through: :custom_collection_products , source: :custom_collection
custom_collection_products是一个中间表,由我们自己定义而得。由它把product和custom_collections连接起来。source: 是跟在through:后面的精确判定,用于告诉rails到哪里去找custom_collection_products。
has_many :smart_collection_products , dependent: :destroy , class_name: 'SmartCollectionProduct'
has_and_belongs_to_many :tags , order: 'id asc'
# 标签
attr_accessor :tags_text,:images
列出了两个属性的getset方法
attr_accessible :handle, :title, :published, :body_html, :price, :product_type, :vendor, :tags_text, :images, :photos_attributes, :variants_attributes, :options_attributes, :custom_collection_ids
属性白名单,这些都可以直接写入数据库
scope :published, where(published: true)
定义了一个可直接调用的方法,published
accepts_nested_attributes_for :photos , allow_destroy: true
accepts_nested_attributes_for :variants, allow_destroy: true
accepts_nested_attributes_for :options, allow_destroy: true
accepts_nested_attributes_for,当创建一个product时,就创建一个photo,一个variants,一个option。生成写方法:photos_attributes,variants_attributes,options_attributes.此属性解释详见这里
validates_presence_of :title, :product_type, :vendor
必须填写这三项
#商品列表中显示的产品图片
def index_photo
photo('thumb')
end
显示缺省图片
def photo(version = :icon)
unless photos.empty?
photos.first.send(version)
else
"/assets/admin/no-image-#{version}.gif"
end
end
这边需要讲解一下了,index_photo方法调用了photo方法,传入了缺省值thumb,如果数据库中有图片,则取出第一张。
photos.first.send(version),调用了ruby的send方法,用一个符号表示方法(具体send的用法请点击这里)
下面让我们来看看photos模块中,到底是如何定义.send调用的方法的。
让我们来到photos模块,以下是photos模块的所有代码:
class Photo < ActiveRecord::Base
belongs_to :product
default_scope order: 'position asc'
attr_accessible :product_image, :position
VERSION_KEYS = []
image_accessor :product_image do image_accessor是插件dragonfly定义的一个方法,从字面意思就可理解:为product_image定义geter/setter方法
storage_path{ |image| 存储路径
"#{self.product.shop.id}/products/#{self.product.id}/#{image.basename}_#{rand(1000)}.#{image.format}" # data/shops/1/products/1/foo_45.jpg
}
end
validates_size_of :product_image, maximum: 8000.kilobytes
validates_with StorageValidator validates_with,指定另一个类来构建更为复杂的验证
StorageValidator 代码如下:
class StorageValidator < ActiveModel::Validator #验证容量是否超过商店限制
# 需要限制的地方:
# 1. 商品图片上传
# 2. 富文本框的图片上传
# 3. 主题上传、复制和安装(直接判断)
# 4. 附件上传(直接判断)
# 5. 主题设置中的图片上传(直接判断)
def validate(record)
record.errors[:base] << I18n.t('activerecord.errors.models.shop.attributes.storage.full') unless record.shop.nil? or record.shop.storage_idle?
end
end
validates_property :mime_type, of: :product_image, in: %w(image/jpeg image/jpg image/png image/gif), message: "格式不正确"
#定义图片显示大小种类
def self.versions(opt={})
opt.each_pair do |k,v|
VERSION_KEYS << k
define_method k do
if product_image
product_image.thumb(v).url 先用imagemagick进行剪切后,给出url
end
end
end
end
def shop # 直接使用delegate :shop, to: :product在新增商品带图片的情况下会报500错误 #416
product ? product.shop : nil
end
#显示在产品列表的缩略图(icon)
#后台管理商品详情(small)
versions pico: '16x16', icon: '32x32', thumb: '50x50', small:'100x100', compact: '160x160', medium: '240x240', large: '480x480', grande: '600x600', original: '1024x1024'
end
从以上代码,photo类定义了类方法versions,在最后又带入参数执行了它,把key值传给了VERSION_KEYS,随后以key值的名字作为方法名,定义了一系列的方法,内容都为:方法名为key,如果product_image存在,那么返回product_image的url。
好了,现在我们就能理解photos.first.send(version),调用了photos类里面的pico|icon|thumb|small|compact|.....方法,返回一个图片的url。
需要说明的一点是,photo中运用了dragonfly插件,这里是此插件官网,请自行察看。
searchable do
integer :shop_id, references: Shop
text :title, :body_html, :product_type, :vendor
text :variants_text do
variants.map do |variant|
[variant.option1, variant.option2, variant.option3, variant.sku]
end
end
end
sunspot插件,solr搜索引擎
后面代码就不解释了,大同小异比较简单,其它关注点:
product类里面还包含了ProductVariant(产品款式)和ProductOption(产品选项)类。
这里是default_scope的一些用法:
default_scope
default_scope(scope = {}) protected
Use this macro in your model to set a default scope for all operations on the model.
class Article < ActiveRecord::Base
default_scope where(:published => true)
end
Article.all # => SELECT * FROM articles WHERE published = true
The default_scope is also applied while creating/building a record. It is not applied while updating a record.
Article.new.published # => true
Article.create.published # => true