Rails association with delegate methods. possible bug with AR? - ruby-on-rails

Isolated this question into it's own rails app: and added a git repo as an example
Module:
module SeoMeta
def self.included(base)
base.extend(ClassMethods)
base.send :include, InstanceMethods
end
module ClassMethods
def is_seo_meta
has_one :meta,
class_name: SeoMetum,
as: :metumable,
dependent: :destroy,
autosave: true
delegate :browser_title, :meta_description, :meta_author,
:meta_keywords, :browser_title=, :meta_keywords=,
:meta_description=, :meta_author=,
to: :meta
after_save :save_meta_tags!
attr_accessible :browser_title, :meta_keywords,
:meta_description, :meta_author
end
end
module InstanceMethods
class << self
def included(base)
base.module_eval do
alias :original_meta_method :meta
end
end
end
def meta
find_meta || build_meta
end
def find_meta
#meta ||= ::SeoMetum.where(metumable_type: self.class.name, metumable_id: self.id).first
end
def build_meta
#meta ||= ::SeoMetum.new(metumable_type: self.class.name, metumable_id: self.id)
end
def save_meta_tags!
meta.metumable_id ||= self.id
meta.save
end
end
end
Models:
class User < ActiveRecord::Base
include SeoMeta
is_seo_meta
has_many :collections
accepts_nested_attributes_for :collections
def collection
default_collection = self.collections.first
default_collection ||= self.collections.create
default_collection
end
end
class Collection < ActiveRecord::Base
include SeoMeta
is_seo_meta
belongs_to :user
end
class SeoMetum < ActiveRecord::Base
attr_accessible :browser_title, :meta_author, :meta_description, :meta_keywords,
:metumable, :metumable_id, :metumable_type
belongs_to :metumable, polymorphic: true
end
Rspec Tests:
context "user and collection" do
context 'responds to' do
it 'meta_description' do
user.collection.respond_to?(:meta_description).should be_true
end
it 'browser_title' do
user.collection.respond_to?(:browser_title).should be_true
end
end
context 'individual allows us to assign to' do
it 'meta_description' do
the_collection = user.collection
the_collection.meta_description = 'This is my description of the user for search results.'
the_collection.meta_description.should == 'This is my description of the user for search results.'
end
it 'browser_title' do
the_collection = user.collection
the_collection.browser_title = 'An awesome browser title for SEO'
the_collection.browser_title.should == 'An awesome browser title for SEO'
end
end
context 'allows us to assign to' do
it 'meta_description' do
user.collection.meta_description = 'This is my description of the user for search results.'
user.collection.meta_description.should == 'This is my description of the user for search results.'
end
it 'browser_title' do
user.collection.browser_title = 'An awesome browser title for SEO'
user.collection.browser_title.should == 'An awesome browser title for SEO'
end
end
context 'allows us to update' do
it 'meta_description' do
user.collection.meta_description = 'This is my description of the user for search results.'
user.collection.save
user.collection.reload
user.collection.meta_description.should == 'This is my description of the user for search results.'
end
it 'browser_title' do
user.collection.browser_title = 'An awesome browser title for SEO'
user.collection.save
user.collection.reload
user.collection.browser_title.should == 'An awesome browser title for SEO'
end
end
end
The first four tests pass and the second four fail. I think it might be a bug with rails polymorphic associations but I'm not sure how to isolate it further. Comments on my module design are also appreciated.
Best,
Scott

The problem in your code is in this place:
class User
#Other stuff
#HERE!
def collection
default_collection = self.collections.first
default_collection ||= self.collections.create
default_collection
end
end
Every time you call collection method you look up the first collection in the database. Therefore even if you set some values by user.collection.meta_description = "abc" then later when you call user.collection it's not the same collection object cause it's been new lookup from the database. Therefore all the attributes not saved to the database are gone. You can see this by looking at logs - every time you call user.collection you get new hit to db and also every time you call user.collection.object_id you get a different value.
You can fix it by doing something like
def collection
return #collection if #collection
default_collection = self.collections.first
default_collection ||= self.collections.create
#collection = default_collection
end

