When user #1 likes/comments on user #2, user #2 gets a notification:
notifications/_notifications.html.erb
<%= link_to "", notification_path(notification.id), method: :delete, class: "glyphicon glyphicon-remove" %> <%= link_to Comment.find_by(notification.comment_id).user.name, user_path(Comment.find_by(notification.comment_id).user.id) %> commented on <%= link_to "your activity", (notification.activity_id) %>
but upon user #2 clicking the notification it doesn't lead anywhere since I removed the activity_path, if I put it back before (notification.activity_id) we get an error:
No route matches
{:action=>"show", :controller=>"activities", > :id=>nil} missing required keys: [:id]
I don't know if this is possible, but upon clicking on the notification user #2 would be taken to the paginated page where the activity is. There are 20 activities per page via the gem will_paginate so if the activity that was commented on is on page 2 then upon clicking on the activity, user #2 should be directed to: http://0.0.0.0:3000/activities?page=2
This would at least narrow down where the comment is on the activity feed for the user.
activities/index.html.erb
<% #activities.each do |activity| %>
<%= link_to activity.user.name, activity.user %>
<%= render "activities/#{activity.trackable_type.underscore}/#{activity.action}", activity: activity %>
<% activity.activity_likers.each do |user| %>
<%= user.name %>
<% end %>
<%= link_to like_activity_path(:id => activity.id), class: "btn", method: :post do %>
<span class='glyphicon glyphicon-thumbs-up'></span> Like
<% end %>
<%= render "comments/comments", comments: activity.comments %>
<%= render "comments/form", new_comment: Comment.new(commentable_id: activity.id, commentable_type: activity.class.model_name), create_url: :activity_comments_path %>
<% end %>
<%= will_paginate #activities %>
activities_controller.rb
class ActivitiesController < ApplicationController
def index
#activities = Activity.order("created_at desc").paginate(:page => params[:page], :per_page => 20)
end
def show
redirect_to(:back)
end
end
Please let me know if you need anymore code if you find that this is possible to do :)
UPDATE
class NotificationsController < ApplicationController
def index
#notifications = current_user.notifications
#notifications.each do |notification|
notification.update_attribute(:read, true)
activity = Activity.find(notification.activity_id) #Gives "Couldn't find Activity with 'id'=". I then removed ".activity_id" to see what happens next.
index = Activity.order(created_at: :desc).index(activity)
page_number = (index / per_page.to_f).ceil #I am then told "undefined local variable or method `per_page'" so I tried making it :per_page to which I then get: "undefined method `to_f' for :per_page:Symbol"
end
end
def destroy
#notification = Notification.find(params[:id])
#notification.destroy
redirect_to :back
end
end
routes.rb
resources :activities do
resources :comments
resources :notifications
member do
post :like
post :notifications
end
end
UPDATE #2
notifications/index.html.erb
<% if !#notifications.blank? %>
<%= render partial: "notifications/notification", collection: #notifications %>
<% else %>
<p>No notifications... yet</p>
<% end %>
comment.rb
class Comment < ActiveRecord::Base
after_create :create_notification
has_many :notifications
has_many :comment_likes
has_many :likers, through: :comment_likes, class_name: 'User', source: :liker
belongs_to :commentable, polymorphic: true
belongs_to :user
belongs_to :activity
private
def create_notification
#activity = Activity.find_by(self.activity)
#user = User.find_by(#activity.user_id).id
Notification.create(
activity_id: self.activity,
user_id: #user,
comment_id: self,
read: false
)
end
end
_create_notifications.rb
class CreateNotifications < ActiveRecord::Migration
def change
create_table :notifications do |t|
t.references :activity, index: true
t.references :comment, index: true
t.references :user, index: true
t.boolean :read
t.timestamps null: false
end
add_foreign_key :notifications, :activities
add_foreign_key :notifications, :comments
add_foreign_key :notifications, :users
end
end
To determine on which page the activity is located, you could do the following:
activity = Activity.find(notification.activity_id)
index = Activity.order(created_at: :desc).index(activity)
The above will determine the index of the activity within all of the activities.
So, let's say you've got 85 activities. You've got 20 activities per page, so in this case you would have 5 pages, right? Alright, let's assume the above index returns 42. To calculate the page number you would have to do this (assuming that you've got a variable called per_page which is 20):
page_number = (index / per_page.to_f).ceil
You've got index 42. Which you'll have to divide by the number of activities per page (it needs to be a float!), so that would be 20.0. That results in 2.1. ceil that and you've got your page number, which would be 3.
So, to create the correct paginated path, you can now do this:
activities_path(page: page_number)
Update
Now that we know that the activity_id isn't correctly set on the notification we can fix that (also note that the comment_id isn't set either). Change the last part of your Comment model to this:
...
belongs_to :activity
validates :activity_id, presence: true
validates :user_id, presence: true
private
def create_notification
Notification.create(activity_id: self.activity_id, user_id: self.user_id, comment_id: self.id, read: false)
end
I've added two validations here to make sure the activity_id and user_id are set. Now as I said before the comment_id isn't set either. That's because the id is only assigned on save, on create you are just setting it up to be saved. So, change the after_create :create_notification to after_save :create_notification to be able to set the comment_id as well.
That should set the activity_id and comment_id. Now for the next part. Getting the correct page number which should be added to the link in your _notification.html.erb partial.
Add these methods to your Activity class:
def page_number
(index / per_page.to_f).ceil
end
private
def index
Activity.order(created_at: :desc).index self
end
Now change the path in your notifications/_notification.html.erb partial to:
activities_path(page: notification.activity.page_number)
Note: If you get an error about the per_page in the page_number method you probably haven't set the value per_page in the model itself (like this; Basically add self.per_page = 20 to your model right below class Activity < ActiveRecord::Base)
If you decide to do it like this, you can remove the , :per_page => 20 part in your ActivitiesController. If not, simply replace per_page.to_f with 20.to_f or 20.0.
Also, remove the 3 lines from your NotificationsController which you've commented out previously to find out whether the activity_id was set or not. You don't need them anymore, since we've placed them in the Activity class.
Related
I current have my project set up like this:
resources :boards, :path => '' do
resources :posts, :path => 'thread' do
resources :replies
On /board1/ only posts from board1 show, same for board2. In /board1/thread/1/ it shows post 1 and the replies to it.
However in /board2/thread/1/ the post that is showing is from board1/thread/1/, and in the reverse board1/thread/2/ shows the post from board2/thread/2/.
Each post has a related board_id in the db, and each reply has the related post_id in the db.
How can I keep these separate?
class Board < ActiveRecord::Base
has_many :posts
has_many :replies, through: :posts
include FriendlyId
friendly_id :name, use: :slugged
accepts_nested_attributes_for :posts, :replies
end
class Post < ActiveRecord::Base
belongs_to :board
has_many :replies, dependent: :destroy
accepts_nested_attributes_for :replies
include FriendlyId
friendly_id :pid, use: :slugged
after_create :set_pid
def set_pid
post_max = self.board.posts.maximum(:pid)
reply_max = self.board.replies.maximum(:pid)
if post_max.to_i < reply_max.to_i
self.update_attributes(:pid => reply_max.to_i + 1)
else
self.update_attributes(:pid => post_max.to_i + 1)
end
end
end
Code to display post in /:board_id/show:
<% #board.posts.find_each do |post| %>
<%= post.subject %>
<%= post.name %>
<%= post.email %>
<%= post.created_at %>
No.<%= post.pid %>
<%= link_to "[reply]", board_posts_path(#board, #post)%>
<br>
<%= post.comment %><br><br>
<%= render "replies/replies" %>
<% end %>
Code to display post in /:board_id/thread/:id:
<p>
<%= #post.subject %>
<%= #post.name %>
<%= #post.email %>
<%= #post.created_at %>
No.<%= #post.pid %>
<br>
<%= #post.comment %>
</p>
Edit:
class RepliesController < ApplicationController
def create
#board = Board.friendly.find(params[:board_id])
#post = Post.friendly.find(params[:post_id])
#reply = #post.replies.create(reply_params)
redirect_to #board
end
private
def reply_params
params.require(:reply).permit(:name, :email, :subject, :comment, :pid)
end
end
class PostsController < ApplicationController
def show
#boards = Board.all
#replies = Reply.all
#post = Post.friendly.find(params[:id])
end
def create
#board = Board.friendly.find(params[:board_id])
#post = #board.posts.create(post_params)
if #post.save
redirect_to #board
else render #board
end
end
private
def post_params
params.require(:post).permit(:name, :email, :subject, :comment, :pid)
end
end
The missing part here is the RepliesController which is the source of the problem if I got the question correctly.
Most probably you have there something like #replies = current_post.replies which fetch all replies of the given post regardless of the current board. Scoping post by board will solve the problem:
current_post = Post.find_by(board_id: params[:board_id], id: params[:post_id])
if current_post
#replies = current_post.replies
end
On your friendly_id declaration in the Post model, you don't have the pid as globally unique. Use this form of friendly_id, instead:
friendly_id :pid, use: :scoped, scope: :board
In this way, duplicate friendly_id values for pid are kept separate by the board that they belong to. This is necessary for slugging nested resources properly. The :scoped value says that it's for nested (scoped) models, and the scope: key indicates that posts is nested within boards. Note that you may have to do this with replies, as well.
You'll also want to make sure that your indexes for your :slug are correct. Typically when the :scope is incorrect, you'll find it when you try to save the record. In this case, it looks like the indexes might not be set correctly to ensure the uniqueness of the board name/post pid combination. Check out Friendly ID 4 Using scoped module for more information.
When you have the indexes sorted out, you'll find that inserting new records will require you to have the friendly_id (based on your pid) already assigned. You may also want to look into using slug candidates to dynamically generate the proper slug at creation time. Also check out slug candidates rails 4 for some usage information.
In my quiz game Rails project, I have a table for "Participations" that stores information on the user, the quiz category, and the quiz score after a user completes the test.
class CreateParticipations < ActiveRecord::Migration
def change
create_table :participations do |t|
t.references :user
t.string :category
t.boolean :finished, default: false
t.integer :current_question_index, default: 0
t.integer :score, default: 0
t.timestamps
end
end
end
In my user.rb, I specify an association that a user has_many :participations, which allows a user to play multiple quizzes while storing categories/scores in the table.
If I want to show a user a table of his results (so return all Participations results, but only for those that match the user) in a view, can I call that without generating a new controller?
You can just do like below
#in the controller action
#user_participations = Participation.where(user_id: current_user.id)
and just call #user_participations in the view.
#config/routes.rb
resources :users do
resources :participations, path: "results", only: :index #-> url.com/users/:user_id/results
end
#app/controllers/participations_controller.rb
class ParticipationsController < ApplicationController
def index
#user = User.find params[:user_id]
#participations = #user.participations
end
end
#app/views/participations/index.html.erb
<% #participations.each do |participation| %>
<%= participation.score %>
<% end %>
--
If you have your associations set up correctly, you should be using the associative method (#user.participations), which will basically do what Pavan has suggested:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :participations
end
#app/models/participation.rb
class Participation < ActiveRecord::Base
belongs_to :user
end
Users controller
has_many :participations
def participations
#user = User.find(params[:id])
#participations = #users.participations.build(params[:participation_id])
end
end
ParticipationsController
belongs_to :user
In your routes file you can create the route by
GET 'users/:id/participations', to: 'users#participations', as: 'user_participations'
That will give you a user_participations_path route
So if you wanted to link to it you could add
<%= link_to 'Show user games!', user_participations_path(#participations.user) %>
Then in views/users/participations
<% #participations.each do |participation| %>
<%= participation.inspect %>
<% end %>
Let me know how you go!
EDIT
Please not that the has_many and belongs_to declarations should be in the user.rb and participation.rb models respectively. Thanks to #richpeck for picking up the mistake.
I'm getting a "undefined method `name' for nil:NilClass" when I'm trying to get the users name of a comment from the articles controller. I've tryed to do the same request from the comments controller and it works just fine.
NoMethodError in Articles#show
Showing C:/xampp/htdocs/app/views/comments/_show.html.erb where line #2 raised:
undefined method `name' for nil:NilClass
I' using ruby -v 2.1.5 and rails -v 4.2.2
Here is my comments controller code:
class CommentsController < ApplicationController
before_filter :logged_in_user, only: [:create, :destroy]
def show
#comments= Comment.all
end
def new
#comment = Comment.new
end
def create
#article = Article.find(params[:article_id])
#comment = #article.comments.create(comment_params)
#comment.user_id = current_user.id
if #comment.save
flash[:success] = "Comment created!"
redirect_to article_path(#article)
end
end
private
def comment_params
params.require(:comment).permit(:body)
end
end
And my Articles controller code
class ArticlesController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: [:edit, :update, :destroy]
def show
#article = Article.find(params[:id])
#user = #article.user
#comments = #article.comments
end
...
private
def article_params
params.require(:article).permit(:title, :lead, :content, :hashtags, :front_picture, :category_id, :league_id, :front_pic_src)
end
end
And the views:
/articles => show
<!-- A little peace of my Articles view where I use "user" but under the #article -->
<h3> Article Autor</h3>
<div id="about-info">
<div class="post-profile-img-wrapper">
<%= link_to gravatar_for(#article.user, size: 50), #article.user %>
</div>
<span class="user"><%= link_to #article.user.name, #article.user %> </span>
</div>
<!-- And here I call the render for comments-->
<section class="comments">
<h2>Comments</h2>
<%= render 'comments/show' %>
</section>
And the rendered partial is _show.html.erb inside the comments view
<% #comments.each do|c| %>
<h1><%= c.user.name %></h1>
<% end %>
What am I doing wrong? I've tryed to create an instance variable in articles for user comments and it doesn't work.
The comments table has a foreing key but it's pointing to the article_id I don't know if this is the best way, but it shouldn't affect the controllers behaviour, right?
UPDATE
Here is my comments schema, it may help
create_table "comments", force: :cascade do |t|
t.text "body", limit: 65535
t.integer "article_id", limit: 4
t.integer "user_id", limit: 4
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "comments", ["article_id"], name: "index_comments_on_article_id", using: :btree
add_index "comments", ["user_id"], name: "index_comments_on_user_id", using: :btree
UPDATE FOR Models
Comment:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :article
default_scope -> { order(created_at: :desc) }
validates :body, presence: true
end
Article:
class Article < ActiveRecord::Base
belongs_to :category
belongs_to :user
belongs_to :league
has_many :comments, :dependent => :destroy
default_scope -> { order(created_at: :desc) }
mount_uploader :front_picture, ArticleFrontUploader
validates :user_id, presence: true
validates :category_id, presence: true
validates :content, presence: true
#validate :picture_size
private
# Validates the size of an uploaded picture.
def picture_size
if front_picture.size > 5.megabytes
errors.add(:front_picture, "should be less than 5MB")
end
end
end
Related peace of user
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
has_many :articles, dependent: :destroy
has_many :comments, dependent: :destroy
...
Thanks!
STEP ONE: Setting up a partial
You can try naming local variables when rendering a partial. For example, in /articles/_show.html.erb, you would enter the following at the (current) rendering line:
<%= render partial: 'comments/show', locals: {comments: #comments} %>
That will treat the /comments/_show.html.erb as a piece of a page, rather than a complete one. This is ideal for your situation, since the page itself is about articles, but you only want to render a part of the page to show comments.
The locals hash sets up variables so your partial can render the correct objects, assuming they are named in the current controller. (In this case, the current controller is ArticlesController.) After setting local values, your next step in the comments show page is to replace #comments with simply comments.
Now, say you have no comments associated with the article. It is the same as Comment.where(article_id: #article.id), which is an array. Calling each on an empty array will take the first element, pretty much nil, and raise an error. In the app I work on, the best way to handle this is to prepend the code block with the following:
unless comments.empty?
This makes sense, because why show contents for comments if there are none present? Mind you, if you plan to have a form for entering comments, place that form outside the unless statement, and below the comments show partial.
STEP TWO: Editing associations
I do not know how you want to set this up exactly, so rather than provide copy/paste code, I will just explain. In most cases, comments do not stand alone, they always belong to something. A comment immediately belongs to an article. In your models, your associations should have a user having a comment through an article. Given the code you provided, I believe the hierarcy is the following:
User
Article
Comment
You need a user to make an article. You need an article to make a comment. While a user is required to make a comment, it is not a direct association. Again, I will not provide you with the exact code, as you may want to handle this slightly differently, but along the same lines.
The mistake is in the _show.html.erb in the comments view. The comment has only a user_id attribute. So if you call #comment.user there is nothing to show since the comment doesn't have user attribute. You have to search for the user by the user_id
<% #comments.each do |c| %>
<h1><%= User.find_by_id(c.user_id).name %></h1>
<% end %>
Try this. It would solve the problem.
Pass the locals to partial
<%= render partial: 'comments/show', locals: { comments: #comments } %>
comments/_show.html.erb
<% comments.each do|c| %>
<h1><%= c.try(:user).try(:name) %></h1>
<% end %>
I'm trying to set up a page where there are 4 dropdown boxes, each of which have a full list of Products. A user can select any combination of 4 products, and 'create' a new print page, which has the product information list
I only have one box right now, but when I try to create a new row for Print from this page, it doesn't return anything to :p1
new.html.erb:
<%= f.collection_select :p1, Product.all, :id, :name, :prompt => 'Select One' %>
<%= f.submit "Create" %>
class PrintsController < ApplicationController
def new
#print = Print.new
end
def create
#print = Print.new(print_params)
if #print.save
redirect_to #print, alert: "Created successfully."
else
redirect_to new_print_path, alert: "Error creating print page."
end
end
def show
#print = Print.find(params[:id])
end
private
def print_params
params.require(:p1).permit(:p2, :p3, :p4)
end
end
Model
class Print < ActiveRecord::Base
belongs_to :product
end
Migrate
class CreatePrints < ActiveRecord::Migration
def change
create_table :prints do |t|
t.integer :p1
t.integer :p2
t.integer :p3
t.integer :p4
t.timestamps
end
end
end
Routes:
Rails.application.routes.draw do
resources :categories, :products, :prints
I'm a total rails newbie, so I know I'm probably making a stupid mistake somewhere, but I've been fiddling with code for hours and still haven't figured out what I did wrong.
Your print_params method is wrong :
def print_params
params.require(:print).permit(:p1, :p2, :p3, :p4)
end
This is the right format.
I have the following models:
class User < ActiveRecord::Base
has_many :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :queue
end
class Queue < ActiveRecord::Base
has_many :subscriptions
end
I want to have some meta-data in the Subscription class and allow users to maintain the details of each of their subscriptions with each subscriptions meta-data. Queues produce messages, and these will be sent to users who have Subscriptions to the Queue.
As I see it the resource I want to have is a list of subscriptions, ie the user will fill in a form that has all the Queues they can subscribe to and set some metadata for each one. How can I create a RESTful Rails resource to achieve this? Have I designed my Subscription class wrong?
I presently have this in my routes.rb:
map.resources :users do |user|
user.resources :subscriptions
end
But this makes each subscription a resource and not the list of subscriptions a single resource.
Thanks.
This can be done quite easily using accepts_nested_attributes_for and fields_for:
First in the User model you do the following:
class User < ActiveRecord::Base
has_many :subscriptions
accepts_nested_attributes_for :subscriptions, :reject_if => proc { |attributes| attributes['queue_id'].to_i.zero? }
# if you hit scaling issues, optimized the following two methods
# at the moment this code is suffering from the N+1 problem
def subscription_for(queue)
subscriptions.find_or_initialize_by_queue_id queue.id
end
def subscribed_to?(queue)
subscriptions.find_by_queue_id queue.id
end
end
That will allow you to create and update child records using the subscriptions_attributes setter. For more details on the possibilities see accepts_nested_attributes_for
Now you need to set up the routes and controller to do the following:
map.resources :users do |user|
user.resource :subscriptions # notice the singular resource
end
class SubscriptionsController < ActionController::Base
def edit
#user = User.find params[:user_id]
end
def update
#user = User.find params[:user_id]
if #user.update_attributes(params[:user])
flash[:notice] = "updated subscriptions"
redirect_to account_path
else
render :action => "edit"
end
end
end
So far this is bog standard, the magic happens in the views and how you set up the params:
app/views/subscriptions/edit.html.erb
<% form_for #user, :url => user_subscription_path(#user), :method => :put do |f| %>
<% for queue in #queues %>
<% f.fields_for "subscriptions[]", #user.subscription_for(queue) do |sf| %>
<div>
<%= sf.check_box :queue_id, :value => queue.id, :checked => #user.subscribed_to?(queue) %>
<%= queue.name %>
<%= sf.text_field :random_other_data %>
</div>
<% end %>
<% end %>
<% end %>
I found this tutorial very useful, as I was trying to relate Users to Users via a Follows join table: http://railstutorial.org/chapters/following-users