diff --git a/app/assets/javascripts/app/app.js b/app/assets/javascripts/app/app.js index a4027d1ae..b9f497686 100644 --- a/app/assets/javascripts/app/app.js +++ b/app/assets/javascripts/app/app.js @@ -55,6 +55,7 @@ var app = { }); app.hovercard = new app.views.Hovercard(); + app.aspectMemberships = new app.views.AspectMembership(); }, hasPreload : function(prop) { diff --git a/app/assets/javascripts/app/models/aspect_membership.js b/app/assets/javascripts/app/models/aspect_membership.js new file mode 100644 index 000000000..73a213f45 --- /dev/null +++ b/app/assets/javascripts/app/models/aspect_membership.js @@ -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" +}); \ No newline at end of file diff --git a/app/assets/javascripts/app/views/aspect_membership_view.js b/app/assets/javascripts/app/views/aspect_membership_view.js new file mode 100644 index 000000000..aea67f090 --- /dev/null +++ b/app/assets/javascripts/app/views/aspect_membership_view.js @@ -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() }); + } +}); diff --git a/app/assets/javascripts/app/views/aspects_dropdown_view.js b/app/assets/javascripts/app/views/aspects_dropdown_view.js index 406532cfa..11e2a7d59 100644 --- a/app/assets/javascripts/app/views/aspects_dropdown_view.js +++ b/app/assets/javascripts/app/views/aspects_dropdown_view.js @@ -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 : { diff --git a/app/assets/javascripts/aspect-edit-pane.js b/app/assets/javascripts/aspect-edit-pane.js index b76f71a9c..26aa42a5d 100644 --- a/app/assets/javascripts/aspect-edit-pane.js +++ b/app/assets/javascripts/aspect-edit-pane.js @@ -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(); +}); diff --git a/app/assets/javascripts/contact-edit.js b/app/assets/javascripts/contact-edit.js index 0e00c1c08..82d9f5dc6 100644 --- a/app/assets/javascripts/contact-edit.js +++ b/app/assets/javascripts/contact-edit.js @@ -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(); }); diff --git a/app/controllers/aspect_memberships_controller.rb b/app/controllers/aspect_memberships_controller.rb index 118ec33af..2fd459828 100644 --- a/app/controllers/aspect_memberships_controller.rb +++ b/app/controllers/aspect_memberships_controller.rb @@ -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 diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 486ff56a5..a7ea9541d 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -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 diff --git a/app/helpers/aspect_global_helper.rb b/app/helpers/aspect_global_helper.rb index 0e6cdd5fb..501cfab1a 100644 --- a/app/helpers/aspect_global_helper.rb +++ b/app/helpers/aspect_global_helper.rb @@ -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 = < +
  • #{aspect.name}
  • LISTITEM diff --git a/app/helpers/aspects_helper.rb b/app/helpers/aspects_helper.rb index 3beca488b..77fb5a55d 100644 --- a/app/helpers/aspects_helper.rb +++ b/app/helpers/aspects_helper.rb @@ -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 diff --git a/app/helpers/contacts_helper.rb b/app/helpers/contacts_helper.rb index 68ac4ac32..07a0df90e 100644 --- a/app/helpers/contacts_helper.rb +++ b/app/helpers/contacts_helper.rb @@ -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 diff --git a/app/models/aspect_membership.rb b/app/models/aspect_membership.rb index ebd3756ab..8155c241f 100644 --- a/app/models/aspect_membership.rb +++ b/app/models/aspect_membership.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index ad4cf3ad4..105b9806f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/aspect_memberships/create.js.erb b/app/views/aspect_memberships/create.js.erb index f1165c0dd..69abb4b4b 100644 --- a/app/views/aspect_memberships/create.js.erb +++ b/app/views/aspect_memberships/create.js.erb @@ -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') ) { diff --git a/app/views/aspect_memberships/destroy.js.erb b/app/views/aspect_memberships/destroy.js.erb index 5093635f2..8c7eb4106 100644 --- a/app/views/aspect_memberships/destroy.js.erb +++ b/app/views/aspect_memberships/destroy.js.erb @@ -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); diff --git a/app/views/aspects/create.js.erb b/app/views/aspects/create.js.erb index 08e88088c..71976d074 100644 --- a/app/views/aspects/create.js.erb +++ b/app/views/aspects/create.js.erb @@ -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"); +} diff --git a/app/views/shared/_aspect_dropdown.html.haml b/app/views/shared/_aspect_dropdown.html.haml index 1f278cdd6..baae64f8c 100644 --- a/app/views/shared/_aspect_dropdown.html.haml +++ b/app/views/shared/_aspect_dropdown.html.haml @@ -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 diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml index 83e95433e..0adf06432 100644 --- a/config/locales/javascript/javascript.en.yml +++ b/config/locales/javascript/javascript.en.yml @@ -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" diff --git a/lib/exceptions.rb b/lib/exceptions.rb index 5b1d92f19..79b5283bb 100644 --- a/lib/exceptions.rb +++ b/lib/exceptions.rb @@ -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 diff --git a/spec/controllers/aspect_memberships_controller_spec.rb b/spec/controllers/aspect_memberships_controller_spec.rb index 8e7192704..a3d4666c1 100644 --- a/spec/controllers/aspect_memberships_controller_spec.rb +++ b/spec/controllers/aspect_memberships_controller_spec.rb @@ -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 diff --git a/spec/javascripts/app/views/aspect_membership_view_spec.js b/spec/javascripts/app/views/aspect_membership_view_spec.js new file mode 100644 index 000000000..a0ee61128 --- /dev/null +++ b/spec/javascripts/app/views/aspect_membership_view_spec.js @@ -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( + '' + ); + + 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 })); + }); + }); +});