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 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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in user_preferences.gemspec
4
+ gemspec
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,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ desc 'Runs the specs'
6
+ task default: :spec
7
+
@@ -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
@@ -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,9 @@
1
+ module UserPreferences
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'user_preferences.has_preferences' do
4
+ ActiveSupport.on_load(:active_record) do
5
+ extend UserPreferences::HasPreferences::ActiveRecordExtension
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module UserPreferences
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,19 @@
1
+ hobbies:
2
+ outdoors: true
3
+ cultural: false
4
+
5
+ food:
6
+ vegetarian: false
7
+ a_la_carte: true
8
+ courses:
9
+ default: 2
10
+ values:
11
+ - 1
12
+ - 2
13
+ - 3
14
+ wine:
15
+ default: red
16
+ values:
17
+ - red
18
+ - white
19
+
@@ -0,0 +1,11 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.timestamps
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :users
10
+ end
11
+ 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
@@ -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