I'm trying to call a method when a button is clicked to go and fetch a tweet using the Twitter gem, and store that in my database.
I have a model called Sponsor (which includes a column storing a twitter user name), and a model called Sponsortweet:
models/sponsor.rb:
class Sponsor < ActiveRecord::Base
attr_accessible :facebook, :name, :twitter
has_many :sponsortweets, dependent: :destroy
validates :name, presence: true, uniqueness: { case_sensitive: false }
VALID_TWITTER_REGEX = /\A^([a-zA-Z](_?[a-zA-Z0-9]+)*_?|_([a-zA-Z0-9]+_?)*)$/
validates :twitter, format: { with: VALID_TWITTER_REGEX },
uniqueness: { case_sensitive: false }
def create_tweet
tweet = Twitter.user_timeline(self.twitter).first
self.sponsortweets.create!(content: tweet.text,
tweet_id: tweet.id,
tweet_created_at: tweet.created_at,
profile_image_url: tweet.user.profile_image_url,
from_user: tweet.from_user,)
end
end
models/sponsortweet.rb:
class Sponsortweet < ActiveRecord::Base
attr_accessible :content, :from_user, :profile_image_url, :tweet_created_at, :tweet_id
belongs_to :sponsor
validates :content, presence: true
validates :sponsor_id, presence: true
default_scope order: 'sponsortweets.created_at DESC'
end
In controllers/sponsors_controller.rb:
def tweet
#sponsor = Sponsor.find_by_id(params[:id])
#sponsor.create_tweet
end
Relevant line in my routes.rb:
match 'tweet', to: 'sponsors#tweet', via: :post
In my view (views/sponsors/show.html.haml):
= button_to :tweet, tweet_path
With this code, I get the following error when clicking on the button:
undefined methodcreate_tweet' for nil:NilClass`
If I change to use find (instead of find_by_id), the error is:
Couldn't find Sponsor without an ID
...which makes me think that an ID isn't being passed, since as far as I know, using find raises an error, whereas find_by_id returns nil.
What should I change to cause an ID to be passed?
You need to pass through the id parameter with the path helper:
= button_to :tweet, tweet_path(:id => #sponsor.id)
If you don't want it in the query string:
= form_tag tweet_path do |f|
= hidden_field_tag :id => #sponsor.id
= submit_tag "Tweet"
This does the same thing as your button_to, but adds a hidden field to the form that is generated.
Related
Issue
I'm encountering a problem when editing a form with a belongs_to relationship (extra_guest belongs_to age_table).
I am able to create a new extra_guest and assign it to an age_table, but I cannot get the edit/update to work as my update function returns a falseClass.--> #extra_guest.update(extra_guest_params).errors.full_messages returns undefined method `errors' for false:FalseClass
Code
models
class ExtraGuest < ApplicationRecord
belongs_to :age_table
validates :age_table, presence: true
end
class AgeTable < ApplicationRecord
belongs_to :park
has_many :extra_guests, dependent: :destroy
validates :name, :age_from, :age_to, presence: true
validates_associated :extra_guests
end
class Attraction < ApplicationRecord
belongs_to :park
has_many :extra_guests, dependent: :destroy
accepts_nested_attributes_for :extra_guests, allow_destroy: true
validates :name, presence: true
end
class Park < ApplicationRecord
has_many :attractions, dependent: :destroy
has_many :age_tables, dependent: :destroy
validates :name, :currency, presence: true
end
extra_guests_controller
def edit
#extra_guest = ExtraGuest.find(params[:id])
#age_table = #extra_guest.age_table
#age_table_list = AgeTable.where(park: #attraction.park)
end
def update
#extra_guest = #attraction.extra_guests.find(params[:id])
#age_table = AgeTable.find(params[:age_table])
authorize #extra_guest
if #extra_guest = #extra_guest.update(extra_guest_params)
redirect_to root_path
else
#attraction = Attraction.find(params[:attraction_id])
#extra_guest = ExtraGuest.find(params[:id])
#age_table_list = #attraction.park.age_tables
render 'edit'
end
end
private
def extra_guest_params
params.require(:extra_guest).permit(:name, :age_table_id,
extra_guest_prices_attributes: [:id, :name, :price_type, :start_date, :end_date, :price, :duration, :duration_min, :duration_max, :backend_only, :weekend_extra, :_destroy])
end
views/extra_guests/form
<%= simple_form_for [#attraction, #extra_guest] do |f|%>
<%= f.input :age_table, :as => :select, :selected => #age_table.id, :collection => #age_table_list.map {|u| [u.name, u.id]}, :include_blank => false %>
<% f.button :submit %>
Error message + params
Couldn't find AgeTable without an ID
{"utf8"=>"✓",
"_method"=>"patch",
"authenticity_token"=>"l8HMnVIRybZg==",
"extra_guest"=>
{"age_table"=>"104",
"extra_guest_prices_attributes"=>
{"0"=>{"price"=>"1.0", "weekend_extra"=>"", "start_date"=>"2019-10-15", "end_date"=>"20-09-2019", "duration"=>"", "duration_min"=>"", "duration_max"=>"", "_destroy"=>"false", "id"=>"42"},
"1"=>{"price"=>"1.0", "weekend_extra"=>"", "start_date"=>"2019-10-15", "end_date"=>"2019-10-16", "duration"=>"", "duration_min"=>"", "duration_max"=>"", "_destroy"=>"false", "id"=>"43"}}},
"commit"=>"Save new option",
"attraction_id"=>"185",
"id"=>"55"}
First of all, you say you have an error with this code #extra_guest.update(extra_guest_params).errors.full_messages but the code you show does not have that line.
Now, the update method returns false if it fails https://apidock.com/rails/ActiveRecord/Persistence/update
This line:
#extra_guest = #extra_guest.update(extra_guest_params)
will set #extra_guest to false if it fails, you don't need to set #extra_guest, just use if #extra_guest.update(extra_guest_params)
Using the line of code you name but it's not on the code you showed,#extra_guest.update(extra_guest_params).errors.full_messages, if there are errors then #extra_guest.update(extra_guest_params) will be false, so no .errors method is found.
you have to split it in two lines:
#extra_guest.update(extra_guest_params) # after this, #extra_guest will have the errors hash set
#extra_guest.errors.full_messages # call it on the object and not on the result value from the update method
EDIT: you are permitting age_table_id but the parameter is age_table, fix the name of the parameter to be age_table_id too
It looks to me like you tried to use #attraction before defining it. You could fix this by moving your definition of #attraction further up in the method, but I would move it into its own method like so:
private
def attraction
#attraction ||= Attraction.find(params[:attraction_id])
end
Then you use the method name, which is now defined for the whole controller and invoked when you use it (as opposed to an instance variable which will just be 'nil' if you invoke it without defining it). The ||= allows the method to return the existing value of the instance variable if it is defined, rather than running the query every time the method is called. So first line of your update action would be
#extra_guest = attraction.extra_guests.find(params[:id])
I would do something similar for the other instance variables you have there (#extra_guest, #age_table, and #age_table_list should be defined in private methods separately). Incidentally, using a lot of instance variables for a single controller (you have 4 in this controller, which is a lot) is considered a bit of a code smell, but you should make something that works first and then refactor. Reference for later: https://thoughtbot.com/blog/sandi-metz-rules-for-developers
I have one table for Products and the product can either be in the interior or exterior or both. So I created another table to save the Products location. Now when admin adds the product I have provided the option to select the location(s) the product can be in, but when it is posted the code says the field can't be blank because of the validation. I am not sure what I am missing or the approach is wrong.
The product model:
class Product < ApplicationRecord
validates :name, presence: true
has_many :product_locations
accepts_nested_attributes_for :product_locations
end
The product location model:
class ProductLocation < ApplicationRecord
enum locations: [:exterior, :interior]
validates :location, presence: true
validates :product_id, presence: true
belongs_to :product
end
The ActiveAdmin file for Product:
ActiveAdmin.register Product do
permit_params :name, product_locations_attributes: {}
actions :all, except: [:show, :destroy]
filter :name
index do
column 'Product Name', :name
actions
end
form do |f|
f.semantic_errors *f.object.errors.keys
f.inputs "Products" do
f.input :name
end
f.has_many :product_locations do |location|
location.inputs "Locations" do
location.input :location, as: :select, multiple: true, collection: ProductLocation.locations.keys
end
end
f.actions
end
controller do
def scoped_collection
Product.where(user_id: nil)
end
end
end
I get a multi-select for the locations which has "Interior" and "Exterior" for selection, but it says the field can't be blank when I select the location and submit the form
The error on save click I get is:
Location can't be blank
The params that get posted are:
Parameters: {"utf8"=>"✓", "product"=>{"name"=>"Test Product", "product_locations_attributes"=>{"0"=>{"location"=>["0", "1"]}}}, "commit"=>"Create Product"}
First, the permit attributes should be,
product_locations_attributes: [:id, :location]
Then, in your form
location.input :location, as: :select, multiple: true, collection: ProductLocation.locations.keys
Since ProductLocation.locations is an array, array.keys is an invalid method.
So, use directly
location.input :location, as: :select, multiple: true, collection: ProductLocation.locations.map { |n| [n,n] }
To store an array of multiple values take serialize field as an array,
class ProductLocation < ApplicationRecord
enum locations: [:exterior, :interior]
serialize :location, Array
validates :location, presence: true
validates :product_id, presence: true
belongs_to :product
end
Note: Inorder to get the serialize work, you need to have the dataType of the location as a text. If it is not text run a migration to change to text data type
Reason for text field: Rails will convert all those object into plain text when storing in database
I have a study that can have participants. I have a simple_form where the user can add participants. It looks a bit like a table:
name | company | email OR mobile | timezone
name | company | email OR mobile | timezone
name | company | email OR mobile | timezone
By default, the screen has three fieldset rows, and the user can add more rows if needed. Each row is one participant.
I would like my participant model to validate only the rows that have been filled out, and ignore rows that are blank because even though we are showing three by default to the user, not all three are required fields.
Here's the relevant portion of app/models/participants.rb.
class Participant < ApplicationRecord
belongs_to :study
validates :name, presence: true
validates :company, presence: true
validates :time_zone, presence: true
if :channel == 'sms'
validates :mobile_number, presence: true
elsif :channel == 'email'
validates :email, presence: true
end
end
In participants_controller.rb I have:
def index
3.times { #study.participants.build } if #study.participants.length.zero?
end
The problem is that I get an error because simple_form thinks that all three fields are required, and not just the first row.
Rails' validators accept conditions:
validates :mobile_number, presence: true, if: Proc.new { |p| p.study.channel == 'sms' }
validates :email, presence: true, if: Proc.new { |p| p.study.channel == 'email' }
By default all inputs are required. When the form object includes
ActiveModel::Validations (which, for example, happens with Active
Record models), fields are required only when there is presence
validation. Otherwise, Simple Form will mark fields as optional. For
performance reasons, this detection is skipped on validations that
make use of conditional options, such as :if and :unless.
And of course, the required property of any input can be overwritten
as needed:
<%= simple_form_for #user do |f| %>
<%= f.input :name, required: false %>
<%= f.input :username %>
<%= f.input :password %>
<%= f.button :submit %>
<% end %>
Try to put all the inputs as required: false. That should allow skip simple_form validations and the data came into the controller and the model can be filtered or/and validated and every other things you want to do before persist.
In the model class you can use several ways of validations for example:
you also can use the :if and :unless options with a symbol corresponding to the name of a method that will get called right before validation happens. This is the most commonly used option.
for example
class Participant < ApplicationRecord
belongs_to :study
validates :name, presence: true
validates :company, presence: true
validates :time_zone, presence: true
validates :mobile_number, presence: true if: :channel_is_sms?
validates :email, presence: true if: :channel_is_email?
def channel_is_sms?
channel == "sms"
end
def channel_is_email?
channel == "email"
end
end
or also you can use custom validator where you do all that you need validate. for example
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.channel == 'sms'
...
... actions here
...
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
Real newb question here: I've got courses
class Course < ActiveRecord::Base
attr_accessible :name, :number
has_many :posts
validates :name, presence: true
validates :number, presence: true
validates :number, :format => { :with => /\A\d\d[-]?\d\d\d\z/}
end
and I've got posts
class Post < ActiveRecord::Base
attr_accessible :comments, :course_id, :difficulty, :enjoyability, :hours, :professor, :ranking, :semester, :usefulness
belongs_to :course
end
Almost everything I have was auto-generated by Rails. There are a couple of things I try to do that I can't get to work:
When I "show" a course, I want to show each post associated with that course. However, everything I've tried has given me an error.
After even entering one post into the database (heroku forced me to use PostgreSQL) the index form no longer renders.
I'm almost positive I'm missing something with my associations between them. Does anybody have any ideas as to what I'm doing wrong?
You may get all posts through Course instance:
#course = Course.find(params[:id])
#posts = #course.posts
I've been puzzling over this for quite some time now and can't figure it out.
I've got 2 models:
class Vehicle < ActiveRecord::Base
attr_accessible :year, :capacity,
:size, :body, :model_id, :maker_id, :parameters_attributes
validates :year, numericality: { greater_than: 1900 }
validates :year, :capacity, :size, :body, presence: true
belongs_to :model
belongs_to :maker
has_many :parameters
accepts_nested_attributes_for :parameters
end
and
class Parameter < ActiveRecord::Base
attr_accessible :tag, :value
validates :tag, :value, presence: true
belongs_to :vehicle
end
in new vehicle view i've got:
= form_for [:admin, #vehicle], html: { multipart: true } do |f|
=# some other stuff in between
= f.text_field :value, size: 4
I get this error
undefined method `value'
Just can't seem to get it working. Help, anyone?
EDIT
routes.rb
resources :vehicles
resources :parameters
resources :makers do
resources :models
end
If you are using nested form, you should have something like
f.fields_for :parameters do |parameter|
and than:
parameter.text_field :value, size: 4
Also, remember to create the some parameters in the controller, for example:
def new
#vehicle = Vehicle.new
2.times { #vehicle.parameters.build } #it will create 2 parameters
...
end
f refers to #vehicle, it seems only Parameter bears this field. That's why it fails.
Sidenotes:
In Vehicle you have accepts_nested_attributes_for :parameters but you don't have parameters_attributes in the attr_accessible, can't be good.
If you want to call the relationship in the form consider using fields_for
Ok, I've made a mess of things.
Firstly I've been trying to
def new
#vehicle = #vehicle.parameters.build
end
hence the error undefined method. After a while I got to the correct syntax, which is the one gabrielhilal added after a while.
def new
#vehicle = Vehicle.new
#vehicle.parameters.build
end
No matter ;) Still had problems, because after clicking "create" he wouldn't add records in the database. Turned out that I've set the validates presence: true for tag, but didn't assign any value to it. After fixing that, it worked like a charm. Thanks a lot for all the help.
On to the next puzzle.