move user modules into user namespace.

This commit is contained in:
Dennis Collinson 2012-02-09 14:19:14 -08:00 committed by Maxwell Salzberg
parent b0e81af1fa
commit c9487ee12f
16 changed files with 294 additions and 302 deletions

View file

@ -103,7 +103,7 @@ class AccountDeleter
end end
def normal_ar_person_associates_to_delete def normal_ar_person_associates_to_delete
[:posts, :photos, :mentions] [:posts, :photos, :mentions, :participations]
end end
def ignored_or_special_ar_person_associations def ignored_or_special_ar_person_associations

View file

@ -44,6 +44,7 @@ class Person < ActiveRecord::Base
has_many :posts, :foreign_key => :author_id, :dependent => :destroy # This person's own posts has_many :posts, :foreign_key => :author_id, :dependent => :destroy # This person's own posts
has_many :photos, :foreign_key => :author_id, :dependent => :destroy # This person's own photos has_many :photos, :foreign_key => :author_id, :dependent => :destroy # This person's own photos
has_many :comments, :foreign_key => :author_id, :dependent => :destroy # This person's own comments has_many :comments, :foreign_key => :author_id, :dependent => :destroy # This person's own comments
has_many :participations, :through => :posts, :foreign_key => :author_id, :dependent => :destroy
belongs_to :owner, :class_name => 'User' belongs_to :owner, :class_name => 'User'

View file

@ -9,6 +9,8 @@ class Post < ActiveRecord::Base
include Diaspora::Commentable include Diaspora::Commentable
include Diaspora::Shareable include Diaspora::Shareable
has_many :participations, :dependent => :delete_all, :as => :target
attr_accessor :user_like attr_accessor :user_like
# NOTE API V1 to be extracted # NOTE API V1 to be extracted

View file

@ -2,15 +2,17 @@
# licensed under the Affero General Public License version 3 or later. See # licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file. # the COPYRIGHT file.
require File.join(Rails.root, 'lib/diaspora/user')
require File.join(Rails.root, 'lib/salmon/salmon') require File.join(Rails.root, 'lib/salmon/salmon')
require File.join(Rails.root, 'lib/postzord/dispatcher') require File.join(Rails.root, 'lib/postzord/dispatcher')
require 'rest-client' require 'rest-client'
class User < ActiveRecord::Base class User < ActiveRecord::Base
include Diaspora::UserModules
include Encryptor::Private include Encryptor::Private
include Connecting
include Querying
include SocialActions
devise :invitable, :database_authenticatable, :registerable, devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :recoverable, :rememberable, :trackable, :validatable,
:timeoutable, :token_authenticatable, :lockable, :timeoutable, :token_authenticatable, :lockable,
@ -33,7 +35,7 @@ class User < ActiveRecord::Base
serialize :hidden_shareables, Hash serialize :hidden_shareables, Hash
has_one :person, :foreign_key => :owner_id has_one :person, :foreign_key => :owner_id
delegate :public_key, :posts, :photos, :owns?, :diaspora_handle, :name, :public_url, :profile, :first_name, :last_name, :to => :person delegate :public_key, :posts, :photos, :owns?, :diaspora_handle, :name, :public_url, :profile, :first_name, :last_name, :participations, :to => :person
has_many :invitations_from_me, :class_name => 'Invitation', :foreign_key => :sender_id has_many :invitations_from_me, :class_name => 'Invitation', :foreign_key => :sender_id
has_many :invitations_to_me, :class_name => 'Invitation', :foreign_key => :recipient_id has_many :invitations_to_me, :class_name => 'Invitation', :foreign_key => :recipient_id
@ -284,22 +286,6 @@ class User < ActiveRecord::Base
Salmon::EncryptedSlap.create_by_user_and_activity(self, post.to_diaspora_xml) Salmon::EncryptedSlap.create_by_user_and_activity(self, post.to_diaspora_xml)
end end
def comment!(post, text, opts={})
Comment::Generator.new(self.person, post, text).create!(opts)
end
def participate!(target, opts={})
Participation::Generator.new(self.person, target).create!(opts)
end
def like!(target, opts={})
Like::Generator.new(self.person, target).create!(opts)
end
def build_comment(options={})
Comment::Generator.new(self.person, options.delete(:post), options.delete(:text)).build(options)
end
# Check whether the user has liked a post. # Check whether the user has liked a post.
# @param [Post] post # @param [Post] post
def liked?(target) def liked?(target)

View file

@ -0,0 +1,70 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
module User::Connecting
# This will create a contact on the side of the sharer and the sharee.
# @param [Person] person The person to start sharing with.
# @param [Aspect] aspect The aspect to add them to.
# @return [Contact] The newly made contact for the passed in person.
def share_with(person, aspect)
contact = self.contacts.find_or_initialize_by_person_id(person.id)
return false unless contact.valid?
unless contact.receiving?
contact.dispatch_request
contact.receiving = true
end
contact.aspects << aspect
contact.save
if notification = Notification.where(:target_id => person.id).first
notification.update_attributes(:unread=>false)
end
register_share_visibilities(contact)
contact
end
# This puts the last 100 public posts by the passed in contact into the user's stream.
# @param [Contact] contact
# @return [void]
def register_share_visibilities(contact)
#should have select here, but proven hard to test
posts = Post.where(:author_id => contact.person_id, :public => true).limit(100)
p = posts.map do |post|
ShareVisibility.new(:contact_id => contact.id, :shareable_id => post.id, :shareable_type => 'Post')
end
ShareVisibility.import(p) unless posts.empty?
nil
end
def remove_contact(contact, opts={:force => false})
posts = contact.posts.all
if !contact.mutual? || opts[:force]
contact.destroy
else
contact.update_attributes(:receiving => false)
end
end
def disconnect(bad_contact, opts={})
person = bad_contact.person
Rails.logger.info("event=disconnect user=#{diaspora_handle} target=#{person.diaspora_handle}")
retraction = Retraction.for(self)
retraction.subscribers = [person]#HAX
Postzord::Dispatcher.build(self, retraction).post
AspectMembership.where(:contact_id => bad_contact.id).delete_all
remove_contact(bad_contact, opts)
end
def disconnected_by(person)
Rails.logger.info("event=disconnected_by user=#{diaspora_handle} target=#{person.diaspora_handle}")
if contact = self.contact_for(person)
remove_contact(contact)
end
end
end

164
app/models/user/querying.rb Normal file
View file

@ -0,0 +1,164 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require File.join(Rails.root, 'lib', 'evil_query')
#TODO: THIS FILE SHOULD NOT EXIST, EVIL SQL SHOULD BE ENCAPSULATED IN EvilQueries,
#throwing all of this stuff in user violates demeter like WHOA
module User::Querying
def find_visible_shareable_by_id(klass, id, opts={} )
key = (opts.delete(:key) || :id)
::EvilQuery::VisibleShareableById.new(self, klass, key, id, opts).post!
end
def visible_shareables(klass, opts={})
opts = prep_opts(klass, opts)
shareable_ids = visible_shareable_ids(klass, opts)
klass.where(:id => shareable_ids).select('DISTINCT '+klass.to_s.tableize+'.*').limit(opts[:limit]).order(opts[:order_with_table])
end
def visible_shareable_ids(klass, opts={})
opts = prep_opts(klass, opts)
visible_ids_from_sql(klass, opts)
end
# @return [Array<Integer>]
def visible_ids_from_sql(klass, opts={})
opts = prep_opts(klass, opts)
opts[:klass] = klass
opts[:by_members_of] ||= self.aspect_ids
post_ids = klass.connection.select_values(visible_shareable_sql(klass, opts)).map { |id| id.to_i }
post_ids += klass.connection.select_values(construct_public_followings_sql(opts).to_sql).map {|id| id.to_i }
end
def visible_shareable_sql(klass, opts={})
table = klass.table_name
opts = prep_opts(klass, opts)
opts[:klass] = klass
shareable_from_others = construct_shareable_from_others_query(opts)
shareable_from_self = construct_shareable_from_self_query(opts)
"(#{shareable_from_others.to_sql} LIMIT #{opts[:limit]}) UNION ALL (#{shareable_from_self.to_sql} LIMIT #{opts[:limit]}) ORDER BY #{opts[:order]} LIMIT #{opts[:limit]}"
end
def ugly_select_clause(query, opts)
klass = opts[:klass]
select_clause ='DISTINCT %s.id, %s.updated_at AS updated_at, %s.created_at AS created_at' % [klass.table_name, klass.table_name, klass.table_name]
query.select(select_clause).order(opts[:order_with_table]).where(klass.arel_table[opts[:order_field]].lt(opts[:max_time]))
end
def construct_shareable_from_others_query(opts)
conditions = {
:pending => false,
:share_visibilities => {:hidden => opts[:hidden]},
:contacts => {:user_id => self.id, :receiving => true}
}
conditions[:type] = opts[:type] if opts.has_key?(:type)
query = opts[:klass].joins(:contacts).where(conditions)
if opts[:by_members_of]
query = query.joins(:contacts => :aspect_memberships).where(
:aspect_memberships => {:aspect_id => opts[:by_members_of]})
end
ugly_select_clause(query, opts)
end
def construct_public_followings_sql(opts)
aspects = Aspect.where(:id => opts[:by_members_of])
person_ids = Person.connection.select_values(people_in_aspects(aspects).select("people.id").to_sql)
query = opts[:klass].where(:author_id => person_ids, :public => true, :pending => false)
unless(opts[:klass] == Photo)
query = query.where(:type => opts[:type])
end
ugly_select_clause(query, opts)
end
def construct_shareable_from_self_query(opts)
conditions = {:pending => false }
conditions[:type] = opts[:type] if opts.has_key?(:type)
query = self.person.send(opts[:klass].to_s.tableize).where(conditions)
if opts[:by_members_of]
query = query.joins(:aspect_visibilities).where(:aspect_visibilities => {:aspect_id => opts[:by_members_of]})
end
ugly_select_clause(query, opts)
end
def contact_for(person)
return nil unless person
contact_for_person_id(person.id)
end
def aspects_with_shareable(base_class_name_or_class, shareable_id)
base_class_name = base_class_name_or_class
base_class_name = base_class_name_or_class.base_class.to_s if base_class_name_or_class.is_a?(Class)
self.aspects.joins(:aspect_visibilities).where(:aspect_visibilities => {:shareable_id => shareable_id, :shareable_type => base_class_name})
end
def contact_for_person_id(person_id)
Contact.where(:user_id => self.id, :person_id => person_id).includes(:person => :profile).first
end
# @param [Person] person
# @return [Boolean] whether person is a contact of this user
def has_contact_for?(person)
Contact.exists?(:user_id => self.id, :person_id => person.id)
end
def people_in_aspects(requested_aspects, opts={})
allowed_aspects = self.aspects & requested_aspects
aspect_ids = allowed_aspects.map(&:id)
people = Person.in_aspects(aspect_ids)
if opts[:type] == 'remote'
people = people.where(:owner_id => nil)
elsif opts[:type] == 'local'
people = people.where('people.owner_id IS NOT NULL')
end
people
end
def aspects_with_person person
contact_for(person).aspects
end
def posts_from(person)
::EvilQuery::ShareablesFromPerson.new(self, Post, person).make_relation!
end
def photos_from(person)
::EvilQuery::ShareablesFromPerson.new(self, Photo, person).make_relation!
end
protected
# @return [Hash]
def prep_opts(klass, opts)
defaults = {
:order => 'created_at DESC',
:limit => 15,
:hidden => false
}
defaults[:type] = Stream::Base::TYPES_OF_POST_IN_STREAM if klass == Post
opts = defaults.merge(opts)
opts[:order_field] = opts[:order].split.first.to_sym
opts[:order_with_table] = klass.table_name + '.' + opts[:order]
opts[:max_time] = Time.at(opts[:max_time]) if opts[:max_time].is_a?(Integer)
opts[:max_time] ||= Time.now + 1
opts
end
end

View file

@ -0,0 +1,19 @@
module User::SocialActions
def comment!(post, text, opts={})
participations.where(:target_id => post).first || participate!(post)
Comment::Generator.new(self.person, post, text).create!(opts)
end
def participate!(target, opts={})
Participation::Generator.new(self.person, target).create!(opts)
end
def like!(target, opts={})
participations.where(:target_id => target).first || participate!(target)
Like::Generator.new(self.person, target).create!(opts)
end
def build_comment(options={})
Comment::Generator.new(self.person, options.delete(:post), options.delete(:text)).build(options)
end
end

View file

@ -1,9 +0,0 @@
require File.join(Rails.root, 'lib/diaspora/user/connecting')
require File.join(Rails.root, 'lib/diaspora/user/querying')
module Diaspora
module UserModules
include Connecting
include Querying
end
end

View file

@ -1,74 +0,0 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
module Diaspora
module UserModules
module Connecting
# This will create a contact on the side of the sharer and the sharee.
# @param [Person] person The person to start sharing with.
# @param [Aspect] aspect The aspect to add them to.
# @return [Contact] The newly made contact for the passed in person.
def share_with(person, aspect)
contact = self.contacts.find_or_initialize_by_person_id(person.id)
return false unless contact.valid?
unless contact.receiving?
contact.dispatch_request
contact.receiving = true
end
contact.aspects << aspect
contact.save
if notification = Notification.where(:target_id => person.id).first
notification.update_attributes(:unread=>false)
end
register_share_visibilities(contact)
contact
end
# This puts the last 100 public posts by the passed in contact into the user's stream.
# @param [Contact] contact
# @return [void]
def register_share_visibilities(contact)
#should have select here, but proven hard to test
posts = Post.where(:author_id => contact.person_id, :public => true).limit(100)
p = posts.map do |post|
ShareVisibility.new(:contact_id => contact.id, :shareable_id => post.id, :shareable_type => 'Post')
end
ShareVisibility.import(p) unless posts.empty?
nil
end
def remove_contact(contact, opts={:force => false})
posts = contact.posts.all
if !contact.mutual? || opts[:force]
contact.destroy
else
contact.update_attributes(:receiving => false)
end
end
def disconnect(bad_contact, opts={})
person = bad_contact.person
Rails.logger.info("event=disconnect user=#{diaspora_handle} target=#{person.diaspora_handle}")
retraction = Retraction.for(self)
retraction.subscribers = [person]#HAX
Postzord::Dispatcher.build(self, retraction).post
AspectMembership.where(:contact_id => bad_contact.id).delete_all
remove_contact(bad_contact, opts)
end
def disconnected_by(person)
Rails.logger.info("event=disconnected_by user=#{diaspora_handle} target=#{person.diaspora_handle}")
if contact = self.contact_for(person)
remove_contact(contact)
end
end
end
end
end

View file

