[issue #347] Added backbone. Changed collaborators page.

This commit is contained in:
George Vinogradov 2012-04-09 21:11:39 +04:00
parent 851fd9d722
commit 71c8efabf9
25 changed files with 557 additions and 37 deletions

View File

@ -44,6 +44,8 @@ gem 'rails3-jquery-autocomplete', '~> 1.0.6'
gem 'will_paginate', '~> 3.0.3'
gem 'meta-tags', '~> 1.2.5', :require => 'meta_tags'
gem "haml-rails", '~> 0.3.4'
gem 'ruby-haml-js'
gem 'rails-backbone'
gem 'jquery-rails', '~> 2.0.1'
group :assets do

View File

@ -110,6 +110,7 @@ GEM
warden (~> 1.1.1)
diff-display (0.0.1)
diff-lcs (1.1.3)
ejs (1.0.0)
erubis (2.7.0)
eventmachine (0.12.10)
eventmachine (0.12.10-java)
@ -218,6 +219,10 @@ GEM
activesupport (= 3.2.2)
bundler (~> 1.0)
railties (= 3.2.2)
rails-backbone (0.7.1)
coffee-script (~> 2.2.0)
ejs (~> 1.0.0)
railties (>= 3.1.0)
rails3-generators (0.17.4)
railties (>= 3.0.0)
rails3-jquery-autocomplete (1.0.6)
@ -249,6 +254,9 @@ GEM
activesupport (>= 3.0)
railties (>= 3.0)
rspec (~> 2.9.0)
ruby-haml-js (0.0.2)
execjs
sprockets (>= 2.0.0)
ruby-openid (2.1.8)
russian (0.6.0)
i18n (>= 0.5.0)
@ -351,6 +359,7 @@ DEPENDENCIES
paperclip (~> 2.7.0)
pg (~> 0.13.2)
rails (= 3.2.2)
rails-backbone
rails3-generators
rails3-jquery-autocomplete (~> 1.0.6)
rdiscount
@ -358,6 +367,7 @@ DEPENDENCIES
redhillonrails_core!
rr (~> 1.0.4)
rspec-rails (~> 2.9.0)
ruby-haml-js
russian (~> 0.6.0)
sass-rails (~> 3.2.5)
shotgun

View File

@ -6,6 +6,12 @@
//= require jquery.dataTables_ext
//= require_tree ./design
//= require_tree ./extra
//= require underscore
//= require backbone
//= require backbone_rails_sync
//= require backbone_datalink
//= require backbone/rosa
//= require_self
function disableNotifierCbx(global_cbx) {

View File

@ -0,0 +1 @@
Rosa.bootstrapedData.ROLES = <%= Relation::ROLES.to_json %>;

View File

@ -0,0 +1,52 @@
Rosa.Models.Collaborator = Backbone.Model.extend({
paramRoot: 'collaborator',
defaults: {
id: null,
name: null,
role: null,
removed: false
},
changeRole: function(r) {
this._prevState = this.get('role');
this.save({role: r},
{wait: true,
error: function(model, response) {
model.set({role: model._prevState});
}
});
return this;
},
toggleRemoved: function() {
if (this.get('removed') === false) {
this.set({removed: true});
} else {
this.set({removed: false});
}
return this;
}
});
Rosa.Collections.CollaboratorsCollection = Backbone.Collection.extend({
model: Rosa.Models.Collaborator,
initialize: function() {
this.url = window.location.pathname;
this.on('change:removed add', this.sort, this);
},
comparator: function(m) {
return ((m.get('removed') === true) ? '0' : '1') + m.get('name');
},
removeMarked: function(params) {
var marked = this.where({removed: true});
if (params['type'] !== undefined) {
marked = marked.where({type: params['type']});
}
marked.forEach(function(el) {
el.destroy({wait: true, silent: true});
});
// this.trigger('reset');
}
});

View File

@ -0,0 +1,15 @@
//= require_self
//= require ./additionals
//= require_tree ./templates
//= require_tree ./models
//= require_tree ./views
//= require_tree ./routers
window.Rosa = {
Models: {},
Collections: {},
Routers: {},
Views: {},
bootstrapedData: {}
}

View File

@ -0,0 +1,12 @@
Rosa.Routers.CollaboratorsRouter = Backbone.Router.extend({
routes: {},
initialize: function() {
this.collaboratorsCollection = new Rosa.Collections.CollaboratorsCollection(Rosa.bootstrapedData.collaborators);
this.usersView = new Rosa.Views.CollaboratorsView({collection_type: 'user', collection: this.collaboratorsCollection});
this.groupsView = new Rosa.Views.CollaboratorsView({collection_type: 'group', collection: this.collaboratorsCollection});
this.usersView.render();
this.groupsView.render();
}
});

View File

@ -0,0 +1,38 @@
%td
%span#niceCheckbox1.nicecheck-main{ style: "background-position: 0px 0px; "}
- if (removed === true) {
%input{ type: 'checkbox', value: 1, id: type + '_remove_' + id + '_', name: type + '_remove[' + id + '][]', checked: 'checked' }
- } else {
%input{ type: 'checkbox', value: 1, id: type + '_remove_' + id + '_', name: type + '_remove[' + id + '][]' }
- }
%td
- if (type === 'user') {
.img
%img{src: avatar, alt: avatar}
- }
.forimg
%a{href: collaborator_link}
= name
- var ROLES = Rosa.bootstrapedData.ROLES;
- for (var i = 0; i < ROLES.length; i++) {
%td
.radio
- var radio_id = type + '_' + id + '_' + ROLES[i];
- var radio_type = type + '[' + id + ']';
- if (ROLES[i] === role) {
- if ( removed ) {
%input.niceRadio{type: 'radio', value: ROLES[i], id: radio_id, name: radio_type, disabled: 'disabled', checked: 'checked'}
- } else {
%input.niceRadio{type: 'radio', value: ROLES[i], id: radio_id, name: radio_type, checked: 'checked'}
- };
- } else {
- if ( removed ) {
%input.niceRadio{type: 'radio', value: ROLES[i], id: radio_id, name: radio_type, disabled: 'disabled' }
- } else {
%input.niceRadio{type: 'radio', value: ROLES[i], id: radio_id, name: radio_type }
- };
- }
.forradio
%label{for: radio_id}
= ROLES[i]
- }

View File

@ -0,0 +1,39 @@
Rosa.Views.CollaboratorView = Backbone.View.extend({
template: JST['backbone/templates/collaborators/collaborator'],
tagName: 'tr',
events: {
'change input[type="radio"]': 'changeRole',
'change input[type="checkbox"]': 'toggleRemoved'
},
initialize: function() {
this.$el.attr('id', 'admin-table-members-row' + this.options.model.get('id') + this.options.model.get('type'));
this.model.on('change', this.render, this);
this.model.on('destroy', this.hide, this);
},
render: function() {
if (this.model.get('removed')) {
this.$el.addClass('removed');
} else {
this.$el.removeClass('removed');
};
this.$el.html(this.template(this.model.toJSON()));
return this;
},
changeRole: function(e) {
this.model.changeRole(e.target.value);
},
toggleRemoved: function(e) {
//var mod = this.model
//this.$el.addClass('removed').fadeOut(1000, function() { mod.toggleRemoved() });
this.model.toggleRemoved();
},
hide: function() {
this.remove();
}
});

View File

@ -0,0 +1,38 @@
Rosa.Views.CollaboratorsView = Backbone.View.extend({
initialize: function() {
this._type = this.options['collection_type'];
this.setupDeleter();
this.$el = $('#' + this._type + 's_collaborators > tbody');
this.collection.on('add', this.addOne, this);
this.collection.on('reset', this.render, this);
},
addOne: function(collaborator) {
if (collaborator.get('type') === this._type) {
var cView = new Rosa.Views.CollaboratorView({ model: collaborator });
this.$el.append(cView.render().el);
};
},
render: function() {
this.$el.empty();
var col = new Rosa.Collections.CollaboratorsCollection(this.collection.where({type: this._type}));
col.forEach(this.addOne, this);
if (col.where({ removed: true }).length > 0) {
this._$deleter.show();
} else {
this._$deleter.hide();
}
return this;
},
setupDeleter: function() {
this._$deleter = $('#' + this._type + 's_deleter');
this._$deleter.on('click.deleter', '', {context: this}, this.deleterClick);
this._$deleter.attr('title', 'Remove selected rows');
},
deleterClick: function(e) {
e.data['context'].collection.removeMarked({type: this._type});
}
});

View File

@ -76,7 +76,9 @@ function changeRadioStart(el) {
}
el.next().bind("mousedown", function(e) {
changeRadio($(this));
if (e.which === 0) {
changeRadio($(this));
};
$(this).find("input:radio").change();
});
if($.browser.msie) {

View File

@ -1,9 +1,13 @@
// PUT custom styles here ONLY
span.error {
span.error, .hidden {
display: none;
}
.centered {
text-align: center;
}
a#manage-labels {
margin-bottom: 10px;
}
@ -456,10 +460,12 @@ table.tablesorter tr td.buttons {
text-align: center;
}
table.tablesorter tr td.buttons a span.delete {
table.tablesorter tr td.buttons a span.delete,
span.delete {
background: image-url('x.png') no-repeat 0 0 transparent;
width: 12px;
display: inline-block;
cursor: pointer;
}
#fork-and-edit {display:block;}
@ -764,3 +770,7 @@ div.tos_sidebar ul li a {
padding-top: 5px;
text-decoration: none;
}
table.tablesorter tbody tr.removed td {
background-color: #FFECEC;
}

View File

@ -1,5 +1,7 @@
# -*- encoding : utf-8 -*-
class CollaboratorsController < ApplicationController
respond_to :html, :json
before_filter :authenticate_user!
before_filter :find_project
@ -10,7 +12,9 @@ class CollaboratorsController < ApplicationController
before_filter :authorize_collaborators
def index
redirect_to edit_project_collaborators_path(@project)
# redirect_to edit_project_collaborators_path(@project)
@collaborators = Collaborator.find_by_project(@project)
respond_with @collaborators
end
def show
@ -30,38 +34,44 @@ class CollaboratorsController < ApplicationController
end
def update
params['user'].keys.each { |user_id|
role = params['user'][user_id]
if relation = @project.relations.find_by_object_id_and_object_type(user_id, 'User')
unless @project.owner_type == 'User' and @project.owner_id.to_i == user_id.to_i
relation.update_attribute(:role, role)
end
else
relation = @project.relations.build(:object_id => user_id, :object_type => 'User', :role => role)
relation.save
end
} if params['user']
params['group'].keys.each { |group_id|
role = params['group'][group_id]
if relation = @project.relations.find_by_object_id_and_object_type(group_id, 'Group')
unless @project.owner_type == 'Group' and @project.owner_id.to_i == group_id.to_i
relation.update_attribute(:role, role)
end
else
relation = @project.relations.build(:object_id => user_id, :object_type => 'Group', :role => role)
relation.save
end
} if params['group']
if @project.save
flash[:notice] = t("flash.collaborators.successfully_changed")
@c = Collaborator.new(params[:collaborator])
if @c.save
respond_with @c
else
flash[:error] = t("flash.collaborators.error_in_changing")
raise
end
redirect_to edit_project_collaborators_path(@project)
# params['user'].keys.each { |user_id|
# role = params['user'][user_id]
#
# if relation = @project.relations.find_by_object_id_and_object_type(user_id, 'User')
# unless @project.owner_type == 'User' and @project.owner_id.to_i == user_id.to_i
# relation.update_attribute(:role, role)
# end
# else
# relation = @project.relations.build(:object_id => user_id, :object_type => 'User', :role => role)
# relation.save
# end
# } if params['user']
#
# params['group'].keys.each { |group_id|
# role = params['group'][group_id]
# if relation = @project.relations.find_by_object_id_and_object_type(group_id, 'Group')
# unless @project.owner_type == 'Group' and @project.owner_id.to_i == group_id.to_i
# relation.update_attribute(:role, role)
# end
# else
# relation = @project.relations.build(:object_id => user_id, :object_type => 'Group', :role => role)
# relation.save
# end
# } if params['group']
#
# if @project.save
# flash[:notice] = t("flash.collaborators.successfully_changed")
# else
# flash[:error] = t("flash.collaborators.error_in_changing")
# end
#
# redirect_to edit_project_collaborators_path(@project)
end
def remove
@ -88,6 +98,12 @@ class CollaboratorsController < ApplicationController
redirect_to edit_project_collaborators_path(@project) + "##{params['user_remove'].present? ? 'users' : 'groups'}"
end
def destroy
@cb = Collaborator.find_by_project(@project, :id => params[:id])
@cb.destroy if @cb
respond_with @cb
end
def add
# TODO: Here is used Chelyabinsk method to display Flash messages.
@ -120,7 +136,7 @@ class CollaboratorsController < ApplicationController
end
# if add an anchor, adding will be more pleasant, but flash message wouldn't be shown.
redirect_to edit_project_collaborators_path(@project) # + "##{(params['member_id'].present?) ? 'users' : 'groups'}"
redirect_to project_collaborators_path(@project) # + "##{(params['member_id'].present?) ? 'users' : 'groups'}"
end
protected

View File

@ -30,4 +30,8 @@ module ProjectsHelper
def alone_member?(project)
Relation.by_target(project).by_object(current_user).size > 0
end
def participant_path(participant)
participant.kind_of?(User) ? user_path(participant) : group_path(participant)
end
end

160
app/models/collaborator.rb Normal file
View File

@ -0,0 +1,160 @@
# -*- encoding : utf-8 -*-
class Collaborator
include ActiveModel::Conversion
include ActiveModel::Validations
include ActiveModel::Serializers::JSON
include ActiveModel::MassAssignmentSecurity
attr_accessor :role, :actor, :project
attr_reader :id, :type, :name, :project_id
attr_accessible :role
delegate :new_record?, :to => :relation
class << self
def find_by_project(project, opts = {})
(id, type) = if opts[:id].present?
if opts[:type].present?
[opts[:id], opts[:type]]
else
opts[:id].split('-', 2)
end
else
[nil, nil]
end
puts id
puts type
if id.present? and type.present?
rel = project.relations.where(:object_id => id, :object_type => type.classify).first
puts rel.inspect
res = from_relation(project.relations.where(:object_id => id, :object_type => type.classify).first)
else
res = []
project.relations.each do |r|
res << from_relation(r) unless project.owner_id == r.object_id and project.owner_type == r.object_type
end
end
return res
end
def from_relation(relation)
return self.new(:relation => relation, :id => relation.object_id,
:type => relation.object_type, :project_id => relation.target_id)
end
end
def initialize(args = {})
args.to_options!
acc_options = args.select{ |(k, v)| k.in? [:actor, :project] }
acc_options.each_pair do |name, value|
send("#{name}=", value)
end
if @project.nil? and args[:project_id].present?
@project = Project.find(args[:project_id])
end
if @actor.nil? and args[:type].present? and args[:id].present?
@actor = args[:type].classify.constantize.find(args[:id].to_s.split('-', 2).first.to_i) rescue nil
end
if args[:relation]
@relation = args[:relation]
else
setup_relation
end
@relation.role = args[:role] if args[:role]
end
def update_attributes(attributes, options = {})
sanitize_for_mass_assignment(attributes, options[:as]).each_pair do |k, v|
send("#{k}=", v)
end
save
end
def actor=(model)
@actor = model
setup_relation
end
def project=(model)
@project = model
setup_relation
end
def id
@actor.try(:id)
end
def type
@actor.class.to_s.underscore
end
def name
if @actor.present?
@actor.instance_of?(User) ? "#{@actor.uname} (#{@actor.name})" : @actor.uname
else
nil
end
end
def project_id
@project.try(:id)
end
def role
@relation.role
end
def role=(arg)
@relation.role = arg
end
def save
@relation.try(:save)
end
def save!
@relation.try(:save!)
end
def destroy
@relation.try(:destroy)
end
def attributes
%w{ id type name project_id role}.inject({}) do |h, e|
h.merge(e => send(e))
end
end
def persisted?
false
end
protected
def relation
setup_relation
@relation
end
def setup_relation
if @actor.present? and @project.present?
@relation = Relation.by_object(@actor).by_target(@project).limit(1).first
@relation ||= Relation.new(:object_id => @actor.id, :object_type => @actor.class.to_s.underscore,
:target_id => @project.id, :target_type => 'Project')
else
@relation = Relation.new
@relation.object = @actor
@relation.target = @project
end
end
end
Collaborator.include_root_in_json = false

View File

@ -0,0 +1,5 @@
json.id collaborator.id.to_s + '-' + collaborator.type
json.name collaborator.name
json.type collaborator.type
json.project_id collaborator.project_id
json.role collaborator.role

View File

@ -0,0 +1,9 @@
json.array!(collaborators) do |json, cb|
json.id cb.id.to_s + '-' + cb.type
json.name cb.name
json.collaborator_link participant_path(cb.actor)
json.avatar avatar_url(cb.actor) if cb.actor.kind_of?(User)
json.type cb.type
json.project_id cb.project_id
json.role cb.role
end

View File

@ -0,0 +1,100 @@
-set_meta_tags :title => [title_object(@project), t('layout.projects.members')]
= render :partial => 'projects/sidebar'
= render :partial => 'projects/submenu'
%a{:name => 'users'}
%h3= t("layout.users.list_header")
= form_tag add_project_collaborators_path(@project) do
.admin-search
= autocomplete_field_tag 'member_id', params[:member_id], autocomplete_user_uname_users_path, :id_element => '#member_id_field'
.admin-role
.lineForm
= select_tag 'role', options_for_collaborators_roles_select
= hidden_field_tag 'member_id', nil, :id => 'member_id_field'
= submit_tag t("layout.add"), :class => 'button'
.both
= form_tag project_collaborators_path(@project), :id => 'members_form', :delete_url => remove_project_collaborators_path(@project) do
= hidden_field_tag "_method", "post"
%table#users_collaborators.tablesorter{:cellpadding => "0", :cellspacing => "0"}
%thead
%tr
%th.centered
%span#users_deleter.hidden
%span.delete &nbsp; 
%th
= t("layout.collaborators.members")
%th{:colspan => "3"}
= t("layout.collaborators.roles")
%tbody
-# @users.each_with_index do |user, num|
%tr{:id => "admin-table-members-row#{num}"}
%td
%span#niceCheckbox1.niceCheck-main{ :style => "background-position: 0px 0px; "}
= check_box_tag "user_remove[#{user.id}][]"
%td
.img
= image_tag avatar_url(user)
.forimg= link_to "#{user.uname} (#{user.name})", user_path(user)
- Relation::ROLES.each_with_index do |role, i|
%td
.radio
= radio_button_tag "user[#{user.id}]", role, ((@project.relations.exists? :object_id => user.id, :object_type => 'User', :role => role) ? :checked : nil), :class => 'niceRadio'
.forradio= t("layout.collaborators.role_names.#{ role }")
=# link_to_function t("layout.delete_selected"), "deleteAdminMember();", :class => 'button'
=# link_to_function t("layout.save"), "saveAdminMember();", :class => 'button right_floated'
.both
%br
.hr.bottom
.both
%a{:name => 'groups'}
%h3= t("layout.groups.list_header")
= form_tag add_project_collaborators_path(@project) do
.admin-search
= autocomplete_field_tag 'group_id', params[:group_id], autocomplete_group_uname_groups_path, :id_element => '#group_id_field'
.admin-role
.lineForm
= select_tag 'role', options_for_collaborators_roles_select, :id => 'group_role'
= hidden_field_tag 'group_id', nil, :id => 'group_id_field'
= submit_tag t("layout.add"), :class => 'button'
.both
= form_tag project_collaborators_path(@project), :id => 'groups_form', :delete_url => remove_project_collaborators_path(@project) do
= hidden_field_tag "_method", "post", :id => 'groups_method'
%table#groups_collaborators.tablesorter{:cellpadding => "0", :cellspacing => "0"}
%thead
%tr
%th.centered
%span#groups_deleter.hidden
%span.delete &nbsp;
%th
= t("layout.collaborators.members")
%th{:colspan => "3"}
= t("layout.collaborators.roles")
%tbody
-# @groups.each_with_index do |group, num|
%tr{:id => "admin-table-members-row#{num + @users.size + 1}"}
%td
%span#niceCheckbox1.niceCheck-main{ :style => "background-position: 0px 0px; "}
= check_box_tag "group_remove[#{group.id}][]"
%td
.forimg= link_to "#{group.uname}", group_path(group)
- Relation::ROLES.each_with_index do |role, i|
%td
.radio
= radio_button_tag "group[#{group.id}]", role, ((@project.relations.exists? :object_id => group.id, :object_type => 'Group', :role => role) ? :checked : nil), :class => 'niceRadio'
.forradio= t("layout.collaborators.role_names.#{ role }")
=# link_to_function t("layout.delete_selected"), "deleteAdminGroup();", :class => 'button'
=# link_to_function t("layout.save"), "saveAdminGroup();", :class => 'button right_floated'
.both
.both
:javascript
$(function() {
Rosa.bootstrapedData.collaborators = #{ render :partial => 'collaborators.json.jbuilder', :locals => {:collaborators => @collaborators } };
var r = new Rosa.Routers.CollaboratorsRouter();
});

View File

@ -0,0 +1 @@
json.partial! 'collaborators', :collaborators => @collaborators

View File

@ -33,7 +33,7 @@ Rosa::Application.configure do
config.assets.compress = false
# Expands the lines which load the assets
config.assets.debug = false
config.assets.debug = true
# Raise exception on mass assignment protection for Active Record models
config.active_record.mass_assignment_sanitizer = :strict

View File

@ -110,7 +110,7 @@ Rosa::Application.routes.draw do
resources :build_lists, :only => [:index, :new, :create] do
collection { post :search }
end
resources :collaborators, :only => [:index, :edit, :update, :add] do
resources :collaborators, :only => [:index, :edit, :update, :add, :destroy] do
collection do
get :edit
post :update