How can I override destroy_if_previously_invited in devise_invitable? - ruby-on-rails

I'm using devise_invitable with devise in my Rails 3 app. I want to give my users the ability to invite other users and group those invited users in advance of the invitees signing up.
My problem is that once the invitee comes along and signs up (but doesn't use the invite URL), the destroy_if_previously_invited around_filter comes along, destroys the original user record and recreates a new record for the user, retaining the invitation data but not transferring the user_groups records along with it.
I'd like to simply override this around_filter by doing a search for any user_groups that match the originally invited user_id and saving them with the newly created user_id.
I keep getting the error:
LocalJumpError in Users::RegistrationsController#create
no block given (yield)
My route looks like this:
devise_for :users, :controllers => { :registrations => "users/registrations" }
I've set this as the override in app/controllers/users/registrations_controller.rb:
class Users::RegistrationsController < Devise::RegistrationsController
around_filter :destroy_if_previously_invited, :only => :create
private
def destroy_if_previously_invited
invitation_info = {}
user_hash = params[:user]
if user_hash && user_hash[:email]
#user = User.find_by_email_and_encrypted_password(user_hash[:email], '')
if #user
invitation_info[:invitation_sent_at] = #user[:invitation_sent_at]
invitation_info[:invited_by_id] = #user[:invited_by_id]
invitation_info[:invited_by_type] = #user[:invited_by_type]
invitation_info[:user_id] = #user[:id]
#user.destroy
end
end
# execute the action (create)
yield
# Note that the after_filter is executed at THIS position !
# Restore info about the last invitation (for later reference)
# Reset the invitation_info only, if invited_by_id is still nil at this stage:
#user = User.find_by_email_and_invited_by_id(user_hash[:email], nil)
if #user
#user[:invitation_sent_at] = invitation_info[:invitation_sent_at]
#user[:invited_by_id] = invitation_info[:invited_by_id]
#user[:invited_by_type] = invitation_info[:invited_by_type]
user_groups = UserGroup.find_all_by_user_id(invitation_info[:user_id])
for user_group in user_groups do
user_group.user_id = #user.id
user_group.save!
end
#user.save!
end
end
end
I may also just be going about this all wrong. Any ideas would be appreciated.

First, there is no need to set the around_filter again, as devise_invitable already sets this. All you need to do is redefine the methods in your UsersController and those will be called instead of the devise_invitable ones.
Second, it looks like you are combining the two methods when they should remain seperate and overridden (I am basing this off latest version of devise_invitatable 1.1.1)
Try something like this:
class Users::RegistrationsController < Devise::RegistrationsController
protected
def destroy_if_previously_invited
hash = params[resource_name]
if hash && hash[:email]
resource = resource_class.where(:email => hash[:email], :encrypted_password => '').first
if resource
#old_id = resource.id #saving the old id for use in reset_invitation_info
#invitation_info = Hash[resource.invitation_fields.map {|field|
[field, resource.send(field)]
}]
resource.destroy
end
end
end
def reset_invitation_info
# Restore info about the last invitation (for later reference)
# Reset the invitation_info only, if invited_by_id is still nil at this stage:
resource = resource_class.where(:email => params[resource_name][:email], :invited_by_id => nil).first
if resource && #invitation_info
resource.invitation_fields.each do |field|
resource.send("#{field}=", #invitation_info[field])
end
### Adding your code here
if #old_id
user_groups = UserGroup.find_all_by_user_id(#old_id)
for user_group in user_groups do
user_group.user_id = resource.id
user_group.save!
end
end
### End of your code
resource.save!
end
end

Related

Rails 5 - service class to find users who have email addresses that match an organisation's domain name

I am trying to learn how to use Rails 5 (generally) but specifically, I'm trying to learn how to use service classes.
I'm trying to write a service class that maps a user's given email address (user's have an attribute called :email) to organisation's domain names. Organisations have attributes called :email_format. I use that attribute to hold the part of the email address that follows the "#".
When a user creates an account, I want to take their email address that they use to sign up with, and match the bit after the # to each of the organisations that I know about and try to find a matching one.
My attempts at this are plainly wrong, but I'm struggling to figure out why.
I have resources called User, Organisation and OrgRequest. The associations are:
User
belongs_to :organisation, optional: true
has_one :org_request
Organisation
has_many :org_requests
has_many :users
OrgRequest
belongs_to :user
belongs_to :organisation
I have tried to write a service class as:
class User::OrganisationMapperService #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
if matching_organisation.present?
# user.organisation_request.new(organisation_id: matching_organisation.id)
# user.update_attributes!(organisation_id: matching_organisation.id)
else
#SystemMailer.unmatched_organisation(user: user).deliver_now
end
end
private
attr_accessor :user
def matching_organisation
# User::OrganisationMapperService.new(user).matching_organisation
User::OrganisationMapperService.new(user: user)
end
end
I then have an org requests controller with:
class Users::OrgRequestsController < ApplicationController
before_action :authenticate_user!, except: [:new, :create, :requested]
before_action :set_org_request, only: [:approved, :rejected, :removed]
# skip_before_action :redirect_for_unrequested_organisation
# skip_before_action :redirect_for_unknown_organisation
def index
organisation = Organisation.find_by(owner_id: current_user.id)
return redirect_to(user_path(current_user.id)) if organisation.nil?
#org_requests = organisation.org_requests
end
def new
#all_organisations = Organisation.select(:title, :id).map { |org| [org.title, org.id] }
#org_request = OrgRequest.new#form(OrganisationRequest::Create)
matched_organisation = User::OrganisationMapperService.new(current_user).matching_organisation
#org_request.organisation_id = matched_organisation.try(:id)
end
def create
#org_request = OrgRequest.new(org_request_params)
#org_request.user_id = current_user.id
if #org_request.save
OrgRequest::ProcessService.new(org_request).process
return redirect_to(user_path(current_user),
flash[:alert] => 'Your request is being processed.')
else
# Failure scenario below
#all_organisations = Organisation.select(:title, :id).map { |org| [org.title, org.id] }
render :new
end
end
def requested
# Need help - if this is contained in form inputs - how do i stop from overriding the submit path?
redirect_to(user_path(current_user))
#not sure about this - a similar redirect isnt required for articles or project create
end
def approve
#org_request = current_user.organisation.org_requests.find(params[:id])
if #org_request.state_machine.transition_to!(:approved)
flash[:notice] = "You've added this member."
redirect_to org_requests_path
else
flash[:error] = "You're not able to manage this organisation's members"
redirect_to :index
end
end
def remove
#org_request = current_user.organisation.org_requests.find(params[:id])
if #org_request.state_machine.transition_to!(:removed)
flash[:notice] = "Removed from the organisation."
redirect_to action: :index
# format.html { redirect_to :index }
# format.json { render :show, status: :ok, location: #project }
# redirect_to action: :show, id: project_id
# add mailer to send message to article owner that article has been approved
else
flash[:error] = "You're not able to manage this organisation's members"
redirect_to(user_path(current_user))
# redirect_to action: :show, id: project_id
end
end
def decline
#org_request = current_user.organisation.org_requests.find(params[:id])
if #org_request.state_machine.transition_to!(:declined)
flash[:notice] = "You're not eligible to join this organisation"
redirect_to action: :index
# redirect_back(fallback_location: root_path)
# format.html { redirect_to :index }
# redirect_to action: :show, id: organisation_request.profile
# add mailer to send message to article owner that article has been approved
else
flash[:error] = "You're not able to manage this organisation's members"
redirect_to(user_path(current_user))
# redirect_to action: :show, id: organisation_request.profile
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_org_request
#org_request = OrgRequest.find(params[:id])
authorize #org_request
end
# Never trust parameters from the scary internet, only allow the white list through.
def org_request_params
params.require(:org_request).permit(:organisation_id, :name) # Need help - not sure if I need to put user id and organisation id in this permission
end
end
I can't figure out another approach to this. When I try this, I get this error:
wrong number of arguments (given 1, expected 0)
The error message highlights line 7 of my service class, which has:
def initialize(user: u)
self.user = user
end
I have previously asked questions about this problem here: superclass mismatch for class User - inheriting from ActiveRecord::Base
but I haven't managed to catch the drift of the advice or what is causing the problem. This attempt is a mash up of suggestions that I have gleaned from at least 10 different tutorials - so I appreciate that its highly unlikely to be correct, but I'm struggling to understand how the different parts of this work to know what to try differently.
Can anyone give me a steer on how to try to progress this attempt?
Organisation mapper decorator has:
class User < ActiveRecord::Base
class OrganisationMapper < ::ApplicationDecorator
def matching_organisation
#matching_organisation ||= Organisation.by_email_format(email_format).first
end
def email_format
user.email.split('#').last
end
private
def user
#model
end
end
end
Application decorator has:
class ApplicationDecorator
def initialize(model)
#model = model
end
private
def method_missing(method, *args)
args.empty? ? #model.send(method) : #model.send(method, *args)
end
end
Org request service class has:
class OrgRequest::CreateService < ActiveRecord::Base
attr_accessor :org_request
def self.call(user_id: user_id, organisation_id: org_id)
new(user_id: user_id, organisation_id: organisation_id).call
end
def initialize(user_id: user_id, organisation_id: org_id)
self.user_id = user_id
self.organisation_id = organisation_id
end
def call
self.org_request \
= OrgRequest.new(user_id: current_user.id,
organisation_id: params[:org_request][:organisation_id])
if org_request.save
# send the email
true
else
false
end
end
end
NEXT ATTEMPT
I have tried every variation on this that I can think of. Nothing I'm trying makes any sense to me but I can't make sense out of any examples that I can find.
My service class currently has:
class User::OrganisationMapperService #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
# if matching_organisation.present?
# user.org_request.new(organisation_id: matching_organisation.id)
# if found create a request for that user to enter the organisation
if match_domain.present?
OrgRequest.create(user: #user, organisation_id: #organisation_domain.organisation.id) #if organisation
# user.update_attributes!(organisation_id: matching_organisation.id)
else
#SystemMailer.unmatched_organisation(user: user).deliver_now
end
end
private
attr_accessor :user
# def matching_organisation
# # User::OrganisationMapperService.new(user).matching_organisation
# User::OrganisationMapperService.new(user: user).Organisation.by_email_format(email_format).first
# end
# def matching_organisation
# #matching_organisation ||= Organisation.by_email_format(email_format).first
# end
def user_domain
user.email.split('#').last
end
def organisation_domain
#organisation = Organisation.find_by(email_format: user_domain)
end
# def user_email_domain
# # extract domain from users email
# user_email_domain = #user.email.split('#').last
# end
def match_domain
return unless #user_domain == #organisation.email_format
end
# find an organisation with a matching domain
# end
end
It's plainly wrong. The error message says:
NameError - undefined local variable or method `organisation' for #<User::OrganisationMapperService:0x007faec6ec06b8>
I can't make sense of the error message either because I have put '#' in front of every instance of 'organisation' just to try to make that error go away. It doesn't.
Please help.
ANOTHER COMPLETELY SENSELESS ERROR MESSAGE
I had another go at trying to write the method to check whether an email domain matches an organisation's email format in my service class.
The call method now has:
def call
if user_domain == Organisation.email_format.any?
OrgRequest.create(user: #user, organisation_id: #organisation_domain.organisation.id) #if organisation
else
end
end
The error message in the console says:
NoMethodError - undefined method `email_format' for #<Class:0x007faec72d8ac0>
That has to be nonsense because my organisation table has an attribute in it called :email_format. In the console, I can write:
o = Organisation.first.email_format
Organisation Load (3.3ms) SELECT "organisations".* FROM "organisations" ORDER BY "organisations"."id" ASC LIMIT $1 [["LIMIT", 1]]
That gives me the result I'm looking for.
I'm trying (to my wits end) to learn how rails communicates. I can't make any sense of any of it.
NEXT ATTEMPT
Next guess of a go at the call method:
def call
if user_domain == organisation_domain?
OrgRequest.create(user: #user, organisation_id: #organisation_domain.organisation.id) #if organisation
else
end
Produces this error:
NoMethodError - undefined method `organisation_domain?' for #<User::OrganisationMapperService:0x007faec3be3600>:
I can't seem to find a single form of expression that doesnt produce this error.
The problem appears to be in the following line:
matched_organisation = User::OrganisationMapperService.new(current_user).matching_organisation
It should be this instead:
matched_organisation = User::OrganisationMapperService.new(user: current_user).matching_organisation
I had a session on code mentor. This is the answer. I hope it might help someone else who is trying to learn.
class User::OrganisationMapperService #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
if organisation_domain.present?
OrgRequest.create(user: #user, organisation_id: organisation_domain.id) #if organisation
else
end
end
private
attr_accessor :user
def user_domain
user.email.split('#').last
end
def organisation_domain
#organisation ||= Organisation.find_by(email_format: user_domain)
end
end

How can I redirect a request to the record of the current_user and mask route at same time?

Each User has_one :family_tree.
So the family_tree route looks like a normal resources :family_trees.
I have a route that looks like this:
get "dashboard/my_tree" => "dashboard#my_tree", as: :my_tree, path: "/my_tree"
What I want to happen is, whenever someone goes to family_tree/:my_id they should be redirected to (or just shown the URL path for) /my_tree. Please note: that the :my_id is the ID of the family_tree that belongs to the current_user.
The issue is that my FamilyTree#Show controller action looks like this:
def show
#user = #family_tree.user
#memberships = #family_tree.memberships
#memberships_grouped_by_relations = #memberships.includes(user: :family_tree).group_by(&:relation)
#nodes = #family_tree.nodes
render "dashboard/my_tree"
end
And my DashboardController#MyTree looks like this:
def my_tree
#user = current_user
#family_tree = #user.family_tree
#memberships_grouped_by_relations = #family_tree.memberships.group_by(&:relation)
end
Both work, but I just don't know how to mask the URL of family_tree/51 to redirect to my_tree. But, obviously, I don't want all requests to family_tree/:id to show /my_tree. E.g. if the family_tree associated with the current_user is id=51, then when that user goes to family_tree/52, that URL should say 'family_tree/52`.
# app/controllers/family_trees_controller.rb
class FamilyTreesController < ApplicationController
# family_tree GET /family_trees/:id(.:format)
#
# #note !IMPORTANT
# Should only have 1 or 2 instance vars per action
def show
# #user = #family_tree.user #=> not needed, available on the primary instance var (#family_tree)
if current_user == family_tree.user
redirect_to my_tree_index_path
else
# #memberships = #family_tree.memberships
# #memberships_grouped_by_relations #=> too long of a name!
# There is currently only one grouped membership,
# why not rename it to:
#grouped_memberships = family_tree.memberships.includes(user: :family_tree).group_by(&:relation)
# Shouldn't create another ivar if it's available on the primary ivar
# #nodes = #family_tree.nodes
end
end
protected
def family_tree
#family_tree ||= FamilyTree.find(params[:id])
end
end
# app/controllers/my_trees_controller.rb
class MyTreesController < ApplicationController
# my_tree_index GET /my_tree(.:format)
def index
#grouped_memberships = current_user.family_tree.memberships.group_by(&:relation)
end
end
And the routes:
# config/routes.rb
My::Application.routes.draw do
resources :family_trees
resources :my_tree, only: :index
end

Rails - Limited Browsing of User Attachments using ckeditor Gem w/ CanCan & Paperclip

I have everything working correctly, but now I want to limit some user Abilities to perform some attachment actions.
Specifically, the ability to limit the viewing of all uploaded attachments, to those actually uploaded by the User.
Here is the applicable snippet from ability.rb I tried ...
if user.id
can :access, :ckeditor
can [:read, :create, :destroy], Ckeditor::Picture, assetable_id: user.id
can [:read, :create, :destroy], Ckeditor::AttachmentFile, assetable_id: user.id
end
The situation arises when I am using the CKeditor UI, click the Image button, and then click the Browse Server button to see the previously uploaded images -- right now the image browser shows the uploads of all users. I would like the viewed images to be limited to those of the current_user only.
Since the Ckeditor table saves the assetable_id of the attachment (i.e. the user.id), and the logic above does not work on its own, I'm guessing some custom Controller logic is also needed here.
Thanks.
I was able to solve this issue with custom Ckeditor controllers & some guidance from here:
https://github.com/galetahub/ckeditor/issues/246
First I needed to make copies of the Ckeditor controllers pictures_controller.rb & attachment_files_controller.rb and place them here:
/app/controllers/ckeditor/
Then a few updates to their suggestions to update index were necessary, particularly picture_model.find_all needed to be picture_adapter.find_all in pictures_controller.rb (and similarly attachment_file_adapter.find_all in attachment_files_controller.rb)
The key to it all is setting the proper scope with: ckeditor_pictures_scope(assetable_id: ckeditor_current_user) & ckeditor_attachment_files_scope(assetable_id: ckeditor_current_user)
Once these revisions are in place, the file browsers for pictures & attachments show only the appropriate files for that user.
Here are the revised files ... the changes are on line 4 of both.
/app/controllers/ckeditor/pictures_controller.rb
class Ckeditor::PicturesController < Ckeditor::ApplicationController
def index
#pictures = Ckeditor.picture_adapter.find_all(ckeditor_pictures_scope(assetable_id: ckeditor_current_user))
#pictures = Ckeditor::Paginatable.new(#pictures).page(params[:page])
respond_with(#pictures, :layout => #pictures.first_page?)
end
def create
#picture = Ckeditor.picture_model.new
respond_with_asset(#picture)
end
def destroy
#picture.destroy
respond_with(#picture, :location => pictures_path)
end
protected
def find_asset
#picture = Ckeditor.picture_adapter.get!(params[:id])
end
def authorize_resource
model = (#picture || Ckeditor.picture_model)
#authorization_adapter.try(:authorize, params[:action], model)
end
end
/app/controllers/ckeditor/attachment_files_controller.rb
class Ckeditor::AttachmentFilesController < Ckeditor::ApplicationController
def index
#attachments = Ckeditor.attachment_file_adapter.find_all(ckeditor_attachment_files_scope(assetable_id: ckeditor_current_user))
#attachments = Ckeditor::Paginatable.new(#attachments).page(params[:page])
respond_with(#attachments, :layout => #attachments.first_page?)
end
def create
#attachment = Ckeditor.attachment_file_model.new
respond_with_asset(#attachment)
end
def destroy
#attachment.destroy
respond_with(#attachment, :location => attachment_files_path)
end
protected
def find_asset
#attachment = Ckeditor.attachment_file_adapter.get!(params[:id])
end
def authorize_resource
model = (#attachment || Ckeditor.attachment_file_model)
#authorization_adapter.try(:authorize, params[:action], model)
end
end

Rails retrieve record field

I have database full of data, in database I have fields like title, name, surname. I can retrieve whole record as JSON by doing GET request on /users/1.json. What if I want to retrieve just name field for that record, is something like this /users/1/name.json possible?
You can try the following:
# routes.rb
resources :users do
member do
match ':attribute_name' => "users#get_attribute_value"
end
end
# User controller
class UsersController < ApplicationController
def get_attribute_value
attribute_name = params[:attribute_name]
#user = User.where(id: params[:id])
if User.column_names.include?(attribute_name.to_s)
render json: { "#{attribute_name}" => #user.send(attribute_name) }
else
# trying to access an attribute that does not exists
end
end
end

User Registration with Devise and Paypal

I want to integrate Paypal within the Devise user registration process. What I want is to have a standard rails form based on the devise resource, that also has custom fields belonging to the user's model.
When a user fills in those fields and clicks on signup, it will be redirected to Paypal, when he clears from paypal and returns to our site then the user data must be created.
For the scenario where the user fill's out the paypal form but doesn't come back to our site, we have to keep record of user before redirecting to Paypal.
For this we can create a flag in user model and use Paypal IPN and when the user transaction notified, set that flag.
But in the case when the user is redirected to Paypal but doesn't complete the transaction, if the user returns to registration and signup again, our model should not throw error saying that the email entered already exists in the table.
How can we handle all these scenarios, is there any gem or plugin available to work with?
Here i am posting the detail code for performing the whole process.
registration_controller.rb
module Auth
class RegistrationController < Devise::RegistrationsController
include Auth::RegistrationHelper
def create
#user = User.new params[:user]
if #user.valid?
redirect_to get_subscribe_url(#user, request)
else
super
end
end
end
end
registration_helper.rb
module Auth::RegistrationHelper
def get_subscribe_url(user, request)
url = Rails.env == "production" ? "https://www.paypal.com/cgi-bin/webscr/?" : "https://www.sandbox.paypal.com/cgi-bin/webscr/?"
url + {
:ip => request.remote_ip,
:cmd => '_s-xclick',
:hosted_button_id => (Rails.env == "production" ? "ID_FOR_BUTTON" : "ID_FOR_BUTTON"),
:return_url => root_url,
:cancel_return_url => root_url,
:notify_url => payment_notifications_create_url,
:allow_note => true,
:custom => Base64.encode64("#{user.email}|#{user.organization_type_id}|#{user.password}")
}.to_query
end
end
payment_notification_controller.rb
class PaymentNotificationsController < ApplicationController
protect_from_forgery :except => [:create]
layout "single_column", :only => :show
def create
#notification = PaymentNotification.new
#notification.transaction_id = params[:ipn_track_id]
#notification.params = params
#notification.status = "paid"
#custom = Base64.decode64(params[:custom])
#custom = #custom.split("|")
#user = User.new
#user.email = #custom[0]
#user.organization_type_id = #custom[1].to_i
#user.password = #custom[2]
if #user.valid?
#user.save
#notification.user = #user
#notification.save
#user.send_confirmation_instructions
end
render :nothing => true
end
def show
end
end

Resources