Access current user within liquid drop rails 5 - ruby-on-rails

I'm currently implementing liquid templates in my application. As part of that I have created a set of liquid drop (https://github.com/Shopify/liquid/wiki/Trying-to-Understand-Drops) classes to act as intermediates between my models and my templates. I'm currently using devise for authentication on rails 5.
In my product drop class I would like to be able to check if my current user owns the product:
class ProductDrop < Liquid::Drop
def initialize(model)
#model = model
end
def owned_by_user?
#somehow access the current_user provided by devise.
end
end
But haven't been able to figure out how to access the user.
I notice in this method on shopify: https://help.shopify.com/en/themes/liquid/objects/variant#variant-selected
They are able to access the current url to work out if the variant is selected. I thought perhaps it might be possible if they can access the url, to access the session and get the user identifier to look up the user.
So I can do something like:
def owned_by_user?
User.find_by_id(session[:user_id]).owns_product?(#model.id)
end
I'm not having any luck accessing the session. Does anyone have any suggestions or ideas? Or am I going about this completely the wrong way?

So after digging around in the liquid drop source code. I noticed that the context is accessible to the drop (https://github.com/Shopify/liquid/blob/master/lib/liquid/drop.rb). I totally missed it the first time I looked.
So the solution ended up being:
First add the user so it is available to the controller action the view is rendered for. this then gets added to the context by the liquid template handler (and therefore exists in the context)
class ApplicationController < ActionController::Base
before_action :set_common_variables
def set_common_variables
#user = current_user # Or how ever you access your currently logged in user
end
end
Add the method to the product to get the user from the liquid context
class ProductDrop < Liquid::Drop
def initialize(model)
#model = model
end
def name
#model.name
end
def user_owned?
return #context['user'].does_user_own_product?(#model.id)
end
end
Then add the method to the user to check if the user owns the product or not:
class UserDrop < Liquid::Drop
def initialize(model)
#model = model
end
def nick_name
#model.nick_name
end
def does_user_own_product?(id)
#model.products.exists?(id: id)
end
end
Obviously this needs error handling and so on. but hopefully that helps someone. Also if anyone knows of a better way, keen to hear it.

Related

Custom url_for for non rolling ids

Right now if I create a URL for a model show action I simply call:
link_to model_instance
which creates something like this the when model is User and the id is 1:
/user/1
I like to customize that behavior without having to go through all instances in the codebase where a URL for such a model is generated. The motivation behind the change is avoiding rolling id's in the URL that lets anybody discover other entries by simply increasing the id. So something like
/user/88x11bc1200
Is there a place where I can simply override how a URL for selected models are generated? I am using RoR 4.x
There are essentially two places you'll have to update.
In the model
class User < ActiveRecord::Base
# Override the to_param method
def to_param
# Whatever your field is called
non_rolling_id
end
end
In the controller
class UsersController < ApplicationController
def show
# Can't use `find` anymore, but will still come over as `id`
#user = User.find_by_non_rolling_id(params[:id])
end
end

Rails: Why the default application layout is not used?

I added #sort_by attribute to my controller, and initialized it's value like this:
class ProductsController < ApplicationController
def initialize
#sort_by = :shop_brand
end
...
end
This caused the default application layout not to be used.
Why ?
What is the right way to add an attribute to a controller and initialize it ?
Overriding the constructor is probably a bad idea (as you have found). You should use a before_filter:
class ProductsController < ApplicationController
before_filter :set_defaults
...
private
def set_defaults
#sort_by = :shop_brand
end
end
However, it sounds like you want to keep state. The easiest is to store in the user's session which will automatically persist per user until they close the browser:
def set_defaults
session[:sort_by] ||= :shop_brand
end
The other option would be to pass the current sort_by value in the URL. This is harder to implement though as you'll need to ensure each link or form copies the value over to the next request. The advantage of this however is the user could have multiple tabs open with different orderings and any bookmarked link would restore the same ordering next time. This is the approach that things like search engines would use.

Determine the domain in an ActiveRecord model

I am in the middle of migrating my application from using subdirectories for userspace to subdomains (ie. domain.com/~user to user.domain.com). I've got a method in my user class currently to get the "home" URL for each user:
class User
def home_url
"~#{self.username}"
# How I'd like to do it for subdomains:
#"http://#{self.username}.#{SubdomainFu.host_without_subdomain(request.host)}"
end
end
I'd like to update this for subdomains, but without hardcoding the domain into the method. As you can see, I am using the subdomain-fu plugin, which provides some methods that I could use to do this, except that they need access to request, which is not available to the model.
I know it's considered bad form to make request available in a model, so I'd like to avoid doing that, but I'm not sure if there's a good way to do this. I could pass the domain along every time the model is initialized, I guess, but I don't think this is a good solution, because I'd have to remember to do so every time a class is initialized, which happens often.
The model shouldn't know about the request, you're right. I would do something like this:
# app/models/user.rb
class User
def home_url(domain)
"http://#{username}.#{domain}"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def domain
SubdomainFu.host_without_subdomain(request.host)
end
# Make domain available to all views too
helper_method :domain
end
# where you need it (controller or view)
user.home_url(domain)
If there is such a thing as a canonical user home URL, I would make a configurable default domain (e.g. YourApp.domain) that you can use if you call User#home_url without arguments. This allows you to construct a home URL in places where, conceptually, the "current domain" does not exist.
While molf's answer is good, it did not solve my specific problem as there were some instances where other models needed to call User#home_url, and so there would be a lot of methods I'd have to update in order to pass along the domain.
Instead, I took inspiration from his last paragraph and added a base_domain variable to my app's config class, which is the set in a before_filter in ApplicationController:
module App
class << self
attr_accessor :base_domain
end
end
class ApplicationController < ActionController::Base
before_filter :set_base_domain
def set_base_domain
App.base_domain = SubdomainFu.host_without_subdomain(request.host)
end
end
And thus, when I need to get the domain in a model, I can just use App.base_domain.

getting the `current_user` in my User class

Oddly enough, most of this works as it has been written, however I'm not sure how I can evaluate if the current_user has a badge, (all the relationships are proper, I am only having trouble with my methods in my class (which should partially be moved into a lib or something), regardless, the issue is specifically 1) checking if the current user has a record, and 2) if not create the corresponding new record.
If there is an easier or better way to do this, please share. The following is what I have:
# Recipe Controller
class RecipesController < ApplicationController
def create
# do something
if #recipe.save
current_user.check_if_badges_earned(current_user)
end
end
So as for this, it definitely seems messy, I'd like for it to be just check_if_badges_earned and not have to pass the current_user into the method, but may need to because it might not always be the current user initiating this method.
# User model
class User < ActiveRecord::Base
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1, user)
end
if user.recipes.count > 20
award_badge(2, user)
end
end
def award_badge(badge_id, user)
#see if user already has this badge, if not, give it to them!
unless user.badgings.any? { |b| b[:badge_id] == badge_id}
#badging = Badging.new(:badge_id => badge_id, :user_id => user)
#badging.save
end
end
end
So while the first method (check_if_badges_earned) seems to excucte fine and only give run award_badge() when the conditions are met, the issue happens in the award_badge() method itself the expression unless user.badgings.any? { |b| b[:badge_id] == badge_id} always evaluates as true, so the user is given the badge even if it already had the same one (by badge_id), secondly the issue is that it always saves the user_id as 1.
Any ideas on how to go about debugging this would be awesome!
Regardless of whether you need the current_user behavior above, award_badge should just be a regular instance method acting on self instead of acting on the passed user argument (same goes for check_if_badges_earned). In your award_badge method, try find_or_create_by_... instead of the logic you currently have. For example, try this:
class User < ActiveRecord::Base
# ...
def award_badge(badge_id)
badgings.find_or_create_by_badge_id(badge_id)
end
end
To access the current_user in your model classes, I sometimes like to use thread-local variables. It certainly blurs the separation of MVC, but sometimes this kind of coupling is just necessary in an application.
In your ApplicationController, store the current_user in a thread-local variable:
class ApplicationController < ActionController::Base
before_filter :set_thread_locals
private
# Store thread-local variables so models can access them (Hackish, but useful)
def set_thread_locals
Thread.current[:current_user] = current_user
end
end
Add a new class method to your ActiveRecord model to return the current_user (you could also extend ActiveRecord::Base to make this available to all models):
class User < ActiveRecord::Base
def self.current_user
Thread.current[:current_user]
end
end
Then, you'll be able to access the current user in the instance methods of your User model with self.class.current_user.
What you need to do first of all is make those methods class methods (call on self), which avoids needlessly passing the user reference.
Then, in your award_badge method, you should add the Badging to the user's list of Badgings, e.g.: user.badgings << Badging.new(:badge_id => badge_id)

How to always set a value for account-scope in Rails?

I'm working on a multi-user, multi-account App where 1 account can have n users. It is very important that every user can only access info from its account. My approach is to add an account_id to every model in the DB and than add a filter in every controller to only select objects with the current account_id. I will use the authorization plugin.
Is this approach a good idea?
What is the best way to always set the account_id for every object that is created without writing
object.account = #current_account
in every CREATE action? Maybe a filter?
Also I'm not sure about the best way to implement the filter for the select options. I need something like a general condition: No matter what else appears in the SQL statement, there is always a "WHERE account_id = XY".
Thanks for your help!
This is similar to a User.has_many :emails scenario. You don't want the user to see other peoples emails by changing the ID in the URL, so you do this:
#emails = current_user.emails
In your case, you can probably do something like this:
class ApplicationController < ActionController::Base
def current_account
#current_account ||= current_user && current_user.account
end
end
# In an imagined ProjectsController
#projects = current_account.projects
#project = current_account.projects.find(params[:id])
I know, I know, if you access Session-variables or Instance variables in your Model you didn't understand the MVC pattern and "should go back to PHP". But still, this could be very useful if you have - like us - a lot of controllers and actions where you don't always want to write #current_account.object.do_something (not very DRY).
The solution I found is very easy:
Step 1:
Add your current_account to Thread.current, so for example
class ApplicationController < ActionController::Base
before_filter :get_current_account
protected
def get_current_account
# somehow get the current account, depends on your approach
Thread.current[:account] = #account
end
end
Step 2:
Add a current_account method to all your models
#/lib/ar_current_account.rb
ActiveRecord::Base.class_eval do
def self.current_account
Thread.current[:account]
end
end
Step 3: Voilá, in your Models you can do something like this:
class MyModel < ActiveRecord::Base
belongs_to :account
# Set the default values
def initialize(params = nil)
super
self.account_id ||= current_account.id
end
end
You could also work with something like the before_validation callback in active_record and then make with a validation sure the account is always set.
The same approach could be used if you always want to add the current_user to every created object.
What do you think?
To answer your second question, check out the new default_scope feature in Rails 2.3.
I understand that you don't want to bother about scoping you account all time. Lets be honest, it's a pain in the a**.
To add a bit magic and have this scoping done seamlessly give a look at the following gem
http://gemcutter.org/gems/account_scopper
Hope this helps,
--
Sebastien Grosjean - ZenCocoon

Resources