Related

Validation with override setter find_or_initialize_by

In a form, I have a multiselect dropdown that I can manually sort. I override the setter method to achieve this. However, when I add validation, I get the presence error saying that "Tags cannot be blank" even when there are tags selected in the dropdown.
params: {tag_ids: ['', '1', '3']}
I think it has something to do with validating before saving which happens in the override function. Any suggestions in how I can make it to run the validations after running the setter method or vice-versa? Thanks.
Controller:
def update
if #article.update(article_params)
redirect_to articles_path
end
end
def article_params
params.require(:article).permit(:title, tag_ids: [])
end
Models:
class Article
has_many: :article_tags
has_many: :tags, through: :article_tags
validates: :tag_ids, presence: true
def tag_ids=(ids)
ids = ids.reject(&:blank?)
self.article_tags.where.not(article_tag_id: ids).destroy_all
ids.each_with_index do |id, idx|
article_tag= self.article_tags.find_or_initialize_by(article_tag_id: id)
if self.new_record?
article_tag.ordinal = idx + 1
else
article_tag.update(ordinal: idx + 1)
end
end
end
def tags
super.joins(:article_tags).order("article_tags.ordinal").distinct
end
end
class ArticleTag
belongs_to: :article
belongs_to: :tag
end

Rails 5: permission for user to create record through permission table with Pundit

