Im trying to eliminate two tables from my database. The tables are message_sort_options and per_page_options. These tables basically just have 5 records which are options a user can set as their preference in a preferences table. The preferences table has columns like sort_preferences and per_page_preference which both point to a record in the other two tables containing the options. How can i set up the models with virtual attributes and fixed values for the options - eliminating table lookups every time the preferences are looked up?
Create a app_config.yml file in config directory.
page:
small: 10
medium: 20
large: 30
sort:
name: name DESC
amount: amount ASC
date: created_at DESC
Create UserOptions class in models directory.
class UserOptions
def self.page_option key
options['page'][key] rescue nil
end
def self.sort_option key
options['sort'][key] rescue nil
end
def self.options
#options ||= YAML.load_file( File.join(RAILS_ROOT,
"config", "app_config.yml")) rescue {}
end
# use this in the view to set the preference
def self.page_collection
option_collection 'page'
end
# use this in the view to set the preference
def self.sort_collection
option_collection 'sort'
end
def self.option_collection key
(options[key]|| {}).to_a
end
end
Configure your models:
class User
has_one :preference
end
class Preference
def sort_preference(default = nil)
UserOptions.sort_option(attributes['sort_preference']) || default
end
def per_page_preference(default = nil)
UserOptions.page_option(attributes['per_page_preference']) || default
end
end
Now you can do the following:
current_user.preference.per_page_preference
# use 10 as the page size if no value is given
current_user.preference.per_page_preference(10)
Try this:
class MessageSortOption
def self.get_cached_option(id)
# store the hash of the options in a class variable
(##option_cache ||= Hash[ *all.collect{|o| [o.id, o]}.flatten])[id]
end
end
class PerPageOption
def self.get_cached_option(id)
# store the hash of the options in a class variable
(##option_cache ||= Hash[ *all.collect{|o| [o.id, o]}.flatten])[id]
end
end
class User
has_one :preference
end
class Preference
def sort_preference
MessageSortOption.get_cached_option(attributes['sort_preference'])
end
def per_page_preference
PerPageOption.get_cached_option(attributes['per_page_preference'])
end
end
Now you can access the preference as follows:
current_user.preference.sort_preference
current_user.preference.per_page_preference
Related
Hello all I would like to use Rails 7 attribute encryption on a model and have a unique key for each record. The main objective is that I would like to delete a key and never be able to decipher the encrypted information (on that record) again if requested.
I tested the code below and was able to encrypt the data in the DB, retrieve, and search, but the removal of the key is still allowing the retrieval of the information. Any help appreciated.
class User < ActiveRecord::Base
after_initialize :user_encryption_key
after_save :create_key_record
attr_accessor :p_encryption_key
def self.instance_encryption_key
if self.respond_to?(:p_encryption_key)
puts "This is the key - #{self&.p_encryption_key}"
self&.p_encryption_key
else
nil
end
end
encrypts :last_name,
deterministic: true,
key: self.instance_encryption_key
def user_encryption_key
if user_id
self.p_encryption_key = get_user_encryption_key
else
# new record without an ID let the after save create the DB
# entry
self.p_encryption_key = create_encryption_key
end
end
def profile_key_name
%{/user_key/#{user_id}}
end
def get_user_encryption_key
Rails.cache.
fetch(user_key_name) {create_encryption_key}
end
def create_encryption_key
ActiveRecord::Encryption::KeyGenerator.new.
generate_random_hex_key(length: 16)
end
def create_key_record
if new_record?
if p_encryption_key.nil?
p_encryption_key = create_encryption_key
end
Rails.cache.
fetch(profile_key_name) {create_encryption_key}
end
end
end
I am having a checklist table with following columns:-
id | product_id | content | archived
Once the user signs up product table is created and corresponding checklist table is also created.
I want to add default 5 entries with data for checklist table for each user after he/she signs up. Any help?
I would solve this with a service object.
# app/services/default_checklist_service.rb
class DefaultChecklistService
attr_accessor :user, :defaults
def initialize(user, defaults = nil)
#user = user
#defaults = defaults || self.class.load_defaults
end
def self.call(user, defaults = nil)
new(user, defaults).call
end
def call
self.defaults.map do |attributes|
user.checklists.create(attributes)
end
end
private
def self.load_defaults
YAML.load_file(Rails.root.join('config', 'default_checklist.yml'))
.try(:[], 'checklists')
end
end
This creates a single purpose object which is easily tested. VS model callbacks which add more responsibilities to an already god like model and which are tricky to test and control when and where they fire.
# config/default_checklists.yml
checklists:
-
foo: 1
bar: 2
-
foo: 2
bar: 3
Note that this will create n (where n is the number of default items) separate insert queries which is not very fast. If the performance becomes an issue than you can use a mass insert instead:
# app/services/default_checklist_service.rb
class DefaultChecklistService
# ...
def call
sql = "INSERT INTO users(user_id, foo, bar) VALUES "
values = defaults.map do |a|
"(#{#user.id}, #{a["foo"]}, #{a["bar"]})"
end
ActiveRecord::Base.connection.execute( sql + values.join(', ') )
end
end
Since creating the defaults is expensive and will slow your tests down I would not use a model callback. Instead call the service from the controller where you actually want the seeding to happen:
class UsersController
def create
#user = User.create(user_params)
if #user.save
DefaultChecklistService.call(#user)
# ...
else
# ...
end
end
end
Use after create callback:
User < ActiveRecord::Base
has_many :check_lists
after_create :add_default_checklist
def add_default_checklist
default_check_lists.each do |check_list_data| # Define way to get default values
check_lists.create(check_list_data)
end
end
end
You'll need to add user_id column to checklist
In my model, I'm trying to dynamically expose objects that are inside an array as a top level attribute. Here's the code snippet:
class Widget < ActiveRecord::Base
# attr_accessor :name
end
class MyModel < ActiveRecord::Base
has_many :widgets
#attr_accessor :widgets
after_initialize :init_widgets
def init_widgets
widgets
widgets.each_with_index do |widget, index|
define_method(widget.name) do
widgets[index]
end
end
end
end
Is there any way for me to define the value of index into the new method I am creating so that it will be associated with the proper index?
I might create an accessor / assignment methods that overload the [] operator:
class BracketOperator
def initialize
#values = (1..100).to_a
end
def [](index)
#values[index]
end
def []=(index, value)
#values[index] = value
end
end
bo = BracketOperator.new
bo[3] # => 4
bo[3] = 17
bo[3] # => 17
So for reference, here's the answer I came up with. Apparently I don't understand scoping for ruby too well. The variable n somehow remains referenced to the n within the each loop. Hence for my original question, I can just use the index variable within the method and it will be mapped to what I am expecting it to be mapped to.
class Test
def setup
names = [ "foo","bar" ]
names.each do |n|
self.class.send :define_method, n do
puts "method is called #{n}!"
end
end
end
end
I am facing a design decision I cannot solve. In the application a user will have the ability to create a campaign from a set of different campaign types available to them.
Originally, I implemented this by creating a Campaign and CampaignType model where a campaign has a campaign_type_id attribute to know which type of campaign it was.
I seeded the database with the possible CampaignType models. This allows me to fetch all CampaignType's and display them as options to users when creating a Campaign.
I was looking to refactor because in this solution I am stuck using switch or if/else blocks to check what type a campaign is before performing logic (no subclasses).
The alternative is to get rid of CampaignType table and use a simple type attribute on the Campaign model. This allows me to create Subclasses of Campaign and get rid of the switch and if/else blocks.
The problem with this approach is I still need to be able to list all available campaign types to my users. This means I need to iterate Campaign.subclasses to get the classes. This works except it also means I need to add a bunch of attributes to each subclass as methods for displaying in UI.
Original
CampaignType.create! :fa_icon => "fa-line-chart", :avatar=> "spend.png", :name => "Spend Based", :short_description => "Spend X Get Y"
In STI
class SpendBasedCampaign < Campaign
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
Neither way feels right to me. What is the best approach to this problem?
A not very performant solution using phantom methods. This technique only works with Ruby >= 2.0, because since 2.0, unbound methods from modules can be bound to any object, while in earlier versions, any unbound method can only be bound to the objects kind_of? the class defining that method.
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
enum :campaign_type => [:spend_based, ...]
def method_missing(name, *args, &block)
campaign_type_module.instance_method(name).bind(self).call
rescue NameError
super
end
def respond_to_missing?(name, include_private=false)
super || campaign_type_module.instance_methods(include_private).include?(name)
end
private
def campaign_type_module
Campaigns.const_get(campaign_type.camelize)
end
end
# app/models/campaigns/spend_based.rb
module Campaigns
module SpendBased
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
end
Update
Use class macros to improve performance, and keep your models as clean as possible by hiding nasty things to concerns and builder.
This is your model class:
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
include CampaignAttributes
enum :campaign_type => [:spend_based, ...]
campaign_attr :name, :fa_icon, :avatar, ...
end
And this is your campaign type definition:
# app/models/campaigns/spend_based.rb
Campaigns.build 'SpendBased' do
name 'Spend Based'
fa_icon 'fa-line-chart'
avatar 'spend.png'
end
A concern providing campaign_attr to your model class:
# app/models/concerns/campaign_attributes.rb
module CampaignAttributes
extend ActiveSupport::Concern
module ClassMethods
private
def campaign_attr(*names)
names.each do |name|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}
Campaigns.const_get(campaign_type.camelize).instance_method(:#{name}).bind(self).call
end
EOS
end
end
end
end
And finally, the module builder:
# app/models/campaigns/builder.rb
module Campaigns
class Builder < BasicObject
def initialize
#mod = ::Module.new
end
def method_missing(name, *args)
value = args.shift
#mod.send(:define_method, name) { value }
end
def build(&block)
instance_eval &block
#mod
end
end
def self.build(module_name, &block)
const_set module_name, Builder.new.build(&block)
end
end
Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append