Merge pull request #3864 from Raven24/aspect_memberships

[WIP] aspect membership dropdown Backbone.js rework
This commit is contained in:
Jonne Haß 2013-02-17 05:54:12 -08:00
commit f3093ca8ae
21 changed files with 594 additions and 182 deletions

View file

@ -55,6 +55,7 @@ var app = {
}); });
app.hovercard = new app.views.Hovercard(); app.hovercard = new app.views.Hovercard();
app.aspectMemberships = new app.views.AspectMembership();
}, },
hasPreload : function(prop) { 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({ app.views.AspectsDropdown = app.views.Base.extend({
templateName : "aspects-dropdown", templateName : "aspects-dropdown",
events : { events : {

View file

@ -27,3 +27,88 @@ $(document).ready(function() {
toggleAspectTitle(); 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. // the COPYRIGHT file.
var ContactEdit = { 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) { inviteFriend: function(li, evt) {
$.post('/services/inviter/facebook.json', { $.post('/services/inviter/facebook.json', {
"aspect_id" : li.data("aspect_id"), "aspect_id" : li.data("aspect_id"),
@ -38,61 +10,66 @@ var ContactEdit = {
}, function(data){ }, function(data){
ContactEdit.processSuccess(li, evt, 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 class AspectMembershipsController < ApplicationController
before_filter :authenticate_user! before_filter :authenticate_user!
respond_to :html, :json, :js respond_to :html, :json
def destroy 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] raise ActiveRecord::RecordNotFound unless aspect.present? && contact.present?
@aspect_id = params[:aspect_id]
@contact = current_user.contact_for(Person.where(:id => @person_id).first) raise Diaspora::NotMine unless current_user.mine?(aspect) &&
membership = @contact ? @contact.aspect_memberships.where(:aspect_id => @aspect_id).first : nil current_user.mine?(contact)
if membership && membership.destroy membership = contact.aspect_memberships.where(:aspect_id => aspect.id).first
@aspect = membership.aspect
raise ActiveRecord::RecordNotFound unless membership.present?
# do it!
success = membership.destroy
# set the flash message
if success
flash.now[:notice] = I18n.t 'aspect_memberships.destroy.success' flash.now[:notice] = I18n.t 'aspect_memberships.destroy.success'
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
else else
flash.now[:error] = I18n.t 'aspect_memberships.destroy.failure' 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
}
end 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
end end
def create def create
@person = Person.find(params[:person_id]) @person = Person.find(params[:person_id])
@aspect = current_user.aspects.where(:id => params[:aspect_id]).first @aspect = current_user.aspects.where(:id => params[:aspect_id]).first
@contact = current_user.share_with(@person, @aspect) @contact = current_user.share_with(@person, @aspect)
if @contact if @contact.present?
flash.now[:notice] = I18n.t('aspects.add_to_aspect.success') 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 else
flash.now[:error] = I18n.t('contacts.create.failure') flash.now[:error] = I18n.t('contacts.create.failure')
render :nothing => true, :status => 409 render :nothing => true, :status => 409
@ -58,4 +71,13 @@ class AspectMembershipsController < ApplicationController
rescue_from ActiveRecord::StatementInvalid do rescue_from ActiveRecord::StatementInvalid do
render :text => "Duplicate record rejected.", :status => 400 render :text => "Duplicate record rejected.", :status => 400
end 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 end

View file

@ -163,15 +163,17 @@ class PeopleController < ApplicationController
end end
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 def aspect_membership_dropdown
@person = Person.find_by_guid(params[:person_id]) @person = Person.find_by_guid(params[:person_id])
if @person == current_user.person
render :text => I18n.t('people.person.thats_you') # you are not a contact of yourself...
else return render :text => I18n.t('people.person.thats_you') if @person == current_user.person
@contact = current_user.contact_for(@person) || Contact.new @contact = current_user.contact_for(@person) || Contact.new
render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left'} render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left'}
end end
end
def redirect_if_tag_search def redirect_if_tag_search
if search_query.starts_with?('#') if search_query.starts_with?('#')

View file

@ -4,20 +4,27 @@
module AspectGlobalHelper module AspectGlobalHelper
def aspect_membership_dropdown(contact, person, hang, aspect=nil) def aspect_membership_dropdown(contact, person, hang, aspect=nil)
aspect_membership_ids = {}
selected_aspects = all_aspects.select{|aspect| contact.in_aspect?(aspect)} 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", render "shared/aspect_dropdown",
:selected_aspects => selected_aspects, :selected_aspects => selected_aspects,
:aspect_membership_ids => aspect_membership_ids,
:person => person, :person => person,
:hang => hang, :hang => hang,
:dropdown_class => "aspect_membership" :dropdown_class => "aspect_membership"
end end
def aspect_dropdown_list_item(aspect, checked) def aspect_dropdown_list_item(aspect, am_id=nil)
klass = checked ? "selected" : "" klass = am_id.present? ? "selected" : ""
str = <<LISTITEM 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} #{aspect.name}
</li> </li>
LISTITEM LISTITEM

View file

@ -5,27 +5,27 @@
module AspectsHelper module AspectsHelper
def add_to_aspect_button(aspect_id, person_id) def add_to_aspect_button(aspect_id, person_id)
link_to image_tag('icons/monotone_plus_add_round.png'), link_to image_tag('icons/monotone_plus_add_round.png'),
{:controller => 'aspect_memberships', { :controller => 'aspect_memberships',
:action => 'create', :action => 'create',
:format => :json,
:aspect_id => aspect_id, :aspect_id => aspect_id,
:person_id => person_id}, :person_id => person_id
:remote => true, },
:method => 'post', :method => 'post',
:class => 'add button', :class => 'add button',
'data-aspect_id' => aspect_id, 'data-aspect_id' => aspect_id,
'data-person_id' => person_id 'data-person_id' => person_id
end 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'), link_to image_tag('icons/monotone_check_yes.png'),
{:controller => "aspect_memberships", { :controller => "aspect_memberships",
:action => 'destroy', :action => 'destroy',
:id => 42, :id => membership_id
:aspect_id => aspect_id, },
:person_id => person_id},
:remote => true,
:method => 'delete', :method => 'delete',
:class => 'added button', :class => 'added button',
'data-membership_id' => membership_id,
'data-aspect_id' => aspect_id, 'data-aspect_id' => aspect_id,
'data-person_id' => person_id 'data-person_id' => person_id
end end
@ -33,10 +33,11 @@ module AspectsHelper
def aspect_membership_button(aspect, contact, person) def aspect_membership_button(aspect, contact, person)
return if person && person.closed_account? 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) add_to_aspect_button(aspect.id, person.id)
else else
remove_from_aspect_button(aspect.id, person.id) remove_from_aspect_button(membership.id, aspect.id, person.id)
end end
end end
end end

View file

@ -1,19 +1,23 @@
module ContactsHelper module ContactsHelper
def contact_aspect_dropdown(contact) 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), link_to(image_tag('icons/monotone_close_exit_delete.png', :height => 20, :width => 20),
{:controller => "aspect_memberships", { :controller => "aspect_memberships",
:action => 'destroy', :action => 'destroy',
:id => 42, :id => membership.id
:aspect_id => @aspect.id,
:person_id => contact.person_id
}, },
:title => t('contacts.index.remove_person_from_aspect', :person_name => contact.person_first_name, :aspect_name => @aspect.name), :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 else
render :partial => 'people/relationship_action', render :partial => 'people/relationship_action',
:locals => { :person => contact.person, :contact => contact, :locals => { :person => contact.person,
:contact => contact,
:current_user => current_user } :current_user => current_user }
end end
end end

View file

@ -21,6 +21,7 @@ class AspectMembership < ActiveRecord::Base
:id => self.id, :id => self.id,
:person_id => self.person.id, :person_id => self.person.id,
:contact_id => self.contact.id, :contact_id => self.contact.id,
:aspect_id => self.aspect_id,
:aspect_ids => self.contact.aspects.map{|a| a.id} :aspect_ids => self.contact.aspects.map{|a| a.id}
} }
end end

View file

@ -406,6 +406,15 @@ class User < ActiveRecord::Base
Role.is_admin?(self.person) Role.is_admin?(self.person)
end 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 def guard_unconfirmed_email
self.unconfirmed_email = nil if unconfirmed_email.blank? || unconfirmed_email == 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 // licensed under the Affero General Public License version 3 or later. See
// the COPYRIGHT file. // 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%>]"); var element = $(".add[data-aspect_id=<%= @aspect.id %>][data-person_id=<%= @contact.person_id%>]");
if( $("#no_contacts").is(':visible') ) { if( $("#no_contacts").is(':visible') ) {

View file

@ -2,6 +2,8 @@
// 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.
// 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%>]"); 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.parent().html("<%= escape_javascript(render('aspect_memberships/remove_from_aspect', :aspect => @aspect, :person => @contact.person, :contact => @contact)) %>");
element.fadeTo(200,1); element.fadeTo(200,1);

View file

@ -2,10 +2,16 @@
// 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.
var dropdown = $("ul.dropdown_list[data-person_id=<%= @person.id %>]") // TODO create the aspect and the new aspect membership via Backbone.js and then
$('.newItem', dropdown).before("<%= escape_javascript( aspect_dropdown_list_item(@aspect, @contact.aspects.include?(@aspect))) %>"); // remove this view!
ContactEdit.updateNumber(dropdown, "<%= @person.id %>", <%= @contact.aspects.size %>); if( app.aspectMemberships ) {
$.facebox.close(); var dropdown = $("ul.dropdown_list[data-person_id=<%= @person.id %>]");
$('#profile .dropdown').toggleClass("active"); $('.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 .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)} %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 - 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) - if (dropdown_may_create_new_aspect && defined?(person) && person)
%li.newItem %li.newItem

View file

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

View file

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

View file

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