Doubts in implementing controllers and views - ruby-on-rails

I'm starting with Ruby on Rails and get'm making an application that takes the likes of a facebook user and transforms them into products, creating a personalized gift list.
Well, I have three models: user, and like product. They are thus:
class Like < ActiveRecord::Base
belongs_to :user
has_one :product
attr_accessible :category, :created_time, :name
end
class Product < ActiveRecord::Base
belongs_to :user
has_one :like
attr_accessible :image_url, :price_max, :price_min, :product_name,
:url
end
class User < ActiveRecord::Base
has_many :likes, dependent: :destroy
has_many :products, through: :likes, dependent: :destroy
attr_accessible :email, :password, :password_confirmation, :remember_me,:first_name, :last_name, :gender, :facebook_username, :provider, :uid
end
I am also using two gems that help me catch the likes and turn them into products. I have two methods I use in 'rails console' for this:
#Facebook Client
client = FBGraph::Client.new(
client_id: 'client_id',
secret_id: 'secret_id',
token: 'token'
)
#Getting and Recording Likes
like = client.selection.me.likes.info!.data.data
like.sort_by{ |like| like.created_time.to_i }.reverse.each do |like|
case like.category
when 'Musician/band' then category = 'band'
...
else next
Like.create(category: category, created_time: like.created_time, name: like.name, user_id: current_user.id)
end
#Product API Client
buscape = Buscape.new(
app_id: 'app_id',
sandbox: true
)
#Getting product through likes, and recording
def find_for_products
likes.each do |like|
case like.category
when 'band' then
product = #buscape.products.where(keyword: URI.encode(like.name), categoryId: 2921, results: 1)['product']
...
else next
end
Product.create(
image_url:product['thumbnail']['url'],
price_max:product['priceMax'],
price_min:product['priceMin'],
product_name:product['productName'],
url:product['links']['link'][0]['url']
)
end
end
Using the rails console, I can manually register products and likes using Like.new and Product.new, and assign it to a user accessing and using 'user.products', but I have no idea how to implement it and in my view and in my comtroller to catch the likes of current_user and produce products at the moment the user clicks a button 'Find products for me'
Can anyone help me?

If you're using resources, you can define a new action on User in your routes.rb file, like this:
resources :user do
member do
post 'find_products'
end
end
Then implement this action in the controller class.
One option is to put the code you have for querying FB and finding products into a self method in the Product model, something like Product.find_and_create_products(user). Or alternatively into a separate module in a new file in lib directory in your rails app. Then call it from the controller.
If this operation takes a long time, you can later add some additonal code to run it as a delayed job, with something like delayed_job.

Related

How to structure a has_many association with a dynamic scope?

