2016-10-19 03:11:36 +00:00
|
|
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
class Avatar < ApplicationModel
|
2018-04-12 14:57:37 +00:00
|
|
|
belongs_to :object_lookup
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
=begin
|
|
|
|
|
2015-02-15 08:36:18 +00:00
|
|
|
add an avatar based on auto detection (email address)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
Avatar.auto_detection(
|
2015-12-14 16:09:09 +00:00
|
|
|
object: 'User',
|
|
|
|
o_id: user.id,
|
|
|
|
url: 'somebody@example.com',
|
|
|
|
updated_by_id: 1,
|
|
|
|
created_by_id: 1,
|
2014-12-01 07:32:35 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def self.auto_detection(data)
|
2014-12-01 12:06:47 +00:00
|
|
|
|
|
|
|
# return if we run import mode
|
|
|
|
return if Setting.get('import_mode')
|
2016-12-02 11:24:00 +00:00
|
|
|
return if data[:url].blank?
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
Avatar.add(
|
2018-12-19 17:31:51 +00:00
|
|
|
object: data[:object],
|
|
|
|
o_id: data[:o_id],
|
|
|
|
url: data[:url],
|
|
|
|
source: 'zammad.com',
|
|
|
|
deletable: false,
|
2015-04-27 13:42:53 +00:00
|
|
|
updated_by_id: 1,
|
|
|
|
created_by_id: 1,
|
2014-12-01 07:32:35 +00:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
2016-10-06 16:59:23 +00:00
|
|
|
add avatar by upload
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
Avatar.add(
|
2015-12-14 16:09:09 +00:00
|
|
|
object: 'User',
|
|
|
|
o_id: user.id,
|
|
|
|
default: true,
|
|
|
|
full: {
|
|
|
|
content: '...',
|
|
|
|
mime_type: 'image/png',
|
2014-12-01 07:32:35 +00:00
|
|
|
},
|
2015-12-14 16:09:09 +00:00
|
|
|
resize: {
|
|
|
|
content: '...',
|
|
|
|
mime_type: 'image/png',
|
2014-12-01 07:32:35 +00:00
|
|
|
},
|
2015-12-14 16:09:09 +00:00
|
|
|
source: 'web',
|
2016-10-06 16:59:23 +00:00
|
|
|
deletable: true,
|
|
|
|
updated_by_id: 1,
|
|
|
|
created_by_id: 1,
|
|
|
|
)
|
|
|
|
|
|
|
|
add avatar by url
|
|
|
|
|
|
|
|
Avatar.add(
|
|
|
|
object: 'User',
|
|
|
|
o_id: user.id,
|
|
|
|
default: true,
|
|
|
|
url: ...,
|
|
|
|
source: 'web',
|
|
|
|
deletable: true,
|
2015-12-14 16:09:09 +00:00
|
|
|
updated_by_id: 1,
|
|
|
|
created_by_id: 1,
|
2014-12-01 07:32:35 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def self.add(data)
|
|
|
|
|
|
|
|
# lookups
|
|
|
|
if data[:object]
|
2016-03-08 06:32:58 +00:00
|
|
|
object_id = ObjectLookup.by_name(data[:object])
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
|
2015-02-15 08:36:18 +00:00
|
|
|
# add initial avatar
|
2018-05-08 10:10:19 +00:00
|
|
|
_add_init_avatar(object_id, data[:o_id])
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
record = {
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: data[:o_id],
|
2015-04-27 13:42:53 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
default: true,
|
|
|
|
deletable: data[:deletable],
|
|
|
|
initial: false,
|
|
|
|
source: data[:source],
|
|
|
|
source_url: data[:url],
|
|
|
|
updated_by_id: data[:updated_by_id],
|
|
|
|
created_by_id: data[:created_by_id],
|
2014-12-01 07:32:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# check if avatar with url already exists
|
|
|
|
avatar_already_exists = nil
|
2016-12-02 11:24:00 +00:00
|
|
|
if data[:source].present?
|
2015-05-07 10:15:40 +00:00
|
|
|
avatar_already_exists = Avatar.find_by(
|
2015-04-27 13:42:53 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: data[:o_id],
|
|
|
|
source: data[:source],
|
2015-05-07 10:15:40 +00:00
|
|
|
)
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
|
2015-07-05 22:13:59 +00:00
|
|
|
# fetch image based on http url
|
2017-10-20 13:32:01 +00:00
|
|
|
if data[:url].present?
|
2017-12-13 17:38:21 +00:00
|
|
|
if data[:url].class == Tempfile
|
|
|
|
logger.info "Reading image from tempfile '#{data[:url].inspect}'"
|
|
|
|
content = data[:url].read
|
|
|
|
filename = data[:url].path
|
|
|
|
mime_type = 'image'
|
|
|
|
if filename.match?(/\.png/i)
|
|
|
|
mime_type = 'image/png'
|
|
|
|
end
|
|
|
|
if filename.match?(/\.(jpg|jpeg)/i)
|
|
|
|
mime_type = 'image/jpeg'
|
|
|
|
end
|
|
|
|
data[:resize] ||= {}
|
|
|
|
data[:resize][:content] = content
|
|
|
|
data[:resize][:mime_type] = mime_type
|
|
|
|
data[:full] ||= {}
|
|
|
|
data[:full][:content] = content
|
|
|
|
data[:full][:mime_type] = mime_type
|
|
|
|
|
2018-08-28 12:51:01 +00:00
|
|
|
elsif data[:url].to_s.match?(%r{^https?://})
|
2017-12-13 17:38:21 +00:00
|
|
|
url = data[:url].to_s
|
2017-10-20 13:32:01 +00:00
|
|
|
|
|
|
|
# check if source ist already updated within last 2 minutes
|
2017-12-13 17:38:21 +00:00
|
|
|
if avatar_already_exists&.source_url == url
|
2017-10-20 13:32:01 +00:00
|
|
|
return if avatar_already_exists.updated_at > 2.minutes.ago
|
|
|
|
end
|
|
|
|
|
|
|
|
# twitter workaround to get bigger avatar images
|
|
|
|
# see also https://dev.twitter.com/overview/general/user-profile-images-and-banners
|
2017-12-13 17:38:21 +00:00
|
|
|
if url.match?(%r{//pbs.twimg.com/}i)
|
|
|
|
url.sub!(/normal\.(png|jpg|gif)$/, 'bigger.\1')
|
2017-10-20 13:32:01 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# fetch image
|
|
|
|
response = UserAgent.get(
|
2017-12-13 17:38:21 +00:00
|
|
|
url,
|
2017-10-20 13:32:01 +00:00
|
|
|
{},
|
|
|
|
{
|
2018-12-19 17:31:51 +00:00
|
|
|
open_timeout: 4,
|
|
|
|
read_timeout: 6,
|
2017-10-20 13:32:01 +00:00
|
|
|
total_timeout: 6,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if !response.success?
|
2017-12-13 17:38:21 +00:00
|
|
|
logger.info "Can't fetch '#{url}' (maybe no avatar available), http code: #{response.code}"
|
2017-10-20 13:32:01 +00:00
|
|
|
return
|
|
|
|
end
|
2017-12-13 17:38:21 +00:00
|
|
|
logger.info "Fetchd image '#{url}', http code: #{response.code}"
|
2017-10-20 13:32:01 +00:00
|
|
|
mime_type = 'image'
|
2017-12-13 17:38:21 +00:00
|
|
|
if url.match?(/\.png/i)
|
2017-10-20 13:32:01 +00:00
|
|
|
mime_type = 'image/png'
|
|
|
|
end
|
2017-12-13 17:38:21 +00:00
|
|
|
if url.match?(/\.(jpg|jpeg)/i)
|
2017-10-20 13:32:01 +00:00
|
|
|
mime_type = 'image/jpeg'
|
|
|
|
end
|
2017-12-13 17:38:21 +00:00
|
|
|
|
|
|
|
data[:resize] ||= {}
|
2017-10-20 13:32:01 +00:00
|
|
|
data[:resize][:content] = response.body
|
|
|
|
data[:resize][:mime_type] = mime_type
|
|
|
|
data[:full] ||= {}
|
|
|
|
data[:full][:content] = response.body
|
|
|
|
data[:full][:mime_type] = mime_type
|
|
|
|
|
|
|
|
# try zammad backend to find image based on email
|
2018-08-28 12:51:01 +00:00
|
|
|
elsif data[:url].to_s.match?(URI::MailTo::EMAIL_REGEXP)
|
2017-12-13 17:38:21 +00:00
|
|
|
url = data[:url].to_s
|
2017-10-20 13:32:01 +00:00
|
|
|
|
|
|
|
# check if source ist already updated within last 3 minutes
|
2017-12-13 17:38:21 +00:00
|
|
|
if avatar_already_exists&.source_url == url
|
2017-10-20 13:32:01 +00:00
|
|
|
return if avatar_already_exists.updated_at > 2.minutes.ago
|
|
|
|
end
|
|
|
|
|
|
|
|
# fetch image
|
2017-12-13 17:38:21 +00:00
|
|
|
image = Service::Image.user(url)
|
2017-10-20 13:32:01 +00:00
|
|
|
return if !image
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-10-20 13:32:01 +00:00
|
|
|
data[:resize] = image
|
|
|
|
data[:full] = image
|
2015-07-05 22:13:59 +00:00
|
|
|
end
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# check if avatar need to be updated
|
2017-10-20 13:32:01 +00:00
|
|
|
if data[:resize].present? && data[:resize][:content].present?
|
|
|
|
record[:store_hash] = Digest::MD5.hexdigest(data[:resize][:content])
|
2017-11-23 08:09:44 +00:00
|
|
|
if avatar_already_exists&.store_hash == record[:store_hash]
|
|
|
|
avatar_already_exists.touch # rubocop:disable Rails/SkipsModelValidations
|
2017-10-20 13:32:01 +00:00
|
|
|
return avatar_already_exists
|
|
|
|
end
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# store images
|
|
|
|
object_name = "Avatar::#{data[:object]}"
|
2017-10-20 13:32:01 +00:00
|
|
|
if data[:full].present?
|
2014-12-01 07:32:35 +00:00
|
|
|
store_full = Store.add(
|
2018-12-19 17:31:51 +00:00
|
|
|
object: "#{object_name}::Full",
|
|
|
|
o_id: data[:o_id],
|
|
|
|
data: data[:full][:content],
|
|
|
|
filename: 'avatar_full',
|
|
|
|
preferences: {
|
2014-12-01 07:32:35 +00:00
|
|
|
'Mime-Type' => data[:full][:mime_type]
|
|
|
|
},
|
2015-04-27 13:42:53 +00:00
|
|
|
created_by_id: data[:created_by_id],
|
2014-12-01 07:32:35 +00:00
|
|
|
)
|
|
|
|
record[:store_full_id] = store_full.id
|
2016-03-08 06:32:58 +00:00
|
|
|
record[:store_hash] = Digest::MD5.hexdigest(data[:full][:content])
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
2017-10-20 13:32:01 +00:00
|
|
|
if data[:resize].present?
|
2014-12-01 07:32:35 +00:00
|
|
|
store_resize = Store.add(
|
2018-12-19 17:31:51 +00:00
|
|
|
object: "#{object_name}::Resize",
|
|
|
|
o_id: data[:o_id],
|
|
|
|
data: data[:resize][:content],
|
|
|
|
filename: 'avatar',
|
|
|
|
preferences: {
|
2014-12-01 07:32:35 +00:00
|
|
|
'Mime-Type' => data[:resize][:mime_type]
|
|
|
|
},
|
2015-04-27 13:42:53 +00:00
|
|
|
created_by_id: data[:created_by_id],
|
2014-12-01 07:32:35 +00:00
|
|
|
)
|
|
|
|
record[:store_resize_id] = store_resize.id
|
2017-10-20 13:32:01 +00:00
|
|
|
record[:store_hash] = Digest::MD5.hexdigest(data[:resize][:content])
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
|
2017-10-20 13:32:01 +00:00
|
|
|
return if record[:store_resize_id].blank? || record[:store_hash].blank?
|
|
|
|
|
2014-12-01 07:32:35 +00:00
|
|
|
# update existing
|
|
|
|
if avatar_already_exists
|
2017-09-11 11:16:08 +00:00
|
|
|
avatar_already_exists.update!(record)
|
2014-12-01 07:32:35 +00:00
|
|
|
avatar = avatar_already_exists
|
|
|
|
|
|
|
|
# add new one and set it as default
|
|
|
|
else
|
|
|
|
avatar = Avatar.create(record)
|
|
|
|
set_default_items(object_id, data[:o_id], avatar.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
avatar
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
set avatars as default
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
Avatar.set_default('User', 123, avatar_id)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
=end
|
|
|
|
|
2016-03-08 06:32:58 +00:00
|
|
|
def self.set_default(object_name, o_id, avatar_id)
|
|
|
|
object_id = ObjectLookup.by_name(object_name)
|
2015-05-07 10:15:40 +00:00
|
|
|
avatar = Avatar.find_by(
|
2015-04-27 13:42:53 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
|
|
|
id: avatar_id,
|
2015-05-07 10:15:40 +00:00
|
|
|
)
|
2014-12-01 07:32:35 +00:00
|
|
|
avatar.default = true
|
|
|
|
avatar.save!
|
|
|
|
|
|
|
|
# set all other to default false
|
|
|
|
set_default_items(object_id, o_id, avatar_id)
|
|
|
|
|
|
|
|
avatar
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
remove all avatars of an object
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
Avatar.remove('User', 123)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
=end
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
def self.remove(object_name, o_id)
|
|
|
|
object_id = ObjectLookup.by_name(object_name)
|
2014-12-01 07:32:35 +00:00
|
|
|
Avatar.where(
|
2015-04-27 13:42:53 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
2014-12-01 07:32:35 +00:00
|
|
|
).destroy_all
|
|
|
|
|
|
|
|
object_name_store = "Avatar::#{object_name}"
|
|
|
|
Store.remove(
|
2015-04-27 13:42:53 +00:00
|
|
|
object: "#{object_name_store}::Full",
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
2014-12-01 07:32:35 +00:00
|
|
|
)
|
|
|
|
Store.remove(
|
2015-04-27 13:42:53 +00:00
|
|
|
object: "#{object_name_store}::Resize",
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
2014-12-01 07:32:35 +00:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
remove one avatars of an object
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
Avatar.remove_one('User', 123, avatar_id)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
=end
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
def self.remove_one(object_name, o_id, avatar_id)
|
|
|
|
object_id = ObjectLookup.by_name(object_name)
|
2014-12-01 07:32:35 +00:00
|
|
|
Avatar.where(
|
2015-04-27 13:42:53 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
|
|
|
id: avatar_id,
|
2014-12-01 07:32:35 +00:00
|
|
|
).destroy_all
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
return all avatars of an user
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
avatars = Avatar.list('User', 123)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
2018-05-08 10:10:19 +00:00
|
|
|
avatars = Avatar.list('User', 123, no_init_add_as_boolean) # per default true
|
|
|
|
|
2014-12-01 07:32:35 +00:00
|
|
|
=end
|
|
|
|
|
2018-05-08 10:10:19 +00:00
|
|
|
def self.list(object_name, o_id, no_init_add_as_boolean = true)
|
2015-12-14 16:09:09 +00:00
|
|
|
object_id = ObjectLookup.by_name(object_name)
|
2014-12-01 07:32:35 +00:00
|
|
|
avatars = Avatar.where(
|
2015-04-27 13:42:53 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
2019-04-07 15:23:03 +00:00
|
|
|
).order(initial: :desc, deletable: :asc, created_at: :asc)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
2015-02-15 08:36:18 +00:00
|
|
|
# add initial avatar
|
2018-05-08 10:10:19 +00:00
|
|
|
if no_init_add_as_boolean
|
|
|
|
_add_init_avatar(object_id, o_id)
|
|
|
|
end
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
avatar_list = []
|
|
|
|
avatars.each do |avatar|
|
|
|
|
data = avatar.attributes
|
|
|
|
if avatar.store_resize_id
|
|
|
|
file = Store.find(avatar.store_resize_id)
|
2015-12-14 16:09:09 +00:00
|
|
|
data['content'] = "data:#{file.preferences['Mime-Type']};base64,#{Base64.strict_encode64(file.content)}"
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
avatar_list.push data
|
|
|
|
end
|
|
|
|
avatar_list
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
get default avatar image of user by hash
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
store = Avatar.get_by_hash(hash)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
returns:
|
|
|
|
|
|
|
|
store object
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def self.get_by_hash(hash)
|
2015-05-07 10:15:40 +00:00
|
|
|
avatar = Avatar.find_by(
|
2015-04-27 13:42:53 +00:00
|
|
|
store_hash: hash,
|
2015-05-07 10:15:40 +00:00
|
|
|
)
|
2014-12-01 07:32:35 +00:00
|
|
|
return if !avatar
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-12-13 17:38:21 +00:00
|
|
|
Store.find(avatar.store_resize_id)
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
get default avatar of user by user id
|
|
|
|
|
2015-12-14 16:09:09 +00:00
|
|
|
avatar = Avatar.get_default('User', user_id)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
|
|
|
returns:
|
|
|
|
|
|
|
|
avatar object
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def self.get_default(object_name, o_id)
|
2015-12-14 16:09:09 +00:00
|
|
|
object_id = ObjectLookup.by_name(object_name)
|
2015-05-07 10:15:40 +00:00
|
|
|
Avatar.find_by(
|
2015-04-27 13:42:53 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
|
|
|
default: true,
|
2015-05-07 10:15:40 +00:00
|
|
|
)
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
|
|
|
|
2015-04-27 14:56:32 +00:00
|
|
|
def self.set_default_items(object_id, o_id, avatar_id)
|
|
|
|
avatars = Avatar.where(
|
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
2019-04-07 15:23:03 +00:00
|
|
|
).order(created_at: :asc)
|
2015-04-27 14:56:32 +00:00
|
|
|
avatars.each do |avatar|
|
|
|
|
next if avatar.id == avatar_id
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2015-04-27 14:56:32 +00:00
|
|
|
avatar.default = false
|
|
|
|
avatar.save!
|
2014-12-01 07:32:35 +00:00
|
|
|
end
|
2015-04-27 14:56:32 +00:00
|
|
|
end
|
2014-12-01 07:32:35 +00:00
|
|
|
|
2018-05-08 10:10:19 +00:00
|
|
|
def self._add_init_avatar(object_id, o_id)
|
2014-12-01 07:32:35 +00:00
|
|
|
|
2015-04-27 14:56:32 +00:00
|
|
|
count = Avatar.where(
|
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
2015-04-27 14:56:32 +00:00
|
|
|
).count
|
2016-10-24 21:59:18 +00:00
|
|
|
return if count.positive?
|
2014-12-01 07:32:35 +00:00
|
|
|
|
2018-05-08 10:10:19 +00:00
|
|
|
object_name = ObjectLookup.by_id(object_id)
|
|
|
|
return if !object_name.constantize.exists?(id: o_id)
|
|
|
|
|
|
|
|
Avatar.create!(
|
2018-12-19 17:31:51 +00:00
|
|
|
o_id: o_id,
|
2015-04-27 14:56:32 +00:00
|
|
|
object_lookup_id: object_id,
|
2018-12-19 17:31:51 +00:00
|
|
|
default: true,
|
|
|
|
source: 'init',
|
|
|
|
initial: true,
|
|
|
|
deletable: false,
|
|
|
|
updated_by_id: 1,
|
|
|
|
created_by_id: 1,
|
2015-04-27 14:56:32 +00:00
|
|
|
)
|
|
|
|
end
|
2015-04-27 14:15:29 +00:00
|
|
|
end
|