I’m building JSON API for a mobile social networking application, users be able to have some posts and other users able to like and comments on those posts.
class Post < ActiveRecord::Base
belongs :user
end
class Like < ActiveRecord::Base
belongs :user
belongs :post, counter_cache: true
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :description, :likes_count, :is_liked
def likes_count
object.likes.size # should pull from counter_cache
end
# did current_user already like this post
def is_liked
object.likes(user_id: scope.id).exists? # N+1
end
end
class PostsController < ApplicationController
def index
#posts = Post.recent
render json: #posts, each_serializer: PostSerializer
end
end
# Output of JSON API to mobile client
{
"id": 1,
"body": "...",
"likes_count": 10,
"is_liked": true
}
User logins into mobile clients and make a request to get recent posts when they open the app, but to be able to generate JSON post response with a flag is_liked, we need to issue query to hit DB to able to know if the user is already liked the post, so on mobile screen we show that status on each post on screen.
I was facing the same problem here, so let's see if I can help you...
When you add belongs_to :post, counter_cache: true in the Like model, you must add a column in the posts table called likes_count type integer.
Also, note that counter caches are incremented or decremented when you create or delete a like, so if you have some data in your database it won't work. You have to reset the counters.
I hope it helps...
Related
I have a Posts model which belongs to the User model in my Rails API repo.
I have added a column in the Posts table which is 'username'.
I would like to add the current_user.name to everypost in the username column.
Can I use delegate to add the User who creates the post's Name to the username column on my Post table.
I have already added the user_id as the reference which works.
class Post < ActiveRecord::Base
belongs_to :user
def as_json(options={})
super(only: [:description, :created_at, :user_id, :username])
end
end
This is only for use as a API and everytime I access the Post show route I would like the JSON to return the Users name that the posts belongs to in the JSON under :username
The idea behind relational databases like you are using it with ActiveRecord is that you don't have to take care of things like "copying" over the user names.
If your database schema says that every Post belongs to a User and you can call the User of a Post by calling:
post.user.username
The post you are trying to get the user from has to be.
Some good examples that might help you understand this further can be found here: http://guides.rubyonrails.org/association_basics.html
To get an understanding of how you could build out and structure your Rails API you could start with this guide: https://www.codementor.io/ruby-on-rails/tutorial/creating-simple-api-with-rails
You can use ActiveModelSerializers instead of override as_json.
Look at this article : https://robots.thoughtbot.com/better-serialization-less-as-json
gem 'active_model_serializers', '~> 0.10.0'
bundle install
rails g serializer post
rails g serializer user
Adds the attributes / relations that you want to expose.
class PostSerializer < ActiveModel::Serializer
has_one :user
end
class UserSerializer < ActiveModel::Serializer
attributes :username, :user_id
end
I simply changed the format of the Post JSON to include the User association:
def as_json(options={})
super(only: [:id, :description, :created_at, :user_id],
include: [user: { only: [:name] }]
)
end
If I have a user that has_many user_logins and a user_logins that belongs to user - When a user_login is created I'm using UserLogin.create(userlogin_params) and then my strong params permits the user_id column - but this alone is not saving the current users I.D to the column as it is coming out as nil.
How do I make it save the I.D?
User model:
has_many :user_logins
UserLogin model
belongs_to :user
accepts_nested_attributes_for :user
UserLoginController:
...
def create
#user_login = UserLogin.new(user_login_params)
...
end
...
def user_login_params
param.require(:user_login).permit(
:user_login_attribute1,
:user_login_attribute2,
user_attributes: [
:id,
:user_attribute1,
:user_attribute2
]
)
end
Tell me if it helps.
There are 2 issues here at hand.
First: How do you create an association with the parent record automagically there?
Second: How do you do this so your controller action isn't a giant hole waiting for a hacker to stick their nose in.
You need to start from the parent, then build the child, not start with the child and build the parent.
Consider the following:
class User < ActiveRecord::Base
has_many :logins, class_name: "UserLogin"
end
class UserLogin < ActiveRecord::Base
belongs_to :user
end
class UserLoginsController < ApplicationController
def create
if new_user_login(user_login_params).save
redirect_to :wherever
else
render :new
end
end
private
def new_user_login(attrs={})
current_user.logins.create(attrs)
end
def user_login_params
param.require(:user_login).permit(:attr_1, :attr_1)
end
Do not pass IDs into any secure params hash unless that ID is selectable by the user. If you allow an ID into secure params, a hacker can start moving records around to other objects and destroy your database integrity.
If you would like pairing help on this problem live and in person, you can check out my codementor profile at https://codementor.io/rubycasts/#reviews
Let's keep this simple. Let's say I have a User model and a Post model:
class User < ActiveRecord::Base
# id:integer name:string deleted:boolean
has_many :posts
end
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
end
Now, let's say an admin wants to "delete" (hide) a post. So basically he, through the system, sets a post's deleted attribute to 1. How should I now display this post in the view? Should I create a virtual attribute on the post like this:
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
def administrated_content
if !self.deleted
self.content
else
"This post has been removed"
end
end
end
While that would work, I want to implement the above in a large number of models, and I can't help feeling that copy+pasting the above comparative into all of my models could be DRYer. A lot dryer.
I also think putting a deleted column in every single deletable model in my app feels a bit cumbersome too. I feel I should have a 'state' table. What are your thoughts on this:
class State
#id:integer #deleted:boolean #deleted_by:integer
belongs_to :user
belongs_to :post
end
and then querying self.state.deleted in the comparator? Would this require a polymorphic table? I've only attempted polymorphic once and I couldn't get it to work. (it was on a pretty complex self-referential model, mind). And this still doesn't address the problem of having a very, very similar class method in my models to check if an instance is deleted or not before displaying content.
In the deleted_by attribute, I'm thinking of placing the admin's id who deleted it. But what about when an admin undelete a post? Maybe I should just have an edited_by id.
How do I set up a dependent: :destroy type relationship between the user and his posts? Because now I want to do this: dependent: :set_deleted_to_0 and I'm not sure how to do this.
Also, we don't simply want to set the post's deleted attributes to 1, because we actually want to change the message our administrated_content gives out. We now want it to say, This post has been removed because of its user has been deleted. I'm sure I could jump in and do something hacky, but I want to do it properly from the start.
I also try to avoid gems when I can because I feel I'm missing out on learning.
I usually use a field named deleted_at for this case:
class Post < ActiveRecord::Base
scope :not_deleted, lambda { where(deleted_at: nil) }
scope :deleted, lambda { where("#{self.table_name}.deleted_at IS NOT NULL") }
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
destroy
end
def deleted?
self.deleted_at.present?
end
# ...
Want to share this functionnality between multiple models?
=> Make an extension of it!
# lib/extensions/act_as_fake_deletable.rb
module ActAsFakeDeletable
# override the model actions
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
self.destroy
end
def undestroy # to "restore" the file
self.update(deleted_at: nil)
end
def undelete
self.undestroy
end
# define new scopes
def self.included(base)
base.class_eval do
scope :destroyed, where("#{self.table_name}.deleted_at IS NOT NULL")
scope :not_destroyed, where(deleted_at: nil)
scope :deleted, lambda { destroyed }
scope :not_deleted, lambda { not_destroyed }
end
end
end
class ActiveRecord::Base
def self.act_as_fake_deletable(options = {})
alias_method :destroy!, :destroy
alias_method :delete!, :delete
include ActAsFakeDeletable
options = { field_to_hide: :content, message_to_show_instead: "This content has been deleted" }.merge!(options)
define_method options[:field_to_hide].to_sym do
return options[:message_to_show_instead] if self.deleted_at.present?
self.read_attribute options[:field_to_hide].to_sym
end
end
end
Usage:
class Post < ActiveRecord::Base
act_as_fake_deletable
Overwriting the defaults:
class Book < ActiveRecord::Base
act_as_fake_deletable field_to_hide: :title, message_to_show_instead: "This book has been deleted man, sorry!"
Boom! Done.
Warning: This module overwrite the ActiveRecord's destroy and delete methods, which means you won't be able to destroy your record using those methods anymore. Instead of overwriting you could create a new method, named soft_destroy for example. So in your app (or console), you would use soft_destroy when relevant and use the destroy/delete methods when you really want to "hard destroy" the record.
So, I'm new at this and trying to learn by jumping right in.
I have a slightly odd login in my rails app that works like this:
Admins generate a code and specify an upper limit
Users log in by entering this code and some details which are saved
Models:
class Code < ActiveRecord::Base
has_many :users, dependent: :destroy
attr_accessible :code, :maxusers
end
class User < ActiveRecord::Base
belongs_to :code
attr_accessible :name, :email, :code_id
end
Users controller:
class UsersController < ApplicationController
def create
#user = User.new(params[:user])
if #user.save
redirect_to "/welcome"
end
end
end
So for example the code is 123456 with a limit of 5
The first 5 users can sign up with their Name, Email and 123456
So the question is: How do I check that the code is valid before saving? It must exist and have less than 5 users already assigned.
It seems really simple but I can't figure out the syntax, I was playing with a before_filter in the controller or before save in the model but I'm stuck.
before_filter is a bad idea: models have to bothered about their integrity, not controllers.
You can try something like:
# in your respective model
validate :at_least_five_codes
def at_least_five_codes
errors.add(:base, 'Try another code') unless User.where(code: code).count < 5
end
And you'll be unable to save your model if current code was used for 5 times.
Say you have this structure:
class House < ActiveRecord::Base
has_many :rooms
accepts_nested_attributes_for :rooms
attr_accessible :rooms_attributes
end
class Room < ActiveRecord::Base
has_one :tv
accepts_nested_attributes_for :tv
attr_accessible :tv_attributes
end
class Tv
belongs_to :user
attr_accessible :manufacturer
validates_presence_of :user
end
Notice that Tv's user is not accessible on purpose. So you have a tripple-nested form that allows you to enter house, rooms, and tvs on one page.
Here's the controller's create method:
def create
#house = House.new(params[:house])
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
Question: How in the world would you populate user_id for each tv (it should come from current_user.id)? What's the good practice?
Here's the catch22 I see in this.
Populate user_ids directly into params hash (they're pretty deeply nested)
Save will fail because user_ids are not mass-assignable
Populate user for every tv after #save is finished
Save will fail because user_id must be present
Even if we bypass the above, tvs will be without ids for a moment of time - sucks
Any decent way to do this?
Anything wrong with this?
def create
#house = House.new(params[:house])
#house.rooms.map {|room| room.tv }.each {|tv| tv.user = current_user }
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
I haven't tried this out, but it seems like the objects should be built and accessible at this point, even if not saved.