Skip to content

Instantly share code, notes, and snippets.

@pat
Last active August 29, 2015 14:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pat/b2e9fe07cbde66d306de to your computer and use it in GitHub Desktop.
Save pat/b2e9fe07cbde66d306de to your computer and use it in GitHub Desktop.

We've got a model User, and we're adding in the ability for a user to share their actions on Facebook (and likely other social mediums later on). Obviously, not every user wants to do this, so we want to track their preferences on a per-action-type basis.

I figure there's four ways of handling this:

  • Add a boolean column for every setting to the users table.
  • Add a JSON hash column to the users table to store all preferences.
  • Create a new model (UserPreference) and store each setting as a separate boolean column.
  • Create a new model (UserPreference) and perhaps have separate JSON hash columns per social medium (one for Facebook, one for Twitter, etc).

Whichever approach, it'd be really nice to have form objects for just preferences (and ones that handle situations where not all attributes/settings will be available - some users may only have Twitter connected, others only Facebook, etc).

I've currently gone with the second of the above options - a column called preferences which can store each setting. I'm even tempted to have hashes within the hash, broken down by social medium.

# either
user.preferences #=> {'facebook_share_challenges' => true}
# or
user.preferences #=> {'facebook' => {'share_challenges' => true}}

I'm currently wondering if this is a good approach - maybe a column for each setting in a new model is better (option three) - works within normal conventions of Rails and Reform and other things. Hashes are certainly more flexible, but at what point does the mucking around to get things work become too much of a hassle?

@apotonick
Copy link

I like the idea of one column for a hash. All you need to to is have an intermediate model User::Preferences that just maps all the declared propertys to accessors. Soon, the Disposable gem can take care of this and is integrated into Reform.

class User < AR
  class Preferences
    def initialize(user)
      @user = user
    end

    def facebook
      @user.preferences[:facebook]
    end

    def facebook=(v)
      @user.preferences[:facebook] = v
    end

You then simply instantiate this object and pass it to Reform.

PreferencesForm.new(User::Preferences.new(user))

Does that help?

As noted, Reform will soon be able to do the decorating for you (slash Disposable). That's a cool example I could use in my book.

The good thing is this will be reusable in case you start using Roar/Representable for your HTTP APIs (which you wanna do because fun).

@apotonick
Copy link

BTW, this is a design goal in Reform to NOT include all kinds of data transformations in Reform core - given all the possible data modelling possibilities this would blow up. I decided to handle compositions, delegations, hashes, etc. in Disposable.

@pat
Copy link
Author

pat commented Dec 29, 2014

Went down the path you've suggested, and what I'm finding is that PreferencesForm expects User::Preferences to be an ActiveModel with the to_key method.

Here's the form object as it currently stands:

require 'reform/form/coercion'

class UserPreferenceForm < Reform::Form
  include Coercion
  include Reform::Form::ActiveModel
  include Reform::Form::ActiveModel::FormBuilderMethods

  property :facebook_start_challenge, type: Virtus::Attribute::Boolean,
    default: true
end

And the intermediate model:

class User::Preferences
  attr_reader :user

  def initialize(user)
    @user = user
  end

  %w( facebook_start_challenge ).each do |name|
    define_method name do
      user.preferences[name]
    end

    define_method "#{name}=" do |value|
      user.preferences[name] = value
    end

    define_method "#{name}?" do |value|
      !!user.preferences[name]
    end
  end

  def persisted?
    true
  end

  def save
    user.preferences_will_change!
    user.save
  end
end

Currently thinking I'll shift back to per-setting columns in a new model for preferences, just because it works more neatly with the Rails Way, even though I do prefer the more SRP/OO approach.

@pat
Copy link
Author

pat commented Dec 29, 2014

Adding delegate :to_key, to: :user to the intermediate model has helped... next challenge is to get data types auto-detected if possible (via the attribute coercion, perhaps?).

@pat
Copy link
Author

pat commented Dec 29, 2014

Added these lines to the form to get types detected by Formtastic - I only plan to be dealing with booleans, so that's fine:

def column_for_attribute(attribute)
  Column.new :boolean
end

Column = Struct.new :type

However, I'm not convinced the :default option for the property is being respected - perhaps it's a presumed approach from Virtus that isn't going to fly here. And even without it, coercion isn't working as expected - everything's coming through as true. Have a feeling this is one fight too many against the Rails Way.

@pat
Copy link
Author

pat commented Dec 29, 2014

... I implement a separate model, find I'd made a mistake in the strong params coming through. Fix that, switch back to intermediate and form objects, and it's working as expected.

Fine, maybe I will stick with it :p

@apotonick
Copy link

You can include ModelReflections for automatic column type detection!

@apotonick
Copy link

@pat I am contemplating a DSL for you as follows in Reform. All the mapping is done in Disposable and means you don't have to write that intermediate model yourself.

class UserWithPreferencesForm < Reform::Form
  property :user_name # just an example for plain property.

  hash :preferences, format: :json do
    property :facebook_share_challenges, type: Boolean
    validates :facebook_share_challenges,  presence: true

    # or, if it's a nested hash:
    hash :facebook do
      property :share_challenges
    end
  end
end

Note how you can nest hashes. I love it already! What do you think? 🎆

@pat
Copy link
Author

pat commented Dec 30, 2014

Looks pretty neat :)

Although, having the intermediate object is not necessarily a bad thing - can use it when reading data, as opposed to just editing via a form (i.e. when I want to check whether a user has a certain preference set).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment