acts_as_eav_model 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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