Port contacts page to backbonejs

This commit is contained in:
Steffen van Bergerem 2014-12-12 00:00:08 +01:00
parent c9ef1f290e
commit 9de6a26a22
33 changed files with 746 additions and 390 deletions

View file

@ -0,0 +1,6 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.collections.AspectMemberships = Backbone.Collection.extend({
model: app.models.AspectMembership
})
// @license-end

View file

@ -0,0 +1,21 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.collections.Contacts = Backbone.Collection.extend({
model: app.models.Contact,
comparator : function(con1, con2) {
if( !con1.person || !con2.person ) return 1;
if(app.aspect) {
var inAspect1 = con1.inAspect(app.aspect.get('id'));
var inAspect2 = con2.inAspect(app.aspect.get('id'));
if( inAspect1 && !inAspect2 ) return -1;
if( !inAspect1 && inAspect2 ) return 1;
}
var n1 = con1.person.get('name');
var n2 = con2.person.get('name');
return n1.localeCompare(n2);
}
});
// @license-end

View file

@ -21,7 +21,7 @@ Handlebars.registerHelper('urlTo', function(path_helper, id, data){
return Routes[path_helper+'_path'](id, data.hash);
});
Handlebars.registerHelper('linkToPerson', function(context, block) {
Handlebars.registerHelper('linkToAuthor', function(context, block) {
if( !context ) context = this;
var html = "<a href=\"/people/" + context.guid + "\" class=\"author-name ";
html += Handlebars.helpers.hovercardable(context);
@ -32,6 +32,15 @@ Handlebars.registerHelper('linkToPerson', function(context, block) {
return html
});
Handlebars.registerHelper('linkToPerson', function(context, block) {
if( !context ) context = this;
var html = "<a href=\"/people/" + context.guid + "\" class=\"name\">";
html += block.fn(context);
html += "</a>";
return html
});
// relationship indicator for profile page
Handlebars.registerHelper('sharingMessage', function(person) {
var i18n_scope = 'people.helper.is_not_sharing';
@ -107,5 +116,20 @@ Handlebars.registerHelper('isCurrentProfilePage', function(id, diaspora_handle,
return Handlebars.helpers.isCurrentPage('person', id, options) ||
Handlebars.helpers.isCurrentPage('user_profile', username, options);
});
Handlebars.registerHelper('aspectMembershipIndicator', function(contact,in_aspect) {
if(!app.aspect || !app.aspect.get('id')) return '<div class="aspect_membership_dropdown placeholder"></div>';
var html = '<i class="entypo ';
if( in_aspect == 'in_aspect' ) {
html += 'circled-cross contact_remove-from-aspect" ';
html += 'title="' + Diaspora.I18n.t('contacts.remove_contact') + '" ';
} else {
html += 'circled-plus contact_add-to-aspect" ';
html += 'title="' + Diaspora.I18n.t('contacts.add_contact') + '" ';
}
html += '></i>';
return html;
});
// @license-end

View file

@ -0,0 +1,13 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.models.Contact = Backbone.Model.extend({
initialize : function() {
this.aspect_memberships = new app.collections.AspectMemberships(this.get('aspect_memberships'));
if( this.get('person') ) this.person = new app.models.Person(this.get('person'));
},
inAspect : function(id) {
return this.aspect_memberships.any(function(membership){ return membership.get('aspect').id == id; });
}
});
// @license-end

View file

@ -1,6 +1,6 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.Contacts = Backbone.View.extend({
app.pages.Contacts = Backbone.View.extend({
el: "#contacts_container",
@ -8,17 +8,15 @@ app.views.Contacts = Backbone.View.extend({
"click #contacts_visibility_toggle" : "toggleContactVisibility",
"click #chat_privilege_toggle" : "toggleChatPrivilege",
"click #change_aspect_name" : "showAspectNameForm",
"click .contact_remove-from-aspect" : "removeContactFromAspect",
"click .contact_add-to-aspect" : "addContactToAspect",
"keyup #contact_list_search" : "searchContactList"
},
initialize: function() {
initialize: function(opts) {
this.visibility_toggle = $("#contacts_visibility_toggle .entypo");
this.chat_toggle = $("#chat_privilege_toggle .entypo");
this.stream = opts.stream;
this.stream.render();
$("#people_stream.contacts .header .entypo").tooltip({ 'placement': 'bottom'});
$(".contact_remove-from-aspect").tooltip();
$(".contact_add-to-aspect").tooltip();
$(document).on('ajax:success', 'form.edit_aspect', this.updateAspectName);
},
@ -69,65 +67,8 @@ app.views.Contacts = Backbone.View.extend({
$(".header > h3").show();
},
addContactToAspect: function(e){
var contact = $(e.currentTarget);
var aspect_membership = new app.models.AspectMembership({
'person_id': contact.attr('data-person_id'),
'aspect_id': contact.attr('data-aspect_id')
});
aspect_membership.save({},{
success: function(model,response){
contact.attr('data-membership_id',model.id)
.tooltip('destroy')
.removeAttr('data-original-title')
.removeClass("contact_add-to-aspect").removeClass("circled-plus")
.addClass("contact_remove-from-aspect").addClass("circled-cross")
.attr('title', Diaspora.I18n.t('contacts.remove_contact'))
.tooltip()
.closest('.stream_element').addClass('in_aspect');
},
error: function(model,response){
var msg = Diaspora.I18n.t('contacts.error_add', { 'name':contact.closest('.stream_element').find('.name').text() });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
}
});
},
removeContactFromAspect: function(e){
var contact = $(e.currentTarget);
var aspect_membership = new app.models.AspectMembership({
'id': contact.attr('data-membership_id')
});
aspect_membership.destroy({
success: function(model,response){
contact.removeAttr('data-membership_id')
.tooltip('destroy')
.removeAttr('data-original-title')
.removeClass("contact_remove-from-aspect").removeClass("circled-cross")
.addClass("contact_add-to-aspect").addClass("circled-plus")
.attr('title', Diaspora.I18n.t('contacts.add_contact'))
.tooltip()
.closest('.stream_element').removeClass('in_aspect');
},
error: function(model,response){
var msg = Diaspora.I18n.t('contacts.error_remove', { 'name':contact.closest('.stream_element').find('.name').text() });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
}
});
},
searchContactList: function(e) {
var query = new RegExp($(e.target).val(),'i');
$("#people_stream.stream.contacts .stream_element").each(function(){
if($(this).find(".name").text().match(query)){
$(this).show();
} else {
$(this).hide();
}
});
this.stream.search($(e.target).val());
}
});
// @license-end

View file

@ -43,7 +43,15 @@ app.Router = Backbone.Router.extend({
},
contacts: function() {
app.contacts = new app.views.Contacts();
app.aspect = new app.models.Aspect(gon.preloads.aspect);
app.contacts = new app.collections.Contacts(app.parsePreload('contacts'));
var stream = new app.views.ContactStream({
collection: app.contacts,
el: $('.stream.contacts #contact_stream'),
});
app.page = new app.pages.Contacts({stream: stream});
},
conversations: function() {

View file

@ -0,0 +1,77 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.ContactStream = Backbone.View.extend({
initialize: function() {
this.itemCount = 0;
this.perPage = 25;
this.query = '';
this.resultList = this.collection.toArray();
var throttledScroll = _.throttle(_.bind(this.infScroll, this), 200);
$(window).scroll(throttledScroll);
this.on('renderContacts', this.renderContacts, this);
},
render: function() {
if( _.isEmpty(this.resultList) ) {
var content = document.createDocumentFragment();
content = '<div id="no_contacts" class="well">' +
' <h4>' +
Diaspora.I18n.t('contacts.search_no_results') +
' </h4>' +
'</div>';
this.$el.html(content);
} else {
this.$el.html('');
this.renderContacts();
}
},
renderContacts: function() {
this.$el.addClass("loading");
var content = document.createDocumentFragment();
_.rest(_.first(this.resultList , this.itemCount + this.perPage), this.itemCount).forEach( function(item) {
var view = new app.views.Contact({model: item});
content.appendChild(view.render().el);
});
var size = _.size(this.resultList);
if( this.itemCount + this.perPage >= size ){
this.itemCount = size;
this.off('renderContacts');
} else {
this.itemCount += this.perPage;
}
this.$el.append(content);
this.$el.removeClass("loading");
},
search: function(query) {
query = query.trim();
if( query || this.query ) {
this.off('renderContacts');
this.on('renderContacts', this.renderContacts, this);
this.itemCount = 0;
if( query ) {
this.query = query;
var regex = new RegExp(query,'i');
this.resultList = this.collection.filter(function(contact) {
return regex.test(contact.get('person').name) ||
regex.test(contact.get('person').diaspora_id);
});
} else {
this.resultList = this.collection.toArray();
this.query = '';
}
this.render();
}
},
infScroll: function() {
if( this.$el.hasClass('loading') ) return;
var distanceTop = $(window).height() + $(window).scrollTop(),
distanceBottom = $(document).height() - distanceTop;
if(distanceBottom < 300) this.trigger('renderContacts');
}
});
// @license-end

View file

@ -0,0 +1,72 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.Contact = app.views.Base.extend({
templateName: 'contact',
events: {
"click .contact_add-to-aspect" : "addContactToAspect",
"click .contact_remove-from-aspect" : "removeContactFromAspect"
},
tooltipSelector: '.contact_add-to-aspect, .contact_remove-from-aspect',
presenter: function() {
return _.extend(this.defaultPresenter(), {
person_id : this.model.get('person_id'),
person : this.model.get('person'),
in_aspect: (app.aspect && this.model.inAspect(app.aspect.get('id'))) ? 'in_aspect' : '',
});
},
postRenderTemplate: function() {
var self = this;
var dropdownEl = this.$('.aspect_membership_dropdown.placeholder');
if( dropdownEl.length == 0 ) {
return;
}
// TODO render me client side!!!
var href = this.model.person.url() + '/aspect_membership_button?size=small';
if( gon.bootstrap ) href += '&bootstrap=true';
$.get(href, function(resp) {
dropdownEl.html(resp);
new app.views.AspectMembership({el: $('.aspect_dropdown',dropdownEl)});
// UGLY (re-)attach the facebox
self.$('a[rel*=facebox]').facebox();
});
},
addContactToAspect: function(){
var self = this;
this.model.aspect_memberships.create({
'person_id': this.model.get('person_id'),
'aspect_id': app.aspect.get('id')
},{
success: function(model,response){
self.render();
},
error: function(model,response){
var msg = Diaspora.I18n.t('contacts.error_add', { 'name': self.model.get('person').name });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
}
});
},
removeContactFromAspect: function(){
var self = this;
this.model.aspect_memberships
.find(function(membership){ return membership.get('aspect').id == app.aspect.id; })
.destroy({
success: function(model,response){
self.render();
},
error: function(model,response){
var msg = Diaspora.I18n.t('contacts.error_remove', { 'name': self.model.get('person').name });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
}
});
}
});
// @license-end

View file

@ -66,8 +66,6 @@
}
#no_contacts {
margin-top: 20px;
text-align: center;
padding: 10px;
background-color: #eee;
color: $text-dark-grey;
}

View file

@ -1,8 +1,8 @@
<div id="{{guid}}">
<div class="img">
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{{personImage this "small" "small"}}}
{{/linkToPerson}}
{{/linkToAuthor}}
</div>
<div class="bd">

View file

@ -0,0 +1,23 @@
<div class="stream_element media contact {{in_aspect}}" id={{person_id}}>
<div class="pull-right">
{{{aspectMembershipIndicator this in_aspect}}}
</div>
<div class="media-object pull-left">
{{{personImage person 'small'}}}
</div>
<div class="media-body">
{{#linkToPerson person}}
{{name}}
{{/linkToPerson}}
<div class="info diaspora_handle">
{{person.diaspora_id}}
</div>
<div class="info tags">
{{{fmtTags person.profile.tags}}}
</div>
</div>
</div>

View file

@ -3,26 +3,26 @@
<div id='post-info' class='span8'>
<div class="img pull-left">
{{#if root}}
{{#linkToPerson root.author}}
{{#linkToAuthor root.author}}
{{{personImage this 'medium'}}}
{{/linkToPerson}}
{{/linkToAuthor}}
{{else}}
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{{personImage this 'medium'}}}
{{/linkToPerson}}
{{/linkToAuthor}}
{{/if}}
</div>
<div class="bd">
<span class='author'>
{{#if root}}
{{#linkToPerson root.author}}
{{#linkToAuthor root.author}}
{{name}}
{{/linkToPerson}}
{{/linkToAuthor}}
{{else}}
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{name}}
{{/linkToPerson}}
{{/linkToAuthor}}
{{/if}}
</span>
@ -69,14 +69,14 @@
<div class='span8' id='reshare-info'>
<i class='entypo retweet small pull-left'></i>
<div class="img pull-left">
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{{personImage this 'small'}}}
{{/linkToPerson}}
{{/linkToAuthor}}
</div>
<span class="author">
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{name}}
{{/linkToPerson}}
{{/linkToAuthor}}
</span>
<span class="post-time">
<a href="/posts/{{id}}">

View file

@ -6,9 +6,9 @@
</span>
<span>
{{#each reshares}}
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{{personImage this 'small' 'micro'}}}
{{/linkToPerson}}
{{/linkToAuthor}}
{{/each}}
</span>
</div>
@ -21,9 +21,9 @@
</span>
<span>
{{#each likes}}
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{{personImage this 'small' 'micro'}}}
{{/linkToPerson}}
{{/linkToAuthor}}
{{/each}}
</span>
</div>

View file

@ -1,5 +1,5 @@
{{#people}}
{{#linkToPerson this}}
{{#linkToAuthor this}}
{{{personImage this "small"}}}
{{/linkToPerson}}
{{/linkToAuthor}}
{{/people}}

View file

@ -14,16 +14,16 @@
<div class='stream-frame-feedback'></div>
<div class="media author">
{{#linkToPerson author}}
{{#linkToAuthor author}}
<div class="img">
<div class="profile-image-container smaller" style="background-image : url('{{avatar.large}}')"></div>
</div>
{{/linkToPerson}}
{{/linkToAuthor}}
<div class="bd">
{{#linkToPerson author}}
{{#linkToAuthor author}}
{{name}}
{{/linkToPerson}}
{{/linkToAuthor}}
</div>
</div>

View file

@ -34,10 +34,7 @@ class AspectMembershipsController < ApplicationController
respond_to do |format|
format.json do
if success
render :json => {
:person_id => contact.person_id,
:aspect_ids => contact.aspects.map{|a| a.id}
}
render :json => AspectMembershipPresenter.new(membership).base_hash
else
render :text => membership.errors.full_messages, :status => 403
end
@ -57,7 +54,9 @@ class AspectMembershipsController < ApplicationController
flash.now[:notice] = I18n.t('aspects.add_to_aspect.success')
respond_with do |format|
format.json do
render :json => AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first.to_json
render :json => AspectMembershipPresenter.new(
AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first)
.base_hash
end
format.all { redirect_to :back }

View file

@ -40,32 +40,24 @@ class ContactsController < ApplicationController
@contacts = contacts_by_type(type)
@contacts_size = @contacts.length
gon.preloads[:contacts] = @contacts.map{ |c| ContactPresenter.new(c, current_user).full_hash_with_person }
end
def contacts_by_type(type)
contacts = case type
case type
when "all"
[current_user.contacts]
current_user.contacts
when "only_sharing"
[current_user.contacts.only_sharing]
current_user.contacts.only_sharing
when "receiving"
[current_user.contacts.receiving]
current_user.contacts.receiving
when "by_aspect"
@aspect = current_user.aspects.find(params[:a_id])
@contacts_in_aspect = @aspect.contacts
@contacts_not_in_aspect = current_user.contacts.where.not(contacts: {id: @contacts_in_aspect.pluck(:id) })
[@contacts_in_aspect, @contacts_not_in_aspect].map {|relation|
relation.includes(:aspect_memberships)
}
gon.preloads[:aspect] = AspectPresenter.new(@aspect).as_json
current_user.contacts
else
raise ArgumentError, "unknown type #{type}"
end
contacts.map {|relation|
relation.includes(:person => :profile).to_a.tap {|contacts|
contacts.sort_by! {|contact| contact.person.name }
}
}.inject(:+).paginate(:page => params[:page], :per_page => 25)
end
def set_up_contacts_mobile

View file

@ -1,25 +1,9 @@
module ContactsHelper
def contact_aspect_dropdown(contact)
membership = contact.aspect_memberships.where(:aspect_id => @aspect.id).first unless @aspect.nil?
if membership
content_tag(:i, nil, :class => 'entypo circled-cross contact_remove-from-aspect',
:title => t('contacts.index.remove_contact'),
'data-aspect_id' => @aspect.id,
'data-person_id' => contact.person_id,
'data-membership_id' => membership.id )
elsif @aspect.nil?
render :partial => 'people/relationship_action',
:locals => { :person => contact.person,
:contact => contact,
:current_user => current_user }
else
content_tag(:i, nil, :class => 'entypo circled-plus contact_add-to-aspect',
:title => t('contacts.index.add_contact'),
'data-aspect_id' => @aspect.id,
'data-person_id' => contact.person_id )
end
render :partial => 'people/relationship_action',
:locals => { :person => contact.person,
:contact => contact,
:current_user => current_user }
end
def start_a_conversation_link(aspect, contacts_size)

View file

@ -0,0 +1,11 @@
class AspectMembershipPresenter < BasePresenter
def initialize(membership)
@membership = membership
end
def base_hash
{ id: @membership.id,
aspect: AspectPresenter.new(@membership.aspect).as_json,
}
end
end

View file

@ -0,0 +1,17 @@
class ContactPresenter < BasePresenter
def base_hash
{ id: id,
person_id: person_id
}
end
def full_hash
base_hash.merge({
aspect_memberships: aspect_memberships.map{ |membership| AspectMembershipPresenter.new(membership).base_hash }
})
end
def full_hash_with_person
full_hash.merge({person: PersonPresenter.new(person).full_hash_with_profile})
end
end

View file

@ -1,12 +0,0 @@
- membership = contact.aspect_memberships.where(:aspect_id => @aspect.id).first unless @aspect.nil?
.media.stream_element{:id => contact.person_id, :class => ("in_aspect" if membership)}
.pull-right
= contact_aspect_dropdown(contact)
.media-object.pull-left
= person_image_link(contact.person, :size => :thumb_small)
.media-body
= person_link(contact.person, :class => 'name')
.info.diaspora_handle
= contact.person_diaspora_handle
.info.tags
= Diaspora::Taggable.format_tags(contact.person.profile.tag_string)

View file

@ -11,8 +11,9 @@
= render 'contacts/header'
- if @contacts_size > 0
= render @contacts
= will_paginate @contacts
#contact_stream
-# JS
- else
.no_contacts
%h3

View file

@ -48,6 +48,7 @@ en:
remove_contact: "Remove contact"
error_add: "Couldn't add <%= name %> to the aspect :("
error_remove: "Couldn't remove <%= name %> from the aspect :("
search_no_results: "No contacts found"
my_activity: "My Activity"
my_stream: "Stream"

View file

@ -72,14 +72,14 @@ describe AspectMembershipsController, :type => :controller do
end
context 'json' do
it 'returns a list of aspect ids for the person' do
it 'returns the aspect membership' do
post :create,
:format => :json,
:person_id => @person.id,
:aspect_id => @aspect0.id
contact = @controller.current_user.contact_for(@person)
expect(response.body).to eq(contact.aspect_memberships.first.to_json)
expect(response.body).to eq(AspectMembershipPresenter.new(contact.aspect_memberships.first).base_hash.to_json)
end
end
end

View file

@ -10,12 +10,20 @@ describe ContactsController, :type => :controller do
AppConfig.chat.enabled = true
@aspect = bob.aspects.create(:name => "another aspect")
bob.share_with alice.person, @aspect
bob.share_with eve.person, @aspect
sign_in :user, bob
end
it "generates a jasmine fixture", :fixture => true do
it "generates the aspects_manage fixture", :fixture => true do
get :index, :a_id => @aspect.id
save_fixture(html_for("body"), "aspects_manage")
end
it "generates the contacts_json fixture", :fixture => true do
json = bob.contacts.map { |c|
ContactPresenter.new(c, bob).full_hash_with_person
}.to_json
save_fixture(json, "contacts_json")
end
end
end

View file

@ -0,0 +1,37 @@
describe("app.collections.Contacts", function(){
beforeEach(function(){
this.collection = new app.collections.Contacts();
});
describe("comparator", function() {
beforeEach(function(){
this.aspect = new app.models.Aspect({id: 42, name: "cats"});
this.con1 = new app.models.Contact({
person: { name: "aaa" },
aspect_memberships: []
});
this.con2 = new app.models.Contact({
person: { name: "aaa" },
aspect_memberships: [{id: 23, aspect: this.aspect}]
});
this.con3 = new app.models.Contact({
person: { name: "zzz" },
aspect_memberships: [{id: 23, aspect: this.aspect}]
});
});
it("should compare the username if app.aspect is not present", function() {
expect(this.collection.comparator(this.con1, this.con3)).toBeLessThan(0);
});
it("should compare the aspect memberships if app.aspect is present", function() {
app.aspect = this.aspect;
expect(this.collection.comparator(this.con1, this.con3)).toBeGreaterThan(0);
});
it("should compare the username if the contacts have equal aspect memberships", function() {
app.aspect = this.aspect;
expect(this.collection.comparator(this.con2, this.con3)).toBeLessThan(0);
});
});
});

View file

@ -0,0 +1,20 @@
describe("app.models.Contact", function() {
beforeEach(function(){
this.aspect = factory.aspect();
this.contact = new app.models.Contact({
person: { name: "aaa" },
aspect_memberships: [{id: 42, aspect: this.aspect}]
});
});
describe("inAspect", function(){
it("returns true if the contact has been added to the aspect", function(){
expect(this.contact.inAspect(this.aspect.id)).toBeTruethy;
});
it("returns false if the contact hasn't been added to the aspect", function(){
expect(this.contact.inAspect(this.aspect.id+1)).toBeFalsy;
});
});
});

View file

@ -0,0 +1,101 @@
describe("app.pages.Contacts", function(){
beforeEach(function() {
spec.loadFixture("aspects_manage");
this.view = new app.pages.Contacts({
stream: {
render: function(){}
}
});
Diaspora.I18n.load({
contacts: {
aspect_list_is_visible: "Contacts in this aspect are able to see each other.",
aspect_list_is_not_visible: "Contacts in this aspect are not able to see each other.",
aspect_chat_is_enabled: "Contacts in this aspect are able to chat with you.",
aspect_chat_is_not_enabled: "Contacts in this aspect are not able to chat with you.",
}
});
});
context('toggle chat privilege', function() {
beforeEach(function() {
this.chat_toggle = $("#chat_privilege_toggle");
this.chat_icon = $("#chat_privilege_toggle .entypo");
});
it('updates the title for the tooltip', function() {
expect(this.chat_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_chat_is_not_enabled")
);
this.chat_toggle.trigger('click');
expect(this.chat_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_chat_is_enabled")
);
});
it('toggles the chat icon', function() {
expect(this.chat_icon.hasClass('enabled')).toBeFalsy;
this.chat_toggle.trigger('click');
expect(this.chat_icon.hasClass('enabled')).toBeTruethy;
});
});
context('toggle contacts visibility', function() {
beforeEach(function() {
this.visibility_toggle = $("#contacts_visibility_toggle");
this.lock_icon = $("#contacts_visibility_toggle .entypo");
});
it('updates the title for the tooltip', function() {
expect(this.lock_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_list_is_visible")
);
this.visibility_toggle.trigger('click');
expect(this.lock_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_list_is_not_visible")
);
});
it('toggles the lock icon', function() {
expect(this.lock_icon.hasClass('lock-open')).toBeTruethy;
expect(this.lock_icon.hasClass('lock')).toBeFalsy;
this.visibility_toggle.trigger('click');
expect(this.lock_icon.hasClass('lock')).toBeTruethy;
expect(this.lock_icon.hasClass('lock-open')).toBeFalsy;
});
});
context('show aspect name form', function() {
beforeEach(function() {
this.button = $('#change_aspect_name');
});
it('shows the form', function() {
expect($('#aspect_name_form').css('display')).toBe('none');
this.button.trigger('click');
expect($('#aspect_name_form').css('display')).not.toBe('none');
});
it('hides the aspect name', function() {
expect($('.header > h3').css('display')).not.toBe('none');
this.button.trigger('click');
expect($('.header > h3').css('display')).toBe('none');
});
});
context('search contact list', function() {
beforeEach(function() {
this.searchinput = $('#contact_list_search');
});
it('calls stream.search', function() {
this.view.stream.search = jasmine.createSpy();
this.searchinput.val("Username");
this.searchinput.trigger('keyup');
expect(this.view.stream.search).toHaveBeenCalledWith("Username");
});
});
});

View file

@ -0,0 +1,77 @@
describe("app.views.ContactStream", function() {
beforeEach(function() {
loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}});
spec.loadFixture("aspects_manage");
this.contacts = new app.collections.Contacts($.parseJSON(spec.readFixture("contacts_json")));
app.aspect = new app.models.Aspect(this.contacts.first().get('aspect_memberships')[0].aspect);
this.view = new app.views.ContactStream({
collection : this.contacts,
el: $('.stream.contacts #contact_stream')
});
this.view.perPage=1;
//clean the page
this.view.$el.html('');
});
describe("initialize", function() {
it("binds an infinite scroll listener", function() {
spyOn($.fn, "scroll");
new app.views.ContactStream({collection : this.contacts});
expect($.fn.scroll).toHaveBeenCalled();
});
});
describe("search", function() {
it("filters the contacts", function() {
this.view.render();
expect(this.view.$el.html()).toContain("alice");
this.view.search("eve");
expect(this.view.$el.html()).not.toContain("alice");
expect(this.view.$el.html()).toContain("eve");
});
});
describe("infScroll", function() {
beforeEach(function() {
this.view.off("renderContacts");
this.fn = jasmine.createSpy();
this.view.on("renderContacts", this.fn);
spyOn($.fn, "height").and.returnValue(0);
spyOn($.fn, "scrollTop").and.returnValue(100);
});
it("triggers renderContacts when the user is at the bottom of the page", function() {
this.view.infScroll();
expect(this.fn).toHaveBeenCalled();
});
});
describe("render", function() {
beforeEach(function() {
spyOn(this.view, "renderContacts");
});
it("calls renderContacts", function() {
this.view.render();
expect(this.view.renderContacts).toHaveBeenCalled();
});
});
describe("renderContacts", function() {
beforeEach(function() {
this.view.off("renderContacts");
this.view.renderContacts();
});
it("renders perPage contacts", function() {
expect(this.view.$el.find('.stream_element.contact').length).toBe(1);
});
it("renders more contacts when called a second time", function() {
this.view.renderContacts();
expect(this.view.$el.find('.stream_element.contact').length).toBe(2);
});
});
});

View file

@ -0,0 +1,136 @@
describe("app.views.Contact", function(){
beforeEach(function() {
this.aspect1 = factory.aspect({id: 1});
this.aspect2 = factory.aspect({id: 2});
this.model = new app.models.Contact({
person_id: 42,
person: { id: 42, name: 'alice' },
aspect_memberships: [{id: 23, aspect: this.aspect1}]
});
this.view = new app.views.Contact({ model: this.model });
Diaspora.I18n.load({
contacts: {
add_contact: "Add contact",
remove_contact: "Remove contact",
error_add: "Couldn't add <%= name %> to the aspect :(",
error_remove: "Couldn't remove <%= name %> from the aspect :("
}
});
});
context("#presenter", function() {
it("contains necessary elements", function() {
app.aspect = this.aspect1;
expect(this.view.presenter()).toEqual(jasmine.objectContaining({
person_id: 42,
person: jasmine.objectContaining({id: 42, name: 'alice'}),
in_aspect: 'in_aspect'
}));
});
});
context('add contact to aspect', function() {
beforeEach(function() {
app.aspect = this.aspect2;
this.view.render();
this.button = this.view.$el.find('.contact_add-to-aspect');
this.contact = this.view.$el.find('.stream_element.contact');
this.aspect_membership = {id: 42, aspect: app.aspect.toJSON()};
this.response = JSON.stringify(this.aspect_membership);
});
it('sends a correct ajax request', function() {
this.button.trigger('click');
var obj = $.parseJSON(jasmine.Ajax.requests.mostRecent().params);
expect(obj.person_id).toBe(this.model.get('person_id'));
expect(obj.aspect_id).toBe(app.aspect.get('id'));
});
it('adds a aspect_membership to the contact', function() {
expect(this.model.aspect_memberships.length).toBe(1);
$('.contact_add-to-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: this.response
});
expect(this.model.aspect_memberships.length).toBe(2);
});
it('calls render', function() {
spyOn(this.view, 'render');
$('.contact_add-to-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: this.response
});
expect(this.view.render).toHaveBeenCalled();
});
it('displays a flash message on errors', function(){
$('.contact_add-to-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 400, // fail
});
expect($('[id^="flash"]')).toBeErrorFlashMessage(
Diaspora.I18n.t(
'contacts.error_add',
{name: this.model.get('person').name}
)
);
});
});
context('remove contact from aspect', function() {
beforeEach(function() {
app.aspect = this.aspect1;
this.view.render();
this.button = this.view.$el.find('.contact_remove-from-aspect');
this.contact = this.view.$el.find('.stream_element.contact');
this.aspect_membership = this.model.aspect_memberships.first().toJSON();
this.response = JSON.stringify(this.aspect_membership);
});
it('sends a correct ajax request', function() {
$('.contact_remove-from-aspect',this.contact).trigger('click');
expect(jasmine.Ajax.requests.mostRecent().url).toBe(
"/aspect_memberships/"+this.aspect_membership.id
);
});
it('removes the aspect_membership from the contact', function() {
expect(this.model.aspect_memberships.length).toBe(1);
$('.contact_remove-from-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: this.response
});
expect(this.model.aspect_memberships.length).toBe(0);
});
it('calls render', function() {
spyOn(this.view, 'render');
$('.contact_remove-from-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: this.response,
});
expect(this.view.render).toHaveBeenCalled();
});
it('displays a flash message on errors', function(){
$('.contact_remove-from-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 400, // fail
});
expect($('[id^="flash"]')).toBeErrorFlashMessage(
Diaspora.I18n.t(
'contacts.error_remove',
{name: this.model.get('person').name}
)
);
});
});
});

View file

@ -1,240 +0,0 @@
describe("app.views.Contacts", function(){
beforeEach(function() {
spec.loadFixture("aspects_manage");
this.view = new app.views.Contacts();
Diaspora.I18n.load({
contacts: {
add_contact: "Add contact",
aspect_list_is_visible: "Contacts in this aspect are able to see each other.",
aspect_list_is_not_visible: "Contacts in this aspect are not able to see each other.",
aspect_chat_is_enabled: "Contacts in this aspect are able to chat with you.",
aspect_chat_is_not_enabled: "Contacts in this aspect are not able to chat with you.",
remove_contact: "Remove contact",
error_add: "Couldn't add <%= name %> to the aspect :(",
error_remove: "Couldn't remove <%= name %> from the aspect :("
}
});
});
context('toggle chat privilege', function() {
beforeEach(function() {
this.chat_toggle = $("#chat_privilege_toggle");
this.chat_icon = $("#chat_privilege_toggle .entypo");
});
it('updates the title for the tooltip', function() {
expect(this.chat_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_chat_is_not_enabled")
);
this.chat_toggle.trigger('click');
expect(this.chat_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_chat_is_enabled")
);
});
it('toggles the chat icon', function() {
expect(this.chat_icon.hasClass('enabled')).toBeFalsy;
this.chat_toggle.trigger('click');
expect(this.chat_icon.hasClass('enabled')).toBeTruethy;
});
});
context('toggle contacts visibility', function() {
beforeEach(function() {
this.visibility_toggle = $("#contacts_visibility_toggle");
this.lock_icon = $("#contacts_visibility_toggle .entypo");
});
it('updates the title for the tooltip', function() {
expect(this.lock_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_list_is_visible")
);
this.visibility_toggle.trigger('click');
expect(this.lock_icon.attr('data-original-title')).toBe(
Diaspora.I18n.t("contacts.aspect_list_is_not_visible")
);
});
it('toggles the lock icon', function() {
expect(this.lock_icon.hasClass('lock-open')).toBeTruethy;
expect(this.lock_icon.hasClass('lock')).toBeFalsy;
this.visibility_toggle.trigger('click');
expect(this.lock_icon.hasClass('lock')).toBeTruethy;
expect(this.lock_icon.hasClass('lock-open')).toBeFalsy;
});
});
context('show aspect name form', function() {
beforeEach(function() {
this.button = $('#change_aspect_name');
});
it('shows the form', function() {
expect($('#aspect_name_form').css('display')).toBe('none');
this.button.trigger('click');
expect($('#aspect_name_form').css('display')).not.toBe('none');
});
it('hides the aspect name', function() {
expect($('.header > h3').css('display')).not.toBe('none');
this.button.trigger('click');
expect($('.header > h3').css('display')).toBe('none');
});
});
context('add contact to aspect', function() {
beforeEach(function() {
this.contact = $('#people_stream .stream_element').last();
this.button = this.contact.find('.contact_add-to-aspect');
this.person_id = this.button.attr('data-person_id');
this.aspect_id = this.button.attr('data-aspect_id');
});
it('sends a correct ajax request', function() {
jasmine.Ajax.install();
$('.contact_add-to-aspect',this.contact).trigger('click');
var obj = $.parseJSON(jasmine.Ajax.requests.mostRecent().params);
expect(obj.person_id).toBe(this.person_id);
expect(obj.aspect_id).toBe(this.aspect_id);
});
it('adds a membership id to the contact', function() {
jasmine.Ajax.install();
$('.contact_add-to-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: '{ "id": 42 }'
});
expect(this.button.attr('data-membership_id')).toBe('42');
});
it('displays a flash message on errors', function(){
jasmine.Ajax.install();
$('.contact_add-to-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 400, // fail
});
expect($('[id^="flash"]')).toBeErrorFlashMessage(
Diaspora.I18n.t(
'contacts.error_add',
{name: this.contact.find('.name').text()}
)
);
});
it('changes the appearance of the contact', function() {
expect(this.button.hasClass('contact_add-to-aspect')).toBeTruethy;
expect(this.button.hasClass('circled-cross')).toBeTruethy;
expect(this.contact.hasClass('in_aspect')).toBeTruethy;
expect(this.button.hasClass('contact_remove-from-aspect')).toBeFalsy;
expect(this.button.hasClass('circled-plus')).toBeFalsy;
expect(this.button.attr('data-original-title')).toBe(
Diaspora.I18n.t('contacts.add_contact')
);
jasmine.Ajax.install();
$('.contact_add-to-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: '{ "id": 42 }'
});
expect(this.button.hasClass('contact_add-to-aspect')).toBeFalsy;
expect(this.button.hasClass('circled-cross')).toBeFalsy;
expect(this.contact.hasClass('in_aspect')).toBeFalsy;
expect(this.button.hasClass('contact_remove-from-aspect')).toBeTruethy;
expect(this.button.hasClass('circled-plus')).toBeTruethy;
expect(this.button.attr('data-original-title')).toBe(
Diaspora.I18n.t('contacts.remove_contact')
);
});
});
context('remove contact from aspect', function() {
beforeEach(function() {
this.contact = $('#people_stream .stream_element').first();
this.button = this.contact.find('.contact_remove-from-aspect');
this.person_id = this.button.attr('data-person_id');
this.aspect_id = this.button.attr('data-aspect_id');
this.membership_id = this.button.attr('data-membership_id');
});
it('sends a correct ajax request', function() {
jasmine.Ajax.install();
$('.contact_remove-from-aspect',this.contact).trigger('click');
expect(jasmine.Ajax.requests.mostRecent().url).toBe(
"/aspect_memberships/"+this.membership_id
);
});
it('removes the membership id from the contact', function() {
jasmine.Ajax.install();
$('.contact_remove-from-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: '{}'
});
expect(this.button.attr('data-membership_id')).toBe(undefined);
});
it('displays a flash message on errors', function(){
jasmine.Ajax.install();
$('.contact_remove-from-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 400, // fail
});
expect($('[id^="flash"]')).toBeErrorFlashMessage(
Diaspora.I18n.t(
'contacts.error_remove',
{name: this.contact.find('.name').text()}
)
);
});
it('changes the appearance of the contact', function() {
expect(this.button.hasClass('contact_add-to-aspect')).toBeFalsy;
expect(this.button.hasClass('circled-cross')).toBeFalsy;
expect(this.contact.hasClass('in_aspect')).toBeFalsy;
expect(this.button.hasClass('contact_remove-from-aspect')).toBeTruethy;
expect(this.button.hasClass('circled-plus')).toBeTruethy;
expect(this.button.attr('data-original-title')).toBe(
Diaspora.I18n.t('contacts.remove_contact')
);
jasmine.Ajax.install();
$('.contact_remove-from-aspect',this.contact).trigger('click');
jasmine.Ajax.requests.mostRecent().response({
status: 200, // success
responseText: '{}'
});
expect(this.button.hasClass('contact_add-to-aspect')).toBeTruethy;
expect(this.button.hasClass('circled-cross')).toBeTruethy;
expect(this.contact.hasClass('in_aspect')).toBeTruethy;
expect(this.button.hasClass('contact_remove-from-aspect')).toBeFalsy;
expect(this.button.hasClass('circled-plus')).toBeFalsy;
expect(this.button.attr('data-original-title')).toBe(
Diaspora.I18n.t('contacts.add_contact')
);
});
});
context('search contact list', function() {
beforeEach(function() {
this.searchinput = $('#contact_list_search');
this.username = $('.stream_element .name').first().text();
});
it('filters the contact list by name', function() {
expect($('.stream_element').length).toBeGreaterThan(1);
this.searchinput.val(this.username);
this.searchinput.trigger('keyup');
expect($('.stream_element:visible').length).toBe(1);
expect($('.stream_element:visible .name').first().text()).toBe(this.username);
});
});
});

View file

@ -0,0 +1,15 @@
require 'spec_helper'
describe AspectMembershipPresenter do
before do
@am = alice.aspects.where(:name => "generic").first.aspect_memberships.first
@presenter = AspectMembershipPresenter.new(@am)
end
describe '#base_hash' do
it 'works' do
expect(@presenter.base_hash).to be_present
end
end
end

View file

@ -0,0 +1,26 @@
require 'spec_helper'
describe ContactPresenter do
before do
@presenter = ContactPresenter.new(alice.contact_for(bob.person))
end
describe '#base_hash' do
it 'works' do
expect(@presenter.base_hash).to be_present
end
end
describe '#full_hash' do
it 'works' do
expect(@presenter.full_hash).to be_present
end
end
describe '#full_hash_with_person' do
it 'works' do
expect(@presenter.full_hash_with_person).to be_present
end
end
end