user_preferences 0.0.1

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