@ -1,169 +0,0 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require File.join(Rails.root, 'lib', 'evil_query')
#TODO: THIS FILE SHOULD NOT EXIST, EVIL SQL SHOULD BE ENCAPSULATED IN EvilQueries,
#throwing all of this stuff in user violates demeter like WHOA
module Diaspora
module UserModules
module Querying
def find_visible_shareable_by_id(klass, id, opts={} )
key = (opts.delete(:key) || :id)
::EvilQuery::VisibleShareableById.new(self, klass, key, id, opts).post!
end
def visible_shareables(klass, opts={})
opts = prep_opts(klass, opts)
shareable_ids = visible_shareable_ids(klass, opts)
klass.where(:id => shareable_ids).select('DISTINCT '+klass.to_s.tableize+'.*').limit(opts[:limit]).order(opts[:order_with_table])
end
def visible_shareable_ids(klass, opts={})
opts = prep_opts(klass, opts)
visible_ids_from_sql(klass, opts)
end
# @return [Array<Integer>]
def visible_ids_from_sql(klass, opts={})
opts = prep_opts(klass, opts)
opts[:klass] = klass
opts[:by_members_of] ||= self.aspect_ids
post_ids = klass.connection.select_values(visible_shareable_sql(klass, opts)).map { |id| id.to_i }
post_ids += klass.connection.select_values(construct_public_followings_sql(opts).to_sql).map {|id| id.to_i }
end
def visible_shareable_sql(klass, opts={})
table = klass.table_name
opts = prep_opts(klass, opts)
opts[:klass] = klass
shareable_from_others = construct_shareable_from_others_query(opts)
shareable_from_self = construct_shareable_from_self_query(opts)
"(#{shareable_from_others.to_sql} LIMIT #{opts[:limit]}) UNION ALL (#{shareable_from_self.to_sql} LIMIT #{opts[:limit]}) ORDER BY #{opts[:order]} LIMIT #{opts[:limit]}"
end
def ugly_select_clause(query, opts)
klass = opts[:klass]
select_clause ='DISTINCT %s.id, %s.updated_at AS updated_at, %s.created_at AS created_at' % [klass.table_name, klass.table_name, klass.table_name]
query.select(select_clause).order(opts[:order_with_table]).where(klass.arel_table[opts[:order_field]].lt(opts[:max_time]))
end
def construct_shareable_from_others_query(opts)
conditions = {
:pending => false,
:share_visibilities => {:hidden => opts[:hidden]},
:contacts => {:user_id => self.id, :receiving => true}
}
conditions[:type] = opts[:type] if opts.has_key?(:type)
query = opts[:klass].joins(:contacts).where(conditions)
if opts[:by_members_of]
query = query.joins(:contacts => :aspect_memberships).where(
:aspect_memberships => {:aspect_id => opts[:by_members_of]})
end
ugly_select_clause(query, opts)
end
def construct_public_followings_sql(opts)
aspects = Aspect.where(:id => opts[:by_members_of])
person_ids = Person.connection.select_values(people_in_aspects(aspects).select("people.id").to_sql)
query = opts[:klass].where(:author_id => person_ids, :public => true, :pending => false)
unless(opts[:klass] == Photo)
query = query.where(:type => opts[:type])
end
ugly_select_clause(query, opts)
end
def construct_shareable_from_self_query(opts)
conditions = {:pending => false }
conditions[:type] = opts[:type] if opts.has_key?(:type)
query = self.person.send(opts[:klass].to_s.tableize).where(conditions)
if opts[:by_members_of]
query = query.joins(:aspect_visibilities).where(:aspect_visibilities => {:aspect_id => opts[:by_members_of]})
end
ugly_select_clause(query, opts)
end
def contact_for(person)
return nil unless person
contact_for_person_id(person.id)
end
def aspects_with_shareable(base_class_name_or_class, shareable_id)
base_class_name = base_class_name_or_class
base_class_name = base_class_name_or_class.base_class.to_s if base_class_name_or_class.is_a?(Class)
self.aspects.joins(:aspect_visibilities).where(:aspect_visibilities => {:shareable_id => shareable_id, :shareable_type => base_class_name})
end
def contact_for_person_id(person_id)
Contact.where(:user_id => self.id, :person_id => person_id).includes(:person => :profile).first
end
# @param [Person] person
# @return [Boolean] whether person is a contact of this user
def has_contact_for?(person)
Contact.exists?(:user_id => self.id, :person_id => person.id)
end
def people_in_aspects(requested_aspects, opts={})
allowed_aspects = self.aspects & requested_aspects
aspect_ids = allowed_aspects.map(&:id)
people = Person.in_aspects(aspect_ids)
if opts[:type] == 'remote'
people = people.where(:owner_id => nil)
elsif opts[:type] == 'local'
people = people.where('people.owner_id IS NOT NULL')
end
people
end
def aspects_with_person person
contact_for(person).aspects
end
def posts_from(person)
::EvilQuery::ShareablesFromPerson.new(self, Post, person).make_relation!
end
def photos_from(person)
::EvilQuery::ShareablesFromPerson.new(self, Photo, person).make_relation!
end
protected
# @return [Hash]
def prep_opts(klass, opts)
defaults = {
:order => 'created_at DESC',
:limit => 15,
:hidden => false
}
defaults[:type] = Stream::Base::TYPES_OF_POST_IN_STREAM if klass == Post
opts = defaults.merge(opts)
opts[:order_field] = opts[:order].split.first.to_sym
opts[:order_with_table] = klass.table_name + '.' + opts[:order]
opts[:max_time] = Time.at(opts[:max_time]) if opts[:max_time].is_a?(Integer)
opts[:max_time] ||= Time.now + 1
opts
end
end
end
end

View file

