Here I am, with my rails project, and this question strikes me.
For my User profile, some attributes will be blank and I want them to default to a specific value : if they are not set, I want my view to display the custom message and add a CSS class like "blank" around it.
I am using a Presenter, but as it acts a bit like a Helper, let's say those are helper methods.
In my Helper I have something like that :
def professional_information
handles_not_set user.university, user.job do |university, job|
content_tag(:p, university.content, class: university.error) +
content_tag(:p, job.content, class: job.error)
end
end
The handles_not_set is defined as followed :
def handles_not_set(*objects)
o = []
objects.each do |object|
attr = OpenStruct.new
if object.blank?
attr.content = 'Not specified.'
attr.errors = ['blank']
else
attr.content = object
attr.errors = []
end
o << attr
end
yield *o
end
But I find this rather inelegant and I'd like to keep the code DRY (i.e : I don't want my presenter's methods to be full of 'if xxx.blank?')
Any idea on how I could improve this ?
Thanks a lot !
Related
I'm using Rails 4 and have an Article model that has answer, side_effects, and benefits as attributes.
I am trying to create a before_save method that automatically looks at the side effects and benefits and creates links corresponding to another article on the site.
Instead of writing two virtually identical methods, one for side effects and one for benefits, I would like to use the same method and check to assure the attribute does not equal answer.
So far I have something like this:
before_save :link_to_article
private
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.key.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.key = linked_attrs.join('; ')
end
end
end
but chaining on the key like that gives me an undefined method 'key'.
How can I go about interpolating in the attribute?
in this bit: self.key you are asking for it to literally call a method called key, but what you want, is to call the method-name that is stored in the variable key.
you can use: self.send(key) instead, but it can be a little dangerous.
If somebody hacks up a new form on their browser to send you the attribute called delete! you don't want it accidentally called using send, so it might be better to use read_attribute and write_attribute.
Example below:
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.read_attribute(key).split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.write_attribute(key, linked_attrs.join('; '))
end
end
end
I'd also recommend using strong attributes in the controller to make sure you're only permitting the allowed set of attributes.
OLD (before I knew this was to be used on all attributes)
That said... why do you go through every single attribute and only do something if the attribute is called answer? why not just not bother with going through the attributes and look directly at answer?
eg:
def link_to_article
linked_attrs = []
self.answer.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.answer = linked_attrs.join('; ')
end
I'm currently having trouble finding a nice way to code the following situation:
There is a Model called TcpService, which has two attributes, port_from and port_to, both Integers. It also has a virtual attribute called portrange, which is a String. portrange is the String representation of the attributes port_from and port_to, so portrange = "80 90" should yield port_from = 80, port_to = 90. What I'm trying to do now is using the same Formtastic form for creating AND updating a TcpService-object. The form looks pretty standard (HAML code):
= semantic_form_for #tcp_service do |f|
= f.inputs do
= f.input :portrange, as: :string, label: "Portrange"
-# calls #tcp_service.portrange to determine the shown value
= f.actions do
= f.action :submit, label: "Save"
The thing is, I don't know of a non-messy way to make the values I want appear in the form. On new I want the field to be empty, if create failed I want it to show the faulty user input along with an error, else populate port_from and port_to using portrange. On edit I want the String representation of port_from and port_to to appear, if update failed I want it to show the faulty user input along with an error, else populate port_from and port_to using portrange.
The Model looks like this, which seems quite messy to me.
Is there a better way of making it achieve what I need?
class TcpService < ActiveRecord::Base
# port_from, port_to: integer
attr_accessor :portrange
validate :portrange_to_ports # populates `port_from` and `port_to`
# using `portrange` AND adds errors
# raises exception if conversion fails
def self.string_to_ports(string)
... # do stuff
return port_from, port_to
end
# returns string representation of ports without touching self
def ports_to_string
... # do stuff
return string_representation
end
# is called every time portrange is set, namely during 'create' and 'update'
def portrange=(val)
return if val.nil?
#portrange = val
begin
self.port_from, self.port_to = TcpService.string_to_ports(val)
# catches conversion errors and makes errors of them
rescue StandardError => e
self.errors.add(:portrange, e.to_s())
end
end
# is called every time the form is rendered
def portrange
# if record is freshly loaded from DB, this is true
if self.port_from && self.port_to && #portrange.nil?
self.ports_to_string()
else
#portrange
end
end
private
# calls 'portrange=(val)' in order to add errors during validation
def portrange_to_ports
self.portrange = self.portrange
end
end
Thanks for reading
In your model
def portrange
return "" if self.port_from.nil? || self.port_to.nil?
"#{self.port_from} #{self.port_to}"
end
def portrange=(str)
return false unless str.match /^[0-9]{1,5}\ [0-9]{1,5}/
self.port_from = str.split(" ").first
self.port_to = str.split(" ").last
self.portrange
end
Using this you should be able tu use the portrange setter and getter in your form.
Need a little help over here :-)
I'm trying to extend the Order class using a decorator, but I get an error back, even when I use the exactly same code from source. For example:
order_decorator.rb (the method is exactly like the source, I'm just using a decorator)
Spree::Order.class_eval do
def update_from_params(params, permitted_params, request_env = {})
success = false
#updating_params = params
run_callbacks :updating_from_params do
attributes = #updating_params[:order] ? #updating_params[:order].permit(permitted_params).delete_if { |k,v| v.nil? } : {}
# Set existing card after setting permitted parameters because
# rails would slice parameters containg ruby objects, apparently
existing_card_id = #updating_params[:order] ? #updating_params[:order][:existing_card] : nil
if existing_card_id.present?
credit_card = CreditCard.find existing_card_id
if credit_card.user_id != self.user_id || credit_card.user_id.blank?
raise Core::GatewayError.new Spree.t(:invalid_credit_card)
end
credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present?
attributes[:payments_attributes].first[:source] = credit_card
attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id
attributes[:payments_attributes].first.delete :source_attributes
end
if attributes[:payments_attributes]
attributes[:payments_attributes].first[:request_env] = request_env
end
success = self.update_attributes(attributes)
set_shipments_cost if self.shipments.any?
end
#updating_params = nil
success
end
end
When I run this code, spree never finds #updating_params[:order][:existing_card], even when I select an existing card. Because of that, I can never complete the transaction using a pre-existent card and bogus gateway(gives me empty blanks errors instead).
I tried to bind the method in order_decorator.rb using pry and noticed that the [:existing_card] is actuality at #updating_params' level and not at #updating_params[:order]'s level.
When I delete the decorator, the original code just works fine.
Could somebody explain to me what is wrong with my code?
Thanks,
The method you want to redefine is not really the method of the Order class. It is the method that are mixed by Checkout module within the Order class.
You can see it here: https://github.com/spree/spree/blob/master/core/app/models/spree/order/checkout.rb
Try to do what you want this way:
Create file app/models/spree/order/checkout.rb with code
Spree::Order::Checkout.class_eval do
def self.included(klass)
super
klass.class_eval do
def update_from_params(params, permitted_params, request_env = {})
...
...
...
end
end
end
end
I just don't want to copy and paste. There has to be a better way.
I've got a single controller that uses a single model to collect some data into two arrays. Then I have a single view (index.html.erb) that uses those arrays in a graph. It's absurdly simple. This is the whole view. The arrays from the controller are, obviously, #buildStepArrays and #buildDates.
<% chart = GChart.line(:title=>"Build Times", :size=>"1000x300", :data=>#buildStepArrays, :colors=>#colors, :legend=>#buildDates) %>
<% chart.axis(:left) %>
<%= image_tag chart.to_url %>
Controller is here
def index
# These three arrays should be the same size
#buildStepArrays = []
#buildDates = []
#totalBuildTimes = []
#latestId = Env2.last().BuildId
#latestId = #latestId - 1
for buildNumber in (#latestId-4)..#latestId
#build = Env2.find_all_by_BuildId(buildNumber)
totalTime = 0
#currentBuildTimes = []
for step in #build
#currentBuildTimes << step.Minutes
totalTime += step.Minutes.to_i
end
#buildStepArrays << #currentBuildTimes.map { |e| e.nil? ? 0 : e }
#totalBuildTimes << totalTime
#buildDates << #build.last().Created
#colors = [["FF1300"], ["FF8C00"], ["FFFF00"], ["00CC00"], ["1240AB"]]
end
end
What I would like to do is collect the exact same data from four models (same table in four different databases), not just one, and then show four graphs on the view instead of one. I don't know enough about Rails to know how to do this correctly. My only idea right now is to literally copy and paste the code inside my controller four times and change the variable names around. That is totally awful. What am I "supposed" to do?
One way to achieve this is to create modules that hold code that is repeated, which is your case, is at least four times. What you would do is still create the controller files for each model...
class ModelAController < ApplicationController
include BasicActions
before_filter get_model
def get_model
#model = self.class.to_s.gsub("Controller").classify
end
end
You'll notice the 'get_model' method, which is important, since it sets the #model variable to hold the class you are dealing with. I didn't confirm the exact code to get just the coass name text, so you'll have to play around with that. You'll probably be using #model inside your module.
Here is a skeletal module:
module BasicActions
def some_method
...
end
You'll still need all of related views for each action that renders, but there are ways to limit that to just one set of views...
The most elegant solution would probably be to use a presenter. Presenters can help you either clean up logic from your views or clean up too many instance variables from your controller. The latter seems to be the case here.
Using a presenter would allow you to:
Optimize database queries, in your case doing a single query from all four databases.
Make testing your code much easier by allowing you to write tests directly to the actions in your presenters.
It's hard to say exactly how you would build your presenters without knowing more about your app, but I can give you some general guidelines with mockup code just to show you how it works.
Instead of simply defining your instance variables from the four different models, you would define a new action in your controller similar to this:
/app/controllers/charts_controller.rb
...
def show
#data = ChartPresenter.new(argument)
end
...
And then you would define the new presenter class in a new directory, with actions corresponding to the output you need in your views:
/app/presenters/chart_presenter.rb
class ChartPresenter
def initialize(data)
#data = data
end
def method_name
...
end
end
/app/views/chart.html.erb
...
<%= #data.method_name %>
...
References
RailsCasts #287 - Presenters from Scratch - only available for RailsCasts Pro subscribers
Simplifying your Ruby on Rails code: Presenter pattern, cells plugin - it's from 2009, but might be useful.
P.S.
Most tutorials will tell you to edit your config/application.rb to set config.autoload_paths to your presenters' directory. However, in Rails 3 this is no longer necessary since everything under /app/* is automatically added.
Try this
Controller
def index
#data = [Env1, Env2, Env3].map do |model|
data = Hash.new
data[:title] = "Build Times"
data[:buildStepArrays] = []
data[:buildDates] = []
data[:totalBuildTimes] = []
data[:latestId] = model.last().BuildId
data[:latestId] = data[:latestId] - 1
for buildNumber in (data[:latestId]-4)..data[:latestId]
data[:build] = model.find_all_by_BuildId(buildNumber)
totalTime = 0
data[:currentBuildTimes] = []
for step in data[:build]
data[:currentBuildTimes] << step.Minutes
totalTime += step.Minutes.to_i
end
data[:buildStepArrays] << data[:currentBuildTimes].map { |e| e.nil? ? 0 : e }
data[:totalBuildTimes] << totalTime
data[:buildDates] << data[:build].last().Created
data[:colors] = [["FF1300"], ["FF8C00"], ["FFFF00"], ["00CC00"], ["1240AB"]]
end
end
end
View
<% #data.each do |data| %>
<% chart = GChart.line(:title=>data[:title], :size=>"1000x300", :data=>data[:buildStepArrays], :colors=>data[:colors], :legend=>data[:buildDates]) %>
<% chart.axis(:left) %>
<%= image_tag chart.to_url %>
<% end %>
I just trowed everything into the Hash but you better just put there you need to render in the view.
I have a class that I use to contain select menu options for property types. It works fine. However, I need to be able to verify the selection and perform specific logic based on the selected option. This needs to happen in my Ruby code and in JavaScript.
Here is the class in question:
class PropertyTypes
def self.[](id)
##types[id]
end
def self.options_for_select
##for_select
end
private
##types = {
1 => "Residential",
2 => "Commercial",
3 => "Land",
4 => "Multi-Family",
5 => "Retail",
6 => "Shopping Center",
7 => "Industrial",
8 => "Self Storage",
9 => "Office",
10 => "Hospitality"
}
##for_select = ##types.each_pair.map{|id, display_name| [display_name, id]}
end
What is the best way to verify the selection? I need to perform specific logic and display user interface elements based on each type of property type.
Since I am storing the id, I would be verifying that the id is a particular property type. Something like:
PropertyTypes.isResidential?(id)
Then this method would look like this:
def self.isResidential?(id)
##types[id] == "Residential"
end
But now I am duplicating the string "Residential".
For JavaScript, I assume I would make an ajax call back to the model to keep the verification code DRY, but this seems like over kill.
Do I need to manually create a verification method for each property type or can I use define_method?
This seems so basic yet I am confused and burned out on this problem.
Thanks
===
Here's my solution:
class << self
##types.values.each do |v|
# need to remove any spaces or hashes from the found property type
v = v.downcase().gsub(/\W+/, '')
define_method "is_#{v}?", do |i|
type_name = ##types[i]
return false if type_name == nil #in case a bogus index is passed in
type_name = type_name.downcase().gsub(/\W+/, '')
type_name == v
end
end
end
It sounds like you can benefit from some Ruby meta-programming. Try googling "ruby method_missing". You can probably do something quick & dirty along the lines of:
class PropertyTypes
def method_missing(meth, *args, &block)
if meth.to_s =~ /^is_(.+)\?$/
##types[args.first] == $1
else
super
end
end
end
On the ruby side you could also use something like this to define dynamically these methods:
class << self
##types.values.each do |v|
define_method "is_#{v}?", do |i|
##types[i] == v
end
end
end