[WIP] aspect membership dropdown Backbone.js rework

* initial backbone port
* changed AspectMembershipsController#destroy to use aspect_membership_id
* included rudimentary jasmine specs
* more specs, updating the list elements after de-/selection
* update selected aspect count on button
* don't even try to render html in AspectMembershipsController
* more specs for button summary text
* adapt aspect management on contacts page and in the popup boxes
* adapt inline creation of aspects + memberships

TODO
* more tests
This commit is contained in:
Florian Staudacher 2013-01-21 15:52:30 +01:00
parent ad4ba363a9
commit 4cbae601e8
21 changed files with 594 additions and 182 deletions

View file

@ -55,6 +55,7 @@ var app = {
});
app.hovercard = new app.views.Hovercard();
app.aspectMemberships = new app.views.AspectMembership();
},
hasPreload : function(prop) {

View file

@ -0,0 +1,7 @@
/**
* this model represents the assignment of an aspect to a person.
* (only valid for the context of the current user)
*/
app.models.AspectMembership = Backbone.Model.extend({
urlRoot: "/aspect_memberships"
});

View file

@ -0,0 +1,161 @@
/**
* this view lets the user (de-)select aspect memberships in the context
* of another users profile or the contact page.
*
* updates to the list of aspects are immediately propagated to the server, and
* the results are dislpayed as flash messages.
*/
app.views.AspectMembership = Backbone.View.extend({
initialize: function() {
// attach event handler, removing any previous instances
var selector = '.dropdown.aspect_membership .dropdown_list > li';
$('body')
.off('click', selector)
.on('click', selector, _.bind(this._clickHandler, this));
this.list_item = null;
this.dropdown = null;
},
// decide what to do when clicked
// -> addMembership
// -> removeMembership
_clickHandler: function(evt) {
this.list_item = $(evt.target);
this.dropdown = this.list_item.parent();
this.list_item.addClass('loading');
if( this.list_item.is('.selected') ) {
var membership_id = this.list_item.data('membership_id');
this.removeMembership(membership_id);
} else {
var aspect_id = this.list_item.data('aspect_id');
var person_id = this.dropdown.data('person_id');
this.addMembership(person_id, aspect_id);
}
return false; // stop the event
},
// return the (short) name of the person associated with the current dropdown
_name: function() {
return this.dropdown.data('person-short-name');
},
// create a membership for the given person in the given aspect
addMembership: function(person_id, aspect_id) {
var aspect_membership = new app.models.AspectMembership({
'person_id': person_id,
'aspect_id': aspect_id
});
aspect_membership.on('sync', this._successSaveCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error');
}, this);
aspect_membership.save();
},
_successSaveCb: function(aspect_membership) {
var aspect_id = aspect_membership.get('aspect_id');
var membership_id = aspect_membership.get('id');
var li = this.dropdown.find('li[data-aspect_id="'+aspect_id+'"]');
// the user didn't have this person in any aspects before, congratulate them
// on their newly found friendship ;)
if( this.dropdown.find('li.selected').length == 0 ) {
var msg = Diaspora.I18n.t('aspect_dropdown.started_sharing_with', { 'name': this._name() });
Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg });
}
li.attr('data-membership_id', membership_id) // just to be sure...
.data('membership_id', membership_id)
.addClass('selected');
this.updateSummary();
this._done();
},
// show an error flash msg
_displayError: function(msg_id) {
this._done();
this.dropdown.removeClass('active'); // close the dropdown
var msg = Diaspora.I18n.t(msg_id, { 'name': this._name() });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
},
// remove the membership with the given id
removeMembership: function(membership_id) {
var aspect_membership = new app.models.AspectMembership({
'id': membership_id
});
aspect_membership.on('sync', this._successDestroyCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error_remove');
}, this);
aspect_membership.destroy();
},
_successDestroyCb: function(aspect_membership) {
var membership_id = aspect_membership.get('id');
var li = this.dropdown.find('li[data-membership_id="'+membership_id+'"]');
li.removeAttr('data-membership_id')
.removeData('membership_id')
.removeClass('selected');
// we just removed the last aspect, inform the user with a flash message
// that he is no longer sharing with that person
if( this.dropdown.find('li.selected').length == 0 ) {
var msg = Diaspora.I18n.t('aspect_dropdown.stopped_sharing_with', { 'name': this._name() });
Diaspora.page.flashMessages.render({ 'success':true, 'notice':msg });
}
this.updateSummary();
this._done();
},
// cleanup tasks after aspect selection
_done: function() {
if( this.list_item ) {
this.list_item.removeClass('loading');
}
},
// refresh the button text to reflect the current aspect selection status
updateSummary: function() {
var btn = this.dropdown.parents('div.aspect_membership').find('.button.toggle');
var aspects_cnt = this.dropdown.find('li.selected').length;
var txt;
if( aspects_cnt == 0 ) {
btn.removeClass('in_aspects');
txt = Diaspora.I18n.t('aspect_dropdown.toggle.zero');
} else {
btn.addClass('in_aspects');
txt = this._pluralSummaryTxt(aspects_cnt);
}
btn.text(txt + ' ▼');
},
_pluralSummaryTxt: function(cnt) {
var all_aspects_cnt = this.dropdown.find('li').length;
if( cnt == 1 ) {
return this.dropdown.find('li.selected').first().text();
}
if( cnt == all_aspects_cnt ) {
return Diaspora.I18n.t('aspect_dropdown.all_aspects');
}
return Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':cnt.toString() });
}
});