@ -17,6 +17,7 @@ module EvilQuery
end end
def posts def posts
# Post.joins(:participations).where(:participations => {:author_id => @user.id}).order("posts.interacted_at DESC")
liked_post_ids = fetch_ids!(LikedPosts.new(@user).posts, "posts.id") liked_post_ids = fetch_ids!(LikedPosts.new(@user).posts, "posts.id")
commented_post_ids = fetch_ids!(CommentedPosts.new(@user).posts, "posts.id") commented_post_ids = fetch_ids!(CommentedPosts.new(@user).posts, "posts.id")
Post.where(:id => liked_post_ids + commented_post_ids).order("posts.interacted_at DESC") Post.where(:id => liked_post_ids + commented_post_ids).order("posts.interacted_at DESC")

View file

@ -6,7 +6,7 @@ describe EvilQuery::Participation do
end end
it "includes posts liked by the user" do it "includes posts liked by the user" do
Factory(:like, :target => @status_message, :author => alice.person) alice.like!(@status_message)
EvilQuery::Participation.new(alice).posts.should include(@status_message) EvilQuery::Participation.new(alice).posts.should include(@status_message)
end end
@ -35,7 +35,7 @@ describe EvilQuery::Participation do
alice.comment!(@status_messageB, "party") alice.comment!(@status_messageB, "party")
Timecop.travel time += 1.month Timecop.travel time += 1.month
Factory(:like, :target => @status_messageA, :author => alice.person) alice.like!(@status_messageA)
Timecop.travel time += 1.month Timecop.travel time += 1.month
alice.comment!(@photoC, "party") alice.comment!(@photoC, "party")

View file

@ -4,7 +4,7 @@
require 'spec_helper' require 'spec_helper'
describe Diaspora::UserModules::Connecting do describe User::Connecting do
let(:aspect) { alice.aspects.first } let(:aspect) { alice.aspects.first }
let(:aspect1) { alice.aspects.create(:name => 'other') } let(:aspect1) { alice.aspects.create(:name => 'other') }

View file

@ -4,8 +4,7 @@
require 'spec_helper' require 'spec_helper'
describe User do describe User::Querying do
before do before do
@alices_aspect = alice.aspects.where(:name => "generic").first @alices_aspect = alice.aspects.where(:name => "generic").first
@eves_aspect = eve.aspects.where(:name => "generic").first @eves_aspect = eve.aspects.where(:name => "generic").first

View file

@ -0,0 +1,27 @@
require "spec_helper"
describe User::SocialActions do
describe 'User#like!' do
before do
@bobs_aspect = bob.aspects.where(:name => "generic").first
@status = bob.post(:status_message, :text => "hello", :to => @bobs_aspect.id)
end
it "should be able to like on one's own status" do
like = alice.like!(@status)
@status.reload.likes.first.should == like
end
it "should be able to like on a contact's status" do
like = bob.like!(@status)
@status.reload.likes.first.should == like
end
it "does not allow multiple likes" do
alice.like!(@status)
lambda {
alice.like!(@status)
}.should_not change(@status, :likes)
end
end
end

View file

@ -5,7 +5,6 @@
require 'spec_helper' require 'spec_helper'
describe User do describe User do
describe "private key" do describe "private key" do
it 'has a key' do it 'has a key' do
alice.encryption_key.should_not be nil alice.encryption_key.should_not be nil
@ -688,30 +687,6 @@ describe User do
@like2 = bob.like!(@message) @like2 = bob.like!(@message)
end end
describe 'User#like' do
before do
@status = bob.post(:status_message, :text => "hello", :to => @bobs_aspect.id)
end
it "should be able to like on one's own status" do
like = alice.like!(@status)
@status.reload.likes.first.should == like
end
it "should be able to like on a contact's status" do
like = bob.like!(@status)
@status.reload.likes.first.should == like
end
it "does not allow multiple likes" do
alice.like!(@status)
lambda {
alice.like!(@status)
}.should_not change(@status, :likes)
end
end
describe '#like_for' do describe '#like_for' do
it 'returns the correct like' do it 'returns the correct like' do
alice.like_for(@message).should == @like alice.like_for(@message).should == @like