Conditional email trigger on Model attribute update (Devise & Rails) - ruby-on-rails

I'm still learning Rails, and using Devise. Currently I am working on a bug/ticket logging system. I'n this system we have tickets created by a user, assigned to another user and all users that can view it can post a reply on it.
I want to trigger an email when a user changes the status of a ticket to closed.
HOWEVER, if you are the creator (of the ticket) and you closed it, you do not want an email, but you want to email the user its assigned to. Likewise, if you are the assignee and you close it, you do not want to email, but you do want to email the creator. If you are neither creator or assignee, you still do not want an email, but you do want to email the other two.
The email will be a small notification noting ticket #_ is closed.
I am a bit tripped up as to where this code should go. There is no new code in the controller but I added a before_update :email_update in my ticket model.
def email_update
#status field is changed
if status_changed? && status.description == "Closed"
if(current_user != assigned_to)
UserMailer.new_ticket_admin(assigned_to, self).deliver
end
if(current_user != user)
UserMailer.new_ticket_admin(user, self).deliver
end
end
end
But, is this not bad practice to access the current user in one of the models? What would be a better approach?

Pretty sure, but I don't think that you can access current_user in the model. Even if you could, might I suggest an alternative. Instead, I would use a closed_by_id attribute where it is the current_user's ID. This way you can also track who closed a ticket. From here, you can check to see if the ticket is closed and if the creator of the ticket's ID is equal to the closed_by_id.

As you mentioned you have a creator and a 'closer' (or whatever you want to call that user). Within your user model you want to have something like this:
class Ticket < ActiveRecord::Base
belongs_to :requested_by, class_name: 'User' # foreign_key requested_by_id
belongs_to :closed_by, class_name: 'User' # foreign_key closed_by_id
def close(user)
self.closed_by = user
self.save
end
# bonus method
def closed?
closed_by?
end
end
def User < ActiveRecord::Base
has_many :tickets, foreign_key: 'requested_by_id'
has_many :closed_tickets, foreign_key: 'closed_by_id'
end
And for your controller something like:
class TicketController < ApplicationController
def create
#ticket = current_user.tickets.build params[:ticket]
end
def close
#ticket = Ticket.find(params[:id])
#ticket.close current_user
end
end
This way there is no need to have current_user within your model. Which probably solves your challege.

Related

Create User Account Settings Page in Ruby on Rails with devise

I am new to Ruby on Rails and I have created a project that contains a User table (generated by devise) and a AccountSetting table that contains user specific account settings (this table has a foreign key that relates to the id in the User table thus each User has zero or one AccountSettings). I have my seed data working fine, and I can seed the database with users that have user specific account settings. The User table is related to the AccountSetting table with a "has_one :accountsetting" and the AccountSettings table "belongs_to :user". This all works and makes sense. However, I have a method called "show_user_setting" in my UserSettings controller, and I do not know how to ONLY SHOW the account settings for that specific authenticated user.
So, how can I only display the user setting for the currently logged in user? Again, I am using devise.
My general idea of how to do this would be something like this. However I know this is incorrect, but for the purpose of an explanation, here it is.
def show_user_setting
#setting = AccountSetting.find(current_user)
end
My idea is that the #setting will contain the setting for the currently logged in user. Thanks in advance!
You should do this:
#app/models/account_setting.rb
class AccountSetting < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :account_setting
end
This will allow you to call the following:
#setting = current_user.account_setting
Our Setup
For what it's worth, we do something similar:
#app/models/user.rb
class User < ActiveRecord::Base
before_create :build_profile #-> builds a blank profile on user create
has_one :profile
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
end
This allows us to put all sorts of different options inside the profile model (we have homepage etc):
The important thing to note here is that the above allows you to delegate various methods to the profile model, allowing you to call the following:
current_user.profile_name
current_user.profile_signin_redirect?
current_user.profile_avatar
etc
Have you tried
def show_user_setting
#setting = AccountSetting.find_by(user_id: current_user.id)
end
The way .find() works is it searches the model for the id passed. So, the way you currently have it is your going to try to search for the id of the model, when you want to find the foreign key. So use Model.find_by(column_name: param). You'll what to change user_id: to the column name of what you're storing the foreign key in, I'm just assuming it's something similar to that.
I'm guessing the show_user_setting function is part of a controller, if it is on a model then read this: accessing devise current_user within model
to set the #setting variable you should be able to do this
#setting = AccountSetting.find(user_id: current_user.id)
or
#setting = AccountSetting.find(user: current_user)

Validate using an existing value

I have a Topic model:
class Topic < ActiveRecord::Base
belongs_to :user
def validate_on_update
errors.add(:user, "Only the topic creator can update the topc") if
self.user != user;
end
end
I would like to check before every update that the existing topic.user is the same with the user that is trying to update the model.
I think that
self.user != user
is not working but I do not know how to fix that!
You need to find the record in the controller before doing that, so you can do this in your controller action:
#topic = current_user.topics.find(params[:id])
This will trigger an exception that you can easily catch or leave it.
This is the best method to ensure data integrity, unless you're tinkering in other places of the app and you need to create Topics not in controllers.
If you have such need, it's not bad to have a validation rule in the model to ensure major data integrity, but the model does need to know the user, that's only accessible from the controller.
My recommendation is that you assign the user controller-side or just use scopes like:
current_user.topics.create(params[:topic])
This way you are sure that the user is the same in question, and this invalidates the need to do another validation if it's the only place you're calling topic creation.
If you are unsure and wants to game on with a validate_on_update I suggest creating a virtual attribute like so:
attr_accessor :this_user
But in any case you'd pass this via controller, since your model should know nothing about the current logged in user:
#topic = Topic.new(params[:topic])
#topic.this_user = current_user # or #topic.user_id and check for a attr_changed?
Update: adding example as requested
# #topic = Topic.new(params[:topic])
# #topic.this_user = current_user
class Topic < ActiveRecord::Base
belongs_to :user
attr_accessor :this_user
def validate_on_update
# Make sure that this_user is an instance of User, otherwise just use the id
errors.add(:user, "Only the topic creator can update the topic") if user_id != this_user.id;
end
end
Update:
another suggestion is to use:
attr_readonly :user_id