I have a users table in my db. A user can be either of type 'admin' or 'manager'.
Given the models and schema below, I would like that for each instance of 'manager' user, an 'admin' user could select one, some or all the locations of the tenant that the manager belongs to in order to select which locations the manager can have control over.
My models
class User < ActiveRecord::Base
belongs_to :tenant
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations, dependent: :destroy
class Location < ActiveRecord::Base
belongs_to :tenant, inverse_of: :locations
I've tried two paths
First, trying to establish a scoped has_many association between the User and the Location models. However, I can't wrap my head around structuring this scope so that an 'admin' user could select which locations the 'manager' users can control.
Second, setting up a controlled_locations attribute in the users table. Then I set up some code so that an 'admin' user can select which locations a 'manager' can control, populating its 'controlled_locations' attribute. However, what gets saved in the database (inside the controlled_locations array) is strings instead of instances of locations.
Here's the code that I tried for the second path:
The migration
def change
add_column :users, :controlled_locations, :string, array: true, default: []
end
In the view
= f.input :controlled_locations, label: 'Select', collection: #tenant_locations, include_blank: "Anything", wrapper_html: { class: 'form-group' }, as: :check_boxes, include_hidden: false, input_html: {multiple: true}
In the users controller (inside the update method)
if params["user"]["controlled_locations"]
params["user"]["controlled_locations"].each do |l|
resource.controlled_locations << Location.find(l.to_i)
end
resource.save!
end
What I expect
First of all, I'm not quite sure the second path that I tried is a good approach (storing arrays in the db). So my best choice would be to set up a scoped association if it's possible.
In case the second path is feasible, what I would like to get is something like this. Let's say that logging in an Admin, I selected that the user with ID 1 (a manager) can control one location (Boston Stadium):
user = User.find(1)
user.controlled_locations = [#<Location id: 55, name: "Boston Stadium", created_at: "2018-10-03 12:45:58", updated_at: "2018-10-03 12:45:58", tenant_id: 5>]
Instead, what I get after trying is this:
user = User.find(1)
user.controlled_locations = ["#<Location:0x007fd2be0717a8>"]
Instead of instances of locations, what gets saved in the array is just plain strings.
First, your code is missing the locations association in the Tenant class.
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations
Let's say the variable manager has a User record. Then the locations it can control are:
manager.tenant.locations
If you want, you can shorten this with a delegate statement.
class User < ActiveRecord::Base
belongs_to :tenant
delegate :locations, to: :tenant
then you can call this with
manager.locations
A common pattern used for authorization is roles:
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def add_role(name, location)
self.roles << Role.find_or_create_by(name: name, location: location)
end
def has_role?(name, location)
self.roles.exists?(name: name, location: location)
end
end
# rails g model role name:string
# make sure you add a unique index on name and location
class Role < ApplicationRecord
belongs_to :location
has_many :user_roles
has_many :users, through: :user_roles
validates_uniqueness_of :name, scope: :location_id
end
# rails g model user_role user:references role:references
# make sure you add a unique compound index on role_id and user_id
class UserRole < ApplicationRecord
belongs_to :role
belongs_to :user
validates_uniqueness_of :user_id, scope: :role_id
end
class Location < ApplicationRecord
has_many :roles
has_many :users, through: :roles
end
By making the system a bit more generic than say a controlled_locations association you can re-use it for different cases.
Let's say that logging in an Admin, I selected that the user with ID 1
(a manager) can control one location (Boston Stadium)
User.find(1)
.add_role(:manager, Location.find_by(name: "Boston Stadium"))
In actual MVC terms you can do this by setting up roles as a nested resource that can be CRUD'ed just like any other resource. Editing multiple roles in a single form can be done with accepts_nested_attributes or AJAX.
If you want to scope a query by the presence of a role then join the roles and user roles table:
Location.joins(roles: :user_roles)
.where(roles: { name: :manager })
.where(user_roles: { user_id: 1 })
To authenticate a single resource you would do:
class ApplicationController < ActionController::Base
protected
def deny_access
redirect_to "your/sign_in/path", error: 'You are not authorized.'
end
end
class LocationsController < ApplicationController
# ...
def update
#location = Location.find(params[:location_id])
deny_access and return unless current_user.has_role?(:manger, #location)
# ...
end
end
Instead of rolling your own authorization system though I would consider using rolify and pundit.

Validate presence of nested attributes within a form

I have the following associations:
#models/contact.rb
class Contact < ActiveRecord::Base
has_many :contacts_teams
has_many :teams, through: :contacts
accepts_nested_attributes_for :contacts_teams, allow_destroy: true
end
#models/contacts_team.rb
class ContactsTeam < ActiveRecord::Base
belongs_to :contact
belongs_to :team
end
#models/team.rb
class Team < ActiveRecord::Base
has_many :contacts_team
has_many :contacts, through: :contacts_teams
end
A contact should always have at least one associated team (which is specified in the rich join table of contacts_teams).
If the user tried to create a contact without an associated team: a validation should be thrown. If the user tries to remove all of a contact's associated teams: a validation should be thrown.
How do I do that?
I did look at the nested attributes docs. I also looked at this article and this article which are both a bit dated.
For completion: I am using the nested_form_fields gem to dynamically add new associated teams to a contact. Here is the relevant part on the form (which works, but currently not validating that at least one team was associated to the contact):
<%= f.nested_fields_for :contacts_teams do |ff| %>
<%= ff.remove_nested_fields_link %>
<%= ff.label :team_id %>
<%= ff.collection_select(:team_id, Team.all, :id, :name) %>
<% end %>
<br>
<div><%= f.add_nested_fields_link :contacts_teams, "Add Team"%></div>
So when "Add Team" is not clicked then nothing gets passed through the params related to teams, so no contacts_team record gets created. But when "Add Team" is clicked and a team is selected and form submitted, something like this gets passed through the params:
"contacts_teams_attributes"=>{"0"=>{"team_id"=>"1"}}
This does the validations for both creating and updating a contact: making sure there is at least one associated contacts_team. There is a current edge case which leads to a poor user experience. I posted that question here. For the most part though this does the trick.
#custom validation within models/contact.rb
class Contact < ActiveRecord::Base
...
validate :at_least_one_contacts_team
private
def at_least_one_contacts_team
# when creating a new contact: making sure at least one team exists
return errors.add :base, "Must have at least one Team" unless contacts_teams.length > 0
# when updating an existing contact: Making sure that at least one team would exist
return errors.add :base, "Must have at least one Team" if contacts_teams.reject{|contacts_team| contacts_team._destroy == true}.empty?
end
end
In Rails 5 this can be done using:
validates :contacts_teams, :presence => true
If you have a Profile model nested in a User model, and you want to validate the nested model, you can write something like this: (you also need validates_presence_of because validates_associated doesn't validate the profile if the user doesn't have any associated profile)
class User < ApplicationRecord
has_one :profile
accepts_nested_attributes_for :profile
validates_presence_of :profile
validates_associated :profile
docs recommend using reject_if and passing it a proc:
accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Model Names:
1: approval
2: approval_sirs
Associations:
1: approval
has_many :approval_sirs, :foreign_key => 'approval_id', :dependent => :destroy
accepts_nested_attributes_for :approval_sirs, :allow_destroy => true
2: approval_sirs
belongs_to :approval , :foreign_key => 'approval_id'
In approvals.rb
## nested form validations
validate :mandatory_field_of_demand_report_sirs
private
def mandatory_field_of_demand_report_sirs
self.approval_sirs.each do |approval_sir|
unless approval_sir.marked_for_destruction?
errors.add(:base, "Demand Report Field are mandatory in SIRs' Detail") unless approval_sir.demand_report.present?
end
end
end

'Messaging System' with more than 1 user in conversation, build from scratch in Rails

So I am trying to implement a messaging system in my application, and I have no intention on using any of the gems. I've seen most of them and I'm able to use them. But for learning sake, I want to learn how it works and build one from scratch and be able to customize it how I want it.
I have looked around for tutorial but there isn't any tutorial or anything concrete information except for; this -> http://.novawave.net/public/rails_messaging_tutorial.html, but unfortunately link is down or this -> Rails threaded private messaging, but I still cant wrap my head around everything.
So hope this thread will serve as a point of reference for others.
So based off this thread Rails threaded private messaging, this is what I have including the columns definitions.
But I'm having problem wrapping my head around the logic on adding multiple users in a conversation. The way that I see this:
Click on send a message, which will trigger a new conversation object
Add a subject, and select users that I want in the conversation <-- this is where it gets cloudy
at the bottom without of that same form without any ajax, I guess I could render message form which will submit the text?
Ok so how do I put multiple user ids in the conversation table users_id column? There is a suggestion to use 'act_as_taggable' gem from this thread -> Rails threaded private messaging comes in? If so, how is the database is going to know that it should select all these user in a certain conversation object.
class Conversation < ActiveRecord::Base
#columns -> :subject, :users_id
has_many :messages
has_many :participants
has_many :users, :through => :participants
end
class Message < ActiveRecord::Base
#columns -> :conversation_id, :sender_id, :read
belongs_to :conversation
end
class Participant < ActiveRecord::Base
#columns -> :user_id, :conversation_id
belongs_to :conversation
belongs_to :user
end
class User < ActiveRecord::Base
has_many :conversations
has_many :participants
end
The conversations table should not have a user_id/users_id column as you have setup a many to many relationship in this case.
You can add users to a conversation by doing something like:
#conversation.users << #user
#conversation.save
Or in a form it would be something like:
<%= form_for Conversation.new do |f| %>
<%= f.collection_select :user_ids, User.all, :id, :name, {prompt: true}, {multiple: true} %>
<%= f.submit %>
<% end %>
You can get users in a conversation with #conversion.users.
Also, your user model should be:
class User < ActiveRecord::Base
has_many :participants
has_many :conversations, through: :participants
end

How do I set ActiveRecord model attribute when using .build method with Rails 3.2 (many-to-many)

I'm working on a sort of project management app with Rails (my Rails skills is kinda rusty). I have two model objects, in this case User and Account, which have a many-to-many relationship (Company could maybe be a better name for Account). When a user signs up a new Account is created (with .build) with help form a nested form. The Account model have two fields name and account_admin. When the the initial user creates it's Account I want to set account_admin to the users id. But I can't get this to work.
The models is set up like this:
class Account < ActiveRecord::Base
attr_accessible :name, :account_admin
validates_presence_of :name
has_many :projects, dependent: :destroy
has_many :collaborators
has_many :users, through: :collaborators
end
class User < ActiveRecord::Base
has_secure_password
attr_accessible :email, :name, :password, :password_confirmation, :accounts_attributes
has_many :collaborators
has_many :accounts, through: :collaborators
accepts_nested_attributes_for :accounts
[...]
The UserController looks like this:
def new
if signed_in?
redirect_to root_path
else
#user = User.new
# Here I'm currently trying to set the account_admin value, but it seems to be nil.
account = #user.accounts.build(:account_admin => #user.id)
end
end
I have also tried to move account = #user.accounts.build(:account_admin => #user.id) to the create action, but the the field disappears from the form.
What would be the appropriate way to accomplish what I want (set account_admin to the users id when it is getting created)? Or is there a better approach to find out which user created the account (ie. do something with the relationship table)?
Update
With help from #joelparkerhenderson I think I got it to work. I made a method in my User model that looks like this:
def set_account_admin
account = self.accounts.last
if account.account_admin == nil
account.account_admin = self.id
account.save
end
end
Which I call with after_create :set_account_admin. This works, but is there a more "Rails way" to do the same?
Thanks.
When you call #new, the user doesn't have an id yet (it is nil).
When you #save the user, Rails automatically gives the user a new id.
You can then use the after_create Active Record callback to set the new Account's account_admin

How do I use has_many :through and in_place_edit?

I have two Models: Campaign and Contact.
A Campaign has_many Contacts.
A Contact has_many Campaigns.
Currently, each Contact has a contact.date_entered attribute. A Campaign uses that date as the ate to count down to the different Events that belong_to the Campaign.
However, there are situations where a Campaign for a specific Contact may need to be delayed by X number of days. In this instance, the campaigncontact.delaydays = 10.
In some cases, the Campaign must be stopped altogether for the specific Contact, so for now I set campaigncontact.delaydays = 1. (Are there major problems with that?)
By default, I am assuming that no campaigncontact exists (but not sure how that works?)
So here's what I've tried to do:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
end
class Campaign < ActiveRecord::Base
has_many :campaigncontacts
has_many :contacts, :through => :campaigncontacts
end
script/generate model campaigncontact campaign_id:integer contact_id:integer delaydays:integer
class Campaigncontact < ActiveRecord::Base
belongs_to :campaign
belongs_to :contact
end
So, here's the question: Is the above correct? If so, how do I allow a user to edit the delay of a campaign for a specific Contact.
For now, I want to do so from the Contact View.
This is what I tried:
In the Contact controller (?)
in_place_edit_for :campaigncontact, column.delaydays
And in the View
<%= in_place_editor_field :campaigncontact, :delaydays %>
How can I get it right?
I would add an integer field to your Campaigncontacts resource called days_to_delay_communication_by, since this information relates to the association of a campaign and a contact rather than a contact itself.
in your migration:
def self.up
add_column(:campaigncontacts, :days_to_delay_communication_by, :integer)
end
def self.down
remove_column(:campaigncontacts, :days_to_delay_communication_by)
end
Now you can set that value by:
campaigncontact = Campaigncontacts.find(:first, :conditions => { :campaign_id => campaign_id, :contact_id => contact_id })
campaigncontact.days_to_delay_communication_by = 10
Then in the admin side of your application you can have a controller and a view for campaign communications that lets you set the days_to_delay_communication_by field for campaigncontacts. I can expand on this further for you if you're interested, but I think you get the idea.
Then you'll need to run a background process of some sort (probably a cron job, or use the delayed_job plugin), to find communications that haven't happened yet, and make them happen when the date has passed. You could do this in a rake task like so:
namespace :communications do
namespace :monitor do
desc 'Monitor and send communications for campaigns'
task :erma => :environment do
Rails.logger.info "-----BEGIN COMMUNICATION MONITORING-----"
unsent_communications = Communication.all(:conditions => { :date_sent => nil})
unsent_communications.each do |communication|
Rails.logger.info "**sending communication**"
communication.send if communication.time_to_send < Time.now
Rails.logger.info "**communication sent**"
end
Rails.logger.info "-----END COMMUNICATION MONITORING-----"
end #end erma task
end #end sync namespace
end #end db namespace
Then your cron job would do something like:
cd /path/to/application && rake communications:monitor RAILS_ENV=production
Also, I'd consider changing the name of your join model to something more descriptive of it's purpose, for instance memberships, a campaign has many memberships and a contact has many memberships. Then a membership has a days_to_delay_communication field.
A good way to do this is use a "fake" attribute on your Contact model like so:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
attr_accessor :delay
def delay #edit
self.campaigncontacts.last.delaydays
end
def delay=(val)
self.campaigncontacts.each do |c|
c.delaydays = val
end
end
end
Then just set the in_place_editor for this fake field:
in_place_edit_for :contact, :delay
and
<%= in_place_editor_field :contact, :delay %>
I'm not sure I understood exactly what you wanted to accomplish, but I hope this at least points you into the right direction.

Resources