[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:
parent
ad4ba363a9
commit
4cbae601e8
21 changed files with 594 additions and 182 deletions
|
|
@ -55,6 +55,7 @@ var app = {
|
|||
});
|
||||
|
||||
app.hovercard = new app.views.Hovercard();
|
||||
app.aspectMemberships = new app.views.AspectMembership();
|
||||
},
|
||||
|
||||
hasPreload : function(prop) {
|
||||
|
|
|
|||
7
app/assets/javascripts/app/models/aspect_membership.js
Normal file
7
app/assets/javascripts/app/models/aspect_membership.js
Normal 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"
|
||||
});
|
||||
161
app/assets/javascripts/app/views/aspect_membership_view.js
Normal file
161
app/assets/javascripts/app/views/aspect_membership_view.js
Normal 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() });
|
||||
}
|
||||
});
|
||||
|
|
@ -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 : {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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') ) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
130
spec/javascripts/app/views/aspect_membership_view_spec.js
Normal file
130
spec/javascripts/app/views/aspect_membership_view_spec.js
Normal 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 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue