user_preferences 0.0.1
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.
- checksums.yaml +15 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +137 -0
- data/Rakefile +7 -0
- data/lib/generators/user_preferences/install_generator.rb +21 -0
- data/lib/generators/user_preferences/templates/migration.rb +19 -0
- data/lib/generators/user_preferences/templates/user_preferences.yml +0 -0
- data/lib/user_preferences.rb +37 -0
- data/lib/user_preferences/api.rb +63 -0
- data/lib/user_preferences/defaults.rb +23 -0
- data/lib/user_preferences/has_preferences.rb +36 -0
- data/lib/user_preferences/preference.rb +21 -0
- data/lib/user_preferences/preference_definition.rb +47 -0
- data/lib/user_preferences/railtie.rb +9 -0
- data/lib/user_preferences/version.rb +3 -0
- data/spec/fixtures/user_preferences.yml +19 -0
- data/spec/migrations/001_create_users.rb +11 -0
- data/spec/migrations/002_create_preferences.rb +19 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/user_preferences/api_spec.rb +81 -0
- data/spec/user_preferences/defaults_spec.rb +33 -0
- data/spec/user_preferences/has_preferences_spec.rb +44 -0
- data/spec/user_preferences/preference_definition_spec.rb +90 -0
- data/spec/user_preferences/preference_spec.rb +79 -0
- data/spec/user_preferences_spec.rb +44 -0
- data/user_preferences.gemspec +29 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZDJiY2VjZTUyZTNjNmRlYmNmY2E0Yjk1ODljZWE2ZDQ3ZTQ1NGVlNg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MmM4Zjk4MDMzYjA4ZGUzMDZlZGM0YTY0NjE3ODZkMzA4YWY2Yjc1YQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
N2YyZTlmMzlkYmFiYzNjODU5ZjI4N2YyY2U0MGZlYmI4OTdlYzlhODkzOWQ2
|
10
|
+
YjhmNDRhYWU3ZTRkOWM4OTdiYmM0YTA3NzRkMTQyOTJlZGQ4NDBmOWFlY2Ix
|
11
|
+
YWExMTExMWYxNmRhOTFhNzZmZjRlM2U3ZDU2ODkyM2ZhYTJlMzk=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
N2JkY2I0NTAyMjJjZGE5ZWFmMWQ4NGY3ODRjODA4OTFjMDEzYzdhMDA1OTk0
|
14
|
+
YzJlOGJlNmRlODdiODEyZTdmOTY3YWJmODQ5M2U2MDdiOWE4ZmQ2Nzg4YmZj
|
15
|
+
MGY2ZmNlOWJjZDczMTRjZjhiOTc4NDIzNjA3NGE4NTRlZmJkNWU=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Andy Dust
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# UserPreferences
|
2
|
+
|
3
|
+
An ActiveRecord backed user preference library that supports:
|
4
|
+
* Categories (currently non-optional)
|
5
|
+
* Binary and non-binary preferences
|
6
|
+
* Default values
|
7
|
+
* Value validation
|
8
|
+
* Retrieving users scoped by a particular preference
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'user_preferences'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
```sh
|
21
|
+
$ bundle
|
22
|
+
```
|
23
|
+
|
24
|
+
Run the installation script:
|
25
|
+
|
26
|
+
```sh
|
27
|
+
$ rails g user_preferences:install
|
28
|
+
```
|
29
|
+
|
30
|
+
This will copy across the migration and add an empty preference definition file in config/
|
31
|
+
|
32
|
+
Finally, run the database migrations:
|
33
|
+
|
34
|
+
```sh
|
35
|
+
$ rake db:migrate
|
36
|
+
```
|
37
|
+
|
38
|
+
## Defining preferences
|
39
|
+
|
40
|
+
Your preferences are defined in ``config/user_preferences.yml``. You define each of your
|
41
|
+
preferences within a category. This example definition for a binary preference implies that users receive emails notifications by default but not newsletters:
|
42
|
+
```yaml
|
43
|
+
emails:
|
44
|
+
notifications: true
|
45
|
+
newsletters: false
|
46
|
+
```
|
47
|
+
|
48
|
+
You can configure non-binary preferences. For example, if users could choose periodical notification digests, the configuration might look like this:
|
49
|
+
|
50
|
+
```yaml
|
51
|
+
emails:
|
52
|
+
notifications:
|
53
|
+
default: instant
|
54
|
+
values:
|
55
|
+
- off
|
56
|
+
- instant
|
57
|
+
- daily
|
58
|
+
- weekly
|
59
|
+
newsletters: false
|
60
|
+
```
|
61
|
+
|
62
|
+
You can add as many categories as you like:
|
63
|
+
|
64
|
+
```yaml
|
65
|
+
emails:
|
66
|
+
notifications: true
|
67
|
+
newsletters: false
|
68
|
+
|
69
|
+
beta_features:
|
70
|
+
two_factor_authentication: false
|
71
|
+
the_big_red_button: false
|
72
|
+
```
|
73
|
+
|
74
|
+
## API
|
75
|
+
|
76
|
+
### set
|
77
|
+
Similar to ActiveRecord, setting a preference returns true or false depending on whether or not it was successfully persisted:
|
78
|
+
```ruby
|
79
|
+
user.preferences(:emails).set(notifications: 'instant') # => true
|
80
|
+
user.preferences(:emails).set(notifications: 'some_typo') # => false
|
81
|
+
```
|
82
|
+
|
83
|
+
You can set multiple preferences at once:
|
84
|
+
```ruby
|
85
|
+
user.preferences(:emails).set(notifications: 'instant', newsletter: true) # => true
|
86
|
+
```
|
87
|
+
|
88
|
+
### get
|
89
|
+
A single preference:
|
90
|
+
```ruby
|
91
|
+
user.preferences(:emails).get(:notifications) # => 'instant'
|
92
|
+
```
|
93
|
+
|
94
|
+
### all
|
95
|
+
All preferences for a category:
|
96
|
+
```ruby
|
97
|
+
user.preferences(:emails).all # => { notifications: 'instant', newsletter: true }
|
98
|
+
```
|
99
|
+
|
100
|
+
### reload
|
101
|
+
Reload the preferences from the database; since something else might have changed the user's state.
|
102
|
+
```ruby
|
103
|
+
user.preferences(:emails).reload # => { notifications: 'instant', newsletter: true }
|
104
|
+
```
|
105
|
+
|
106
|
+
## Scoping users
|
107
|
+
```ruby
|
108
|
+
newsletter_users = User.with_preference(:email, :newsletter, true) #=> an ActiveRecord::Relation
|
109
|
+
```
|
110
|
+
Note: this _will_ include users who have not overriden the default value if the value incidentally matches the default value.
|
111
|
+
|
112
|
+
## Other useful stuff
|
113
|
+
|
114
|
+
### Single preference definition
|
115
|
+
* Get your preference definition (as per your .yml) as a hash: ``UserPreferences.definitions``
|
116
|
+
* Get the definition for a single preference:
|
117
|
+
```ruby
|
118
|
+
preference = UserPreferences[:emails, :notifications]
|
119
|
+
preference.default # => 'instant'
|
120
|
+
preference.binary? # => false
|
121
|
+
preference.permitted_values # => ['off', 'instant', 'daily', 'weekly']
|
122
|
+
```
|
123
|
+
* Retrieve the default preference state with ``UserPreferences.defaults``. You can also scope to a category: ``UserPreferences.defaults(:emails)``
|
124
|
+
|
125
|
+
## Testing
|
126
|
+
|
127
|
+
```sh
|
128
|
+
$ rake test
|
129
|
+
```
|
130
|
+
|
131
|
+
## Contributing
|
132
|
+
|
133
|
+
1. Fork it ( http://github.com/mubi/user_preferences/fork )
|
134
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
135
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
136
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
137
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module UserPreferences
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
source_root File.expand_path('../templates', __FILE__)
|
6
|
+
|
7
|
+
def create_initializer_file
|
8
|
+
template 'user_preferences.yml', "config/user_preferences.yml"
|
9
|
+
end
|
10
|
+
|
11
|
+
def copy_migrations
|
12
|
+
migration_template 'migration.rb', "db/migrate/create_preferences.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
# TODO get rid of this
|
16
|
+
def self.next_migration_number(dir)
|
17
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreatePreferences < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :preferences do |t|
|
4
|
+
t.integer :user_id, null: false
|
5
|
+
t.string :category, null: false
|
6
|
+
t.string :name, null: false
|
7
|
+
t.integer :value, null: false
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :preferences, :user_id
|
12
|
+
add_index :preferences, [:user_id, :category]
|
13
|
+
add_index :preferences, [:category, :name, :value]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.down
|
17
|
+
drop_table :preferences
|
18
|
+
end
|
19
|
+
end
|
File without changes
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
|
5
|
+
module UserPreferences
|
6
|
+
extend ActiveSupport::Autoload
|
7
|
+
|
8
|
+
autoload :API, 'user_preferences/api'
|
9
|
+
autoload :Defaults
|
10
|
+
autoload :HasPreferences
|
11
|
+
autoload :Preference
|
12
|
+
autoload :PreferenceDefinition
|
13
|
+
autoload :VERSION
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def [](category, name)
|
17
|
+
unless (pref = definitions[category].try(:[], name)).nil?
|
18
|
+
PreferenceDefinition.new(pref, category, name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def defaults(category = nil)
|
23
|
+
@_defaults ||= Defaults.new(definitions)
|
24
|
+
@_defaults.get(category)
|
25
|
+
end
|
26
|
+
|
27
|
+
def yml_path
|
28
|
+
Rails.root.join('config', 'user_preferences.yml') if defined?(Rails)
|
29
|
+
end
|
30
|
+
|
31
|
+
def definitions
|
32
|
+
@_definitions ||= YAML.load_file(yml_path).with_indifferent_access
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
require 'user_preferences/railtie' if defined?(Rails)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module UserPreferences
|
2
|
+
class API
|
3
|
+
def initialize(category, scope)
|
4
|
+
@category = category
|
5
|
+
@scope = scope.where(category: category)
|
6
|
+
end
|
7
|
+
|
8
|
+
def all
|
9
|
+
serialized_preferences
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(name)
|
13
|
+
serialized_preferences[name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def set(hash)
|
17
|
+
hash_setter do
|
18
|
+
hash.each do |name, value|
|
19
|
+
find_or_init_preference(name).update_value!(value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def reload
|
25
|
+
@_saved_preferences = nil
|
26
|
+
all
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def serialized_preferences
|
32
|
+
default_preferences.merge Hash[saved_preferences.map { |p| [p.name.to_sym, p.value] }]
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_preferences
|
36
|
+
@_category_defaults ||= UserPreferences.defaults(@category)
|
37
|
+
end
|
38
|
+
|
39
|
+
def saved_preferences
|
40
|
+
@_saved_preferences ||= @scope.select([:id, :category, :name, :value, :user_id]).all
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_or_init_preference(name)
|
44
|
+
unless preference = saved_preferences.detect { |p| p.name == name }
|
45
|
+
preference = @scope.find_by_name(name) || @scope.build(name: name, category: @category)
|
46
|
+
saved_preferences << preference
|
47
|
+
end
|
48
|
+
preference
|
49
|
+
end
|
50
|
+
|
51
|
+
def hash_setter(&block)
|
52
|
+
ActiveRecord::Base.transaction do
|
53
|
+
result = true
|
54
|
+
begin
|
55
|
+
yield
|
56
|
+
rescue ActiveRecord::RecordInvalid
|
57
|
+
result = false
|
58
|
+
end
|
59
|
+
result
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module UserPreferences
|
2
|
+
class Defaults
|
3
|
+
def initialize(definitions)
|
4
|
+
@definitions = definitions
|
5
|
+
end
|
6
|
+
|
7
|
+
def get(category = nil)
|
8
|
+
if category
|
9
|
+
category_defaults(category)
|
10
|
+
else
|
11
|
+
@definitions.inject({}) { |h, (k,v)| h[k.to_sym] = category_defaults(k); h }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def category_defaults(category)
|
18
|
+
@definitions[category].inject({}) do |h, (k,v)|
|
19
|
+
h[k.to_sym] = v.is_a?(Hash) ? v['default'] : v; h
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module UserPreferences
|
2
|
+
module HasPreferences
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ActiveRecordExtension
|
6
|
+
def has_preferences
|
7
|
+
include HasPreferences
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
included do
|
12
|
+
has_many :saved_preferences, class_name: 'UserPreferences::Preference', dependent: :destroy
|
13
|
+
|
14
|
+
def preferences(category)
|
15
|
+
@_preference_apis ||= {}
|
16
|
+
@_preference_apis[category] ||= UserPreferences::API.new(category, saved_preferences)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.with_preference(category, name, value)
|
20
|
+
definition = UserPreferences[category, name]
|
21
|
+
db_value = definition.to_db(value)
|
22
|
+
scope = select('users.*, p.id as preference_id')
|
23
|
+
join = %Q{
|
24
|
+
%s join #{UserPreferences::Preference.table_name} p
|
25
|
+
on p.category = '#{category}' and p.name = '#{name}'
|
26
|
+
and p.user_id = #{self.table_name}.id
|
27
|
+
}
|
28
|
+
if value != definition.default
|
29
|
+
scope.joins(join % 'inner').where("p.value = #{db_value}")
|
30
|
+
else
|
31
|
+
scope.joins(join % 'left').where("p.value = #{db_value} or p.id is null")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class UserPreferences::Preference < ActiveRecord::Base
|
2
|
+
self.table_name = 'preferences'
|
3
|
+
belongs_to :user
|
4
|
+
validates_uniqueness_of :name, scope: [:user_id, :category]
|
5
|
+
validates_presence_of :user_id, :category, :name
|
6
|
+
validates :value, inclusion: { in: ->(p) { p.permitted_values }}
|
7
|
+
|
8
|
+
delegate :binary?, :default, :permitted_values, :lookup, :to_db, to: :definition
|
9
|
+
|
10
|
+
def update_value!(v)
|
11
|
+
update_attributes!(value: to_db(v))
|
12
|
+
end
|
13
|
+
|
14
|
+
def value
|
15
|
+
lookup(attributes['value'])
|
16
|
+
end
|
17
|
+
|
18
|
+
def definition
|
19
|
+
UserPreferences[category, name]
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module UserPreferences
|
2
|
+
class PreferenceDefinition
|
3
|
+
attr_reader :name, :category
|
4
|
+
|
5
|
+
def initialize(definition, category, name)
|
6
|
+
@definition = definition
|
7
|
+
@category = category
|
8
|
+
@name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def permitted_values
|
12
|
+
if binary?
|
13
|
+
result = [false, true]
|
14
|
+
else
|
15
|
+
@definition[:values]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def binary?
|
20
|
+
!@definition.is_a?(Hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
def default
|
24
|
+
binary? ? @definition : @definition[:default]
|
25
|
+
end
|
26
|
+
|
27
|
+
def lookup(index)
|
28
|
+
permitted_values[index] if index
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_db(value)
|
32
|
+
value = to_bool(value) if binary?
|
33
|
+
permitted_values.index(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def to_bool(value)
|
39
|
+
return true if value == 1
|
40
|
+
return true if value == true || value =~ (/^(true|t|yes|y|1)$/i)
|
41
|
+
|
42
|
+
return false if value == 0
|
43
|
+
return false if value == false || value.blank? || value =~ (/^(false|f|no|n|0)$/i)
|
44
|
+
raise ArgumentError.new("invalid value for Boolean: \"#{value}\"")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreatePreferences < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :preferences do |t|
|
4
|
+
t.integer :user_id, null: false
|
5
|
+
t.string :category, null: false
|
6
|
+
t.string :name, null: false
|
7
|
+
t.integer :value, null: false
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :preferences, :user_id
|
12
|
+
add_index :preferences, [:user_id, :category]
|
13
|
+
add_index :preferences, [:category, :name, :value]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.down
|
17
|
+
drop_table :preferences
|
18
|
+
end
|
19
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'user_preferences'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/connection_adapters/sqlite3_adapter'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
$stdout = StringIO.new # silence migrations
|
7
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
8
|
+
ActiveRecord::Migrator.migrate(File.expand_path('../migrations', __FILE__))
|
9
|
+
$stdout = STDOUT
|
10
|
+
|
11
|
+
# prevent deprecation warnings
|
12
|
+
I18n.enforce_available_locales = true
|
13
|
+
|
14
|
+
config.before(:each) { stub_yml }
|
15
|
+
end
|
16
|
+
|
17
|
+
def stub_yml
|
18
|
+
fixture = File.expand_path("../fixtures/user_preferences.yml", __FILE__)
|
19
|
+
UserPreferences.stub(:yml_path).and_return(fixture)
|
20
|
+
end
|
21
|
+
|
22
|
+
class User < ActiveRecord::Base
|
23
|
+
include UserPreferences::HasPreferences
|
24
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UserPreferences::API do
|
4
|
+
let(:user) { User.create! }
|
5
|
+
subject(:api) { UserPreferences::API.new(:food, user.saved_preferences) }
|
6
|
+
|
7
|
+
describe '#all' do
|
8
|
+
it 'returns preference state' do
|
9
|
+
expect(api.all).to eq({
|
10
|
+
vegetarian: false,
|
11
|
+
a_la_carte: true,
|
12
|
+
courses: 2,
|
13
|
+
wine: 'red'
|
14
|
+
})
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#get' do
|
19
|
+
it 'gets the preference value' do
|
20
|
+
expect(api.get(:a_la_carte)).to eq(true)
|
21
|
+
expect(api.get(:wine)).to eq('red')
|
22
|
+
expect(api.get(:courses)).to eq(2)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#set' do
|
27
|
+
it 'persists the preference values' do
|
28
|
+
api.set(wine: 'white')
|
29
|
+
api.reload
|
30
|
+
expect(api.get(:wine)).to eq('white')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'allows setting of multiple preferences' do
|
34
|
+
api.set(wine: 'white', courses: 3)
|
35
|
+
api.reload
|
36
|
+
expect(api.get(:wine)).to eq('white')
|
37
|
+
expect(api.get(:courses)).to eq(3)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns true' do
|
41
|
+
expect(api.set(wine: 'white')).to eq(true)
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'one or more of the preference values were invalid' do
|
45
|
+
it 'returns false' do
|
46
|
+
expect(api.set(wine: 'sparkling')).to eq(false)
|
47
|
+
expect(api.set(wine: 'white', courses: 10)).to eq(false)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'does not persist anything if any of the values were invalid' do
|
51
|
+
api.set(wine: 'sparkling', courses: 3)
|
52
|
+
api.reload
|
53
|
+
expect(api.get(:wine)).to eq('red')
|
54
|
+
expect(api.get(:courses)).to eq(2)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#reload' do
|
60
|
+
before(:each) do
|
61
|
+
api.set(courses: 1)
|
62
|
+
other_thread_api = UserPreferences::API.new('food', User.last.saved_preferences)
|
63
|
+
other_thread_api.set(courses: 3)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'reloads the preference state' do
|
67
|
+
expect(api.get(:courses)).to eq(1)
|
68
|
+
api.reload
|
69
|
+
expect(api.get(:courses)).to eq(3)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'returns the preference state' do
|
73
|
+
expect(api.reload).to eq({
|
74
|
+
vegetarian: false,
|
75
|
+
a_la_carte: true,
|
76
|
+
courses: 3,
|
77
|
+
wine: 'red'
|
78
|
+
})
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UserPreferences::Defaults do
|
4
|
+
subject(:defaults) { UserPreferences::Defaults.new(UserPreferences.definitions) }
|
5
|
+
|
6
|
+
describe '.get' do
|
7
|
+
it 'returns the default preference state' do
|
8
|
+
expect(defaults.get).to eq(
|
9
|
+
{
|
10
|
+
hobbies: {
|
11
|
+
outdoors: true,
|
12
|
+
cultural: false
|
13
|
+
},
|
14
|
+
food: {
|
15
|
+
vegetarian: false,
|
16
|
+
a_la_carte: true,
|
17
|
+
courses: 2,
|
18
|
+
wine: 'red'
|
19
|
+
}
|
20
|
+
}
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'a category is supplied' do
|
25
|
+
it 'returns the default preference state for the category' do
|
26
|
+
expect(defaults.get(:hobbies)).to eq({
|
27
|
+
outdoors: true,
|
28
|
+
cultural: false
|
29
|
+
})
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UserPreferences::HasPreferences do
|
4
|
+
let(:user) { User.create }
|
5
|
+
|
6
|
+
it 'should mix in preference methods' do
|
7
|
+
expect(user).to respond_to(:saved_preferences)
|
8
|
+
expect(user).to respond_to(:preferences)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#preferences' do
|
12
|
+
it 'should return an API instance' do
|
13
|
+
expect(user.preferences(:food)).to be_kind_of(UserPreferences::API)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.with_preference' do
|
18
|
+
before(:each) { User.destroy_all }
|
19
|
+
|
20
|
+
it 'only returns users with the matching preference' do
|
21
|
+
user_1, user_2 = 2.times.map { User.create }
|
22
|
+
user_1.preferences(:food).set(wine: 'white')
|
23
|
+
user_2.preferences(:food).set(wine: 'red')
|
24
|
+
expect(User.with_preference(:food, :wine, 'white')).to eq([user_1])
|
25
|
+
|
26
|
+
user_2.preferences(:food).set(wine: 'white')
|
27
|
+
expect(User.with_preference(:food, :wine, 'white')).to eq([user_1, user_2])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'returns a chainable active record relation' do
|
31
|
+
user.preferences(:food).set(wine: 'white')
|
32
|
+
expect(User.with_preference(:food, :wine, 'white')).to be_kind_of(ActiveRecord::Relation)
|
33
|
+
expect(User.with_preference(:food, :wine, 'white').where('1=1')).to eq([user])
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'the desired preference matches the default value' do
|
37
|
+
it 'includes users who have not explicitely overriden the preference' do
|
38
|
+
user.preferences(:food).set(wine: 'red') # the default value
|
39
|
+
user_2 = User.create
|
40
|
+
expect(User.with_preference(:food, :wine, 'red')).to eq([user, user_2])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UserPreferences::PreferenceDefinition do
|
4
|
+
subject(:preference) do
|
5
|
+
definition = UserPreferences.definitions[:food][:wine]
|
6
|
+
UserPreferences::PreferenceDefinition.new(definition, :food, :wine)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#name' do
|
10
|
+
it 'returns the preference name' do
|
11
|
+
expect(preference.name).to eq(:wine)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#category' do
|
16
|
+
it 'returns the preference category' do
|
17
|
+
expect(preference.category).to eq(:food)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#permitted_values' do
|
22
|
+
it 'returns an array of permitted values' do
|
23
|
+
expect(preference.permitted_values).to eq(['red', 'white'])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#binary?' do
|
28
|
+
it 'is false' do
|
29
|
+
expect(preference.binary?).to eq(false)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#default' do
|
34
|
+
it 'returns the default preference value' do
|
35
|
+
expect(preference.default).to eq('red')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#lookup' do
|
40
|
+
it 'returns the value at the supplied index' do
|
41
|
+
expect(preference.lookup(0)).to eq('red')
|
42
|
+
expect(preference.lookup(1)).to eq('white')
|
43
|
+
expect(preference.lookup(2)).to be_nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#to_db' do
|
48
|
+
it 'returns the value index for the value' do
|
49
|
+
expect(preference.to_db('red')).to eq(0)
|
50
|
+
expect(preference.to_db('white')).to eq(1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'the preference is binary' do
|
55
|
+
subject(:preference) do
|
56
|
+
definition = UserPreferences.definitions[:food][:vegetarian]
|
57
|
+
UserPreferences::PreferenceDefinition.new(definition, :food, :vegetarian)
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#permitted_values' do
|
61
|
+
it 'returns an array of booleans' do
|
62
|
+
expect(preference.permitted_values).to eq([false, true])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#binary?' do
|
67
|
+
it 'is true' do
|
68
|
+
expect(preference.binary?).to eq(true)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#default' do
|
73
|
+
it 'returns the default boolean' do
|
74
|
+
expect(preference.default).to eq(false)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#to_db' do
|
79
|
+
it 'casts true/false strings to booleans' do
|
80
|
+
expect(preference.to_db('false')).to eq(0)
|
81
|
+
expect(preference.to_db('true')).to eq(1)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'casts integers to booleans' do
|
85
|
+
expect(preference.to_db(0)).to eq(0)
|
86
|
+
expect(preference.to_db(1)).to eq(1)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UserPreferences::Preference do
|
4
|
+
|
5
|
+
let(:user) { User.create }
|
6
|
+
subject(:preference) { UserPreferences::Preference.new(category: :food, name: :wine, user: user) }
|
7
|
+
|
8
|
+
it 'must have a name' do
|
9
|
+
stub_definition
|
10
|
+
preference.name = nil
|
11
|
+
expect(preference.valid?).to eq(false)
|
12
|
+
expect(preference.errors.full_messages).to include("Name can't be blank")
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'must have a category' do
|
16
|
+
stub_definition
|
17
|
+
preference.category = nil
|
18
|
+
expect(preference.valid?).to eq(false)
|
19
|
+
expect(preference.errors.full_messages).to include("Category can't be blank")
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'must have a value' do
|
23
|
+
expect(preference.valid?).to eq(false)
|
24
|
+
expect(preference.errors.full_messages).to include("Value is not included in the list")
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'must have a user' do
|
28
|
+
preference.user = nil
|
29
|
+
expect(preference.valid?).to eq(false)
|
30
|
+
expect(preference.errors.full_messages).to include("User can't be blank")
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'must have a unique name, scoped within user and category' do
|
34
|
+
preference.update_value! 'white'
|
35
|
+
second_preference = UserPreferences::Preference.new(category: :food, name: :wine, user: user, value: 1)
|
36
|
+
expect(second_preference.valid?).to eq(false)
|
37
|
+
expect(second_preference.errors.full_messages).to include('Name has already been taken')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'must have a valid value' do
|
41
|
+
expect(preference.valid?).to eq(false)
|
42
|
+
expect(preference.errors.full_messages).to include("Value is not included in the list")
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#update_value' do
|
46
|
+
it 'persists the updated the value' do
|
47
|
+
preference.update_value! 'white'
|
48
|
+
expect(preference.reload.value).to eq('white')
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'the validation fails' do
|
52
|
+
it 'raises' do
|
53
|
+
expect { preference.update_value! 'sparkling' }.to raise_error(ActiveRecord::RecordInvalid)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#value' do
|
59
|
+
it 'casts the persisted value index back to its human readable form' do
|
60
|
+
preference.update_value!('red')
|
61
|
+
expect(preference.attributes['value']).to eq(0)
|
62
|
+
expect(preference.value).to eq('red')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#definition' do
|
67
|
+
it 'returns the preference definition' do
|
68
|
+
expect(preference.definition).to be_kind_of(UserPreferences::PreferenceDefinition)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def stub_definition
|
75
|
+
preference.stub(:attributes).and_return({'value' => 1})
|
76
|
+
preference.stub(:value).and_return('white')
|
77
|
+
preference.stub(:permitted_values).and_return(['white'])
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
describe UserPreferences do
|
5
|
+
it 'should be valid' do
|
6
|
+
expect(UserPreferences).to be_a(Module)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '.[]' do
|
10
|
+
it 'returns a preference definition instance for supplied category and name' do
|
11
|
+
result = UserPreferences[:food, :wine]
|
12
|
+
expect(result).to be_kind_of(UserPreferences::PreferenceDefinition)
|
13
|
+
expect(result.category).to eq(:food)
|
14
|
+
expect(result.name).to eq(:wine)
|
15
|
+
expect(result.default).to eq('red')
|
16
|
+
end
|
17
|
+
|
18
|
+
context "the category doesn't exist" do
|
19
|
+
it 'returns nil' do
|
20
|
+
expect(UserPreferences[:fashion, :hats]).to be_nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "the name doesn't exist" do
|
25
|
+
it 'returns nil' do
|
26
|
+
expect(UserPreferences[:food, :dressing]).to be_nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.defaults' do
|
32
|
+
it 'returns the defaults from definitions' do
|
33
|
+
expect_any_instance_of(UserPreferences::Defaults).to receive(:get)
|
34
|
+
UserPreferences.defaults(:food)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '.definitions' do
|
39
|
+
it 'returns the loaded preference yml' do
|
40
|
+
file = File.expand_path("../fixtures/user_preferences.yml", __FILE__)
|
41
|
+
expect(UserPreferences.definitions).to eq(YAML.load_file(file).with_indifferent_access)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'user_preferences/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "user_preferences"
|
8
|
+
spec.version = UserPreferences::VERSION
|
9
|
+
spec.authors = ["Andy Dust"]
|
10
|
+
spec.email = ["adust@mubi.com"]
|
11
|
+
spec.summary = "User preferences gem for Rails."
|
12
|
+
spec.description = %Q{user_preference is a small library for setting and getting categorized user preferences.
|
13
|
+
Supports both binary and multivalue preferences and default values. }
|
14
|
+
spec.homepage = "http://github.com/mubi/user_preferences"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency 'activerecord', '>= 3.0'
|
23
|
+
spec.add_dependency 'activesupport', '>= 3.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency 'rspec', '~> 2.14'
|
28
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: user_preferences
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andy Dust
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.14'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.14'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
description: ! "user_preference is a small library for setting and getting categorized
|
98
|
+
user preferences.\n Supports both binary and multivalue preferences
|
99
|
+
and default values. "
|
100
|
+
email:
|
101
|
+
- adust@mubi.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- .gitignore
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- lib/generators/user_preferences/install_generator.rb
|
112
|
+
- lib/generators/user_preferences/templates/migration.rb
|
113
|
+
- lib/generators/user_preferences/templates/user_preferences.yml
|
114
|
+
- lib/user_preferences.rb
|
115
|
+
- lib/user_preferences/api.rb
|
116
|
+
- lib/user_preferences/defaults.rb
|
117
|
+
- lib/user_preferences/has_preferences.rb
|
118
|
+
- lib/user_preferences/preference.rb
|
119
|
+
- lib/user_preferences/preference_definition.rb
|
120
|
+
- lib/user_preferences/railtie.rb
|
121
|
+
- lib/user_preferences/version.rb
|
122
|
+
- spec/fixtures/user_preferences.yml
|
123
|
+
- spec/migrations/001_create_users.rb
|
124
|
+
- spec/migrations/002_create_preferences.rb
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
- spec/user_preferences/api_spec.rb
|
127
|
+
- spec/user_preferences/defaults_spec.rb
|
128
|
+
- spec/user_preferences/has_preferences_spec.rb
|
129
|
+
- spec/user_preferences/preference_definition_spec.rb
|
130
|
+
- spec/user_preferences/preference_spec.rb
|
131
|
+
- spec/user_preferences_spec.rb
|
132
|
+
- user_preferences.gemspec
|
133
|
+
homepage: http://github.com/mubi/user_preferences
|
134
|
+
licenses:
|
135
|
+
- MIT
|
136
|
+
metadata: {}
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
require_paths:
|
140
|
+
- lib
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ! '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
requirements: []
|
152
|
+
rubyforge_project:
|
153
|
+
rubygems_version: 2.2.1
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: User preferences gem for Rails.
|
157
|
+
test_files:
|
158
|
+
- spec/fixtures/user_preferences.yml
|
159
|
+
- spec/migrations/001_create_users.rb
|
160
|
+
- spec/migrations/002_create_preferences.rb
|
161
|
+
- spec/spec_helper.rb
|
162
|
+
- spec/user_preferences/api_spec.rb
|
163
|
+
- spec/user_preferences/defaults_spec.rb
|
164
|
+
- spec/user_preferences/has_preferences_spec.rb
|
165
|
+
- spec/user_preferences/preference_definition_spec.rb
|
166
|
+
- spec/user_preferences/preference_spec.rb
|
167
|
+
- spec/user_preferences_spec.rb
|