acts_as_eav_model 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. data/.rvmrc +1 -0
  2. data/CHANGELOG +3 -0
  3. data/Gemfile +13 -0
  4. data/Gemfile.lock +92 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +117 -0
  7. data/Rakefile +46 -0
  8. data/SPECDOC +23 -0
  9. data/TODO +0 -0
  10. data/VERSION +1 -0
  11. data/doc/classes/ActiveRecord/Acts/EavModel.html +213 -0
  12. data/doc/classes/ActiveRecord/Acts/EavModel/ClassMethods.html +343 -0
  13. data/doc/classes/ActiveRecord/Acts/EavModel/InstanceMethods.html +474 -0
  14. data/doc/classes/ActiveRecord/Acts/EavModel/InstanceMethods/ClassMethods.html +192 -0
  15. data/doc/classes/ActiveRecord/Base.html +170 -0
  16. data/doc/created.rid +1 -0
  17. data/doc/files/CHANGELOG.html +113 -0
  18. data/doc/files/MIT-LICENSE.html +129 -0
  19. data/doc/files/README_rdoc.html +248 -0
  20. data/doc/files/SPECDOC.html +170 -0
  21. data/doc/files/lib/acts_as_eav_model_rb.html +101 -0
  22. data/doc/fr_class_index.html +31 -0
  23. data/doc/fr_file_index.html +31 -0
  24. data/doc/fr_method_index.html +40 -0
  25. data/doc/index.html +24 -0
  26. data/doc/rdoc-style.css +208 -0
  27. data/init.rb +1 -0
  28. data/install.rb +1 -0
  29. data/lib/acts_as_eav_model.rb +542 -0
  30. data/spec/dummy/Rakefile +7 -0
  31. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  32. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  33. data/spec/dummy/app/models/document.rb +7 -0
  34. data/spec/dummy/app/models/person.rb +13 -0
  35. data/spec/dummy/app/models/person_contact_info.rb +2 -0
  36. data/spec/dummy/app/models/post.rb +4 -0
  37. data/spec/dummy/app/models/post_attribute.rb +2 -0
  38. data/spec/dummy/app/models/preference.rb +5 -0
  39. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/spec/dummy/config.ru +4 -0
  41. data/spec/dummy/config/application.rb +45 -0
  42. data/spec/dummy/config/boot.rb +10 -0
  43. data/spec/dummy/config/database.yml +22 -0
  44. data/spec/dummy/config/environment.rb +5 -0
  45. data/spec/dummy/config/environments/development.rb +26 -0
  46. data/spec/dummy/config/environments/production.rb +49 -0
  47. data/spec/dummy/config/environments/test.rb +35 -0
  48. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/spec/dummy/config/initializers/inflections.rb +10 -0
  50. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  51. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  52. data/spec/dummy/config/initializers/session_store.rb +8 -0
  53. data/spec/dummy/config/locales/en.yml +5 -0
  54. data/spec/dummy/config/routes.rb +58 -0
  55. data/spec/dummy/db/development.sqlite3 +0 -0
  56. data/spec/dummy/db/schema.rb +51 -0
  57. data/spec/dummy/public/404.html +26 -0
  58. data/spec/dummy/public/422.html +26 -0
  59. data/spec/dummy/public/500.html +26 -0
  60. data/spec/dummy/public/favicon.ico +0 -0
  61. data/spec/dummy/public/javascripts/application.js +2 -0
  62. data/spec/dummy/public/javascripts/controls.js +965 -0
  63. data/spec/dummy/public/javascripts/dragdrop.js +974 -0
  64. data/spec/dummy/public/javascripts/effects.js +1123 -0
  65. data/spec/dummy/public/javascripts/prototype.js +6001 -0
  66. data/spec/dummy/public/javascripts/rails.js +175 -0
  67. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  68. data/spec/dummy/script/rails +6 -0
  69. data/spec/fixtures/people.yml +4 -0
  70. data/spec/fixtures/person_contact_infos.yml +10 -0
  71. data/spec/fixtures/post_attributes.yml +15 -0
  72. data/spec/fixtures/posts.yml +9 -0
  73. data/spec/fixtures/preferences.yml +10 -0
  74. data/spec/models/eav_model_with_no_arguments_spec.rb +82 -0
  75. data/spec/models/eav_model_with_options_spec.rb +37 -0
  76. data/spec/models/eav_validation_spec.rb +11 -0
  77. data/spec/schema.rb +50 -0
  78. data/spec/spec_helper.rb +38 -0
  79. data/uninstall.rb +1 -0
  80. metadata +213 -0