In my app I have Permission table, which stores all the logic what User can do. With Pundit I want to allow User to create new Campaign if Permission table allows. User can access Campaigns and create new, if Permission table contains this info:
permitable_type: Sysmodule // another table where I store info on System sections, where Campaigns is one of
permitable_id: 2 // means Campaigns from Sysmodule
level: 3 // means User can edit something in Campaigns section
So far I keep getting error "Pundit::NotDefinedError", unable to find policy of nil policies/application_policy.rb is standart, no changes.
Obviously I am doing sothing wrong. How do I do this authorization correctly? Many thanks for any help! I am on Rails 5 + Pundit.
models/permission.rb
class Permission < ApplicationRecord
belongs_to :permitable, polymorphic: true
belongs_to :user
enum level: {owner: 1, view: 2, edit: 3}
end
models/user.rb
has_many :permissions
has_many :campaigns, through: :permissions, source: :permitable, source_type: 'Campaign' do
def owner_of
where('`permissions`.`level` & ? > 0', Permission::owner )
end
end
has_many :sysmodules, through: :permissions, source: :permitable, source_type: 'Sysmodule' do
def can_access
where('`permissions`.`level` & ? > 1', Permission::can_access )
end
end
controllers/campaigns_controller.rb
def new
#campaign = Campaign.new
authorize #campaign
end
policies/campaign_policy.rb
class CampaignPolicy < ApplicationPolicy
attr_reader :user, :campaign, :permission
#user = user
#permission = permission
end
def new?
user.permission? ({level: 3, permitable_type: "Sysmodule", permitable_id: 2})
end
views/campaigns/index.html.erb
<% if policy(#campaign).new? %>
</li>
<li><%= link_to "New campaign", new_campaign_path(#campaign) %></li>
</li>
<% end %>
Instead of dealing directly with what permissions a user should have try thinking of it what roles users can have in a system.
This makes it much easier to create authorization rules that map to real world problems.
Lets imagine an example where we have users and groups. The rules are as follows:
groups can be created by any user
the user that creates the group automatically becomes an admin
groups are private
only admins or members can view a group
only an admin can modify a group
The models:
class User < ApplicationRecord
rolify
end
class Group < ApplicationRecord
resourcify
end
The policy:
class GroupsPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.with_roles([:admin, :member], current_user)
end
end
def show?
user.has_role?([:member, :admin], record)
end
def index?
true
end
def create?
true # any user can create
end
def new?
create?
end
def update?
user.has_role?(:admin, record)
end
def edit?
update?
end
def destroy?
update?
end
end
The controller
class GroupsController < ApplicationController
respond_to :html
before_action :autenticate!
before_action :set_group!, only: [:show, :edit, :update, :destroy]
def show
respond_with(#group)
end
def index
#groups = policy_scope(Group.all)
respond_with(#groups)
end
def new
#group = authorize( Group.new )
end
def create
#group = authorize( Group.new(group_attributes) )
if #group.save
current_user.add_role(:member, #group)
current_user.add_role(:admin, #group)
end
respond_with(#group)
end
def edit
end
def update
#group.update(group_params)
respond_with(#group)
end
def destroy
#group.destroy
respond_with(#group)
end
private
def set_group!
#group = authorize( Group.find(params[:id]) )
end
def group_params
params.require(:group).permit(:name)
end
end

Testing Associations With RSpec in a Chess Application

I'm currently working with a small team on an open source Rails 4 chess application, and I'm trying to test out various possible piece moves in RSpec (including special cases such as en passant and castling). A senior web developer suggested that I use a separate table to keep track of the moves taken in each game of chess. After taking him up on his suggestion, I'm having trouble testing out valid moves, as shown in the error message below. I suspect that it might be a problem with my associations, but the teammates that I was able to talk to about this were unsure about the cause.
Any help would be greatly appreciated.
Error message:
Failures:
1) PiecesController Action: pieces#update should create a move when a move is valid
Failure/Error: #current_game ||= current_piece.game
NoMethodError:
undefined method `game' for nil:NilClass
# ./app/controllers/pieces_controller.rb:36:in `current_game'
# ./app/controllers/pieces_controller.rb:40:in `require_authorized_for_current_game'
# ./spec/controllers/pieces_controller_spec.rb:12:in `block (3 levels) in <top (required)>'
The test:
RSpec.describe PiecesController, type: :controller do
describe "Action: pieces#update" do
it "should create a move when a move is valid" do
user_sign_in
game = FactoryGirl.create(:game)
# Test a white pawn's movement on its first turn:
piece = FactoryGirl.create(:piece)
move = FactoryGirl.create(:move)
# Why can't I call game.id below?
patch :update, :id => game.id, :pieces => { }
piece_params = { :x_position => piece.x_position, :y_position => piece.y_position, :type => "Pawn" }
if piece.valid_move?(piece_params)
...
end
end
end
private
def user_sign_in
user = FactoryGirl.create(:user)
sign_in user
end
end
Associations:
class Game < ActiveRecord::Base
has_many :pieces
has_many :moves, through: :pieces
belongs_to :white_player, class_name: 'User', foreign_key: :white_player_id
belongs_to :black_player, class_name: 'User', foreign_key: :black_player_id
...
end
class Piece < ActiveRecord::Base
belongs_to :game
has_many :moves
def valid_move?(params)
...
end
...
end
class Move < ActiveRecord::Base
belongs_to :piece
end
Factories:
FactoryGirl.define do
factory :user do
...
end
factory :game do
association :white_player, factory: :user
association :black_player, factory: :user
turn 1
end
factory :piece do
association :game
...
end
# Set up an initially empty move, then adjust the values after checking that a piece can be moved:
factory :move do
association :piece
...
end
end
The controller:
class PiecesController < ApplicationController
before_action :authenticate_user!
before_action :require_authorized_for_current_game, only: [:update]
before_action :require_authorized_for_current_piece, only: [:update]
def update
...
end
...
private
def current_piece
#current_piece ||= Piece.find_by_id(params[:id])
end
...
def piece_params
params.require(:piece).permit(:x_position, :y_position, :type, :captured)
end
def current_game
#current_game ||= current_piece.game
end
def require_authorized_for_current_game
if current_game.white_player != current_user && current_game.black_player != current_user
render text: 'Unauthorized', status: :unauthorized
end
end
end

scope and class method with the same name

I'm trying to use the Mailboxer Gem and I ran into problems when creating a conversations controller.
Now when I call current_user.mailbox.inbox I get the following:
You tried to define a scope named "new" on the model "Mailboxer::Conversation", but Active Record already defined a class method with the same name.
I have removed my conversations controller and actions but this is still showing.
Is it possible that a Gem Scope and method are clashing? perhaps the rails g controller added files else where?
Below is the model where the scope is defined. (This is from the Mailboxer Gem)
class Mailboxer::Conversation < ActiveRecord::Base
self.table_name = :mailboxer_conversations
attr_accessible :subject if Mailboxer.protected_attributes?
has_many :opt_outs, :dependent => :destroy, :class_name => "Mailboxer::Conversation::OptOut"
has_many :messages, :dependent => :destroy, :class_name => "Mailboxer::Message"
has_many :receipts, :through => :messages, :class_name => "Mailboxer::Receipt"
validates :subject, :presence => true,
:length => { :maximum => Mailboxer.subject_max_length }
before_validation :clean
scope :participant, lambda {|participant|
where('mailboxer_notifications.type'=> Mailboxer::Message.name).
order("mailboxer_conversations.updated_at DESC").
joins(:receipts).merge(Mailboxer::Receipt.recipient(participant)).uniq
}
scope :inbox, lambda {|participant|
participant(participant).merge(Mailboxer::Receipt.inbox.not_trash.not_deleted)
}
scope :sentbox, lambda {|participant|
participant(participant).merge(Mailboxer::Receipt.sentbox.not_trash.not_deleted)
}
scope :new, lambda {|participant|
participant(participant).merge(Mailboxer::Receipt.trash)
}
scope :unread, lambda {|participant|
participant(participant).merge(Mailboxer::Receipt.is_unread)
}
scope :not_trash, lambda {|participant|
participant(participant).merge(Mailboxer::Receipt.not_trash)
}
#Mark the conversation as read for one of the participants
def mark_as_read(participant)
return unless participant
receipts_for(participant).mark_as_read
end
#Mark the conversation as unread for one of the participants
def mark_as_unread(participant)
return unless participant
receipts_for(participant).mark_as_unread
end
#Move the conversation to the trash for one of the participants
def move_to_trash(participant)
return unless participant
receipts_for(participant).move_to_trash
end
#Takes the conversation out of the trash for one of the participants
def untrash(participant)
return unless participant
receipts_for(participant).untrash
end
#Mark the conversation as deleted for one of the participants
def mark_as_deleted(participant)
return unless participant
deleted_receipts = receipts_for(participant).mark_as_deleted
if is_orphaned?
destroy
else
deleted_receipts
end
end
#Returns an array of participants
def recipients
return [] unless original_message
Array original_message.recipients
end
#Returns an array of participants
def participants
recipients
end
#Originator of the conversation.
def originator
#originator ||= original_message.sender
end
#First message of the conversation.
def original_message
#original_message ||= messages.order('created_at').first
end
#Sender of the last message.
def last_sender
#last_sender ||= last_message.sender
end
#Last message in the conversation.
def last_message
#last_message ||= messages.order('created_at DESC').first
end
#Returns the receipts of the conversation for one participants
def receipts_for(participant)
Mailboxer::Receipt.conversation(self).recipient(participant)
end
#Returns the number of messages of the conversation
def count_messages
Mailboxer::Message.conversation(self).count
end
#Returns true if the messageable is a participant of the conversation
def is_participant?(participant)
return false unless participant
receipts_for(participant).any?
end
#Adds a new participant to the conversation
def add_participant(participant)
messages.each do |message|
Mailboxer::ReceiptBuilder.new({
:notification => message,
:receiver => participant,
:updated_at => message.updated_at,
:created_at => message.created_at
}).build.save
end
end
#Returns true if the participant has at least one trashed message of the conversation
def is_trashed?(participant)
return false unless participant
receipts_for(participant).new.count != 0
end
#Returns true if the participant has deleted the conversation
def is_deleted?(participant)
return false unless participant
return receipts_for(participant).deleted.count == receipts_for(participant).count
end
#Returns true if both participants have deleted the conversation
def is_orphaned?
participants.reduce(true) do |is_orphaned, participant|
is_orphaned && is_deleted?(participant)
end
end
#Returns true if the participant has trashed all the messages of the conversation
def is_completely_trashed?(participant)
return false unless participant
receipts_for(participant).new.count == receipts_for(participant).count
end
def is_read?(participant)
!is_unread?(participant)
end
#Returns true if the participant has at least one unread message of the conversation
def is_unread?(participant)
return false unless participant
receipts_for(participant).not_trash.is_unread.count != 0
end
# Creates a opt out object
# because by default all particpants are opt in
def opt_out(participant)
return unless has_subscriber?(participant)
opt_outs.create(:unsubscriber => participant)
end
# Destroys opt out object if any
# a participant outside of the discussion is, yet, not meant to optin
def opt_in(participant)
opt_outs.unsubscriber(participant).destroy_all
end
# tells if participant is opt in
def has_subscriber?(participant)
!opt_outs.unsubscriber(participant).any?
end
protected
#Use the default sanitize to clean the conversation subject
def clean
self.subject = sanitize subject
end
def sanitize(text)
::Mailboxer::Cleaner.instance.sanitize(text)
end
end
As you can see you have:
scope :new, lambda {|participant|
participant(participant).merge(Mailboxer::Receipt.trash)
}
Which is already defined in ActiveRecord::Base
Mailboxer::Conversation.new
exists as a class method and a scope. Changing the scope name should be enough to fix it.
As pointed in the comments, reinstall the gem will be the fastest solution.

Setter methods can't see child built in parents after_initialize callback

An Email has many Variants (for ab testing purposes) and always has one set as the master
I want to ensure an email always has a master variant built on initialization.
I also want to delegate attr accessors 'subject' and 'body' to the master variant.
I originally tried using
delegate :subject, :body, to: :master
but rails complained master was nil.
So I tried hand rolling my own subject= setter method and via pry I found that whilst my master is being set in the after_initialize callback, the subsequent call to subject= complains master is nil. I dont understand why.
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
def subject=(str)
master.subject = str # Rails says master is nil here
end
#
# Callbacks
#-----------------------------------------
after_initialize :ensure_master
def ensure_master
return unless new_record?
self.master ||= build_master
end
end
Email.new(:subject => 'yah') # undefined method `subject=' for nil:NilClass
when your email instance is initialized your master is nil, you need to trigger build_master before you set anything on it
how about:
# app/models/email.rb
class Email < ActiveRecord::Base
def subject=(str)
master.subject = str # Rails says master is nil here
end
def master
super || build_master
end
end
Don't know much when and why you need after_initialize callback in your project, but if you sure you need this functionality, I would consider to use custom service class to achieve this
# app/models/email.rb
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
def subject=(str)
master.subject = str # this way Rails won't says master is nil here
end
end
# app/lib/email_builder.rb
class EmailBuilder
attr_reader :args
def self.build(args={})
new(args).build
end
def initialize(args)
#args = args
end
def build
email = Email.new
email.build_master
email.attributes = args
email
end
end
email = EmailBuilder.build subject: 'yah'
email.class # => Email
...and another variation of this for persisted record
Update
or even better
# app/models/email.rb
class Email < ActiveRecord::Base
has_one :master,
-> { where is_master: true },
class_name: 'Tinycourse::Variant',
dependent: :destroy,
inverse_of: :email
end
# app/lib/email_builder.rb
class EmailBuilder
attr_reader :args, :subject
def self.build(args={})
new(args).build
end
def initialize(args)
#subject = args.fetch(:subject)
#args = args
end
def build
email = Email.new args
email.build_master
email.master.subject = subject
email
end
end
email = EmailBuilder.build subject: 'yah'
email.class # => Email

Resources