View file

@ -1,3 +1,10 @@
/**
* the aspects dropdown specifies the scope of a posted status message.
*
* this view is part of the publisher where users are presented the options
* 'public', 'all aspects' and a list of their personal aspects, for limiting
* 'the audience of created contents.
*/
app.views.AspectsDropdown = app.views.Base.extend({
templateName : "aspects-dropdown",
events : {

View file

@ -27,3 +27,88 @@ $(document).ready(function() {
toggleAspectTitle();
});
});
/**
* TEMPORARY SOLUTION
* TODO remove me, when the contacts section is done with Backbone.js ...
* (this is about as much covered by tests as the old code ... not at all)
*
* see also 'contact-edit.js'
*/
app.tmp || (app.tmp = {});
// on the contacts page, viewing the facebox for single aspect
app.tmp.ContactAspectsBox = function() {
$('body').on('click', '#aspect_edit_pane a.add.button', _.bind(this.addToAspect, this));
$('body').on('click', '#aspect_edit_pane a.added.button', _.bind(this.removeFromAspect, this));
};
_.extend(app.tmp.ContactAspectsBox.prototype, {
addToAspect: function(evt) {
var el = $(evt.currentTarget);
var aspect_membership = new app.models.AspectMembership({
'person_id': el.data('person_id'),
'aspect_id': el.data('aspect_id')
});
aspect_membership.on('sync', this._successSaveCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error', el);
}, this);
aspect_membership.save();
return false;
},
_successSaveCb: function(aspect_membership) {
var membership_id = aspect_membership.get('id');
var person_id = aspect_membership.get('person_id');
var el = $('li.contact').find('a.add[data-person_id="'+person_id+'"]');
el.removeClass('add')
.addClass('added')
.attr('data-membership_id', membership_id) // just to be sure...
.data('membership_id', membership_id);
},
removeFromAspect: function(evt) {
var el = $(evt.currentTarget);
var aspect_membership = new app.models.AspectMembership({
'id': el.data('membership_id')
});
aspect_membership.on('sync', this._successDestroyCb, this);
aspect_membership.on('error', function(aspect_membership) {
this._displayError('aspect_dropdown.error_remove', el);
}, this);
aspect_membership.destroy();
return false;
},
_successDestroyCb: function(aspect_membership) {
var membership_id = aspect_membership.get('id');
var el = $('li.contact').find('a.added[data-membership_id="'+membership_id+'"]');
el.removeClass('added')
.addClass('add')
.removeAttr('data-membership_id')
.removeData('membership_id');
},
_displayError: function(msg_id, contact_el) {
var name = $('li.contact')
.has(contact_el)
.find('h4.name')
.text();
var msg = Diaspora.I18n.t(msg_id, { 'name': name });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
}
});
$(function() {
var contact_aspects_box = new app.tmp.ContactAspectsBox();
});