Rails associations and MVC

in my Rails app I have a User model and a Team model, linked through a User_Team model.
What I'm trying to do is to ask Rails to validate and create a new user only if the params used to create it come with a valid team_code param.
Basically I need to:
check the Team table
look if a team with the provided team_code exists
only in this case allow the creation of the user
link the user to the team
Which is the best way to do this? Where should I put the logic? In the controller? In the user validation model?
Even just a few hints would be helpful!
UPDATE #1
The team_code is an attribute of the Team model. Teams are created previously (not during user creation). I need that each user - in order to be created - has an existing team to enter at user creation time.
Thanks,
Augusto
Validation logic belongs in the model. Here's how I'd do it:
class User
belongs_to :team
attr_accessor :team_code
def team_code
#team_code ? #team_code : (team ? team.team_code : nil)
end
def team_code= (value)
#team_code = value
self.team = Team.where('team_code = ?', value).first
end
validates_presence_of :team
validates_each :team_code do |record, attr, value|
record.errors.add attr, 'does not exist' if !value.blank? && !team
end
end

What is the best way of preventing the last record in a has_many collection being removed?

I have two ActiveRecord classes. A simplified view of these classes:
class Account < ActiveRecord::Base
has_many :user_account_roles
end
class UserAccountRole < ActiveRecord::Base
belongs_to :account
# Has a boolean attribute called 'administrator'.
end
What I'm struggling with is that I'd like to be able to apply two validation rules to this:
* Ensuring that the last UserAccountRole cannot be removed.
* Ensuring that the last UserAccountRole that is an administrator cannot be removed.
I'm really struggling to understand the best way of achieving this kind of structural validation. I've tried adding a before_remove callback to the association, but I don't like that this has to throw an error which would need to be caught by the controller. I'd rather this be treated as 'just another validation'.
class Account < ActiveRecord::Base
has_many :user_account_roles, :before_remove => check_remove_role_ok
def check_remove_relationship_ok(relationship)
if self.user_account_relationships.size == 1
errors[:base] << "Cannot remove the last user from this account."
raise RuntimeError, "Cannot remove the last user from this account."
end
end
end
I don't think this makes any difference, but I'm also using accepts_nested_attributes_for.
Why not use a simple validation on Account?
class Account < ActiveRecord::Base
has_many :user_account_roles
validate :at_least_one_user_account_role
validate :at_least_one_administrator_role
private
def at_least_one_user_account_role
if user_account_roles.size < 1
errors.add_to_base('At least one role must be assigned.')
end
end
def at_least_one_administrator_role
if user_account_roles.none?(&:administrator?)
errors.add_to_base('At least one administrator role must be assigned.')
end
end
end
This way nothing needs to be raised, and the record won't be saved unless there's at least one role, and at least one administrator role. Thus when you re-render your edit form on error, this message will show up.
You could place the validation on UserAccountRole. If it is the only UserAccountRole associated with the Account, then it can't be deleted.
An easier solution may be to question an underlying assumption of your design. Why have UserAccountRole be an AR backed model? Why not just make it a plain ruby class? Is the end user going to dynamically define roles? If not, then you could greatly simplify your dilemma by making it a regular ruby class.

Don't reshow seen posts in Rails

I'm currently developing an application whereby a user clicks a button and they're offered up a new page of content, and was wondering how I would go about hiding or skipping past those that the user has already interacted with (a separate table stores the post_id and user_id for each view).
I currently use this code in the model for displaying a random page:
def self.random
if (c = count) != 0
find(:first, :offset =>rand(c))
end
end
The user authentication system is built off of Authlogic, and I have User, Post and View models.
So if a user has already seen a post "foo", how would I not display that in the future and instead serve up a random "bar".
Thanks
Steve,
I would set a boolean field for each post called "read" (default => false).
Upon firing the "show" action of your controller (and any other action you consider the person seeing the post), you can automatically set that to true and perform a save without validation. When you then show your list of records, you can add the condition .where("read = ?", false).
Of course, you can decide whether you want to give users the flexibility of setting individual posts to 'unseen' or 'unread' - if you want to do that it's the subject of another question :).
You could store an array of viewed post ids on the session in the show action of the posts_controller. EDIT -- find random post not already viewed. Not tested, but the idea is here:
def show_random_post
while (id == nil || (session[:viewed_posts] ||= []).include?(id)) # initialize array if it hasn't been initialized
id = rand(Post.count) + 1
end
session[:viewed_posts] << id
#post = Post.find(id)
# etc.
end
Do you want to keep a record of viewed posts between sessions?
EDIT: If you want to keep a user-level record of viewed posts between sessions, you'll probably want to do it at the db level. Since this means a many-to-many relationship between users and posts, you'll likely want to manage that with a relational table, and the best way to do that in Rails is with has_many :through. Something like (again, not tested):
class ViewedPostRecord < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class User < ActiveRecord::Base
has_many :viewed_post_records
has_many :viewed_posts, :class => 'Post', :through => :viewed_post_records
end
class PostsController < ApplicationController
def show_random_post
while (id == nil || current_user.viewed_posts.map(&:id).include?(id))
id = rand(Post.count) + 1
end
#post = Post.find(id)
current_user.viewed_posts << #post
# etc.
end
end

Resources