goncalossilva-kaltura_fu 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +50 -0
- data/Rakefile +68 -0
- data/VERSION.yml +5 -0
- data/generators/kaltura_fu_install/kaltura_fu_install_generator.rb +11 -0
- data/generators/kaltura_fu_install/templates/kaltura.yml +22 -0
- data/generators/kaltura_fu_install/templates/kaltura_upload.js +67 -0
- data/install.rb +1 -0
- data/kaltura_fu.gemspec +79 -0
- data/lib/kaltura_fu.rb +57 -0
- data/lib/kaltura_fu/configuration.rb +92 -0
- data/lib/kaltura_fu/entry.rb +107 -0
- data/lib/kaltura_fu/entry/class_methods.rb +61 -0
- data/lib/kaltura_fu/entry/flavor.rb +117 -0
- data/lib/kaltura_fu/entry/instance_methods.rb +28 -0
- data/lib/kaltura_fu/entry/metadata.rb +156 -0
- data/lib/kaltura_fu/entry/metadata/class_and_instance_methods.rb +54 -0
- data/lib/kaltura_fu/entry/metadata/class_methods.rb +71 -0
- data/lib/kaltura_fu/railtie.rb +28 -0
- data/lib/kaltura_fu/view_helpers.rb +182 -0
- data/rails/init.rb +22 -0
- data/spec/debug.log +1 -0
- data/spec/entry_spec.rb +74 -0
- data/spec/flavor_spec.rb +91 -0
- data/spec/kaltura_fu_spec.rb +63 -0
- data/spec/metadata_spec.rb +271 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +28 -0
- data/uninstall.rb +1 -0
- metadata +105 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
module KalturaFu
|
2
|
+
|
3
|
+
##
|
4
|
+
# The entry module provides a slightly more intuitive interface to the
|
5
|
+
# Kaltura media service. It determines what retrieval and setting actions
|
6
|
+
# you can perform based upon the version of the Kaltura-Ruby library using
|
7
|
+
# reflection. This allows Kaltura_Fu to be a bit more future proof than the
|
8
|
+
# Kaltura API client itself! The tradeoff is that getting/adding/setting
|
9
|
+
# attributes are defined dynamically. The current behavior is that the first
|
10
|
+
# call to a dynamic method will then define all of Kaltura's media entries
|
11
|
+
# methods. This allows the module to be slightly lighter weight in the event
|
12
|
+
# that it is included in a class but never used, and faster than using method_missing
|
13
|
+
# lookups for 100% of the dynamic methods. The first call will be slower though,
|
14
|
+
# as it generates numerous other methods.
|
15
|
+
#
|
16
|
+
# == Usage
|
17
|
+
# The entry module is intended to be used to link Rails Models to Kaltura MediaEntry's.
|
18
|
+
# However, you should not perform these actions during a web application request. Doing
|
19
|
+
# so will slow down your request unecessarily. There is nearly nothing gained from adding a
|
20
|
+
# round trip to your Kaltura server to make an update synchronus. Instead, this module
|
21
|
+
# should mostly be used in processing a background request from an observer.
|
22
|
+
#
|
23
|
+
# == Uploading to Kaltura
|
24
|
+
# The entry module provides convienance to uploading directly to your installation of
|
25
|
+
# Kaltura. For your web application, there are two Kaltura flash widgets that perform
|
26
|
+
# a much better job of uploading files though. This functionality has been used in
|
27
|
+
# production environments that use lecture capture. A video file is placed in a folder,
|
28
|
+
# a script picks the file up, and then uploads it into Kaltura.
|
29
|
+
#
|
30
|
+
# The Kaltura API supports uploading media from files, URL's, and also has a batch action.
|
31
|
+
# The implementation of Kaltura Fu currently ignores the URL and batch methods, instead
|
32
|
+
# focusing on file uploading.
|
33
|
+
#
|
34
|
+
# @example A basic file upload example:
|
35
|
+
# media_file = File.new("/path/to/video.mp4")
|
36
|
+
# upload(media_file,:source => :file)
|
37
|
+
#
|
38
|
+
# @example A file upload with metadata fields populated:
|
39
|
+
# media_file = File.new("/path/to/video.mp4")
|
40
|
+
# upload(media_file, :source => :file,
|
41
|
+
# :name => "My Rad video",
|
42
|
+
# :description => "I'm capable of such rad things.",
|
43
|
+
# :tags => "rad,rowdy,video,h.264",
|
44
|
+
# :categories => "raditude"
|
45
|
+
# )
|
46
|
+
# == Getting, Setting, and Adding Metadata
|
47
|
+
# The entry module provides an easy mean to retrieve the current state and modify a Kaltura entry.
|
48
|
+
# For metadata fields that act as a list of objects, it also provides an easy way to append values
|
49
|
+
# onto the list. It uses get_ and set_ instead of the more common Ruby practice of using just the
|
50
|
+
# attribute and attribute= so that you can include this module in your model without conflict. Also,
|
51
|
+
# when you are performing actions on the category fields, the module is making sure these are available
|
52
|
+
# in the KMC by calling Kaltura's Category service.
|
53
|
+
#
|
54
|
+
# @example Retrieving metadata:
|
55
|
+
# get_name("1_q34aa52a")
|
56
|
+
# get_categories("1_q34aa52a")
|
57
|
+
#
|
58
|
+
# @example Setting metadata:
|
59
|
+
# set_name("1_q34aa52a", "waffles")
|
60
|
+
# set_categories("1_q34aa52a", "HD,h.264,live recording")
|
61
|
+
#
|
62
|
+
# @example Appending tags to an existing set:
|
63
|
+
# add_tags("1_q34aa52a","eductation, lecture capture")
|
64
|
+
#
|
65
|
+
# == Checking the Status of an Entry
|
66
|
+
# One unfortunate aspect of the Kaltura API is that an entry will report it's status as "ready"
|
67
|
+
# while flavors are still encoding. When you embed the entry on a webpage, it will render an
|
68
|
+
# error "Media is currently converting". The only solution is to instead check the status of
|
69
|
+
# each flavor instead to ensure total readiness.
|
70
|
+
#
|
71
|
+
# @example Checking an entries status:
|
72
|
+
# check_status("1_q34aa52a")
|
73
|
+
#
|
74
|
+
# == Retrieving Status About the Source Video
|
75
|
+
# Occasionally, you need to interact with the original video in some form or another with Kaltura.
|
76
|
+
# One production situation I have encountered in the past is maintaining a copy of the source video
|
77
|
+
# on a large data store seperate from Kaltura. It is extremely difficult to work with the download
|
78
|
+
# URL that Kaltura provides for that.
|
79
|
+
#
|
80
|
+
# @example Getting the Flavor ID of the original video associated with an entry:
|
81
|
+
# original_flavor("1_q34aa52a")
|
82
|
+
#
|
83
|
+
# @example Getting the file extension of the original video associated with an entry:
|
84
|
+
# original_file_extension("1_q34aa52a")
|
85
|
+
#
|
86
|
+
# @example Getting a usable download URL for the entries original file:
|
87
|
+
# original_download_url("1_q34aa52a")
|
88
|
+
#
|
89
|
+
# @author Patrick Robertson
|
90
|
+
##
|
91
|
+
module Entry
|
92
|
+
|
93
|
+
##
|
94
|
+
# @private
|
95
|
+
##
|
96
|
+
def self.included(base)
|
97
|
+
base.extend ClassMethods
|
98
|
+
base.class_eval do
|
99
|
+
include Metadata
|
100
|
+
include InstanceMethods
|
101
|
+
include Flavor
|
102
|
+
end
|
103
|
+
super
|
104
|
+
end
|
105
|
+
|
106
|
+
end #Entry
|
107
|
+
end #KalturFu
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module KalturaFu
|
2
|
+
module Entry
|
3
|
+
|
4
|
+
##
|
5
|
+
# Class level methods for the Entry module.
|
6
|
+
##
|
7
|
+
module ClassMethods
|
8
|
+
#extend KalturaFu::Entry::Metadata
|
9
|
+
##
|
10
|
+
# Allows you to upload some variety of media into Kaltura.
|
11
|
+
# This isn't going to be as great to use as one of their flash widgets, and
|
12
|
+
# should likely be used "off" the web process to not slow the application down.
|
13
|
+
#
|
14
|
+
# @param upload_object The object to upload. Currently, it only works with a File.
|
15
|
+
# @param [Hash] options An options hash of Kaltura Media Entry attributes to add & the
|
16
|
+
# upload_object source.
|
17
|
+
# @option options [Symbol] :source(nil) Currently only accepts :file
|
18
|
+
#
|
19
|
+
# @return [String] Returns the Kaltura Media Entry's ID that was created.
|
20
|
+
# @raise [Kaltura::APIError] It will force a Kaltura::APIError if something went terribly wrong.
|
21
|
+
#
|
22
|
+
# @todo Add the other upload types.
|
23
|
+
##
|
24
|
+
def upload(upload_object,options={})
|
25
|
+
KalturaFu.check_for_client_session
|
26
|
+
|
27
|
+
#options_for_media_entry = options.delete(:source)
|
28
|
+
media_entry = construct_entry_from_options(options)
|
29
|
+
upload_from_file(upload_object,media_entry) if options[:source] == :file
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# @todo Build find.
|
34
|
+
##
|
35
|
+
def find
|
36
|
+
#pending
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# @private
|
41
|
+
##
|
42
|
+
def upload_from_file(upload_object,media_entry)
|
43
|
+
upload_token = KalturaFu.client.media_service.upload(upload_object)
|
44
|
+
KalturaFu.client.media_service.add_from_uploaded_file(media_entry,upload_token).id
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# @private
|
49
|
+
##
|
50
|
+
def construct_entry_from_options(options={})
|
51
|
+
media_entry = Kaltura::MediaEntry.new
|
52
|
+
options.each do |key,value|
|
53
|
+
media_entry.send("#{key}=",value) if valid_entry_attribute?(key)
|
54
|
+
end
|
55
|
+
media_entry.media_type = Kaltura::Constants::Media::Type::VIDEO if options[:media_type].nil?
|
56
|
+
return media_entry
|
57
|
+
end
|
58
|
+
|
59
|
+
end #ClassMethods
|
60
|
+
end #Entry
|
61
|
+
end #KalturaFu
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module KalturaFu
|
2
|
+
module Entry
|
3
|
+
##
|
4
|
+
# The flavor module mixes in instance methods for a class that includes the Entry module. This
|
5
|
+
# module primarily provides means to operate on the original upload to Kaltura. It also contains
|
6
|
+
# a very important method for checking the overall status of a specific entry.
|
7
|
+
#
|
8
|
+
# @author Patrick Robertson
|
9
|
+
##
|
10
|
+
module Flavor
|
11
|
+
##
|
12
|
+
# Checks each flavor under a Kaltura entry for readiness. It is possible under v3 of the Kaltura API
|
13
|
+
# to receive a 'ready' status for the entry while flavors are still encoding. Attempting to view the entry
|
14
|
+
# with a player will result in a 'Media is converting' error screen. This prevents that occurance.
|
15
|
+
#
|
16
|
+
# @param [String] video_id Kaltura entry_id of the video.
|
17
|
+
#
|
18
|
+
# @return [Number] Kaltura::Constants::FlavorAssetStatus. 2 is ready.
|
19
|
+
##
|
20
|
+
def check_status(entry_id)
|
21
|
+
KalturaFu.check_for_client_session
|
22
|
+
|
23
|
+
entry_status = get_status(entry_id)
|
24
|
+
if entry_status == Kaltura::Constants::Entry::Status::READY
|
25
|
+
flavor_array = KalturaFu.client.flavor_asset_service.get_by_entry_id(entry_id)
|
26
|
+
error_count = 0
|
27
|
+
not_ready_count = 0
|
28
|
+
ready_count = 0
|
29
|
+
flavor_array.each do |flavor|
|
30
|
+
case flavor.status
|
31
|
+
when Kaltura::Constants::FlavorAssetStatus::READY || Kaltura::Constants::FlavorAssetStatus::DELETED || Kaltura::Constants::FlavorAssetStatus::NOT_APPLICABLE
|
32
|
+
ready_count +=1
|
33
|
+
when Kaltura::Constants::FlavorAssetStatus::ERROR
|
34
|
+
error_count +=1
|
35
|
+
when Kaltura::Constants::FlavorAssetStatus::QUEUED || Kaltura::Constants::FlavorAssetStatus::CONVERTING
|
36
|
+
not_ready_count +=1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
#puts "errors: #{error_count} ready:#{ready_count} not_ready:#{not_ready_count} total:#{video_array.size} \n"
|
40
|
+
if error_count > 0
|
41
|
+
Kaltura::Constants::FlavorAssetStatus::ERROR
|
42
|
+
elsif not_ready_count > 0
|
43
|
+
Kaltura::Constants::FlavorAssetStatus::CONVERTING
|
44
|
+
else
|
45
|
+
Kaltura::Constants::FlavorAssetStatus::READY
|
46
|
+
end
|
47
|
+
else
|
48
|
+
entry_status
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Returns the flavor ID of the original file uploaded to Kaltura.
|
54
|
+
#
|
55
|
+
# @param [String] video_id Kaltura entry_id of the video.
|
56
|
+
#
|
57
|
+
# @return [String] flavor_id
|
58
|
+
##
|
59
|
+
def original_flavor(entry_id)
|
60
|
+
KalturaFu.check_for_client_session
|
61
|
+
|
62
|
+
flavor_array = KalturaFu.client.flavor_asset_service.get_by_entry_id(entry_id)
|
63
|
+
ret_flavor = nil
|
64
|
+
|
65
|
+
flavor_array.each do |flavor|
|
66
|
+
if flavor.is_original
|
67
|
+
ret_flavor = flavor.id.to_s
|
68
|
+
break
|
69
|
+
end
|
70
|
+
end
|
71
|
+
ret_flavor
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Returns the file extension of the original file uploaded to Kaltura for a given entry
|
76
|
+
#
|
77
|
+
# @param [String] video_id Kaltura entry_id of the video.
|
78
|
+
#
|
79
|
+
# @return [String] file extension
|
80
|
+
##
|
81
|
+
def original_file_extension(entry_id)
|
82
|
+
KalturaFu.check_for_client_session
|
83
|
+
|
84
|
+
flavor_array = KalturaFu.client.flavor_asset_service.get_by_entry_id(entry_id)
|
85
|
+
source_extension = nil
|
86
|
+
flavor_array.each do |flavor|
|
87
|
+
if flavor.is_original
|
88
|
+
source_extension = flavor.file_ext
|
89
|
+
break
|
90
|
+
end
|
91
|
+
end
|
92
|
+
source_extension
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Returns a download URL suitable to be used for iTunes one-click syndication. serveFlavor is not documented in KalturaAPI v3
|
97
|
+
# nor is the ?novar=0 paramter.
|
98
|
+
#
|
99
|
+
# @param [String] video_id Kaltura entry_id of the video
|
100
|
+
#
|
101
|
+
# @return [String] URL that works with RSS/iTunes syndication. Normal flavor serving is flakey with syndication.
|
102
|
+
##
|
103
|
+
def original_download_url(video_id)
|
104
|
+
KalturaFu.check_for_client_session
|
105
|
+
|
106
|
+
service_url = KalturaFu.config[:service_url] || "http://www.kaltura.com"
|
107
|
+
partner_id = KalturaFu.config[:partner_id]
|
108
|
+
subpartner_id = (partner_id.to_i * 100).to_s
|
109
|
+
flavor = original_flavor(video_id)
|
110
|
+
extension = original_file_extension(video_id)
|
111
|
+
|
112
|
+
"#{service_url}/p/#{partner_id}/sp/#{subpartner_id}/serveFlavor/flavorId/#{flavor}/name/#{flavor}.#{extension}?novar=0"
|
113
|
+
end
|
114
|
+
|
115
|
+
end #Flavor
|
116
|
+
end #Entry
|
117
|
+
end #KalturaFu
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module KalturaFu
|
2
|
+
module Entry
|
3
|
+
##
|
4
|
+
# Instance level methods for the Entry module.
|
5
|
+
##
|
6
|
+
module InstanceMethods
|
7
|
+
##
|
8
|
+
# Deletes a Kaltura entry. Unlike the base API delete method, this returns true/false based on success.
|
9
|
+
#
|
10
|
+
# @param [String] entry_id Kaltura entry ID to delete.
|
11
|
+
#
|
12
|
+
# @return [Boolean] returns true if the delete was successful or false otherwise.
|
13
|
+
#
|
14
|
+
##
|
15
|
+
def delete_entry(entry_id)
|
16
|
+
KalturaFu.check_for_client_session
|
17
|
+
|
18
|
+
begin
|
19
|
+
KalturaFu.client.media_service.delete(entry_id)
|
20
|
+
true
|
21
|
+
rescue Kaltura::APIError => e
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module KalturaFu
|
4
|
+
module Entry
|
5
|
+
|
6
|
+
##
|
7
|
+
# The Metadata module provides methods that get/set and add metadata to the
|
8
|
+
# Kaltura installation.
|
9
|
+
#
|
10
|
+
# @author Patrick Robertson
|
11
|
+
##
|
12
|
+
module Metadata
|
13
|
+
|
14
|
+
##
|
15
|
+
# @private
|
16
|
+
##
|
17
|
+
def self.included(base)
|
18
|
+
base.extend ClassAndInstanceMethods
|
19
|
+
base.extend ClassMethods
|
20
|
+
base.class_eval do
|
21
|
+
include ClassAndInstanceMethods
|
22
|
+
end
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# @private
|
28
|
+
##
|
29
|
+
def method_missing(name, *args)
|
30
|
+
method_name = name.to_s
|
31
|
+
unless self.class.generated_methods?
|
32
|
+
self.class.define_attribute_methods
|
33
|
+
if self.class.generated_methods.include?(method_name)
|
34
|
+
return self.send(name,*args)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# @private
|
45
|
+
##
|
46
|
+
def respond_to?(method)
|
47
|
+
case method.to_s
|
48
|
+
when /^(get|set)_(.*)/
|
49
|
+
valid_entry_attribute?($2.to_sym) || super
|
50
|
+
when /^(add)_(.*)/
|
51
|
+
(valid_entry_attribute?($2.pluralize.to_sym) && valid_add_attribute?($2) ) || super
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Gets a Kaltura::MediaEntry given a Kaltura entry.
|
59
|
+
#
|
60
|
+
# @param [String] video_id Kaltura entry_id of the video.
|
61
|
+
#
|
62
|
+
# @return [Kaltura::MediaEntry] The MediaEntry object for the Kaltura entry.
|
63
|
+
# @raose [Kaltura::APIError] Raises a kaltura error if it can't find the entry.
|
64
|
+
##
|
65
|
+
def get_entry(entry_id)
|
66
|
+
KalturaFu.check_for_client_session
|
67
|
+
|
68
|
+
KalturaFu.client.media_service.get(entry_id)
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Sets a specific Kaltura::MediaEntry attribute given a Kaltura entry.
|
73
|
+
# This method is called by method_missing, allowing this module set attributes based
|
74
|
+
# off of the current API wrapper, rather than having to update along side the API wrapper.
|
75
|
+
#
|
76
|
+
# @param [String] attr_name The attribute to set.
|
77
|
+
# @param [String] entry_id The Kaltura entry ID.
|
78
|
+
# @param [String] value The value you wish to set the attribute to.
|
79
|
+
#
|
80
|
+
# @return [String] Returns the value as stored in the Kaltura database. Tag strings come back
|
81
|
+
# slightly funny.
|
82
|
+
#
|
83
|
+
# @raise [Kaltura::APIError] Passes Kaltura API errors directly through.
|
84
|
+
##
|
85
|
+
def set_attribute(attr_name,entry_id,value)
|
86
|
+
KalturaFu.check_for_client_session
|
87
|
+
|
88
|
+
add_categories_to_kaltura(value) if (attr_name =~ /^(.*)_categories/ || attr_name =~ /^categories/)
|
89
|
+
|
90
|
+
media_entry = Kaltura::MediaEntry.new
|
91
|
+
media_entry.send("#{attr_name}=",value)
|
92
|
+
KalturaFu.client.media_service.update(entry_id,media_entry).send(attr_name.to_sym)
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# @private
|
98
|
+
##
|
99
|
+
def add_categories_to_kaltura(categories)
|
100
|
+
KalturaFu.check_for_client_session
|
101
|
+
|
102
|
+
categories.split(",").each do |category|
|
103
|
+
unless category_exists?(category)
|
104
|
+
cat = Kaltura::Category.new
|
105
|
+
cat.name = category
|
106
|
+
KalturaFu.client.category_service.add(cat)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# @private
|
113
|
+
##
|
114
|
+
def category_exists?(category_name)
|
115
|
+
KalturaFu.check_for_client_session
|
116
|
+
|
117
|
+
category_filter = Kaltura::Filter::CategoryFilter.new
|
118
|
+
category_filter.full_name_equal = category_name
|
119
|
+
category_check = KalturaFu.client.category_service.list(category_filter).objects
|
120
|
+
if category_check.nil?
|
121
|
+
false
|
122
|
+
else
|
123
|
+
category_check
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
##
|
129
|
+
# Appends a specific Kaltura::MediaEntry attribute to the end of the original attribute given a Kaltura entry.
|
130
|
+
# This method is called by method_missing, allowing this module add attributes based
|
131
|
+
# off of the current API wrapper, rather than having to update along side the API wrapper.
|
132
|
+
#
|
133
|
+
# @param [String] attr_name The attribute to set.
|
134
|
+
# @param [String] entry_id The Kaltura entry ID.
|
135
|
+
# @param [String] value The value you wish to append the attribute with.
|
136
|
+
#
|
137
|
+
# @return [String] Returns the value as stored in the Kaltura database. Tag strings come back
|
138
|
+
# slightly funny.
|
139
|
+
#
|
140
|
+
# @raise [Kaltura::APIError] Passes Kaltura API errors directly through.
|
141
|
+
##
|
142
|
+
def add_attribute(attr_name,entry_id,value)
|
143
|
+
KalturaFu.check_for_client_session
|
144
|
+
|
145
|
+
|
146
|
+
add_categories_to_kaltura(value) if (attr_name =~ /^(.*)_categor(ies|y)/ || attr_name =~ /^categor(ies|y)/)
|
147
|
+
|
148
|
+
old_attributes = KalturaFu.client.media_service.get(entry_id).send(attr_name.to_sym)
|
149
|
+
media_entry = Kaltura::MediaEntry.new
|
150
|
+
media_entry.send("#{attr_name}=","#{old_attributes},#{value}")
|
151
|
+
KalturaFu.client.media_service.update(entry_id,media_entry).send(attr_name.to_sym)
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|