View file

@ -3,34 +3,6 @@
// the COPYRIGHT file.
var ContactEdit = {
init: function(){
$.extend(ContactEdit, AspectsDropdown);
$('.dropdown.aspect_membership .dropdown_list > li, .dropdown.inviter .dropdown_list > li').live('click', function(evt){
ContactEdit.processClick($(this), evt);
});
},
updateNumber: function(dropdown, personId, number){
var button = dropdown.parents(".dropdown").children('.button.toggle'),
replacement;
if (number == 0) {
button.removeClass("in_aspects");
replacement = Diaspora.I18n.t("aspect_dropdown.toggle.zero");
}else if (number == 1) {
button.addClass("in_aspects");
replacement = dropdown.find(".selected").first().text();
}else if (number < 3) {
replacement = Diaspora.I18n.t('aspect_dropdown.toggle.few', { count: number.toString()})
}else if (number > 3) {
replacement = Diaspora.I18n.t('aspect_dropdown.toggle.many', { count: number.toString()})
}else {
//the above one are a tautology, but I want to have them here once for once we figure out a neat way i18n them
replacement = Diaspora.I18n.t('aspect_dropdown.toggle.other', { count: number.toString()})
ContactEdit.toggleAspectMembership(li, evt);
}
},
inviteFriend: function(li, evt) {
$.post('/services/inviter/facebook.json', {
"aspect_id" : li.data("aspect_id"),
@ -38,61 +10,66 @@ var ContactEdit = {
}, function(data){
ContactEdit.processSuccess(li, evt, data);
});
},
processSuccess: function(element, evt, data) {
element.removeClass('loading')
if (data.url != undefined) {
window.location = data.url;
} else {
element.toggleClass("selected");
Diaspora.widgets.flashes.render({'success':true, 'notice':data.message});
}
},
processClick: function(li, evt){
var dropdown = li.closest('.dropdown');
li.addClass('loading');
if (dropdown.hasClass('inviter')) {
ContactEdit.inviteFriend(li, evt);
dropdown.html('sending, please wait...');
}
else {
ContactEdit.toggleAspectMembership(li, evt);
}
},
toggleAspectMembership: function(li, evt) {
var button = li.find('.button'),
dropdown = li.closest('.dropdown'),
dropdownList = li.parent('.dropdown_list');
if(button.hasClass('disabled') || li.hasClass('newItem')){ return; }
var selected = li.hasClass("selected"),
routedId = selected ? "/42" : "";
$.post("/aspect_memberships" + routedId + ".json", {
"aspect_id": li.data("aspect_id"),
"person_id": li.parent().data("person_id"),
"_method": (selected) ? "DELETE" : "POST"
}, function(aspectMembership) {
ContactEdit.toggleCheckbox(li);
ContactEdit.updateNumber(li.closest(".dropdown_list"), li.parent().data("person_id"), aspectMembership.aspect_ids.length, 'in_aspects');
Diaspora.page.publish("aspectDropdown/updated", [li.parent().data("person_id"), li.parents(".dropdown").parent(".right").html()]);
})
.error(function() {
var message = Diaspora.I18n.t("aspect_dropdown.error", {name: dropdownList.data('person-short-name')});
Diaspora.page.flashMessages.render({success: false, notice: message});
dropdown.removeClass('active');
})
.complete(function() {
li.removeClass("loading");
});
}
};
$(document).ready(function(){
ContactEdit.init();
/*
TODO remove me
ContactEdit.toggleCheckbox(li);
Diaspora.page.publish("aspectDropdown/updated", [li.parent().data("person_id"), li.parents(".dropdown").parent(".right").html()]);
*/
/**
* TEMPORARY SOLUTION
* TODO remove me, when the contacts section is done with Backbone.js ...
* (this is about as much covered by tests as the old code ... not at all)
*
* see also 'aspect-edit-pane.js'
*/
app.tmp || (app.tmp = {});
// on the contacts page, viewing the list of people in a single aspect
app.tmp.ContactAspects = function() {
$('#people_stream').on('click', '.contact_remove-from-aspect', _.bind(this.removeFromAspect, this));
};
_.extend(app.tmp.ContactAspects.prototype, {
removeFromAspect: function(evt) {
evt.stopImmediatePropagation();
evt.preventDefault();
var el = $(evt.currentTarget);
var id = el.data('membership_id');
var aspect_membership = new app.models.AspectMembership({'id':id});
aspect_membership.on('sync', this._successDestroyCb, this);
aspect_membership.on('error', function(aspect_membership) {
this._displayError('aspect_dropdown.error_remove', aspect_membership.get('id'));
}, this);
aspect_membership.destroy();
return false;
},
_successDestroyCb: function(aspect_membership) {
var membership_id = aspect_membership.get('id');
$('.stream_element').has('[data-membership_id="'+membership_id+'"]')
.fadeOut(300, function() { $(this).remove() });
},
_displayError: function(msg_id, membership_id) {
var name = $('.stream_element')
.has('[data-membership_id="'+membership_id+'"]')
.find('div.bd > a')
.text();
var msg = Diaspora.I18n.t(msg_id, { 'name': name });
Diaspora.page.flashMessages.render({ 'success':false, 'notice':msg });
}
});
$(function() {
var contact_aspects = new app.tmp.ContactAspects();
});

View file

@ -6,49 +6,62 @@
class AspectMembershipsController < ApplicationController
before_filter :authenticate_user!
respond_to :html, :json, :js
respond_to :html, :json
def destroy
#note :id is garbage
aspect = current_user.aspects.joins(:aspect_memberships).where(:aspect_memberships=>{:id=>params[:id]}).first
contact = current_user.contacts.joins(:aspect_memberships).where(:aspect_memberships=>{:id=>params[:id]}).first
@person_id = params[:person_id]
@aspect_id = params[:aspect_id]
raise ActiveRecord::RecordNotFound unless aspect.present? && contact.present?
@contact = current_user.contact_for(Person.where(:id => @person_id).first)
membership = @contact ? @contact.aspect_memberships.where(:aspect_id => @aspect_id).first : nil
raise Diaspora::NotMine unless current_user.mine?(aspect) &&
current_user.mine?(contact)
if membership && membership.destroy
@aspect = membership.aspect
flash.now[:notice] = I18n.t 'aspect_memberships.destroy.success'
membership = contact.aspect_memberships.where(:aspect_id => aspect.id).first
respond_with do |format|
format.json{ render :json => {
:person_id => @person_id,
:aspect_ids => @contact.aspects.map{|a| a.id}
} }
format.html{ redirect_to :back }
end
raise ActiveRecord::RecordNotFound unless membership.present?
else
flash.now[:error] = I18n.t 'aspect_memberships.destroy.failure'
errors = membership ? membership.errors.full_messages : t('aspect_memberships.destroy.no_membership')
respond_to do |format|
format.js { render :text => errors, :status => 403 }
format.html{
redirect_to :back
# do it!
success = membership.destroy
# set the flash message
if success
flash.now[:notice] = I18n.t 'aspect_memberships.destroy.success'
else
flash.now[:error] = I18n.t 'aspect_memberships.destroy.failure'
end
respond_with do |format|
format.json do
if success
render :json => {
:person_id => contact.person_id,
:aspect_ids => contact.aspects.map{|a| a.id}
}
else
render :text => membership.errors.full_messages, :status => 403
end
end
format.all { redirect_to :back }
end
end
def create
@person = Person.find(params[:person_id])
@aspect = current_user.aspects.where(:id => params[:aspect_id]).first
@contact = current_user.share_with(@person, @aspect)
if @contact
if @contact.present?
flash.now[:notice] = I18n.t('aspects.add_to_aspect.success')
respond_with AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first
respond_with do |format|
format.json do
render :json => AspectMembership.where(:contact_id => @contact.id, :aspect_id => @aspect.id).first.to_json
end
format.all { redirect_to :back }
end
else
flash.now[:error] = I18n.t('contacts.create.failure')
render :nothing => true, :status => 409
@ -58,4 +71,13 @@ class AspectMembershipsController < ApplicationController
rescue_from ActiveRecord::StatementInvalid do
render :text => "Duplicate record rejected.", :status => 400
end
rescue_from ActiveRecord::RecordNotFound do
render :text => I18n.t('aspect_memberships.destroy.no_membership'), :status => 404
end
rescue_from Diaspora::NotMine do
render :text => "You are not allowed to do that.", :status => 403
end
end

View file

@ -163,14 +163,16 @@ class PeopleController < ApplicationController
end
end
# shows the dropdown list of aspects the current user has set for the given person.
# renders "thats you" in case the current user views himself
def aspect_membership_dropdown
@person = Person.find_by_guid(params[:person_id])
if @person == current_user.person
render :text => I18n.t('people.person.thats_you')
else
@contact = current_user.contact_for(@person) || Contact.new
render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left'}
end
# you are not a contact of yourself...
return render :text => I18n.t('people.person.thats_you') if @person == current_user.person
@contact = current_user.contact_for(@person) || Contact.new
render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left'}
end
def redirect_if_tag_search

View file

@ -4,20 +4,27 @@
module AspectGlobalHelper
def aspect_membership_dropdown(contact, person, hang, aspect=nil)
aspect_membership_ids = {}
selected_aspects = all_aspects.select{|aspect| contact.in_aspect?(aspect)}
selected_aspects.each do |a|
record = a.aspect_memberships.find { |am| am.contact_id == contact.id }
aspect_membership_ids[a.id] = record.id
end
render "shared/aspect_dropdown",
:selected_aspects => selected_aspects,
:aspect_membership_ids => aspect_membership_ids,
:person => person,
:hang => hang,
:dropdown_class => "aspect_membership"
end
def aspect_dropdown_list_item(aspect, checked)
klass = checked ? "selected" : ""
def aspect_dropdown_list_item(aspect, am_id=nil)
klass = am_id.present? ? "selected" : ""
str = <<LISTITEM
<li data-aspect_id=#{aspect.id} class='#{klass} aspect_selector'>
<li data-aspect_id="#{aspect.id}" data-membership_id="#{am_id}" class="#{klass} aspect_selector">
#{aspect.name}
</li>
LISTITEM

View file

@ -5,38 +5,39 @@
module AspectsHelper
def add_to_aspect_button(aspect_id, person_id)
link_to image_tag('icons/monotone_plus_add_round.png'),
{:controller => 'aspect_memberships',
{ :controller => 'aspect_memberships',
:action => 'create',
:format => :json,
:aspect_id => aspect_id,
:person_id => person_id},
:remote => true,
:person_id => person_id
},
:method => 'post',
:class => 'add button',
'data-aspect_id' => aspect_id,
'data-person_id' => person_id
end
def remove_from_aspect_button(aspect_id, person_id)
def remove_from_aspect_button(membership_id, aspect_id, person_id)
link_to image_tag('icons/monotone_check_yes.png'),
{:controller => "aspect_memberships",
{ :controller => "aspect_memberships",
:action => 'destroy',
:id => 42,
:aspect_id => aspect_id,
:person_id => person_id},
:remote => true,
:id => membership_id
},
:method => 'delete',
:class => 'added button',
'data-membership_id' => membership_id,
'data-aspect_id' => aspect_id,
'data-person_id' => person_id
end
def aspect_membership_button(aspect, contact, person)
return if person && person.closed_account?
if contact.nil? || !contact.aspect_memberships.detect{ |am| am.aspect_id == aspect.id}
membership = contact.aspect_memberships.where(:aspect_id => aspect.id).first
if contact.nil? || membership.nil?
add_to_aspect_button(aspect.id, person.id)
else
remove_from_aspect_button(aspect.id, person.id)
remove_from_aspect_button(membership.id, aspect.id, person.id)
end
end
end

View file

@ -1,20 +1,24 @@
module ContactsHelper
def contact_aspect_dropdown(contact)
if @aspect
membership = contact.aspect_memberships.where(:aspect_id => @aspect.id).first unless @aspect.nil?
if membership
link_to(image_tag('icons/monotone_close_exit_delete.png', :height => 20, :width => 20),
{:controller => "aspect_memberships",
:action => 'destroy',
:id => 42,
:aspect_id => @aspect.id,
:person_id => contact.person_id
{ :controller => "aspect_memberships",
:action => 'destroy',
:id => membership.id
},
:title => t('contacts.index.remove_person_from_aspect', :person_name => contact.person_first_name, :aspect_name => @aspect.name),
:method => 'delete')
:class => 'contact_remove-from-aspect',
:method => 'delete',
'data-membership_id' => membership.id
)
else
render :partial => 'people/relationship_action',
:locals => { :person => contact.person, :contact => contact,
:current_user => current_user }
:locals => { :person => contact.person,
:contact => contact,
:current_user => current_user }
end
end

View file

@ -19,8 +19,9 @@ class AspectMembership < ActiveRecord::Base
def as_json(opts={})
{
:id => self.id,
:person_id => self.person.id,
:person_id => self.person.id,
:contact_id => self.contact.id,
:aspect_id => self.aspect_id,
:aspect_ids => self.contact.aspects.map{|a| a.id}
}
end

View file

@ -406,6 +406,15 @@ class User < ActiveRecord::Base
Role.is_admin?(self.person)
end
def mine?(target)
if target.present? && target.respond_to?(:user_id)
return self.id == target.user_id
end
false
end
def guard_unconfirmed_email
self.unconfirmed_email = nil if unconfirmed_email.blank? || unconfirmed_email == email

View file

@ -2,6 +2,8 @@
// licensed under the Affero General Public License version 3 or later. See
// the COPYRIGHT file.
// TODO handle this completely in Backbone.js, then remove this view!
var element = $(".add[data-aspect_id=<%= @aspect.id %>][data-person_id=<%= @contact.person_id%>]");
if( $("#no_contacts").is(':visible') ) {

View file

@ -2,6 +2,8 @@
// licensed under the Affero General Public License version 3 or later. See
// the COPYRIGHT file.
// TODO handle this completely in Backbone.js, then remove this view!
var element = $(".added[data-aspect_id=<%= @aspect.id %>][data-person_id=<%= @contact.person_id%>]");
element.parent().html("<%= escape_javascript(render('aspect_memberships/remove_from_aspect', :aspect => @aspect, :person => @contact.person, :contact => @contact)) %>");
element.fadeTo(200,1);

View file

@ -2,10 +2,16 @@
// licensed under the Affero General Public License version 3 or later. See
// the COPYRIGHT file.
var dropdown = $("ul.dropdown_list[data-person_id=<%= @person.id %>]")
$('.newItem', dropdown).before("<%= escape_javascript( aspect_dropdown_list_item(@aspect, @contact.aspects.include?(@aspect))) %>");
// TODO create the aspect and the new aspect membership via Backbone.js and then
// remove this view!
ContactEdit.updateNumber(dropdown, "<%= @person.id %>", <%= @contact.aspects.size %>);
$.facebox.close();
$('#profile .dropdown').toggleClass("active");
if( app.aspectMemberships ) {
var dropdown = $("ul.dropdown_list[data-person_id=<%= @person.id %>]");
$('.newItem', dropdown).before("<%= escape_javascript( aspect_dropdown_list_item(@aspect, @contact.aspects.include?(@aspect))) %>");
app.aspectMemberships.dropdown = dropdown;
app.aspectMemberships.updateSummary();
$.facebox.close();
$('#profile .dropdown').toggleClass("active");
}

View file

@ -15,7 +15,7 @@
.wrapper
%ul.dropdown_list{:unSelectable => 'on', 'data-person_id' => (person.id if defined?(person) && person), 'data-service_uid' => (service_uid if defined?(service_uid)), 'data-person-short-name' => (person.first_name if defined?(person) && person)}
- for aspect in all_aspects
= aspect_dropdown_list_item(aspect, selected_aspects.include?(aspect) )
= aspect_dropdown_list_item(aspect, aspect_membership_ids[aspect.id] )
- if (dropdown_may_create_new_aspect && defined?(person) && person)
%li.newItem

View file

@ -50,6 +50,7 @@ en:
stopped_sharing_with: "You have stopped sharing with <%= name %>."
started_sharing_with: "You have started sharing with <%= name %>!"
error: "Couldn't start sharing with <%= name %>. Are you ignoring them?"
error_remove: "Couldn't remove <%= name %> from the aspect :("
toggle:
zero: "Select aspects"
one: "In <%= count %> aspect"

View file

@ -11,4 +11,10 @@ module Diaspora
# to continue
class AccountClosed < StandardError
end
# something that should be accessed does not belong to the current user and
# that prevents further execution
class NotMine < StandardError
end
end

View file

@ -25,7 +25,7 @@ describe AspectMembershipsController do
it 'succeeds' do
post :create,
:format => 'js',
:format => :json,
:person_id => bob.person.id,
:aspect_id => @aspect1.id
response.should be_success
@ -34,7 +34,7 @@ describe AspectMembershipsController do
it 'creates an aspect membership' do
lambda {
post :create,
:format => 'js',
:format => :json,
:person_id => bob.person.id,
:aspect_id => @aspect1.id
}.should change{
@ -47,7 +47,7 @@ describe AspectMembershipsController do
alice.contacts.reload
lambda {
post :create,
:format => 'js',
:format => :json,
:person_id => @person.id,
:aspect_id => @aspect0.id
}.should change{
@ -58,14 +58,14 @@ describe AspectMembershipsController do
it 'failure flashes error' do
alice.should_receive(:share_with).and_return(nil)
post :create,
:format => 'js',
:format => :json,
:person_id => @person.id,
:aspect_id => @aspect0.id
flash[:error].should_not be_blank
end
it 'does not 500 on a duplicate key error' do
params = {:format => 'js', :person_id => @person.id, :aspect_id => @aspect0.id}
params = {:format => :json, :person_id => @person.id, :aspect_id => @aspect0.id}
post :create, params
post :create, params
response.status.should == 400
@ -74,7 +74,7 @@ describe AspectMembershipsController do
context 'json' do
it 'returns a list of aspect ids for the person' do
post :create,
:format => 'json',
:format => :json,
:person_id => @person.id,
:aspect_id => @aspect0.id
@ -86,44 +86,25 @@ describe AspectMembershipsController do
describe "#destroy" do
it 'removes contacts from an aspect' do
alice.add_contact_to_aspect(@contact, @aspect1)
delete :destroy,
:format => 'js', :id => 123,
:person_id => bob.person.id,
:aspect_id => @aspect0.id
membership = alice.add_contact_to_aspect(@contact, @aspect1)
delete :destroy, :format => :json, :id => membership.id
response.should be_success
@aspect0.reload
@aspect0.contacts.include?(@contact).should be false
@aspect1.reload
@aspect1.contacts.include?(@contact).should be false
end
it 'does not 500 on an html request' do
alice.add_contact_to_aspect(@contact, @aspect1)
delete :destroy,
:id => 123,
:person_id => bob.person.id,
:aspect_id => @aspect0.id
membership = alice.add_contact_to_aspect(@contact, @aspect1)
delete :destroy, :id => membership.id
response.should redirect_to :back
@aspect0.reload
@aspect0.contacts.include?(@contact).should be false
@aspect1.reload
@aspect1.contacts.include?(@contact).should be false
end
context 'aspect membership does not exist' do
it 'person does not exist' do
delete :destroy,
:format => 'js', :id => 123,
:person_id => 4324525,
:id => @aspect0.id
response.should_not be_success
response.body.should include "Could not find the selected person in that aspect"
end
it 'contact is not in the aspect' do
delete :destroy,
:format => 'js', :id => 123,
:person_id => bob.person.id,
:aspect_id => 2321
response.should_not be_success
response.body.should include "Could not find the selected person in that aspect"
end
it 'aspect membership does not exist' do
delete :destroy, :format => :json, :id => 123
response.should_not be_success
response.body.should include "Could not find the selected person in that aspect"
end
end
end

View file

@ -0,0 +1,130 @@
describe("app.views.AspectMembership", function(){
beforeEach(function() {
// mock a dummy aspect dropdown
this.person = factory.author({name: "My Name"});
spec.content().html(
'<div class="aspect_membership dropdown">'+
' <div class="button toggle">The Button</div>'+
' <ul class="dropdown_list" data-person-short-name="'+this.person.name+'" data-person_id="'+this.person.id+'">'+
' <li data-aspect_id="10">Aspect 10</li>'+
' <li data-membership_id="99" data-aspect_id="11" class="selected">Aspect 11</li>'+
' <li data-aspect_id="12">Aspect 12</li>'+
' </ul>'+
'</div>'
);
this.view = new app.views.AspectMembership();
});
it('attaches to the aspect selector', function(){
spyOn($.fn, 'on');
view = new app.views.AspectMembership();
expect($.fn.on).toHaveBeenCalled();
});
context('adding to aspects', function() {
beforeEach(function() {
this.newAspect = spec.content().find('li:eq(0)');
this.newAspectId = 10;
});
it('calls "addMembership"', function() {
spyOn(this.view, "addMembership");
this.newAspect.trigger('click');
expect(this.view.addMembership).toHaveBeenCalledWith(this.person.id, this.newAspectId);
});
it('tries to create a new AspectMembership', function() {
spyOn(app.models.AspectMembership.prototype, "save");
this.view.addMembership(1, 2);
expect(app.models.AspectMembership.prototype.save).toHaveBeenCalled();
});
it('displays an error when it fails', function() {
spyOn(this.view, "_displayError");
spyOn(app.models.AspectMembership.prototype, "save").andCallFake(function() {
this.trigger('error');
});
this.view.addMembership(1, 2);
expect(this.view._displayError).toHaveBeenCalledWith('aspect_dropdown.error');
});
});
context('removing from aspects', function(){
beforeEach(function() {
this.oldAspect = spec.content().find('li:eq(1)');
this.oldMembershipId = 99;
});
it('calls "removeMembership"', function(){
spyOn(this.view, "removeMembership");
this.oldAspect.trigger('click');
expect(this.view.removeMembership).toHaveBeenCalledWith(this.oldMembershipId);
});
it('tries to destroy an AspectMembership', function() {
spyOn(app.models.AspectMembership.prototype, "destroy");
this.view.removeMembership(1);
expect(app.models.AspectMembership.prototype.destroy).toHaveBeenCalled();
});
it('displays an error when it fails', function() {
spyOn(this.view, "_displayError");
spyOn(app.models.AspectMembership.prototype, "destroy").andCallFake(function() {
this.trigger('error');
});
this.view.removeMembership(1);
expect(this.view._displayError).toHaveBeenCalledWith('aspect_dropdown.error_remove');
});
});
context('summary text in the button', function() {
beforeEach(function() {
this.btn = spec.content().find('div.button.toggle');
this.btn.text(""); // reset
this.view.dropdown = spec.content().find('ul.dropdown_list');
});
it('shows "no aspects" when nothing is selected', function() {
spec.content().find('li[data-aspect_id]').removeClass('selected');
this.view.updateSummary();
expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.toggle.zero'));
});
it('shows "all aspects" when everything is selected', function() {
spec.content().find('li[data-aspect_id]').addClass('selected');
this.view.updateSummary();
expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.all_aspects'));
});
it('shows the name of the selected aspect ( == 1 )', function() {
var list = spec.content().find('li[data-aspect_id]');
list.removeClass('selected'); // reset
list.eq(1).addClass('selected');
this.view.updateSummary();
expect(this.btn.text()).toContain(list.eq(1).text());
});
it('shows the number of selected aspects ( > 1)', function() {
var list = spec.content().find('li[data-aspect_id]');
list.removeClass('selected'); // reset
$([list.eq(1), list.eq(2)]).addClass('selected');
this.view.updateSummary();
expect(this.btn.text()).toContain(Diaspora.I18n.t('aspect_dropdown.toggle', { 'count':2 }));
});
});
});