Refactor app.views.AspectMembership

in order to support adding new aspect to a dropdown without full
page reload
This commit is contained in:
cmrd Senya 2016-06-08 01:43:24 +03:00
parent 15e0f88758
commit 923fb8a763
No known key found for this signature in database
GPG key ID: 5FCC5BA680E67BFE
44 changed files with 526 additions and 285 deletions

View file

@ -120,9 +120,6 @@ var app = {
setupGlobalViews: function() {
app.hovercard = new app.views.Hovercard();
$('.aspect_membership_dropdown').each(function(){
new app.views.AspectMembership({el: this});
});
app.sidebar = new app.views.Sidebar();
app.backToTop = new app.views.BackToTop({el: $(document)});
app.flashMessages = new app.views.FlashMessages({el: $("#flash-container")});

View file

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

View file

@ -0,0 +1,7 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.collections.Aspects = Backbone.Collection.extend({
model: app.models.Aspect,
url: "/aspects"
});
// @license-end

View file

@ -5,6 +5,11 @@
* (only valid for the context of the current user)
*/
app.models.AspectMembership = Backbone.Model.extend({
urlRoot: "/aspect_memberships"
urlRoot: "/aspect_memberships",
belongsToAspect: function(aspectId) {
var aspect = this.get("aspect");
return aspect && aspect.id === aspectId;
}
});
// @license-end

View file

@ -3,11 +3,14 @@
app.models.Contact = Backbone.Model.extend({
initialize : function() {
this.aspectMemberships = new app.collections.AspectMemberships(this.get("aspect_memberships"));
if(this.get("person")) { this.person = new app.models.Person(this.get("person")); }
if (this.get("person")) {
this.person = new app.models.Person(this.get("person"));
this.person.contact = this;
}
},
inAspect : function(id) {
return this.aspectMemberships.any(function(membership){ return membership.get("aspect").id === id; });
return this.aspectMemberships.any(function(membership) { return membership.belongsToAspect(id); });
}
});
// @license-end

View file

@ -6,8 +6,13 @@ app.models.Person = Backbone.Model.extend({
},
initialize: function() {
if( this.get('profile') )
this.profile = new app.models.Profile(this.get('profile'));
if (this.get("profile")) {
this.profile = new app.models.Profile(this.get("profile"));
}
if (this.get("contact")) {
this.contact = new app.models.Contact(this.get("contact"));
this.contact.person = this;
}
},
isSharing: function() {

View file

@ -0,0 +1,20 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.pages.GettingStarted = app.views.Base.extend({
el: "#hello-there",
templateName: false,
subviews: {
".aspect_membership_dropdown": "aspectMembershipView"
},
initialize: function(opts) {
this.inviter = opts.inviter;
app.events.on("aspect:create", this.render, this);
},
aspectMembershipView: function() {
return new app.views.AspectMembership({person: this.inviter, dropdownMayCreateNewAspect: true});
}
});
// @license-end

View file

@ -5,6 +5,7 @@ app.Router = Backbone.Router.extend({
"help/:section": "help",
"help/": "help",
"help": "help",
"getting_started": "gettingStarted",
"contacts": "contacts",
"conversations": "conversations",
"user/edit": "settings",
@ -60,6 +61,7 @@ app.Router = Backbone.Router.extend({
contacts: function() {
app.aspect = new app.models.Aspect(gon.preloads.aspect);
app.contacts = new app.collections.Contacts(app.parsePreload("contacts"));
this._loadAspects();
var stream = new app.views.ContactStream({
collection: app.contacts,
@ -69,6 +71,13 @@ app.Router = Backbone.Router.extend({
app.page = new app.pages.Contacts({stream: stream});
},
gettingStarted: function() {
this._loadAspects();
this.renderPage(function() {
return new app.pages.GettingStarted({inviter: new app.models.Person(app.parsePreload("inviter"))});
});
},
conversations: function() {
app.conversations = new app.views.Conversations();
},
@ -134,6 +143,7 @@ app.Router = Backbone.Router.extend({
},
aspects: function() {
this._loadAspects();
app.aspectSelections = app.aspectSelections ||
new app.collections.AspectSelections(app.currentUser.get("aspects"));
this.aspectsList = this.aspectsList || new app.views.AspectsList({collection: app.aspectSelections});
@ -157,6 +167,7 @@ app.Router = Backbone.Router.extend({
},
profile: function() {
this._loadAspects();
this.renderPage(function() {
return new app.pages.Profile({
el: $("body > #profile_container")
@ -164,6 +175,10 @@ app.Router = Backbone.Router.extend({
});
},
_loadAspects: function() {
app.aspects = new app.collections.Aspects(app.currentUser.get("aspects"));
},
_hideInactiveStreamLists: function() {
if(this.aspectsList && Backbone.history.fragment !== "aspects") {
this.aspectsList.hideAspectsList();

View file

@ -9,20 +9,18 @@ app.views.AspectCreate = app.views.Base.extend({
},
initialize: function(opts) {
this._personId = _.has(opts, "personId") ? opts.personId : null;
if (opts && opts.person) {
this.person = opts.person;
this._personId = opts.person.id;
}
},
presenter: function() {
return _.extend(this.defaultPresenter(), {
addPersonId: this._personId !== null,
personId : this._personId
});
},
postRenderTemplate: function() {
this.modal = this.$(".modal");
},
_contactsVisible: function() {
return this.$("#aspect_contacts_visible").is(":checked");
},
@ -38,28 +36,52 @@ app.views.AspectCreate = app.views.Base.extend({
}
},
createAspect: function() {
var aspect = new app.models.Aspect({
"person_id": this._personId,
"name": this._name(),
"contacts_visible": this._contactsVisible()
postRenderTemplate: function() {
this.$(".modal").on("hidden.bs.modal", null, this, function(e) {
e.data.ensureEventsOrder();
});
},
var self = this;
aspect.on("sync", function(response) {
var aspectId = response.get("id"),
aspectName = response.get("name");
createAspect: function() {
this._eventsCounter = 0;
self.modal.modal("hide");
app.events.trigger("aspect:create", aspectId);
this.$(".modal").modal("hide");
this.listenToOnce(app.aspects, "sync", function(response) {
var aspectName = response.get("name"),
membership = response.get("aspect_membership");
this._newAspectId = response.get("id");
if (membership) {
if (!this.person.contact) {
this.person.contact = new app.models.Contact();
}
this.person.contact.aspectMemberships.add([membership]);
}
this.ensureEventsOrder();
app.flashMessages.success(Diaspora.I18n.t("aspects.create.success", {"name": aspectName}));
});
aspect.on("error", function() {
self.modal.modal("hide");
this.listenToOnce(app.aspects, "error", function() {
app.flashMessages.error(Diaspora.I18n.t("aspects.create.failure"));
this.stopListening(app.aspects, "sync");
});
return aspect.save();
app.aspects.create({
"person_id": this._personId || null,
"name": this._name(),
"contacts_visible": this._contactsVisible()
});
},
// ensure that we trigger the aspect:create event only after both hidden.bs.modal and and aspects sync happens
ensureEventsOrder: function() {
this._eventsCounter++;
if (this._eventsCounter > 1) {
app.events.trigger("aspect:create", this._newAspectId);
}
}
});
// @license-end

View file

@ -1,7 +1,5 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
//= require ./aspects_dropdown_view
/**
* this view lets the user (de-)select aspect memberships in the context
* of another users profile or the contact page.
@ -9,7 +7,13 @@
* updates to the list of aspects are immediately propagated to the server, and
* the results are dislpayed as flash messages.
*/
app.views.AspectMembership = app.views.AspectsDropdown.extend({
app.views.AspectMembership = app.views.Base.extend({
templateName: "aspect_membership_dropdown",
className: "btn-group aspect_dropdown aspect_membership_dropdown",
subviews: {
".newAspectContainer": "aspectCreateView"
},
events: {
"click ul.aspect_membership.dropdown-menu > li.aspect_selector"
@ -18,70 +22,89 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
: "_clickHandler"
},
initialize: function() {
initialize: function(opts) {
_.extend(this, opts);
this.list_item = null;
this.dropdown = null;
if (this.$(".newAspectContainer").length > 0) {
this.aspectCreateView = new app.views.AspectCreate({
el: this.$(".newAspectContainer"),
personId: this.$("ul.dropdown-menu").data("person_id")
},
presenter: function() {
var aspectMembershipsLength = this.person.contact ? this.person.contact.aspectMemberships.length : 0;
return _.extend(this.defaultPresenter(), {
aspects: this.aspectsPresenter(),
dropdownMayCreateNewAspect: this.dropdownMayCreateNewAspect
}, aspectMembershipsLength === 0 ? {
extraButtonClass: "btn-default",
noAspectIsSelected: true
} : { // this.contact.aspectMemberships.length > 0
aspectMembershipsLength: aspectMembershipsLength,
allAspectsAreSelected: aspectMembershipsLength === app.aspects.length,
onlyOneAspectIsSelected: aspectMembershipsLength === 1,
firstMembershipName: this.person.contact.aspectMemberships.at(0).get("aspect").name,
extraButtonClass: "btn-success"
});
},
aspectsPresenter: function() {
return _.map(app.aspects.models, function(aspect) {
return _.extend(
this.person.contact ?
{membership: this.person.contact.aspectMemberships.findByAspectId(aspect.attributes.id)} : {},
aspect.attributes // id, name
);
}, this);
},
aspectCreateView: function() {
return new app.views.AspectCreate({
person: this.person
});
this.aspectCreateView.render();
}
},
// decide what to do when clicked
// -> addMembership
// -> removeMembership
_clickHandler: function(evt) {
var promise = null;
this.list_item = $(evt.target).closest('li.aspect_selector');
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');
promise = this.removeMembership(membership_id);
if (this.list_item.is(".selected")) {
this.removeMembership(this.list_item.data("membership_id"));
} else {
var aspect_id = this.list_item.data('aspect_id');
var person_id = this.dropdown.data('person_id');
promise = this.addMembership(person_id, aspect_id);
this.addMembership(this.list_item.data("aspect_id"));
}
promise && promise.always(function() {
// trigger a global event
app.events.trigger('aspect_membership:update');
});
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');
return this.person.name || this.person.get("name");
},
_personId: function() {
return this.person.id;
},
// 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
addMembership: function(aspectId) {
if (!this.person.contact) {
this.person.contact = new app.models.Contact();
}
this.listenToOnce(this.person.contact.aspectMemberships, "sync", this._successSaveCb);
this.listenToOnce(this.person.contact.aspectMemberships, "error", function() {
this._displayError('aspect_dropdown.error');
});
aspect_membership.on('sync', this._successSaveCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error');
}, this);
return aspect_membership.save();
return this.person.contact.aspectMemberships.create({"aspect_id": aspectId, "person_id": this._personId()});
},
_successSaveCb: function(aspectMembership) {
var aspectId = aspectMembership.get("aspect_id"),
membershipId = aspectMembership.get("id"),
li = this.dropdown.find("li[data-aspect_id='" + aspectId + "']"),
personId = li.closest("ul.dropdown-menu").data("person_id"),
startSharing = false;
// the user didn't have this person in any aspects before, congratulate them
@ -93,15 +116,11 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
}
app.events.trigger("aspect_membership:create", {
membership: { aspectId: aspectId, personId: personId },
membership: {aspectId: aspectId, personId: this._personId()},
startSharing: startSharing
});
li.attr("data-membership_id", membershipId) // just to be sure...
.data("membership_id", membershipId);
this.updateSummary(li);
this._done();
this.render();
app.events.trigger("aspect_membership:update");
},
// show an error flash msg
@ -114,44 +133,35 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
},
// remove the membership with the given id
removeMembership: function(membership_id) {
var aspect_membership = new app.models.AspectMembership({
'id': membership_id
removeMembership: function(membershipId) {
var membership = this.person.contact.aspectMemberships.get(membershipId);
this.listenToOnce(membership, "sync", this._successDestroyCb);
this.listenToOnce(membership, "error", function() {
this._displayError("aspect_dropdown.error_remove");
});
aspect_membership.on('sync', this._successDestroyCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error_remove');
}, this);
return aspect_membership.destroy();
return membership.destroy();
},
_successDestroyCb: function(aspectMembership) {
var membershipId = aspectMembership.get("id"),
li = this.dropdown.find("li[data-membership_id='" + membershipId + "']"),
aspectId = li.data("aspect_id"),
personId = li.closest("ul.dropdown-menu").data("person_id"),
aspectId = aspectMembership.get("aspect").id,
stopSharing = false;
li.removeAttr("data-membership_id")
.removeData("membership_id");
this.updateSummary(li);
this.render();
// 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 ) {
if (this.$el.find("li.selected").length === 0) {
var msg = Diaspora.I18n.t("aspect_dropdown.stopped_sharing_with", { "name": this._name() });
stopSharing = true;
app.flashMessages.success(msg);
}
app.events.trigger("aspect_membership:destroy", {
membership: { aspectId: aspectId, personId: personId },
membership: {aspectId: aspectId, personId: this._personId()},
stopSharing: stopSharing
});
this._done();
app.events.trigger("aspect_membership:update");
},
// cleanup tasks after aspect selection
@ -160,11 +170,5 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
this.list_item.removeClass('loading');
}
},
// refresh the button text to reflect the current aspect selection status
updateSummary: function(target) {
this._toggleCheckbox(target);
this._updateButton("btn-success");
}
});
// @license-end

View file

@ -3,6 +3,10 @@
app.views.Contact = app.views.Base.extend({
templateName: 'contact',
subviews: {
".aspect_membership_dropdown": "AspectMembershipView"
},
events: {
"click .contact_add-to-aspect" : "addContactToAspect",
"click .contact_remove-from-aspect" : "removeContactFromAspect"
@ -10,6 +14,12 @@ app.views.Contact = app.views.Base.extend({
tooltipSelector: '.contact_add-to-aspect, .contact_remove-from-aspect',
initialize: function() {
this.AspectMembershipView = new app.views.AspectMembership(
{person: _.extend(this.model.get("person"), {contact: this.model})}
);
},
presenter: function() {
return _.extend(this.defaultPresenter(), {
person_id : this.model.get('person_id'),
@ -18,21 +28,6 @@ app.views.Contact = app.views.Base.extend({
});
},
postRenderTemplate: function() {
var dropdownEl = this.$('.aspect_membership_dropdown.placeholder');
if( dropdownEl.length === 0 ) {
return;
}
// TODO render me client side!!!
var href = this.model.person.url() + '/aspect_membership_button?size=small';
$.get(href, function(resp) {
dropdownEl.html(resp);
new app.views.AspectMembership({el: $('.aspect_dropdown',dropdownEl)});
});
},
addContactToAspect: function(){
var self = this;
// do we create the first aspect membership for this person?

View file

@ -3,13 +3,16 @@
app.views.ProfileHeader = app.views.Base.extend({
templateName: 'profile_header',
subviews: {
".aspect_membership_dropdown": "aspectMembershipView"
},
events: {
"click #mention_button": "showMentionModal",
"click #message_button": "showMessageModal"
},
initialize: function(opts) {
app.events.on('aspect:create', this.postRenderTemplate, this);
this.photos = _.has(opts, 'photos') ? opts.photos : null;
this.contacts = _.has(opts, 'contacts') ? opts.contacts : null;
},
@ -29,6 +32,10 @@ app.views.ProfileHeader = app.views.Base.extend({
});
},
aspectMembershipView: function() {
return new app.views.AspectMembership({person: this.model, dropdownMayCreateNewAspect: true});
},
_hasTags: function() {
return (this.model.get('profile')['tags'].length > 0);
},
@ -52,21 +59,6 @@ app.views.ProfileHeader = app.views.Base.extend({
showMessageModal: function(){
app.helpers.showModal("#conversationModal");
},
postRenderTemplate: function() {
var dropdownEl = this.$('.aspect_membership_dropdown.placeholder');
if( dropdownEl.length === 0 ) {
return;
}
// TODO render me client side!!!
var href = this.model.url() + '/aspect_membership_button?create=true&size=normal';
$.get(href, function(resp) {
dropdownEl.html(resp);
new app.views.AspectMembership({el: $('.aspect_dropdown',dropdownEl)});
});
}
});
// @license-end

View file

@ -120,3 +120,21 @@ $default-border-radius: 3px;
}
}
}
@mixin selectable-list() {
.glyphicon-ok,
.glyphicon-refresh {
display: none;
padding-right: 5px;
}
&.selected {
.glyphicon-ok { display: inline-block;}
.glyphicon-refresh { display: none;}
}
&.loading {
.glyphicon-refresh { display: inline-block;}
.glyphicon-ok { display: none;}
}
}

View file

@ -1,23 +1,14 @@
.aspect_dropdown {
li {
@include selectable-list;
.status_indicator {
width: 19px;
height: 14px;
display: inline-block;
}
.glyphicon-ok, .icon-refresh {
padding-right: 5px;
display: none;
}
&.selected {
.glyphicon-ok { display: inline-block;}
.icon-refresh { display: none;}
}
&.loading {
.icon-refresh { display: inline-block;}
.glyphicon-ok { display: none;}
}
a {
.text {
color: #333333;

View file

@ -11,7 +11,7 @@
<div class="modal-body">
<form>
<fieldset>
{{#if addPersonId}}
{{#if personId}}
<input id="aspect_person_id" type="hidden" value="{{ personId }}">
{{/if}}

View file

@ -0,0 +1,52 @@
<button class="btn dropdown-toggle {{extraButtonClass}}" data-toggle="dropdown" tabindex="0">
<span class="text">
{{#if allAspectsAreSelected }}
{{ t "aspect_dropdown.all_aspects" }}
{{else if onlyOneAspectIsSelected}}
{{ firstMembershipName }}
{{else if noAspectIsSelected}}
{{ t "aspect_dropdown.add_to_aspect"}}
{{else}}
{{ t "aspect_dropdown.toggle" count=aspectMembershipsLength }}
{{/if}}
</span>
<span class="caret" />
</button>
<ul class="dropdown-menu aspect_membership pull-right" unselectable="on">
{{#each aspects}}
<li
{{#if membership}}
class="aspect_selector selected"
{{else}}
class="aspect_selector"
{{/if}}
data-aspect_id="{{id}}"
{{#if membership}}
data-membership_id="{{membership.id}}"
{{/if}}
>
<a>
<span class="status_indicator">
<i class="glyphicon glyphicon-ok" />
<i class="glyphicon glyphicon-refresh" />
</span>
<span class="text">
{{name}}
</span>
</a>
</li>
{{/each}}
{{#if dropdownMayCreateNewAspect}}
<li class="divider" />
<li class="newItem add_aspect">
<a data-target="#newAspectModal" data-toggle="modal" href="#">
{{ t "aspects.create.add_a_new_aspect" }}
</a>
</li>
{{/if}}
</ul>
{{#if dropdownMayCreateNewAspect}}
<div class="newAspectContainer"/>
{{/if}}

View file

@ -14,10 +14,13 @@ class AspectsController < ApplicationController
aspecting_person_id = params[:person_id]
if @aspect.save
result = {id: @aspect.id, name: @aspect.name}
if aspecting_person_id.present?
connect_person_to_aspect(aspecting_person_id)
aspect_membership = connect_person_to_aspect(aspecting_person_id)
result[:aspect_membership] = AspectMembershipPresenter.new(aspect_membership).base_hash if aspect_membership
end
render json: {id: @aspect.id, name: @aspect.name}
render json: result
else
render nothing: true, status: 422
end
@ -96,9 +99,10 @@ class AspectsController < ApplicationController
def connect_person_to_aspect(aspecting_person_id)
@person = Person.find(aspecting_person_id)
if @contact = current_user.contact_for(@person)
@contact.aspects << @aspect
@contact.aspect_memberships.create(aspect: @aspect)
else
@contact = current_user.share_with(@person, @aspect)
@contact.aspect_memberships.first
end
end

View file

@ -146,21 +146,6 @@ 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])
# 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
@aspect = :profile if params[:create] # let aspect dropdown create new aspects
size = params[:size] || "small"
render :partial => 'aspect_membership_dropdown', :locals => {:contact => @contact, :person => @person, :hang => 'left', :size => size}
end
private
def find_person

View file

@ -129,6 +129,7 @@ class UsersController < ApplicationController
@user = current_user
@person = @user.person
@profile = @user.profile
gon.preloads[:inviter] = PersonPresenter.new(current_user.invited_by.try(:person), current_user).as_json
render "users/getting_started"
end

View file

@ -3,7 +3,7 @@
# the COPYRIGHT file.
module AspectGlobalHelper
def aspect_membership_dropdown(contact, person, hang, aspect=nil, size="small")
def aspect_membership_dropdown(contact, person, hang)
aspect_membership_ids = {}
selected_aspects = all_aspects.select{|aspect| contact.in_aspect?(aspect)}
@ -13,41 +13,15 @@ module AspectGlobalHelper
end
button_class = selected_aspects.size > 0 ? "btn-success" : "btn-default"
button_class << case size
when "small"
" btn-small"
when "normal"
""
when "large"
" btn-large"
else
raise ArgumentError, "unknown size #{size}"
end
render "aspect_memberships/aspect_membership_dropdown",
:selected_aspects => selected_aspects,
:aspect_membership_ids => aspect_membership_ids,
:person => person,
:hang => hang,
:dropdown_class => "aspect_membership",
:button_class => button_class
end
def aspect_dropdown_list_item(aspect, am_id=nil)
klass = am_id.present? ? "selected" : ""
str = <<LISTITEM
<li data-aspect_id="#{aspect.id}" data-membership_id="#{am_id}" class="#{klass} aspect_selector" tabindex="0">
#{aspect.name}
</li>
LISTITEM
str.html_safe
end
def dropdown_may_create_new_aspect
@aspect == :profile || @aspect == :tag || @aspect == :notification || params[:action] == "getting_started"
end
def aspect_options_for_select(aspects)
options = {}
aspects.each do |aspect|

View file

@ -12,6 +12,12 @@ class ContactPresenter < BasePresenter
end
def full_hash_with_person
full_hash.merge(person: PersonPresenter.new(person, current_user).as_json)
full_hash.merge(person: person_without_contact)
end
private
def person_without_contact
PersonPresenter.new(person, current_user).as_json.except!(:contact)
end
end

View file

@ -12,7 +12,7 @@ class PersonPresenter < BasePresenter
base_hash.merge(
relationship: relationship,
block: is_blocked? ? BlockPresenter.new(current_user_person_block).base_hash : false,
contact: (!own_profile? && has_contact?) ? {id: current_user_person_contact.id} : false,
contact: (!own_profile? && has_contact?) ? contact_hash : false,
is_own_profile: own_profile?,
show_profile_info: public_details? || own_profile? || person_is_following_current_user
)
@ -58,6 +58,10 @@ class PersonPresenter < BasePresenter
attrs
end
def contact_hash
ContactPresenter.new(current_user_person_contact).full_hash
end
private
def current_user_person_block

View file

@ -1,30 +1 @@
.btn-group.aspect_dropdown.aspect_membership_dropdown
%button.btn.dropdown-toggle{:class => button_class, "data-toggle" => "dropdown", :tabindex => '0'}
%span.text
- if selected_aspects.size == all_aspects.size
= t('all_aspects')
- elsif selected_aspects.size == 1
= selected_aspects.first.name
- else
= t('shared.aspect_dropdown.toggle', :count => selected_aspects.size)
%span.caret
%ul.dropdown-menu{:class => ["pull-#{hang}", defined?(dropdown_class) && dropdown_class], :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
%li.aspect_selector{ :class => ('selected' if aspect_membership_ids[aspect.id].present?), 'data-aspect_id' => aspect.id, 'data-membership_id' => aspect_membership_ids[aspect.id], :tabindex => '0' }
%a
%span.status_indicator
%i.glyphicon.glyphicon-ok
%i.icon-refresh
%span.text
= aspect.name
- if dropdown_may_create_new_aspect && defined?(person) && person
%li.divider
%li.newItem.add_aspect
%a{ href: "#", data: { toggle: "modal", target: "#newAspectModal" }}
= t("contacts.index.add_a_new_aspect")
- if dropdown_may_create_new_aspect && defined?(person) && person
.newAspectContainer
-# JS
.placeholder.aspect_membership_dropdown

View file

@ -2,7 +2,7 @@
= t('.invited_by')
.media
.pull-right
= aspect_membership_dropdown(contact, inviter, false)
= render partial: "aspect_memberships/aspect_membership_dropdown", locals: {person: inviter}
.media-left
= person_image_link(inviter, size: :thumb_small, class: "media-object")
.media-body

View file

@ -1 +0,0 @@
= aspect_membership_dropdown(@contact, @person, 'right', nil, size)

View file

@ -1,7 +1,7 @@
- unless person == current_user.person
- contact = current_user.contacts.find_by_person_id(person.id)
- contact ||= Contact.new(:person => person)
= aspect_membership_dropdown(contact, person, 'right')
.aspect_membership_dropdown
-else
%span.thats_you
= t('people.person.thats_you')

View file

@ -1024,7 +1024,6 @@ en:
mobile_row_checked: "%{name} (remove)"
mobile_row_unchecked: "%{name} (add)"
toggle:
zero: "Add contact"
one: "In %{count} aspect"
other: "In %{count} aspects"
publisher:

View file

@ -144,7 +144,6 @@ en:
error: "Couldnt start sharing with <%= name %>. Are you ignoring them?"
error_remove: "Couldnt remove <%= name %> from the aspect :("
toggle:
zero: "Select aspects"
one: "In <%= count %> aspect"
other: "In <%= count %> aspects"
show_more: "Show more"

View file

@ -169,7 +169,6 @@ Diaspora::Application.routes.draw do
resources :status_messages
resources :photos
get :contacts
get "aspect_membership_button" => :aspect_membership_dropdown, :as => "aspect_membership_button"
get :stream
get :hovercard

View file

@ -17,6 +17,7 @@ Feature: following and being followed
And I go to the edit profile page
And I fill in the following:
| profile_first_name | <script>alert(0)// |
| profile_last_name ||
And I press "update_profile"
Then I should be on my edit profile page

View file

@ -20,17 +20,4 @@ describe PeopleController, :type => :controller do
save_fixture(html_for("body"), "pending_external_people_search")
end
end
describe '#aspect_membership_dropdown' do
before do
aspect = bob.aspects.create name: 'Testing'
bob.share_with alice.person, aspect
sign_in :user, bob
end
it "generates a jasmine fixture", :fixture => true do
get :aspect_membership_dropdown, :person_id => alice.person.guid
save_fixture(html_for("body"), "aspect_membership_dropdown")
end
end
end

View file

@ -0,0 +1,19 @@
require "spec_helper"
describe UsersController, type: :controller do
before do
sign_in :user, alice
end
describe "#getting_started" do
before do
alice.invited_by = bob
alice.save!
end
it "generates a jasmine fixture with no query", fixture: true do
get :getting_started
save_fixture(html_for("body"), "getting_started")
end
end
end

View file

@ -5,6 +5,8 @@
require 'spec_helper'
describe PeopleController, :type => :controller do
include_context :gon
before do
@user = alice
@aspect = @user.aspects.first
@ -277,6 +279,11 @@ describe PeopleController, :type => :controller do
get :show, id: @person.to_param
expect(response.body).to include(@person.profile.bio)
end
it "preloads data using gon for the aspect memberships dropdown" do
get :show, id: @person.to_param
expect_gon_preloads_for_aspect_membership_dropdown(:person, true)
end
end
context "when the person is not a contact of the current user" do
@ -298,6 +305,11 @@ describe PeopleController, :type => :controller do
get :show, id: @person.to_param
expect(response.body).not_to include(@person.profile.bio)
end
it "preloads data using gon for the aspect memberships dropdown" do
get :show, id: @person.to_param
expect_gon_preloads_for_aspect_membership_dropdown(:person, false)
end
end
context "when the user is following the person" do

View file

@ -5,6 +5,8 @@
require 'spec_helper'
describe UsersController, :type => :controller do
include_context :gon
before do
@user = alice
sign_in :user, @user
@ -319,5 +321,19 @@ describe UsersController, :type => :controller do
get :getting_started, :format => :mobile
expect(response).to be_success
end
context "with inviter" do
[bob, eve].each do |inviter|
sharing = !alice.contact_for(inviter.person).nil?
context sharing ? "when sharing" : "when don't share" do
it "preloads data using gon for the aspect memberships dropdown" do
alice.invited_by = inviter
get :getting_started
expect_gon_preloads_for_aspect_membership_dropdown(:inviter, sharing)
end
end
end
end
end
end

View file

@ -0,0 +1,17 @@
describe("app.collections.AspectMemberships", function() {
beforeEach(function() {
this.models = [factory.aspectMembershipAttrs(), factory.aspectMembershipAttrs(), factory.aspectMembershipAttrs()];
this.collection = new app.collections.AspectMemberships(this.models);
});
describe("#findByAspectId", function() {
it("finds a model in collection", function() {
var model = this.collection.findByAspectId(this.models[1].aspect.id);
expect(model.get("id")).toEqual(this.models[1].id);
});
it("returns undefined when nothing found", function() {
expect(this.collection.findByAspectId(factory.id.next())).toEqual(undefined);
});
});
});

View file

@ -8,6 +8,13 @@ describe("app.models.Contact", function() {
});
});
describe("initialize", function() {
it("sets person object with contact reference", function() {
expect(this.contact.person.get("name")).toEqual("aaa");
expect(this.contact.person.contact).toEqual(this.contact);
});
});
describe("inAspect", function(){
it("returns true if the contact has been added to the aspect", function(){
expect(this.contact.inAspect(this.aspect.id)).toBeTruthy();

View file

@ -7,6 +7,15 @@ describe("app.models.Person", function() {
this.blockedContact = factory.person({relationship: "blocked", block: {id: 1}});
});
describe("initialize", function() {
it("sets contact object with person reference", function() {
var contact = {id: factory.id.next()};
var person = factory.person({contact: contact});
expect(person.contact.get("id")).toEqual(contact.id);
expect(person.contact.person).toEqual(person);
});
});
context("#isSharing", function() {
it("indicates if the person is sharing", function() {
expect(this.mutualContact.isSharing()).toBeTruthy();

View file

@ -0,0 +1,15 @@
describe("app.pages.GettingStarted", function() {
beforeEach(function() {
spec.loadFixture("getting_started");
app.aspects = new app.collections.Aspects([factory.aspect()]);
this.view = new app.pages.GettingStarted({
inviter: factory.person()
});
});
it("renders aspect membership dropdown", function() {
this.view.render();
expect($("ul.dropdown-menu.aspect_membership").length).toEqual(1);
});
});

View file

@ -88,6 +88,13 @@ describe('app.Router', function () {
});
});
describe("gettingStarted", function() {
it("renders app.pages.GettingStarted", function() {
app.router.navigate("/getting_started", {trigger: true});
expect(app.page.$el.selector).toEqual("#hello-there");
});
});
describe("_initializeStreamView", function() {
beforeEach(function() {
delete app.page;

View file

@ -3,7 +3,7 @@ describe("app.views.AspectCreate", function() {
app.events.off("aspect:create");
});
context("without a person id", function() {
context("without a person", function() {
beforeEach(function() {
this.view = new app.views.AspectCreate();
});
@ -50,6 +50,7 @@ describe("app.views.AspectCreate", function() {
this.view.render();
this.view.$el.append($("<div id='flash-container'/>"));
app.flashMessages = new app.views.FlashMessages({ el: this.view.$("#flash-container") });
app.aspects = new app.collections.Aspects();
});
it("should send the correct name to the server", function() {
@ -134,9 +135,10 @@ describe("app.views.AspectCreate", function() {
});
});
context("with a person id", function() {
context("with a person", function() {
beforeEach(function() {
this.view = new app.views.AspectCreate({personId: "42"});
var person = new app.models.Person({id: "42"});
this.view = new app.views.AspectCreate({person: person});
});
describe("#render", function() {
@ -161,6 +163,7 @@ describe("app.views.AspectCreate", function() {
describe("#createAspect", function() {
beforeEach(function() {
this.view.render();
app.aspects = new app.collections.Aspects();
});
it("should send the correct name to the server", function() {
@ -193,6 +196,36 @@ describe("app.views.AspectCreate", function() {
expect(obj.person_id).toBe("42");
/* jshint camelcase: true */
});
it("should ensure that events order is fine", function() {
spyOn(this.view, "ensureEventsOrder").and.callThrough();
this.view.$(".modal").removeClass("fade");
this.view.$(".modal").modal("toggle");
this.view.createAspect();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: JSON.stringify({id: 1337, name: "new name"})
});
expect(this.view.ensureEventsOrder.calls.count()).toBe(2);
});
it("should ensure that events order is fine after failure", function() {
spyOn(this.view, "ensureEventsOrder").and.callThrough();
this.view.$(".modal").removeClass("fade");
this.view.$(".modal").modal("toggle");
this.view.createAspect();
jasmine.Ajax.requests.mostRecent().respondWith({status: 422});
expect(this.view.ensureEventsOrder.calls.count()).toBe(1);
this.view.$(".modal").removeClass("fade");
this.view.$(".modal").modal("toggle");
this.view.createAspect();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: JSON.stringify({id: 1337, name: "new name"})
});
expect(this.view.ensureEventsOrder.calls.count()).toBe(3);
});
});
});
});

View file

@ -3,45 +3,53 @@ describe("app.views.AspectMembership", function(){
var resp_fail = {status: 400};
beforeEach(function() {
// mock a dummy aspect dropdown
spec.loadFixture("aspect_membership_dropdown");
this.view = new app.views.AspectMembership({el: $('.aspect_membership_dropdown')});
this.view.$el.append($("<div id='flash-container'/>"));
app.flashMessages = new app.views.FlashMessages({ el: this.view.$("#flash-container") });
this.personId = $(".dropdown-menu").data("person_id");
this.personName = $(".dropdown-menu").data("person-short-name");
var contact = factory.contact();
this.person = contact.person;
this.personName = this.person.get("name");
var aspectAttrs = contact.aspectMemberships.at(0).get("aspect");
app.aspects = new app.collections.Aspects([factory.aspect(aspectAttrs), factory.aspect()]);
this.view = new app.views.AspectMembership({person: this.person});
this.view.render();
spec.content().append($("<div id='flash-container'/>"));
app.flashMessages = new app.views.FlashMessages({el: spec.content().find("#flash-container")});
});
context('adding to aspects', function() {
beforeEach(function() {
this.newAspect = $('li:not(.selected)');
this.newAspect = this.view.$("li:not(.selected)");
this.newAspectId = this.newAspect.data('aspect_id');
});
it('marks the aspect as selected', function() {
this.newAspect.trigger('click');
jasmine.Ajax.requests.mostRecent().respondWith(success);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: JSON.stringify({
id: factory.id.next(),
aspect: app.aspects.at(1).attributes
})
});
expect(this.newAspect.attr('class')).toContain('selected');
expect(this.view.$("li[data-aspect_id=" + this.newAspectId + "]").attr("class")).toContain("selected");
});
it('displays flash message when added to first aspect', function() {
spec.content().find('li').removeClass('selected');
this.view.$("li").removeClass("selected");
this.newAspect.trigger('click');
jasmine.Ajax.requests.mostRecent().respondWith(success);
expect(this.view.$(".flash-message")).toBeSuccessFlashMessage(
expect(spec.content().find(".flash-message")).toBeSuccessFlashMessage(
Diaspora.I18n.t("aspect_dropdown.started_sharing_with", {name: this.personName})
);
});
it("triggers aspect_membership:create", function() {
spyOn(app.events, "trigger");
spec.content().find("li").removeClass("selected");
this.view.$("li").removeClass("selected");
this.newAspect.trigger("click");
jasmine.Ajax.requests.mostRecent().respondWith(success);
expect(app.events.trigger).toHaveBeenCalledWith("aspect_membership:create", {
membership: {aspectId: this.newAspectId, personId: this.personId},
membership: {aspectId: this.newAspectId, personId: this.person.id},
startSharing: true
});
});
@ -50,7 +58,7 @@ describe("app.views.AspectMembership", function(){
this.newAspect.trigger('click');
jasmine.Ajax.requests.mostRecent().respondWith(resp_fail);
expect(this.view.$(".flash-message")).toBeErrorFlashMessage(
expect(spec.content().find(".flash-message")).toBeErrorFlashMessage(
Diaspora.I18n.t("aspect_dropdown.error", {name: this.personName})
);
});
@ -58,34 +66,32 @@ describe("app.views.AspectMembership", function(){
context('removing from aspects', function(){
beforeEach(function() {
this.oldAspect = $('li.selected').first();
this.oldMembershipId = this.oldAspect.data('membership_id');
this.oldAspect = this.view.$("li.selected").first();
this.oldAspectId = this.oldAspect.data("aspect_id");
});
it('marks the aspect as unselected', function(){
this.oldAspect.trigger('click');
jasmine.Ajax.requests.mostRecent().respondWith(success);
expect(this.oldAspect.attr('class')).not.toContain('selected');
expect(this.view.$("li[data-aspect_id=" + this.oldAspectId + "]").attr("class")).not.toContain("selected");
});
it('displays a flash message when removed from last aspect', function() {
spec.content().find('li.selected:last').removeClass('selected');
this.oldAspect.trigger('click');
jasmine.Ajax.requests.mostRecent().respondWith(success);
expect(this.view.$(".flash-message")).toBeSuccessFlashMessage(
expect(spec.content().find(".flash-message")).toBeSuccessFlashMessage(
Diaspora.I18n.t("aspect_dropdown.stopped_sharing_with", {name: this.personName})
);
});
it("triggers aspect_membership:destroy", function() {
spyOn(app.events, "trigger");
spec.content().find("li.selected:last").removeClass("selected");
this.oldAspect.trigger("click");
jasmine.Ajax.requests.mostRecent().respondWith(success);
expect(app.events.trigger).toHaveBeenCalledWith("aspect_membership:destroy", {
membership: {aspectId: this.oldAspect.data("aspect_id"), personId: this.personId},
membership: {aspectId: this.oldAspectId, personId: this.person.id},
stopSharing: true
});
});
@ -94,29 +100,9 @@ describe("app.views.AspectMembership", function(){
this.oldAspect.trigger('click');
jasmine.Ajax.requests.mostRecent().respondWith(resp_fail);
expect(this.view.$(".flash-message")).toBeErrorFlashMessage(
expect(spec.content().find(".flash-message")).toBeErrorFlashMessage(
Diaspora.I18n.t("aspect_dropdown.error_remove", {name: this.personName})
);
});
});
context('button summary text', function() {
beforeEach(function() {
this.Aspect = $('li:eq(0)');
});
it('calls "_toggleCheckbox"', function() {
spyOn(this.view, "_toggleCheckbox");
this.view.updateSummary(this.Aspect);
expect(this.view._toggleCheckbox).toHaveBeenCalledWith(this.Aspect);
});
it('calls "_updateButton"', function() {
spyOn(this.view, "_updateButton");
this.view.updateSummary(this.Aspect);
expect(this.view._updateButton).toHaveBeenCalledWith("btn-success");
});
});
});

View file

@ -21,6 +21,16 @@ var factory = {
return _.extend(defaultAttrs, overrides);
},
aspectMembershipAttrs: function(overrides) {
var id = this.id.next();
var defaultAttrs = {
"id": id,
"aspect": factory.aspectAttrs()
};
return _.extend(defaultAttrs, overrides);
},
comment : function(overrides) {
var defaultAttrs = {
"created_at" : "2012-01-04T00:55:30Z",
@ -33,6 +43,18 @@ var factory = {
return new app.models.Comment(_.extend(defaultAttrs, overrides));
},
contact: function(overrides) {
var person = factory.personAttrs();
var attrs = {
"id": this.id.next(),
"person_id": person.id,
"person": person,
"aspect_memberships": factory.aspectMembershipAttrs()
};
return new app.models.Contact(_.extend(attrs, overrides));
},
user : function(overrides) {
return new app.models.User(factory.userAttrs(overrides));
},
@ -189,6 +211,7 @@ var factory = {
aspectAttrs: function(overrides) {
var names = ['Work','School','Family','Friends','Just following','People','Interesting'];
var defaultAttrs = {
id: this.id.next(),
name: names[Math.floor(Math.random()*names.length)]+' '+Math.floor(Math.random()*100),
selected: false
};

View file

@ -25,5 +25,13 @@ describe ContactPresenter do
it "has relationship information" do
expect(@presenter.full_hash_with_person[:person][:relationship]).to be(:mutual)
end
it "doesn't have redundant contact object in person hash" do
expect(@presenter.full_hash_with_person[:person]).not_to have_key(:contact)
end
it "has avatar links in person profile hash" do
expect(@presenter.full_hash_with_person[:person][:profile]).to have_key(:avatar)
end
end
end

30
spec/support/gon.rb Normal file
View file

@ -0,0 +1,30 @@
shared_context :gon do
let(:gon) { RequestStore.store[:gon].gon }
end
module HelperMethods
def expect_aspects
expect(gon["user"].aspects).not_to be_nil
expect(gon["user"].aspects.length).not_to be_nil
end
def expect_memberships(memberships)
expect(memberships).not_to be_nil
expect(memberships.length).not_to be_nil
end
def expect_contact(preload_key)
expect(gon["preloads"][preload_key][:contact]).not_to be_falsy
expect_memberships(gon["preloads"][preload_key][:contact][:aspect_memberships])
end
def expect_gon_preloads_for_aspect_membership_dropdown(preload_key, sharing)
expect(gon["preloads"][preload_key]).not_to be_nil
if sharing
expect_contact(preload_key)
else
expect(gon["preloads"][preload_key][:contact]).to be_falsy
end
expect_aspects
end
end