I have a simple post and user app where users submit and upvote posts. I have two models, post and user, and right now I'm working with the post model to add "upvoting" functionality.
Each post has an :id, :upvote and :users_voted_by attribute.
Each user has a :username, :posts_voted_on and good_karma attribute
When a user clicks the upvote button on a post, I need to make sure the current user isn't in the :users_voted_by column for that post, if not, add 1 to the :upvote attribute of that post. This part is done.
But I also need to add the post :id into the current user's :posts_voted_on field and add 1 to the submitter's :good_karma field.
Post model:
class Post < ActiveRecord::Base
attr_accessible :comment_count, :downvote, :id, :text, :title, :upvote, :url, :user, :users_voted_by
end
User model:
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation, :username, :posts_voted_on, :good_karma, :bad_karma
attr_accessor :password
before_save :encrypt_password
validates_confirmation_of :password
validates_presence_of :password, :on => :create
validates_presence_of :email
validates_uniqueness_of :email
validates_presence_of :username
validates_uniqueness_of :username
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
def encrypt_password
if password.present?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end
end
My upvote method is within posts_controller:
def upvote
#post = Post.find(params[:post_id])
respond_to do |format|
if #post.users_voted_by.index(current_user.username) == nil && #post.update_attributes(:upvote => #post.upvote + 1, :users_voted_by => #post.users_voted_by + ',' + current_user.username)
format.html { redirect_to #post }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #post.errors, status: :unprocessable_entity }
end
end
end
The 5th line is where the magic is. Is makes sure the post I'm voting on doesn't have the current user in the :users_voted_by field. Then it updates the :upvote and :users_voted_by fields.
How can I add to this method (action?) so that it also updates the attributes of the voting user and the post submitter to store the increase in karma.
Guess a service object can help here, something like (not tested)
class Voter
def initialize(post, user)
#post = post
#user = user
end
def upvote
return false unless #post.users_voted_by.index(#user.username)
#post.upvote += 1
#post.users_voted_by = #post.users_voted_by + ',' + #user.username
#user.good_carma += 1
#post.save && #user.save
end
def downvote
...
end
end
Then controller will look like
def upvote
#post = Post.find(params[:post_id])
respond_to do |format|
if Voter.new(#post, current_user).upvote
format.html { redirect_to #post }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #post.errors, status: :unprocessable_entity }
end
end
end
Related
i have multiple tags for each product in my rails project and i wanted to search my products through tags and i've been getting the following error when i search:
ActiveRecord::RecordNotFound in ProductsController#index
Couldn't find Tag
I have used a tags model to store the tags and a taggings model to connect tags and products table(with many to many relationship)
PRODUCTS_CONTROLLER
class ProductsController < ApplicationController
# before_action :authenticate_user!
before_action :set_product, only: [:show, :edit, :update, :destroy]
# GET /products
# GET /products.json
def index
if params[:tag]
#products = Product.tagged_with(params[:tag])
else
#products = Product.all
end
end
# GET /products/1
# GET /products/1.json
def show
end
# GET /products/new
def new
#product = Product.new
end
# GET /products/1/edit
def edit
end
# POST /products
# POST /products.json
def create
#product = current_user.products.new(product_params)
respond_to do |format|
if #product.save
format.html { redirect_to #product, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: #product }
else
format.html { render :new }
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /products/1
# PATCH/PUT /products/1.json
def update
respond_to do |format|
if #product.update(product_params)
format.html { redirect_to #product, notice: 'Product was successfully updated.' }
format.json { render :show, status: :ok, location: #product }
else
format.html { render :edit }
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
# DELETE /products/1
# DELETE /products/1.json
def destroy
#product.destroy
respond_to do |format|
format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product
#product = Product.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:filetype, :title, :img_url, :description, :all_tags, :price, :uploaded_by, :tutorial_url)
end
end
PRODUCT MODEL
class Product < ActiveRecord::Base
belongs_to :user
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
has_many :comments, dependent: :destroy
has_attached_file :img_url, styles: { large: "800x600>", medium: "320x200>", thumb: "100x80#" }, validate_media_type: false
validates_attachment_content_type :img_url, :content_type => ["image/jpg", "image/jpeg", "image/png", "image/gif"]
def self.tagged_with(name)
Tag.find_by!(name: name).products
end
def all_tags=(names)
# names="music, spotify"
self.tags = names.split(',').map do |name|
Tag.where(name: name).first_or_create!
end
end
def all_tags
tags.map(&:name).join(", ")
end
end
TAG MODEL
class Tag < ActiveRecord::Base
has_many :taggings, dependent: :destroy
has_many :products, through: :taggings
end
TAGGINGS MODEL
class Tagging < ActiveRecord::Base
belongs_to :product
belongs_to :tag
end
and this is how i m searching the tags
<div id="search-bar">
<%= form_tag products_path, :action=>"index", :method=>"get" do %>
<%= text_field_tag :tag, params[:tag], :name=>"tag", :placeholder=>" Search Anything", :id=>"s-bar" %>
<%= submit_tag "Search",:name=>"tag", :value=>"search", :style=>"width: 100px; border: none; background: #eee; font-size: 25px; position: relative; top: 5px; display: none;"%>
<%end%>
</div>
In your product model,
def self.tagged_with(name)
tags = Tag.where('LOWER(name) like ?', "#{name.downcase}")
products = []
tags.each do |tag|
products << tag.products
end
return products
end
This will return an array of products whose tags are similar to the one searched in your text box.
RAILS 5 PROBLEM. I have a project and I wanted the user to have the option to add extra items to the form. My code renders correctly and it the link to add more fields does add more text field. The problem I have is that is not saving the to my database. I am using mysql2 gem on a MySQl database. I posted the console output. This is what I have:
forms_controller
# forms_controller.rb
def new
#form = Form.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #form }
end
end
def create
#form = Form.new(form_params)
respond_to do |format|
if #form.save
format.html { redirect_to(#form, :notice => 'form was successfully created.') }
format.xml { render :xml => #form, :status => :created, :location => #form }
else
format.html { render :action => "new" }
format.xml { render :xml => #form.errors, :status => :unprocessable_entity }
end
end
end
def form_params
params.require(:form).permit(:name, items_attributes: [:id, :item_name, :_destroy])
end
models
# form.rb
class Form < ApplicationRecord
has_many :items
accepts_nested_attributes_for :items, reject_if: :all_blank, allow_destroy: true
validates_presence_of :name
end
# item.rb
class Item < ApplicationRecord
belongs_to :form
end
views
<!-- _form.html.haml -->
= form_for #form do |f|
.field
= f.label :name
%br
= f.text_field :name
%h3 items
#items
= f.fields_for :items do |item|
= render 'item_fields', f: item
.links
= link_to_add_association 'add item', f, :items
= f.submit
.nested-fields
.field
= f.label :item_name
%br
= f.text_field :item_name
= link_to_remove_association "remove item", f
console
tarted POST "/forms" for 127.0.0.1 at 2016-09-08 23:04:17 -0600
Processing by FormsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"FmYPwlXy93iwBMkWvfyd3QJ+NKDcYnraUaKmMISbUIX3+KRL7KtpD33FW3CK+jxvn4AoUUMhx4zrGncJej5BOw==", "form"=>{"name"=>"sfsf", "items_attributes"=>{"1473397456358"=>{"item_name"=>"sfsf", "_destroy"=>"false"}}}, "commit"=>"Create Form"}
(0.2ms) BEGIN
(0.3ms) ROLLBACK
Rendering forms/new.html.haml within layouts/application
Rendered forms/_item_fields.html.haml (1.5ms)
Rendered forms/_item_fields.html.haml (1.2ms)
Rendered forms/_form.html.haml (8.5ms)
Rendered forms/new.html.haml within layouts/application (10.1ms)
Completed 200 OK in 68ms (Views: 54.5ms | ActiveRecord: 0.5ms)
in your permit/require line (in your controller), item_attribute should probably be item_attributes (there's more than one) ie:
def form_params
params.require(:form).permit(:name, :date_sent, :quantity, :comment, item_attributes: [:id, :name, :form_id, :_destroy])
end
In Rails 5, whenever we define a belongs_to association, it is required to have the associated record present by default after this change.
It triggers validation error if associated record is not present.
class User < ApplicationRecord
end
class Post < ApplicationRecord
belongs_to :user
end
post = Post.create(title: 'Hi')
=> <Post id: nil, title: "Hi", user_id: nil, created_at: nil, updated_at: nil>
post.errors.full_messages.to_sentence
=> "User must exist"
****Opting out of this default behavior in Rails 5****
We can pass optional: true to the belongs_to association which would remove this validation check.
class Post < ApplicationRecord
belongs_to :user, optional: true
end
post = Post.create(title: 'Hi')
=> <Post id: 2, title: "Hi", user_id: nil>
The reason it wouldn't save is because I was using Rails 5 and I needed to add optional: true to the belongs_to in the item model.
# form.rb
class Form < ApplicationRecord
has_many :items
accepts_nested_attributes_for :items, reject_if: :all_blank, allow_destroy: true
validates_presence_of :name
end
# item.rb
class Item < ApplicationRecord
belongs_to :form , optional: true
end
I am new to web development (I have been coding for about 6/7 months now) in hopes of building my own app. I have been reading and working of the Ruby on Rails tutorial by Michael Hartl. I am on the chapter 9 section deleting users and am stuck. Everything works for the most part except that the user link. It just doesn't show on the display page. The users and their avatars fine. I just can't figure out a way to get the user delete link to show. Below is the code for the user model, the user controller, the session controller where the code current user lives and the view.
Also, when ever i take out the if statement from the user partial the delete link shows. I am not sure whats wrong since I already defined admin, and the current_user.
I would be entirely grateful for any kind of guidance. My project is also on github if the information below isn't sufficient enough
https://github.com/krischery2150/Try2150-master/tree/updating-users
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
def new
#user = User.new
end
def show
#user = User.find(params[:id])
end
def create
#user = User.new(user_params)
if #user.save
log_in #user
flash[:success] = "Welcome to the Sample App!"
redirect_to #user
else
render 'new'
end
end
def edit
#user = User.find(params[:id])
end
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
flash[:success] = "Profile Updated"
redirect_to #user
else
render 'edit'
end
end
def index
#users = User.paginate(page: params[:page])
end
def destroy
User.find(params[:id]).destroy
flash[:success] = "Your profile was deleted"
redirect_to users_url
end
private
def user_params
params.require(:user).permit(:username, :email, :password,
:password_confirmation, :user_about_me,
:birthday, :avatar, :gender)
end
##Before filters method
# Confirms that a given user is logged in. Only when these conditions are met the user will
# be able to update or edit their page
def logged_in_user
unless logged_in?
store_location
flash[:danger]= "Please Log In"
redirect_to login_url
end
end
# Confirms the correct user.
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user)
end
# Confirms an admin user.
def admin_user
redirect_to(root_url) unless current_user.admin?
end
end
.
class User < ActiveRecord::Base
before_save {self.email = email.downcase}
attr_accessor :remember_token
has_attached_file :avatar, styles: { medium: "300x300>", thumb: "50x50>" }, default_url: "/images/:thumb/missing.png"
validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\Z/
# this before_save is a callback method. What it does is before it saves the email
#address it calls back and transforms all the letters into lower case. Had to do the indexing
#in active record in order for the method to work
validates :username , presence: true, length: {maximum: 250}
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
#code that ensures that a user puts the right format for emails in signup
#fields
validates :email, presence: true, length:{maximum: 50},
format:{with: VALID_EMAIL_REGEX },
uniqueness:{ case_sensitive: false }
#rails still assumes that uniquess is true
#whether the user types CAMELcase or lowercase
validates :password, presence: true, length:{maximum: 50}, allow_nil: true
validates :user_about_me, presence: true
validates :birthday, presence:true
has_secure_password
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
## returns a random user token
def User.new_token
SecureRandom.urlsafe_base64
end
# Remember a given user to the database for use of persistent sessions
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
##returns true if given token matches the digest
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
def forget
update_attribute(:remember_digest, nil)
end
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
end
This is the code in the partial rendering on the index page where all users show up.
<div class="col-md-9 col-offset-3" id="index-profile">
<li class="users">
<div class="col-xs-3 profilepic-container">
<%= image_tag user.avatar.url %>
</div>
<%= link_to user.username, user %>
<% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</li>
The delete link in your partial has two conditions that are required to be true. The user must be an admin, and the profile must not be their own profile. So if the admin user is the only user, then no delete link will show up.
Try creating a second user and see if the delete link shows up for that user.
My goal is to be able to update a saved record when items are de-selected from a collection_select (and then that record is resubmitted.) Thanks in advance for your help!
Details
I've got a form for Newsavedmaps. Newsavedmaps can have many waypoints. Users can select waypoints in a collection_select, and when they save the Newsavedmap, these waypoints are saved to their own database table.
The problem: when users open the Newsavedmap they've saved, I want them to be able to de-select a waypoint. When they save the Newsavedmap again, I want the de-selected waypoint to be deleted.
This is a Rails 2.3X app I'm maintaining, which is why the collection_select uses a different format below.
Model
class Newsavedmap < ActiveRecord::Base
belongs_to :itinerary
has_many :waypoints, :dependent => :destroy
accepts_nested_attributes_for :waypoints, :reject_if => lambda { |a| a[:waypointaddress].blank? }, :allow_destroy => true
end
View
<% form_for #newsavedmap, :html => { :id => 'createaMap' } do |f| %>
<%= f.error_messages %>
<%= f.text_field :name, {:id=>"savemap_name", :size=>30 }%></p>
<%= collection_select :waypoints, :waypointaddress, #newsavedmap.waypoints, :waypointaddress, :waypointaddress, {}, { :multiple => true, :class => "mobile-waypoints-remove", :id =>"waypoints" } %>
<% end %>
Newsavedmaps Controller
def create
#newsavedmap = Newsavedmap.new(params[:newsavedmap])
waypoint = #newsavedmap.waypoints.build
respond_to do |format|
if #newsavedmap.save
flash[:notice] = 'The new map was successfully created.'
format.html { redirect_to "MYURL"}
format.xml { render :xml => #newsavedmap, :status => :created, :location => #newsavedmap }
else
format.html { render :action => "new" }
format.xml { render :xml => #newsavedmap.errors, :status => :unprocessable_entity }
end
end
end
def update
#newsavedmap = Newsavedmap.find(params[:id])
if #newsavedmap.itinerary.user_id == current_user.id
respond_to do |format|
if #newsavedmap.update_attributes(params[:newsavedmap])
flash[:notice] = 'Newsavedmap was successfully updated.'
format.html { redirect_to "MYURL" }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #newsavedmap.errors, :status => :unprocessable_entity }
end
end
else
redirect_to '/'
end
end
Params when creating new record
Parameters: {"newsavedmap"=>{"name"=>"Name of my map", OTHER FIELDS NOT SHOWN ABOVE, "waypoints"=>{"waypointaddress"=>["1600 Pennsylvania Ave NW, Washington, DC 20500", "350 5th Ave, New York, NY 10118"]}}
I think your problem is in correctly built form (and params which come from it).
I suggest to
look at gem cocoon or nested_form
add waypoints_attributes to attr_accessible
implement helpers from gem (from point #1) in your form
And Rails magic should done other job
Another variant (without using gems) (I think much more difficult way!)
You can remove accept_nested_attributes at all and work with your params directly. But in this case you should manage manually all process: correct inserting records, correct destroying them.
In your case it should smth like this (it is not tested!). The example based on your params which posted in the question.
def create
# delete params 'waypoints' it will be manage manually
waypoints = params[:newsavedmap].delete(:waypoints)
#newsavedmap = Newsavedmap.new(params[:newsavedmap])
waypoints.each do |waypoint|
#newsavedmap.waypoints.build(:waypointaddress => waypoint)
end
if #newsavedmap.save
...
end
end
the main troubles will be in method update
def update
# delete params 'waypoints' it will be manage manually
waypoints = params[:newsavedmap].delete(:waypoints)
# find and setup attributes
#newsavedmap = Newsavedmap.find(params[:id])
#newsavedmap.attributes = params[:newsavedmap]
# TROUBLES start here
# destroy not checked (but existed in DB) waypoints
existed_waypoints = #newsavedmap.waypoints
existed_waypoint_addresses = existed_waypoints.map(&:waypointaddress)
new_waypoints = []
waypoints.each do |waypoint|
if existed_waypoint_addresses.include?(waypoint)
# existed waypoint was selected on the form
# find it and add to new_waypoints
new_waypoints << #newsavedmap.waypoints.find_by_waypointaddress(waypoint)
else
# new waypoint was selected on the form
# build it and add to new_waypoints
new_waypoints << #newsavedmap.waypoints.build(:waypointaddress => waypoint)
end
end
# setup new records for waypoints
#newsavedmap.waypoints = new_waypoints
if #newsavedmap.save
# destroy existed but not selected waypoints
(existed_waypoints - new_waypoints).map(&:destroy)
...
end
end
So I have a service object that submits up votes and voted users into a database:
Here's the posts model:
class Post < ActiveRecord::Base
attr_accessible :comment_count, :downvote, :id, :text, :title, :upvote, :url, :user_id, :users_voted_up_by, :users_voted_down_by
serialize :users_voted_up_by, Array
serialize :users_voted_down_by, Array
belongs_to :user
end
Here's the User model:
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation, :username, :good_karma, :bad_karma, :posts_voted_up_on, :posts_voted_down_on
serialize :posts_voted_up_on, Array
serialize :posts_voted_down_on, Array
has_many :posts
attr_accessor :password
before_save :encrypt_password
validates_confirmation_of :password
validates_presence_of :password, :on => :create
validates_presence_of :email
validates_uniqueness_of :email
validates_presence_of :username
validates_uniqueness_of :username
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
def encrypt_password
if password.present?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end
end
Now here's my Voter class that controls up votes and down votes.
class Voter
def initialize(post, user)
#post = post
#user = user
end
def upvote
return false unless #post.users_voted_up_by
#post.upvote += 1
#post.users_voted_up_by << #user.username
#user.good_karma += 1
#post.save && #user.save
end
def downvote
return false unless #post.users_voted_down_by
#post.upvote += 1
#post.users_voted_down_by << #user.username
#user.bad_karma += 1
#post.save && #user.save
end
end
It adds and retrevies the first one fine:
User1
But when I perform another "upvote" with a different user, instead of adding it to the array it just adds it to the string like this:
User1User2
Am I not using the line correctly?
#post.users_voted_up_by << #user.username
You're trying to model a many-to-many relationship via serialized arrays. In my opinion this is a misuse of Rails serialization feature.
The more appropriate way to design your problem is to create a Vote model which belongs to User and has one Post. It is better both in design perspective and db-performance perspective.
Once you do that you won't add users to a post votes array, you will just create a new Vote instance with the voter (user) id and the post id.