@@ -0,0 +1,101 @@
1
+ <?xml version="1.0" encoding="iso-8859-1"?>
2
+ <!DOCTYPE html
3
+ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
4
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
5
+
6
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
7
+ <head>
8
+ <title>File: acts_as_eav_model.rb</title>
9
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
10
+ <meta http-equiv="Content-Script-Type" content="text/javascript" />
11
+ <link rel="stylesheet" href="../.././rdoc-style.css" type="text/css" media="screen" />
12
+ <script type="text/javascript">
13
+ // <![CDATA[
14
+
15
+ function popupCode( url ) {
16
+ window.open(url, "Code", "resizable=yes,scrollbars=yes,toolbar=no,status=no,height=150,width=400")
17
+ }
18
+
19
+ function toggleCode( id ) {
20
+ if ( document.getElementById )
21
+ elem = document.getElementById( id );
22
+ else if ( document.all )
23
+ elem = eval( "document.all." + id );
24
+ else
25
+ return false;
26
+
27
+ elemStyle = elem.style;
28
+
29
+ if ( elemStyle.display != "block" ) {
30
+ elemStyle.display = "block"
31
+ } else {
32
+ elemStyle.display = "none"
33
+ }
34
+
35
+ return true;
36
+ }
37
+
38
+ // Make codeblocks hidden by default
39
+ document.writeln( "<style type=\"text/css\">div.method-source-code { display: none }</style>" )
40
+
41
+ // ]]>
42
+ </script>
43
+
44
+ </head>
45
+ <body>
46
+
47
+
48
+
49
+ <div id="fileHeader">
50
+ <h1>acts_as_eav_model.rb</h1>
51
+ <table class="header-table">
52
+ <tr class="top-aligned-row">
53
+ <td><strong>Path:</strong></td>
54
+ <td>lib/acts_as_eav_model.rb
55
+ </td>
56
+ </tr>
57
+ <tr class="top-aligned-row">
58
+ <td><strong>Last Update:</strong></td>
59
+ <td>Thu Dec 18 13:30:11 +1300 2008</td>
60
+ </tr>
61
+ </table>
62
+ </div>
63
+ <!-- banner header -->
64
+
65
+ <div id="bodyContent">
66
+
67
+
68
+
69
+ <div id="contextContent">
70
+
71
+
72
+
73
+ </div>
74
+
75
+
76
+ </div>
77
+
78
+
79
+ <!-- if includes -->
80
+
81
+ <div id="section">
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+ <!-- if method_list -->
91
+
92
+
93
+ </div>
94
+
95
+
96
+ <div id="validator-badges">
97
+ <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
98
+ </div>
99
+
100
+ </body>
101
+ </html>
@@ -0,0 +1,31 @@
1
+
2
+ <?xml version="1.0" encoding="iso-8859-1"?>
3
+ <!DOCTYPE html
4
+ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
5
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
6
+
7
+ <!--
8
+
9
+ Classes
10
+
11
+ -->
12
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
13
+ <head>
14
+ <title>Classes</title>
15
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
16
+ <link rel="stylesheet" href="rdoc-style.css" type="text/css" />
17
+ <base target="docwin" />
18
+ </head>
19
+ <body>
20
+ <div id="index">
21
+ <h1 class="section-bar">Classes</h1>
22
+ <div id="index-entries">
23
+ <a href="classes/ActiveRecord/Acts/EavModel.html">ActiveRecord::Acts::EavModel</a><br />
24
+ <a href="classes/ActiveRecord/Acts/EavModel/ClassMethods.html">ActiveRecord::Acts::EavModel::ClassMethods</a><br />
25
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html">ActiveRecord::Acts::EavModel::InstanceMethods</a><br />
26
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods/ClassMethods.html">ActiveRecord::Acts::EavModel::InstanceMethods::ClassMethods</a><br />
27
+ <a href="classes/ActiveRecord/Base.html">ActiveRecord::Base</a><br />
28
+ </div>
29
+ </div>
30
+ </body>
31
+ </html>
@@ -0,0 +1,31 @@
1
+
2
+ <?xml version="1.0" encoding="iso-8859-1"?>
3
+ <!DOCTYPE html
4
+ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
5
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
6
+
7
+ <!--
8
+
9
+ Files
10
+
11
+ -->
12
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
13
+ <head>
14
+ <title>Files</title>
15
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
16
+ <link rel="stylesheet" href="rdoc-style.css" type="text/css" />
17
+ <base target="docwin" />
18
+ </head>
19
+ <body>
20
+ <div id="index">
21
+ <h1 class="section-bar">Files</h1>
22
+ <div id="index-entries">
23
+ <a href="files/CHANGELOG.html">CHANGELOG</a><br />
24
+ <a href="files/MIT-LICENSE.html">MIT-LICENSE</a><br />
25
+ <a href="files/README_rdoc.html">README.rdoc</a><br />
26
+ <a href="files/SPECDOC.html">SPECDOC</a><br />
27
+ <a href="files/lib/acts_as_eav_model_rb.html">lib/acts_as_eav_model.rb</a><br />
28
+ </div>
29
+ </div>
30
+ </body>
31
+ </html>
@@ -0,0 +1,40 @@
1
+
2
+ <?xml version="1.0" encoding="iso-8859-1"?>
3
+ <!DOCTYPE html
4
+ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
5
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
6
+
7
+ <!--
8
+
9
+ Methods
10
+
11
+ -->
12
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
13
+ <head>
14
+ <title>Methods</title>
15
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
16
+ <link rel="stylesheet" href="rdoc-style.css" type="text/css" />
17
+ <base target="docwin" />
18
+ </head>
19
+ <body>
20
+ <div id="index">
21
+ <h1 class="section-bar">Methods</h1>
22
+ <div id="index-entries">
23
+ <a href="classes/ActiveRecord/Base.html#M000014">attributes= (ActiveRecord::Base)</a><br />
24
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods/ClassMethods.html#M000012">create_attribute_table (ActiveRecord::Acts::EavModel::InstanceMethods::ClassMethods)</a><br />
25
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods/ClassMethods.html#M000013">drop_attribute_table (ActiveRecord::Acts::EavModel::InstanceMethods::ClassMethods)</a><br />
26
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000011">each_eav_relation (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
27
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000003">eav_attributes (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
28
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000009">eav_related (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
29
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000010">exec_if_related (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
30
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000008">find_related_eav_attribute (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
31
+ <a href="classes/ActiveRecord/Acts/EavModel/ClassMethods.html#M000001">has_eav_behavior (ActiveRecord::Acts::EavModel::ClassMethods)</a><br />
32
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000002">is_eav_attribute? (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
33
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000007">method_missing_with_eav_behavior (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
34
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000005">read_attribute_with_eav_behavior (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
35
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000004">save_modified_eav_attributes (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
36
+ <a href="classes/ActiveRecord/Acts/EavModel/InstanceMethods.html#M000006">write_attribute_with_eav_behavior (ActiveRecord::Acts::EavModel::InstanceMethods)</a><br />
37
+ </div>
38
+ </div>
39
+ </body>
40
+ </html>
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="iso-8859-1"?>
2
+ <!DOCTYPE html
3
+ PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
4
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
5
+
6
+ <!--
7
+
8
+ acts_as_eav_model
9
+
10
+ -->
11
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
12
+ <head>
13
+ <title>acts_as_eav_model</title>
14
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
15
+ </head>
16
+ <frameset rows="20%, 80%">
17
+ <frameset cols="25%,35%,45%">
18
+ <frame src="fr_file_index.html" title="Files" name="Files" />
19
+ <frame src="fr_class_index.html" name="Classes" />
20
+ <frame src="fr_method_index.html" name="Methods" />
21
+ </frameset>
22
+ <frame src="files/README_rdoc.html" name="docwin" />
23
+ </frameset>
24
+ </html>
@@ -0,0 +1,208 @@
1
+
2
+ body {
3
+ font-family: Verdana,Arial,Helvetica,sans-serif;
4
+ font-size: 90%;
5
+ margin: 0;
6
+ margin-left: 40px;
7
+ padding: 0;
8
+ background: white;
9
+ }
10
+
11
+ h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
12
+ h1 { font-size: 150%; }
13
+ h2,h3,h4 { margin-top: 1em; }
14
+
15
+ a { background: #eef; color: #039; text-decoration: none; }
16
+ a:hover { background: #039; color: #eef; }
17
+
18
+ /* Override the base stylesheet's Anchor inside a table cell */
19
+ td > a {
20
+ background: transparent;
21
+ color: #039;
22
+ text-decoration: none;
23
+ }
24
+
25
+ /* and inside a section title */
26
+ .section-title > a {
27
+ background: transparent;
28
+ color: #eee;
29
+ text-decoration: none;
30
+ }
31
+
32
+ /* === Structural elements =================================== */
33
+
34
+ div#index {
35
+ margin: 0;
36
+ margin-left: -40px;
37
+ padding: 0;
38
+ font-size: 90%;
39
+ }
40
+
41
+
42
+ div#index a {
43
+ margin-left: 0.7em;
44
+ }
45
+
46
+ div#index .section-bar {
47
+ margin-left: 0px;
48
+ padding-left: 0.7em;
49
+ background: #ccc;
50
+ font-size: small;
51
+ }
52
+
53
+
54
+ div#classHeader, div#fileHeader {
55
+ width: auto;
56
+ color: white;
57
+ padding: 0.5em 1.5em 0.5em 1.5em;
58
+ margin: 0;
59
+ margin-left: -40px;
60
+ border-bottom: 3px solid #006;
61
+ }
62
+
63
+ div#classHeader a, div#fileHeader a {
64
+ background: inherit;
65
+ color: white;
66
+ }
67
+
68
+ div#classHeader td, div#fileHeader td {
69
+ background: inherit;
70
+ color: white;
71
+ }
72
+
73
+
74
+ div#fileHeader {
75
+ background: #057;
76
+ }
77
+
78
+ div#classHeader {
79
+ background: #048;
80
+ }
81
+
82
+
83
+ .class-name-in-header {
84
+ font-size: 180%;
85
+ font-weight: bold;
86
+ }
87
+
88
+
89
+ div#bodyContent {
90
+ padding: 0 1.5em 0 1.5em;
91
+ }
92
+
93
+ div#description {
94
+ padding: 0.5em 1.5em;
95
+ background: #efefef;
96
+ border: 1px dotted #999;
97
+ }
98
+
99
+ div#description h1,h2,h3,h4,h5,h6 {
100
+ color: #125;;
101
+ background: transparent;
102
+ }
103
+
104
+ div#validator-badges {
105
+ text-align: center;
106
+ }
107
+ div#validator-badges img { border: 0; }
108
+
109
+ div#copyright {
110
+ color: #333;
111
+ background: #efefef;
112
+ font: 0.75em sans-serif;
113
+ margin-top: 5em;
114
+ margin-bottom: 0;
115
+ padding: 0.5em 2em;
116
+ }
117
+
118
+
119
+ /* === Classes =================================== */
120
+
121
+ table.header-table {
122
+ color: white;
123
+ font-size: small;
124
+ }
125
+
126
+ .type-note {
127
+ font-size: small;
128
+ color: #DEDEDE;
129
+ }
130
+
131
+ .xxsection-bar {
132
+ background: #eee;
133
+ color: #333;
134
+ padding: 3px;
135
+ }
136
+
137
+ .section-bar {
138
+ color: #333;
139
+ border-bottom: 1px solid #999;
140
+ margin-left: -20px;
141
+ }
142
+
143
+
144
+ .section-title {
145
+ background: #79a;
146
+ color: #eee;
147
+ padding: 3px;
148
+ margin-top: 2em;
149
+ margin-left: -30px;
150
+ border: 1px solid #999;
151
+ }
152
+
153
+ .top-aligned-row { vertical-align: top }
154
+ .bottom-aligned-row { vertical-align: bottom }
155
+
156
+ /* --- Context section classes ----------------------- */
157
+
158
+ .context-row { }
159
+ .context-item-name { font-family: monospace; font-weight: bold; color: black; }
160
+ .context-item-value { font-size: small; color: #448; }
161
+ .context-item-desc { color: #333; padding-left: 2em; }
162
+
163
+ /* --- Method classes -------------------------- */
164
+ .method-detail {
165
+ background: #efefef;
166
+ padding: 0;
167
+ margin-top: 0.5em;
168
+ margin-bottom: 1em;
169
+ border: 1px dotted #ccc;
170
+ }
171
+ .method-heading {
172
+ color: black;
173
+ background: #ccc;
174
+ border-bottom: 1px solid #666;
175
+ padding: 0.2em 0.5em 0 0.5em;
176
+ }
177
+ .method-signature { color: black; background: inherit; }
178
+ .method-name { font-weight: bold; }
179
+ .method-args { font-style: italic; }
180
+ .method-description { padding: 0 0.5em 0 0.5em; }
181
+
182
+ /* --- Source code sections -------------------- */
183
+
184
+ a.source-toggle { font-size: 90%; }
185
+ div.method-source-code {
186
+ background: #262626;
187
+ color: #ffdead;
188
+ margin: 1em;
189
+ padding: 0.5em;
190
+ border: 1px dashed #999;
191
+ overflow: hidden;
192
+ }
193
+
194
+ div.method-source-code pre { color: #ffdead; overflow: hidden; }
195
+
196
+ /* --- Ruby keyword styles --------------------- */
197
+
198
+ .standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
199
+
200
+ .ruby-constant { color: #7fffd4; background: transparent; }
201
+ .ruby-keyword { color: #00ffff; background: transparent; }
202
+ .ruby-ivar { color: #eedd82; background: transparent; }
203
+ .ruby-operator { color: #00ffee; background: transparent; }
204
+ .ruby-identifier { color: #ffdead; background: transparent; }
205
+ .ruby-node { color: #ffa07a; background: transparent; }
206
+ .ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
207
+ .ruby-regexp { color: #ffa07a; background: transparent; }
208
+ .ruby-value { color: #7fffd4; background: transparent; }
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acts_as_eav_model'
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,542 @@
1
+ module ActiveRecord # :nodoc:
2
+ module Acts # :nodoc:
3
+ ##
4
+ # ActsAsEavModel allow for the Entity-attribute-value model (EAV), also
5
+ # known as object-attribute-value model and open schema on any of your ActiveRecord
6
+ # models.
7
+ #
8
+ # = What is Entity-attribute-value model?
9
+ # Entity-attribute-value model (EAV) is a data model that is used in circumstances
10
+ # where the number of attributes (properties, parameters) that can be used to describe
11
+ # a thing (an "entity" or "object") is potentially very vast, but the number that will
12
+ # actually apply to a given entity is relatively modest.
13
+ #
14
+ # = Typical Problem
15
+ # A good example of this is where you need to store
16
+ # lots (possible hundreds) of optional attributes on an object. My typical
17
+ # reference example is when you have a User object. You want to store the
18
+ # user's preferences between sessions. Every search, sort, etc in your
19
+ # application you want to keep track of so when the user visits that section
20
+ # of the application again you can simply restore the display to how it was.
21
+ #
22
+ # So your controller might have:
23
+ #
24
+ # Project.find :all, :conditions => current_user.project_search,
25
+ # :order => current_user.project_order
26
+ #
27
+ # But there could be hundreds of these little attributes that you really don't
28
+ # want to store directly on the user object. It would make your table have too
29
+ # many columns so it would be too much of a pain to deal with. Also there might
30
+ # be performance problems. So instead you might do something like
31
+ # this:
32
+ #
33
+ # class User < ActiveRecord::Base
34
+ # has_many :preferences
35
+ # end
36
+ #
37
+ # class Preferences < ActiveRecord::Base
38
+ # belongs_to :user
39
+ # end
40
+ #
41
+ # Now simply give the Preference model a "name" and "value" column and you are
42
+ # set..... except this is now too complicated. To retrieve a attribute you will
43
+ # need to do something like:
44
+ #
45
+ # Project.find :all,
46
+ # :conditions => current_user.preferences.find_by_name('project_search').value,
47
+ # :order => current_user.preferences.find_by_name('project_order').value
48
+ #
49
+ # Sure you could fix this through a few methods on your model. But what about
50
+ # saving?
51
+ #
52
+ # current_user.preferences.create :name => 'project_search',
53
+ # :value => "lastname LIKE 'jones%'"
54
+ # current_user.preferences.create :name => 'project_order',
55
+ # :value => "name"
56
+ #
57
+ # Again this seems to much. Again we could add some methods to our model to
58
+ # make this simpler but do we want to do this on every model. NO! So instead
59
+ # we use this plugin which does everything for us.
60
+ #
61
+ # = Capabilities
62
+ #
63
+ # The ActsAsEavModel plugin is capable of modeling this problem in a intuitive
64
+ # way. Instead of having to deal with a related model you treat all attributes
65
+ # (both on the model and related) as if they are all on the model. The plugin
66
+ # will try to save all attributes to the model (normal ActiveRecord behaviour)
67
+ # but if there is no column for an attribute it will try to save it to a
68
+ # related model whose purpose is to store these many sparsly populated
69
+ # attributes.
70
+ #
71
+ # The main design goals are:
72
+ #
73
+ # * Have the eav attributes feel like normal attributes. Simple gets and sets
74
+ # will add and remove records from the related model.
75
+ # * Allow for more than one related model. So for example on my User model I might
76
+ # have some eav behavior going into a contact_info table while others are
77
+ # going in a user_preferences table.
78
+ # * Allow a model to determine what a valid eav attribute is for a given
79
+ # related model so our model still can generate a NoMethodError.
80
+ #
81
+ module EavModel
82
+
83
+ MAGIC_FIELD_NAMES = [:created_at, :created_on, :updated_at, :updated_on, :created_by, :updated_by,
84
+ :lock_version, :type, :id, :position, :parent_id, :lft, :rgt, :quote_value, :template, :to_ary,
85
+ :marshal_dump, :marshal_load, :_dump, :_load, :to_yaml_type, :to_yaml, :yaml_initialize, :to_xml, :to_json, :as_json,
86
+ :from_json, :from_xml,:validate,:validate_on_create,:validate_on_update].freeze
87
+
88
+ def self.included(base) # :nodoc:
89
+ base.extend ClassMethods
90
+ end
91
+
92
+ module ClassMethods
93
+
94
+ ##
95
+ # Will make the current class have eav behaviour.
96
+ #
97
+ # class Post < ActiveRecord::Base
98
+ # has_eav_behavior
99
+ # end
100
+ # post = Post.find_by_title 'hello world'
101
+ # puts "My post intro is: #{post.intro}"
102
+ # post.teaser = 'An awesome introduction to the blog'
103
+ # post.save
104
+ #
105
+ # The above example should work even though "intro" and "teaser" are not
106
+ # attributes on the Post model.
107
+ #
108
+ # The following options are available on for has_eav_behavior to modify
109
+ # the behavior. Reasonable defaults are provided:
110
+ #
111
+ # * <tt>class_name</tt>:
112
+ # The class for the related model. This defaults to the
113
+ # model name prepended to "Attribute". So for a "User" model the class
114
+ # name would be "UserAttribute". The class can actually exist (in that
115
+ # case the model file will be loaded through Rails dependency system) or
116
+ # if it does not exist a basic model will be dynamically defined for you.
117
+ # This allows you to implement custom methods on the related class by
118
+ # simply defining the class manually.
119
+ # * <tt>table_name</tt>:
120
+ # The table for the related model. This defaults to the
121
+ # attribute model's table name.
122
+ # * <tt>relationship_name</tt>:
123
+ # This is the name of the actual has_many
124
+ # relationship. Most of the type this relationship will only be used
125
+ # indirectly but it is there if the user wants more raw access. This
126
+ # defaults to the class name underscored then pluralized finally turned
127
+ # into a symbol.
128
+ # * <tt>foreign_key</tt>:
129
+ # The key in the attribute table to relate back to the
130
+ # model. This defaults to the model name underscored prepended to "_id"
131
+ # * <tt>name_field</tt>:
132
+ # The field which stores the name of the attribute in the related object
133
+ # * <tt>value_field</tt>:
134
+ # The field that stores the value in the related object
135
+ # * <tt>fields</tt>:
136
+ # A list of fields that are valid eav attributes. By default
137
+ # this is "nil" which means that all field are valid. Use this option if
138
+ # you want some fields to go to one flex attribute model while other
139
+ # fields will go to another. As an alternative you can override the
140
+ # #eav_attributes method which will return a list of all valid flex
141
+ # attributes. This is useful if you want to read the list of attributes
142
+ # from another source to keep your code DRY. This method is given a
143
+ # single argument which is the class for the related model. The following
144
+ # provide an example:
145
+ #
146
+ # class User < ActiveRecord::Base
147
+ # has_eav_behavior :class_name => 'UserContactInfo'
148
+ # has_eav_behavior :class_name => 'Preferences'
149
+ #
150
+ # def eav_attributes(model)
151
+ # case model
152
+ # when UserContactInfo
153
+ # %w(email phone aim yahoo msn)
154
+ # when Preference
155
+ # %w(project_search project_order user_search user_order)
156
+ # else Array.new
157
+ # end
158
+ # end
159
+ # end
160
+ #
161
+ # marcus = User.find_by_login 'marcus'
162
+ # marcus.email = 'marcus@example.com' # Will save to UserContactInfo model
163
+ # marcus.project_order = 'name' # Will save to Preference
164
+ # marcus.save # Carries out save so now values are in database
165
+ #
166
+ # Note the else clause in our case statement. Since an empty array is
167
+ # returned for all other models (perhaps added later) then we can be
168
+ # certain that only the above eav attributes are allowed.
169
+ #
170
+ # If both a :fields option and #eav_attributes method is defined the
171
+ # <tt>fields</tt> option take precidence. This allows you to easily define the
172
+ # field list inline for one model while implementing #eav_attributes
173
+ # for another model and not having #eav_attributes need to determine
174
+ # what model it is answering for. In both cases the list of flex
175
+ # attributes can be a list of string or symbols
176
+ #
177
+ # A final alternative to :fields and #eav_attributes is the
178
+ # #is_eav_attribute? method. This method is given two arguments. The
179
+ # first is the attribute being retrieved/saved the second is the Model we
180
+ # are testing for. If you override this method then the #eav_attributes
181
+ # method or the :fields option will have no affect. Use of this method
182
+ # is ideal when you want to retrict the attributes but do so in a
183
+ # algorithmic way. The following is an example:
184
+ # class User < ActiveRecord::Base
185
+ # has_eav_behavior :class_name => 'UserContactInfo'
186
+ # has_eav_behavior :class_name => 'Preferences'
187
+ #
188
+ # def is_eav_attribute?(attr, model)
189
+ # case attr.to_s
190
+ # when /^contact_/ then true
191
+ # when /^preference_/ then true
192
+ # else
193
+ # false
194
+ # end
195
+ # end
196
+ # end
197
+ #
198
+ # marcus = User.find_by_login 'marcus'
199
+ # marcus.contact_phone = '021 654 9876'
200
+ # marcus.contact_email = 'marcus@example.com'
201
+ # marcus.preference_project_order = 'name'
202
+ # marcus.some_attribute = 'blah' # If some_attribute is not defined on
203
+ # # the model then method not found is thrown
204
+ #
205
+ def has_eav_behavior(options = {})
206
+ Rails.logger.debug("HERE OPTIONS=#{options.inspect}")
207
+ # Provide default options
208
+ options[:class_name] ||= self.name + 'Attribute'
209
+ options[:table_name] ||= options[:class_name].tableize
210
+ options[:relationship_name] ||= options[:class_name].tableize.to_sym
211
+ options[:foreign_key] ||= "#{self.table_name.singularize.downcase}_id" #self.name.foreign_key
212
+ options[:base_foreign_key] ||= self.name.underscore.foreign_key
213
+ options[:name_field] ||= 'name'
214
+ options[:value_field] ||= 'value'
215
+ options[:fields].collect! {|f| f.to_s} unless options[:fields].nil?
216
+ options[:parent] = self.name
217
+
218
+ # Init option storage if necessary
219
+ cattr_accessor :eav_options
220
+ self.eav_options ||= Hash.new
221
+
222
+ # Return if already processed.
223
+ return if self.eav_options.keys.include? options[:class_name]
224
+
225
+ # Attempt to load related class. If not create it
226
+ begin
227
+ options[:class_name].constantize
228
+ rescue
229
+ Object.const_set(options[:class_name],
230
+ Class.new(ActiveRecord::Base)).class_eval do
231
+ def self.reloadable? #:nodoc:
232
+ false
233
+ end
234
+ end
235
+ end
236
+
237
+ # Store options
238
+ self.eav_options[options[:class_name]] = options
239
+
240
+ # Only mix instance methods once
241
+ unless self.included_modules.include?(ActiveRecord::Acts::EavModel::InstanceMethods)
242
+ send :include, ActiveRecord::Acts::EavModel::InstanceMethods
243
+ end
244
+
245
+ # Modify attribute class
246
+ attribute_class = options[:class_name].constantize
247
+ base_class = self.name.underscore.to_sym
248
+
249
+ attribute_class.class_eval do
250
+ belongs_to base_class, :foreign_key => options[:base_foreign_key]
251
+ alias_method :base, base_class # For generic access
252
+ end
253
+
254
+ # Modify main class
255
+ class_eval do
256
+ has_many options[:relationship_name],
257
+ :class_name => options[:class_name],
258
+ :table_name => options[:table_name],
259
+ :foreign_key => options[:foreign_key],
260
+ :dependent => :destroy
261
+
262
+ # The following is only setup once
263
+ unless method_defined? :method_missing_without_eav_behavior
264
+
265
+ # Carry out delayed actions before save
266
+ after_validation :save_modified_eav_attributes,:on=>:update
267
+
268
+ # Make attributes seem real
269
+ alias_method_chain :respond_to?, :eav_behavior
270
+ alias_method_chain :method_missing, :eav_behavior
271
+
272
+ private
273
+
274
+ alias_method_chain :read_attribute, :eav_behavior
275
+ alias_method_chain :write_attribute, :eav_behavior
276
+
277
+ end
278
+ end
279
+
280
+ create_attribute_table
281
+
282
+ end
283
+
284
+ end
285
+
286
+ module InstanceMethods
287
+
288
+ def self.included(base) # :nodoc:
289
+ base.extend ClassMethods
290
+ end
291
+
292
+ module ClassMethods
293
+
294
+ ##
295
+ # Rake migration task to create the versioned table using options passed to has_eav_behavior
296
+ #
297
+ def create_attribute_table(options = {})
298
+ eav_options.keys.each do |key|
299
+ continue if eav_options[key][:parent] != self.name
300
+ model = eav_options[key][:class_name]
301
+
302
+ return if connection.tables.include?(eav_options[model][:table_name])
303
+
304
+ self.connection.create_table(eav_options[model][:table_name], options) do |t|
305
+ t.integer eav_options[model][:foreign_key], :null => false
306
+ t.string eav_options[model][:name_field], :null => false
307
+ t.string eav_options[model][:value_field], :null => false
308
+
309
+ t.timestamps
310
+ end
311
+
312
+ self.connection.add_index eav_options[model][:table_name], eav_options[model][:foreign_key]
313
+ end
314
+
315
+ end
316
+
317
+ ##
318
+ # Rake migration task to drop the attribute table
319
+ #
320
+ def drop_attribute_table(options = {})
321
+ eav_options.keys.each do |key|
322
+ continue if eav_options[key][:parent] != self.name
323
+ model = eav_options[key][:class_name]
324
+ self.connection.drop_table eav_options[model][:table_name]
325
+ end
326
+
327
+ end
328
+
329
+ end
330
+
331
+ ##
332
+ # Will determine if the given attribute is a eav attribute on the
333
+ # given model. Override this in your class to provide custom logic if
334
+ # the #eav_attributes method or the :fields option are not flexible
335
+ # enough. If you override this method :fields and #eav_attributes will
336
+ # not apply at all unless you implement them yourself.
337
+ #
338
+ def is_eav_attribute?(attribute_name, model)
339
+ attribute_name = attribute_name.to_s
340
+ model_options = eav_options[model.name]
341
+ return model_options[:fields].include?(attribute_name) unless model_options[:fields].nil?
342
+ return eav_attributes(model).collect {|field| field.to_s}.include?(attribute_name) unless
343
+ eav_attributes(model).nil?
344
+ true
345
+ end
346
+
347
+ ##
348
+ # Return a list of valid eav attributes for the given model. Return
349
+ # nil if any field is allowed. If you want to say no field is allowed
350
+ # then return an empty array. If you just have a static list the :fields
351
+ # option is most likely easier.
352
+ #
353
+ def eav_attributes(model); nil end
354
+
355
+ ##
356
+ # CLK added a respond_to? implementation so that ActiveRecord AssociationProxy (polymorphic relationships)
357
+ # does not mask the method_missing implementation here. See:
358
+ # https://rails.lighthouseapp.com/projects/8994/tickets/2378-associationproxymethod_missing-masks-method_missing-in-models
359
+ # Updated when certain magic fields (like timestamp columns) were interfering with saves, etc.
360
+ # http://oldwiki.rubyonrails.org/rails/pages/MagicFieldNames
361
+ #
362
+ def respond_to_with_eav_behavior?(method_id, include_private = false)
363
+ if MAGIC_FIELD_NAMES.include?(method_id.to_sym)
364
+ respond_to_without_eav_behavior?(method_id, include_private)
365
+ else
366
+ true
367
+ end
368
+ end
369
+
370
+ private
371
+
372
+ ##
373
+ # Called after validation on update so that eav attributes behave
374
+ # like normal attributes in the fact that the database is not touched
375
+ # until save is called.
376
+ #
377
+ def save_modified_eav_attributes
378
+ return if @save_eav_attr.nil?
379
+ @save_eav_attr.each do |s|
380
+ model, attribute_name = s
381
+ related_attribute = find_related_eav_attribute(model, attribute_name)
382
+ unless related_attribute.nil?
383
+ if related_attribute.value.nil?
384
+ eav_related(model).delete(related_attribute)
385
+ else
386
+ related_attribute.save
387
+ end
388
+ end
389
+ end
390
+ @save_eav_attr = []
391
+ end
392
+
393
+ ##
394
+ # Overrides ActiveRecord::Base#read_attribute
395
+ #
396
+ def read_attribute_with_eav_behavior(attribute_name)
397
+ attribute_name = attribute_name.to_s
398
+ exec_if_related(attribute_name) do |model|
399
+ return nil if !@remove_eav_attr.nil? && @remove_eav_attr.any? do |r|
400
+ r[0] == model && r[1] == attribute_name
401
+ end
402
+
403
+ value_field = eav_options[model.name][:value_field]
404
+ related_attribute = find_related_eav_attribute(model, attribute_name)
405
+
406
+ return nil if related_attribute.nil?
407
+ return related_attribute.send(value_field)
408
+ end
409
+ read_attribute_without_eav_behavior(attribute_name)
410
+ end
411
+
412
+ ##
413
+ # Overrides ActiveRecord::Base#write_attribute
414
+ #
415
+ def write_attribute_with_eav_behavior(attribute_name, value)
416
+ attribute_name = attribute_name.to_s
417
+ exec_if_related(attribute_name) do |model|
418
+ value_field = eav_options[model.name][:value_field]
419
+ @save_eav_attr ||= []
420
+ @save_eav_attr << [model, attribute_name]
421
+ related_attribute = find_related_eav_attribute(model, attribute_name)
422
+ if related_attribute.nil?
423
+ # Used to check for nil? but this caused validation
424
+ # problems that are harder to solve. blank? is probably
425
+ # not correct but it works well for now.
426
+ unless value.blank?
427
+ name_field = eav_options[model.name][:name_field]
428
+ foreign_key = eav_options[model.name][:foreign_key]
429
+ eav_related(model).build name_field => attribute_name,
430
+ value_field => value, foreign_key => self.id
431
+ end
432
+ return value
433
+ else
434
+ value_field = (value_field.to_s + '=').to_sym
435
+ return related_attribute.send(value_field, value)
436
+ end
437
+ end
438
+ write_attribute_without_eav_behavior(attribute_name, value)
439
+ end
440
+
441
+ ##
442
+ # Custom method added by ckraybill!
443
+ #
444
+ def attribute_empty_with_eav_behavior(attribute_name)
445
+ !read_attribute_with_eav_behavior(attribute_name).blank?
446
+ end
447
+
448
+ ##
449
+ # Implements eav-attributes as if real getter/setter methods
450
+ # were defined.
451
+ # Method modified to support '?' behavior
452
+ def method_missing_with_eav_behavior(method_id, *args, &block)
453
+ begin
454
+ method_missing_without_eav_behavior(method_id, *args, &block)
455
+ rescue NoMethodError => e
456
+ m = method_id.to_s
457
+ attribute_name = m.sub(/\=|\?$/, '')
458
+ exec_if_related(attribute_name) do |model|
459
+ if m =~ /\=$/
460
+ return write_attribute_with_eav_behavior(attribute_name, args[0])
461
+ elsif m =~ /\?$/
462
+ return attribute_empty_with_eav_behavior(attribute_name)
463
+ else
464
+ return read_attribute_with_eav_behavior(attribute_name)
465
+ end
466
+ end
467
+ raise e
468
+ end
469
+ end
470
+
471
+ ##
472
+ # Retrieve the related eav attribute object
473
+ #
474
+ def find_related_eav_attribute(model, attribute_name)
475
+ name_field = eav_options[model.name][:name_field]
476
+ eav_related(model).to_a.find do |relation|
477
+ relation.send(name_field) == attribute_name
478
+ end
479
+ end
480
+
481
+ ##
482
+ # Retrieve the collection of related eav attributes
483
+ #
484
+ def eav_related(model)
485
+ relationship = eav_options[model.name][:relationship_name]
486
+ send relationship
487
+ end
488
+
489
+ ##
490
+ # yield only if attribute_name is a eav_attribute
491
+ #
492
+ def exec_if_related(attribute_name)
493
+ return false if self.class.column_names.include?(attribute_name) || MAGIC_FIELD_NAMES.include?(attribute_name.to_sym)
494
+ each_eav_relation do |model|
495
+ if is_eav_attribute?(attribute_name, model)
496
+ yield model
497
+ end
498
+ end
499
+ end
500
+
501
+ ##
502
+ # yields for each eav relation.
503
+ #
504
+ def each_eav_relation
505
+ eav_options.keys.each {|kls| yield kls.constantize}
506
+ end
507
+
508
+ end
509
+
510
+ end
511
+ end
512
+
513
+ class Base
514
+
515
+ ##
516
+ # Overrides ActiveRecord::Base#attributes=
517
+ #
518
+ # Because in version >=2.2.2 of ActiveRecord the behaviour in the attributes
519
+ # have been changed to throw a NoMethodError if the AR object don't respond
520
+ # to an attribute setter, we could not use method missing to allow for our
521
+ # entity-attribute-value behaviour.
522
+ #
523
+ def attributes=(new_attributes, guard_protected_attributes = true)
524
+ return if new_attributes.nil?
525
+ attributes = new_attributes.dup
526
+ attributes.stringify_keys!
527
+ multi_parameter_attributes = []
528
+ attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes
529
+ attributes.each do |k, v|
530
+ if k.include?("(")
531
+ multi_parameter_attributes << [ k, v ]
532
+ else
533
+ send(:"#{k}=", v)
534
+ end
535
+ end
536
+
537
+ assign_multiparameter_attributes(multi_parameter_attributes)
538
+ end
539
+ end
540
+ end
541
+
542
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::EavModel