Merge pull request #388 from warpc/347-javascript_refactoring
[issue #347] Union users and groups into one table in collaborators page. Use backbone(JS framework) for it. Improve UI for collaborators page (AJAX).
This commit is contained in:
commit
24922cc43c
2
Gemfile
2
Gemfile
|
@ -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
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -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
|
||||
|
|
|
@ -4,8 +4,15 @@
|
|||
//= require autocomplete-rails
|
||||
//= require vendor
|
||||
//= require jquery.dataTables_ext
|
||||
//= require_tree ./lib
|
||||
//= 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) {
|
||||
|
@ -19,6 +26,10 @@ function disableNotifierCbx(global_cbx) {
|
|||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// setup all placeholders on page
|
||||
$('input[placeholder], textarea[placeholder]').placeholder();
|
||||
|
||||
|
||||
$('input.user_role_chbx').click(function() {
|
||||
var current = $(this);
|
||||
current.parent().find('input.user_role_chbx').each(function(i,el) {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Rosa.bootstrapedData.ROLES = <%= Relation::ROLES.to_json %>;
|
|
@ -0,0 +1,97 @@
|
|||
Rosa.Models.Collaborator = Backbone.Model.extend({
|
||||
paramRoot: 'collaborator',
|
||||
|
||||
defaults: {
|
||||
id: null,
|
||||
actor_id: null,
|
||||
actor_name: null,
|
||||
actor_type: null,
|
||||
avatar: null,
|
||||
actor_path: null,
|
||||
project_id: null,
|
||||
role: null,
|
||||
removed: false
|
||||
},
|
||||
|
||||
changeRole: function(r) {
|
||||
var self = this;
|
||||
this._prevState = this.get('role');
|
||||
this.save({role: r},
|
||||
{wait: true,
|
||||
success: function(model, response) {
|
||||
self.trigger('sync_success');
|
||||
},
|
||||
error: function(model, response) {
|
||||
model.set({role: model._prevState});
|
||||
self.trigger('sync_failed');
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
setRole: function(r) {
|
||||
this.set({ role: r });
|
||||
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(coll, opts) {
|
||||
if (opts === undefined || opts['url'] === undefined) {
|
||||
this.url = window.location.pathname;
|
||||
} else {
|
||||
this.url = opts['url'];
|
||||
}
|
||||
this.on('change:removed change:id add', this.sort, this);
|
||||
},
|
||||
comparator: function(m) {
|
||||
var res = ''
|
||||
if (m.get('removed') === true) {
|
||||
res = 0;
|
||||
} else if (m.isNew()) {
|
||||
res = 1;
|
||||
} else { res = 2 }
|
||||
return res + m.get('actor_name');
|
||||
},
|
||||
|
||||
removeMarked: function() {
|
||||
var marked = this.where({removed: true});
|
||||
marked.forEach(function(el) {
|
||||
el.destroy({wait: true, silent: true});
|
||||
});
|
||||
},
|
||||
|
||||
saveAndAdd: function(model) {
|
||||
|
||||
model.urlRoot = this.url;
|
||||
var self = this;
|
||||
model.save({}, {
|
||||
wait: true,
|
||||
success: function(m) {
|
||||
self.add(m.toJSON());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
filterByName: function(term, options) {
|
||||
if (term == "") return this;
|
||||
console.log(term);
|
||||
|
||||
var pattern = new RegExp(term, "i");
|
||||
|
||||
return _(this.filter(function(data) {
|
||||
console.log(data.get("actor_name"));
|
||||
console.log(pattern.test(data.get("actor_name")));
|
||||
return pattern.test(data.get("actor_name"));
|
||||
}));
|
||||
}
|
||||
});
|
|
@ -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: {}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
Rosa.Routers.CollaboratorsRouter = Backbone.Router.extend({
|
||||
routes: {},
|
||||
|
||||
initialize: function() {
|
||||
this.collaboratorsCollection = new Rosa.Collections.CollaboratorsCollection(Rosa.bootstrapedData.collaborators, { url: window.location.pathname });
|
||||
this.searchCollection = new Rosa.Collections.CollaboratorsCollection(null, { url: window.location.pathname + '/find' });
|
||||
this.tableView = new Rosa.Views.CollaboratorsView({ collection: this.collaboratorsCollection });
|
||||
this.addView = new Rosa.Views.AddCollaboratorView({ collection: this.searchCollection });
|
||||
|
||||
this.addView.on('collaborator_prepared', this.collaboratorsCollection.saveAndAdd, this.collaboratorsCollection);
|
||||
|
||||
this.tableView.render();
|
||||
this.addView.render();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
%td
|
||||
%span#niceCheckbox1.nicecheck-main{ style: "background-position: 0px 0px; " }
|
||||
- if (removed === true) {
|
||||
%input{ type: 'checkbox', checked: 'checked' }
|
||||
- } else {
|
||||
%input{ type: 'checkbox' }
|
||||
- }
|
||||
|
||||
%td
|
||||
.img
|
||||
%img{ src: avatar, alt: 'avatar' }
|
||||
.forimg
|
||||
%a{ href: actor_path }
|
||||
= actor_name
|
||||
|
||||
- var ROLES = Rosa.bootstrapedData.ROLES;
|
||||
|
||||
- for (var i = 0; i < ROLES.length; i++) {
|
||||
%td
|
||||
.radio
|
||||
- var radio_id = id + '_' + ROLES[i];
|
||||
- var radio_type = 'role[' + 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]
|
||||
- }
|
|
@ -0,0 +1,7 @@
|
|||
%a
|
||||
.collaborator
|
||||
.img
|
||||
%img{width: '16px', src: avatar, alt: 'avatar'}
|
||||
.name
|
||||
= actor_name
|
||||
.both
|
|
@ -0,0 +1,3 @@
|
|||
%li.empty_result
|
||||
%span
|
||||
Nothing found
|
|
@ -0,0 +1,92 @@
|
|||
Rosa.Views.AddCollaboratorView = Backbone.View.extend({
|
||||
result_empty: JST['backbone/templates/shared/autocomplete_result_empty'],
|
||||
|
||||
events: {
|
||||
'click #add_collaborator_button': 'addCollaborator'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'getData', 'renderAll', 'onFocus', 'selectItem', 'addCollaborator');
|
||||
|
||||
this.$el = $('#add_collaborator_form');
|
||||
this.$_search_input = this.$('#collaborator_name');
|
||||
this.$_image = this.$('.admin-search.withimage img');
|
||||
this.$_role = this.$('#role');
|
||||
|
||||
this.ac = this.$_search_input.autocomplete({
|
||||
minLength: 1,
|
||||
source: this.getData,
|
||||
focus: this.onFocus,
|
||||
select: this.selectItem
|
||||
});
|
||||
this.ac.data("autocomplete")._renderItem = this.addOne;
|
||||
this.ac.data("autocomplete")._renderMenu = this.renderAll;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return this;
|
||||
},
|
||||
|
||||
getData: function(request, response) {
|
||||
var self = this;
|
||||
var res = this.collection.fetch({
|
||||
data: {term: request.term},
|
||||
wait: true,
|
||||
success: function(collection) {
|
||||
self.$_image.hide();
|
||||
if (collection.length !== 0) {
|
||||
response(collection.models);
|
||||
} else {
|
||||
response([{result_empty: true}]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addOne: function(ul, item) {
|
||||
var v = new Rosa.Views.SearchedCollaboratorView({ model: item });
|
||||
return v.render().$el.appendTo(ul);
|
||||
},
|
||||
|
||||
renderAll: function( ul, items ) {
|
||||
var self = this;
|
||||
if (items[0]['result_empty'] !== undefined && items[0]['result_empty'] === true) {
|
||||
ul.removeClass('has_results').append(this.result_empty());
|
||||
} else {
|
||||
ul.addClass('has_results');
|
||||
_.each( items, function( item ) {
|
||||
self.addOne( ul, item );
|
||||
});
|
||||
var factor = (items.length > 10) ? 10 : items.length;
|
||||
ul.height(ul.children('li').first() * factor);
|
||||
}
|
||||
},
|
||||
|
||||
onFocus: function( event, ui ) {
|
||||
this.$_search_input.val(ui.item.get('actor_name'));
|
||||
return false;
|
||||
},
|
||||
|
||||
selectItem: function( event, ui ) {
|
||||
var model = ui.item;
|
||||
this.$_image.attr('src', model.get('avatar')).show();
|
||||
|
||||
this.__selected_item = model;
|
||||
return false;
|
||||
},
|
||||
|
||||
addCollaborator: function(e) {
|
||||
e.preventDefault();
|
||||
var model = this.__selected_item;
|
||||
|
||||
if ( model !== undefined ) {
|
||||
model.setRole(this.$_role.val());
|
||||
this.trigger('collaborator_prepared', model);
|
||||
this.__selected_item = undefined;
|
||||
this.$_image.hide();
|
||||
this.$_search_input.val('');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
Rosa.Views.CollaboratorView = Backbone.View.extend({
|
||||
template: JST['backbone/templates/collaborators/collaborator'],
|
||||
tagName: 'tr',
|
||||
className: 'regular',
|
||||
|
||||
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('actor_type'));
|
||||
this.model.on('change', this.render, this);
|
||||
this.model.on('destroy', this.hide, this);
|
||||
this.model.on('sync_failed', this.syncError, this);
|
||||
this.model.on('sync_success', this.syncSuccess, 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.$('input[type="radio"]').attr('disabled', 'disabled');
|
||||
this.model.changeRole(e.target.value);
|
||||
},
|
||||
|
||||
toggleRemoved: function(e) {
|
||||
this.model.toggleRemoved();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.remove();
|
||||
},
|
||||
|
||||
syncError: function() {
|
||||
var self = this;
|
||||
this.$el.addClass('sync_error');
|
||||
this.$('td').animate({
|
||||
'background-color': '#FFFFFF'
|
||||
}, {
|
||||
duration: 800,
|
||||
easing: 'easeInCirc',
|
||||
complete: function() {
|
||||
self.$el.removeClass('sync_error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
syncSuccess: function() {
|
||||
var self = this;
|
||||
this.$el.addClass('sync_success');
|
||||
this.$('td').animate({
|
||||
'background-color': '#FFFFFF'
|
||||
}, {
|
||||
duration: 800,
|
||||
easing: 'easeInCirc',
|
||||
complete: function() {
|
||||
self.$el.removeClass('sync_success');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
Rosa.Views.CollaboratorsView = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'deleterClick', 'processFilter', 'addOne');
|
||||
this.setupDeleter();
|
||||
this.setupFilter();
|
||||
this.$el = $('#collaborators > tbody');
|
||||
this.collection.on('reset', this.render, this);
|
||||
this.collection.on('add', this.clearFilter, this);
|
||||
},
|
||||
|
||||
addOne: function(collaborator) {
|
||||
var cView = new Rosa.Views.CollaboratorView({ model: collaborator });
|
||||
this.$el.append(cView.render().$el);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.clearFilter();
|
||||
this.$el.empty();
|
||||
this.collection.forEach(this.addOne, this);
|
||||
this._$deleter.show();
|
||||
return this;
|
||||
},
|
||||
|
||||
renderList: function(list) {
|
||||
this.$el.empty();
|
||||
|
||||
list.each(this.addOne);
|
||||
return this;
|
||||
},
|
||||
|
||||
setupDeleter: function() {
|
||||
this._$deleter = $('#collaborators_deleter');
|
||||
this._$deleter.on('click.deleter', this.deleterClick);
|
||||
this._$deleter.attr('title', 'Remove selected rows');
|
||||
},
|
||||
|
||||
deleterClick: function() {
|
||||
this.collection.removeMarked();
|
||||
},
|
||||
|
||||
setupFilter: function() {
|
||||
this._$filter = $('#collaborators thead input[type="text"]');
|
||||
this._$filter.on('keyup', this.processFilter);
|
||||
this.clearFilter();
|
||||
},
|
||||
|
||||
clearFilter: function() {
|
||||
this._$filter.val('');
|
||||
},
|
||||
|
||||
processFilter: function() {
|
||||
var term = this._$filter.val();
|
||||
var list = this.collection.filterByName(term, {excludeRemoved: true});
|
||||
console.log(list);
|
||||
this.renderList(list);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
Rosa.Views.SearchedCollaboratorView = Backbone.View.extend({
|
||||
template: JST['backbone/templates/collaborators/searched_collaborator'],
|
||||
tagName: 'li',
|
||||
className: 'item',
|
||||
|
||||
render: function() {
|
||||
this.$el.empty();
|
||||
this.$el.data( "item.autocomplete", this.model )
|
||||
.append(this.template(this.model.toJSON()));
|
||||
return this;
|
||||
}
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Placeholder plugin for jQuery
|
||||
* ---
|
||||
* Copyright 2010, Daniel Stocks (http://webcloud.se)
|
||||
* Released under the MIT, BSD, and GPL Licenses.
|
||||
*/
|
||||
(function($) {
|
||||
function Placeholder(input) {
|
||||
this.input = input;
|
||||
if (input.attr('type') == 'password') {
|
||||
this.handlePassword();
|
||||
}
|
||||
// Prevent placeholder values from submitting
|
||||
$(input[0].form).submit(function() {
|
||||
if (input.hasClass('placeholder') && input[0].value == input.attr('placeholder')) {
|
||||
input[0].value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
Placeholder.prototype = {
|
||||
show : function(loading) {
|
||||
// FF and IE saves values when you refresh the page. If the user refreshes the page with
|
||||
// the placeholders showing they will be the default values and the input fields won't be empty.
|
||||
if (this.input[0].value === '' || (loading && this.valueIsPlaceholder())) {
|
||||
if (this.isPassword) {
|
||||
try {
|
||||
this.input[0].setAttribute('type', 'text');
|
||||
} catch (e) {
|
||||
this.input.before(this.fakePassword.show()).hide();
|
||||
}
|
||||
}
|
||||
this.input.addClass('placeholder');
|
||||
this.input[0].value = this.input.attr('placeholder');
|
||||
}
|
||||
},
|
||||
hide : function() {
|
||||
if (this.valueIsPlaceholder() && this.input.hasClass('placeholder')) {
|
||||
this.input.removeClass('placeholder');
|
||||
this.input[0].value = '';
|
||||
if (this.isPassword) {
|
||||
try {
|
||||
this.input[0].setAttribute('type', 'password');
|
||||
} catch (e) { }
|
||||
// Restore focus for Opera and IE
|
||||
this.input.show();
|
||||
this.input[0].focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
valueIsPlaceholder : function() {
|
||||
return this.input[0].value == this.input.attr('placeholder');
|
||||
},
|
||||
handlePassword: function() {
|
||||
var input = this.input;
|
||||
input.attr('realType', 'password');
|
||||
this.isPassword = true;
|
||||
// IE < 9 doesn't allow changing the type of password inputs
|
||||
if ($.browser.msie && input[0].outerHTML) {
|
||||
var fakeHTML = $(input[0].outerHTML.replace(/type=(['"])?password\1/gi, 'type=$1text$1'));
|
||||
this.fakePassword = fakeHTML.val(input.attr('placeholder')).addClass('placeholder').focus(function() {
|
||||
input.trigger('focus');
|
||||
$(this).hide();
|
||||
});
|
||||
$(input[0].form).submit(function() {
|
||||
fakeHTML.remove();
|
||||
input.show()
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
var NATIVE_SUPPORT = !!("placeholder" in document.createElement( "input" ));
|
||||
$.fn.placeholder = function() {
|
||||
return NATIVE_SUPPORT ? this : this.each(function() {
|
||||
var input = $(this);
|
||||
var placeholder = new Placeholder(input);
|
||||
placeholder.show(true);
|
||||
input.focus(function() {
|
||||
placeholder.hide();
|
||||
});
|
||||
input.blur(function() {
|
||||
placeholder.show(false);
|
||||
});
|
||||
|
||||
// On page refresh, IE doesn't re-populate user input
|
||||
// until the window.onload event is fired.
|
||||
if ($.browser.msie) {
|
||||
$(window).load(function() {
|
||||
if(input.val()) {
|
||||
input.removeClass("placeholder");
|
||||
}
|
||||
placeholder.show(true);
|
||||
});
|
||||
// What's even worse, the text cursor disappears
|
||||
// when tabbing between text inputs, here's a fix
|
||||
input.focus(function() {
|
||||
if(this.value == "") {
|
||||
var range = this.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveStart('character', 0);
|
||||
range.select();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})(jQuery);
|
|
@ -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;}
|
||||
|
@ -658,6 +664,7 @@ table.dataTable {
|
|||
|
||||
table.tablesorter tr.search th {
|
||||
background: none repeat scroll 0 0 #DCECFA;
|
||||
padding: 0 17px 0 5px;
|
||||
}
|
||||
|
||||
table.tablesorter tr.search th input[type="text"] {
|
||||
|
@ -667,7 +674,8 @@ table.tablesorter tr.search th input[type="text"] {
|
|||
font-size: 12px;
|
||||
height: 16px;
|
||||
padding: 5px;
|
||||
width: 830px;
|
||||
width: 100%;
|
||||
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
|
@ -765,6 +773,85 @@ div.tos_sidebar ul li a {
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
table.tablesorter tbody tr.regular td {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
table.tablesorter tbody tr.removed td,
|
||||
table.tablesorter tbody tr.sync_error td {
|
||||
background-color: #FFECEC;
|
||||
}
|
||||
|
||||
table.tablesorter tbody tr.sync_success td {
|
||||
background-color: #E0ECFF;
|
||||
}
|
||||
|
||||
ul.ui-autocomplete li.item {
|
||||
/* padding: 1px 0; */
|
||||
}
|
||||
|
||||
ul.ui-autocomplete li.item div.collaborator {
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
ul.ui-autocomplete li.item a.ui-corner-all {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.ui-autocomplete li.item a.ui-corner-all.ui-state-hover {
|
||||
background: #DCECFA;
|
||||
border: 1px solid #65A6F7;
|
||||
}
|
||||
|
||||
ul.ui-autocomplete li.item div.collaborator div.img {
|
||||
padding: 0;
|
||||
padding-top: 1px;
|
||||
}
|
||||
ul.ui-autocomplete li.item div.collaborator div.name {
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
ul.ui-autocomplete.has_results {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#add_collaborator_form div.search_string {
|
||||
margin-left: 5px;
|
||||
float: left;
|
||||
padding-top: 6px;
|
||||
}
|
||||
#add_collaborator_form div.img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
// border: 1px solid #DDDDDD;
|
||||
// border-radius: 2px;
|
||||
}
|
||||
|
||||
#add_collaborator_form div.img img {
|
||||
// margin-top: 1px;
|
||||
// margin-left: 1px;
|
||||
}
|
||||
|
||||
#add_collaborator_form .admin-role .lineForm {
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
input:-moz-placeholder,
|
||||
textarea:-moz-placeholder {
|
||||
color: #CFCFCF;
|
||||
}
|
||||
|
||||
input::-webkit-input-placeholder,
|
||||
textarea::-webkit-input-placeholder {
|
||||
color: #CFCFCF;
|
||||
}
|
||||
|
||||
input.placeholder,
|
||||
textarea.placeholder {
|
||||
color: #CFCFCF;
|
||||
}
|
||||
|
||||
div.description-top div.git_help {
|
||||
float: left;
|
||||
margin-top: 11px;
|
||||
|
@ -833,4 +920,4 @@ div#git_help_data p {
|
|||
|
||||
.dropdown.open .dropdown-toggle {
|
||||
background: none repeat scroll 0 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -909,6 +909,41 @@ article input.black {
|
|||
color: #333333;
|
||||
}
|
||||
|
||||
article input.gray.withimage, article input.black.withimage {
|
||||
padding-left: 30px;
|
||||
width: 275px;
|
||||
background-size: 25px 25px;
|
||||
}
|
||||
|
||||
div.admin-search.withimage {
|
||||
width: 300px;
|
||||
margin-right: 5px;
|
||||
height: 25px;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 3px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
div.admin-search.withimage img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right div.admin-search.withimage input {
|
||||
width: 270px;
|
||||
float: left;
|
||||
margin-right: 0px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
border-radius: none;
|
||||
margin-top: 1px;
|
||||
font-size: 12px;
|
||||
font-family: Tahoma, Geneva, Helvetica, sans-serif;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
.right div.admin-add {
|
||||
float: left;
|
||||
padding-top: 1px;
|
||||
|
|
|
@ -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,8 @@ class CollaboratorsController < ApplicationController
|
|||
before_filter :authorize_collaborators
|
||||
|
||||
def index
|
||||
redirect_to edit_project_collaborators_path(@project)
|
||||
@collaborators = Collaborator.find_by_project(@project)
|
||||
respond_with @collaborators
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -19,108 +22,48 @@ class CollaboratorsController < ApplicationController
|
|||
def new
|
||||
end
|
||||
|
||||
def edit
|
||||
if params[:id]
|
||||
@user = User.find params[:id]
|
||||
render :edit_rights and return
|
||||
def find
|
||||
users = User.not_member_of(@project)
|
||||
groups = Group.not_member_of(@project)
|
||||
if params[:term].present?
|
||||
users = users.search(params[:term])
|
||||
groups = groups.search(params[:term])
|
||||
end
|
||||
@collaborators = (users | groups).map{|act| Collaborator.new(:actor => act, :project => @project)}
|
||||
respond_with @collaborators do |format|
|
||||
format.json { render 'index' }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
puts params.inspect
|
||||
@collaborator = Collaborator.new(params[:collaborator])
|
||||
@collaborator.project = @project
|
||||
if @collaborator.save
|
||||
respond_with @collaborator do |format|
|
||||
format.json { render :partial => 'collaborator', :locals => {:collaborator => @collaborator} }
|
||||
end
|
||||
else
|
||||
raise
|
||||
end
|
||||
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.find(params[:id])
|
||||
if @c.update_attributes(params[:collaborator])
|
||||
respond_with @c
|
||||
else
|
||||
flash[:error] = t("flash.collaborators.error_in_changing")
|
||||
raise
|
||||
end
|
||||
|
||||
redirect_to edit_project_collaborators_path(@project)
|
||||
end
|
||||
|
||||
def remove
|
||||
all_user_ids = []
|
||||
all_group_ids = []
|
||||
|
||||
params['user_remove'].keys.each { |user_id|
|
||||
all_user_ids << user_id if params['user_remove'][user_id] == ["1"]
|
||||
} if params['user_remove']
|
||||
params['group_remove'].keys.each { |group_id|
|
||||
all_group_ids << group_id if params['group_remove'][group_id] == ["1"]
|
||||
} if params['group_remove']
|
||||
|
||||
|
||||
all_user_ids.each do |user_id|
|
||||
u = User.find(user_id)
|
||||
Relation.by_object(u).by_target(@project).each {|r| r.destroy} unless u.id == @project.owner_id and @project.owner_type == 'User'
|
||||
end
|
||||
all_group_ids.each do |group_id|
|
||||
g = Group.find(group_id)
|
||||
Relation.by_object(g).by_target(@project).each {|r| r.destroy} unless g.id == @project.owner_id and @project.owner_type == 'Group'
|
||||
end
|
||||
|
||||
redirect_to edit_project_collaborators_path(@project) + "##{params['user_remove'].present? ? 'users' : 'groups'}"
|
||||
end
|
||||
|
||||
def add
|
||||
# TODO: Here is used Chelyabinsk method to display Flash messages.
|
||||
|
||||
member = User.find(params['member_id']) if params['member_id'] && !params['member_id'].empty?
|
||||
group = Group.find(params['group_id']) if params['group_id'] && !params['group_id'].empty?
|
||||
|
||||
flash[:notice], flash[:error], flash[:warning] = [], [], []
|
||||
|
||||
[member, group].compact.each do |mem|
|
||||
if mem and @project.relations.exists?(:object_id => mem.id, :object_type => mem.class.to_s)
|
||||
flash[:warning] << [t('flash.collaborators.member_already_added'), mem.uname]
|
||||
end
|
||||
unless @project.relations.exists?(:object_id => mem.id, :object_type => mem.class.to_s)
|
||||
rel = @project.relations.build(:role => params[:role])
|
||||
rel.object = mem
|
||||
if rel.save
|
||||
flash[:notice] << [t('flash.collaborators.successfully_added'), mem.uname]
|
||||
else
|
||||
flash[:notice] << [t('flash.collaborators.error_in_adding'), mem.uname]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:notice, :warning, :error].each do |k|
|
||||
if flash[k].size > 0
|
||||
flash[k] = flash[k].map{|i| (i.is_a? Array) ? sprintf(i.first, i.last) : i}.join('; ')
|
||||
else
|
||||
flash.delete k
|
||||
end
|
||||
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'}"
|
||||
def destroy
|
||||
@cb = Collaborator.find(params[:id])
|
||||
@cb.destroy if @cb
|
||||
respond_with @cb
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,6 +6,7 @@ module UsersHelper
|
|||
end
|
||||
|
||||
def avatar_url(user, size = :small)
|
||||
return image_path('group32.png') if user.kind_of? Group
|
||||
if user.try('avatar?')
|
||||
user.avatar.url(size)
|
||||
else
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
# -*- encoding : utf-8 -*-
|
||||
class Collaborator
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
include ActiveModel::Serializers::JSON
|
||||
include ActiveModel::MassAssignmentSecurity
|
||||
extend ActiveModel::Naming
|
||||
|
||||
attr_accessor :role, :actor, :project, :relation
|
||||
attr_reader :id, :actor_id, :actor_type, :actor_name, :project_id
|
||||
|
||||
attr_accessible :role
|
||||
|
||||
delegate :new_record?, :to => :relation
|
||||
|
||||
class << self
|
||||
def find_by_project(project)
|
||||
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
|
||||
return res
|
||||
end
|
||||
|
||||
def find(id)
|
||||
return self.from_relation(Relation.find(id)) || nil
|
||||
end
|
||||
|
||||
def create(args)
|
||||
self.new(args).save
|
||||
end
|
||||
|
||||
def create!(args)
|
||||
self.new(args).save!
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(args = {})
|
||||
args.to_options!
|
||||
acc_options = args.select{ |(k, v)| k.in? [:actor, :project, :relation] }
|
||||
acc_options.each_pair do |name, value|
|
||||
send("#{name}=", value)
|
||||
end
|
||||
|
||||
if args[:project_id].present?
|
||||
@project = Project.find(args[:project_id])
|
||||
end
|
||||
if args[:actor_id].present? and args[:actor_type].present?
|
||||
@actor = args[:actor_type].classify.constantize.find(args[:actor_id])
|
||||
end
|
||||
|
||||
if @relation.nil? and @actor.present? and @project.present?
|
||||
@relation = Relation.by_object(@actor).by_target(@project).limit(1).first
|
||||
@relation ||= Relation.new(:object => @actor, :target => @project)
|
||||
end
|
||||
@relation.role = args[:role] if @relation.present? and args[:role].present?
|
||||
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 relation=(model)
|
||||
@relation = model
|
||||
@actor = @relation.object
|
||||
@project = @relation.target
|
||||
end
|
||||
|
||||
def id
|
||||
@relation.try(:id)
|
||||
end
|
||||
|
||||
def actor_id
|
||||
@actor.try(:id)
|
||||
end
|
||||
|
||||
def actor_type
|
||||
@actor.class.to_s.underscore
|
||||
end
|
||||
|
||||
def actor_name
|
||||
if @actor.present?
|
||||
@actor.instance_of?(User) ? "#{@actor.uname}#{ @actor.try(:name) and !@actor.name.empty? ? " (#{@actor.name})" : ''}" : @actor.uname
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def project_id
|
||||
@project.try(:id)
|
||||
end
|
||||
|
||||
def role
|
||||
@relation.try(: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 actor_id actor_type actor_name project_id role}.inject({}) do |h, e|
|
||||
h.merge(e => send(e))
|
||||
end
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
class << self
|
||||
|
||||
def from_relation(relation)
|
||||
return nil unless relation.present?
|
||||
return self.new(:relation => relation)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def relation
|
||||
return @relation if @relation.present? and @relation.object == @actor and @relation.target == @project
|
||||
|
||||
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,
|
||||
:target_id => @project.id, :target_type => 'Project')
|
||||
else
|
||||
@relation = Relation.new
|
||||
@relation.object = @actor
|
||||
@relation.target = @project
|
||||
end
|
||||
@relation
|
||||
end
|
||||
|
||||
end
|
||||
Collaborator.include_root_in_json = false
|
|
@ -16,13 +16,12 @@ class Group < ActiveRecord::Base
|
|||
validates :uname, :presence => true, :uniqueness => {:case_sensitive => false}, :format => { :with => /^[a-z0-9_]+$/ }
|
||||
validate { errors.add(:uname, :taken) if User.where('uname LIKE ?', uname).present? }
|
||||
|
||||
scope :search_order, order("CHAR_LENGTH(uname) ASC")
|
||||
scope :without, lambda {|a| where("groups.id NOT IN (?)", a)}
|
||||
scope :search, lambda {|q| where("uname ILIKE ?", "%#{q.to_s.strip}%")}
|
||||
scope :opened, where('1=1')
|
||||
scope :by_owner, lambda {|owner| where(:owner_id => owner.id)}
|
||||
scope :by_admin, lambda {|admin| joins(:objects).where(:'relations.role' => 'admin', :'relations.object_id' => admin.id, :'relations.object_type' => 'User')}
|
||||
|
||||
include Modules::Models::ActsLikeMember
|
||||
|
||||
attr_accessible :description
|
||||
attr_readonly :own_projects_count
|
||||
|
||||
|
|
|
@ -47,14 +47,13 @@ class User < ActiveRecord::Base
|
|||
attr_readonly :uname, :own_projects_count
|
||||
attr_accessor :login
|
||||
|
||||
scope :search_order, order("CHAR_LENGTH(uname) ASC")
|
||||
scope :without, lambda {|a| where("users.id NOT IN (?)", a)}
|
||||
scope :search, lambda {|q| where("uname ILIKE ?", "%#{q.to_s.strip}%")}
|
||||
scope :opened, where('1=1')
|
||||
scope :banned, where(:role => 'banned')
|
||||
scope :admin, where(:role => 'admin')
|
||||
scope :real, where(:role => ['', nil])
|
||||
|
||||
include Modules::Models::ActsLikeMember
|
||||
|
||||
after_create lambda { self.create_notifier }
|
||||
before_create :ensure_authentication_token
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
json.id collaborator.id
|
||||
|
||||
json.actor_id collaborator.actor_id
|
||||
json.actor_name collaborator.actor_name
|
||||
json.actor_type collaborator.actor_type
|
||||
json.avatar avatar_url(collaborator.actor)
|
||||
json.actor_path participant_path(collaborator.actor)
|
||||
|
||||
json.project_id collaborator.project_id
|
||||
json.role collaborator.role
|
|
@ -0,0 +1,12 @@
|
|||
json.array!(collaborators) do |json, cb|
|
||||
json.id cb.id
|
||||
|
||||
json.actor_id cb.actor_id
|
||||
json.actor_name cb.actor_name
|
||||
json.actor_type cb.actor_type
|
||||
json.avatar avatar_url(cb.actor)
|
||||
json.actor_path participant_path(cb.actor)
|
||||
|
||||
json.project_id cb.project_id
|
||||
json.role cb.role
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
-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")
|
||||
|
||||
#add_collaborator_form
|
||||
.admin-search.withimage
|
||||
.img
|
||||
%img{ :alt => 'avatar', :src => '', :style => 'display: none;' }
|
||||
= text_field_tag :collaborator_name, nil
|
||||
.both
|
||||
.admin-role
|
||||
.lineForm
|
||||
= select_tag 'role', options_for_collaborators_roles_select
|
||||
%a{:id => 'add_collaborator_button', :class => 'button', :rel => 'nofollow', :href => 'javascript:void(0)'}
|
||||
= t('layout.add')
|
||||
.both
|
||||
|
||||
%table#collaborators.tablesorter{:cellpadding => "0", :cellspacing => "0"}
|
||||
%thead
|
||||
%tr
|
||||
%th.centered
|
||||
%span#collaborators_deleter.hidden
|
||||
%span.delete
|
||||
%th
|
||||
= t("layout.collaborators.members")
|
||||
%th{:colspan => "3"}
|
||||
= t("layout.collaborators.roles")
|
||||
%tr.search
|
||||
%th{:colspan => "5"}
|
||||
%input{ :type => "text", :placeholder => "#{ t('layout.filter_by_name') }"}
|
||||
%tbody
|
||||
%br
|
||||
|
||||
.both
|
||||
|
||||
:javascript
|
||||
$(function() {
|
||||
Rosa.bootstrapedData.collaborators = #{ render :partial => 'collaborators.json.jbuilder', :locals => {:collaborators => @collaborators } };
|
||||
r = new Rosa.Routers.CollaboratorsRouter();
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
json.partial! 'collaborators', :collaborators => @collaborators
|
|
@ -12,4 +12,4 @@
|
|||
= link_to t("layout.projects.sections"), sections_project_path(@project)
|
||||
- if can? :manage_collaborators, @project
|
||||
%li{:class => (act == :edit && contr == :collaborators) ? 'active' : ''}
|
||||
= link_to t("layout.projects.edit_collaborators"), edit_project_collaborators_path(@project)
|
||||
= link_to t("layout.projects.edit_collaborators"), project_collaborators_path(@project)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,6 +16,7 @@ en:
|
|||
remove: Remove
|
||||
|
||||
find_project: Find project...
|
||||
filter_by_name: Filter by name
|
||||
|
||||
visibilities:
|
||||
open: open
|
||||
|
|
|
@ -16,6 +16,7 @@ ru:
|
|||
remove: Убрать
|
||||
|
||||
find_project: Найти проект...
|
||||
filter_by_name: Фильтр по имени
|
||||
|
||||
visibilities:
|
||||
open: открытая
|
||||
|
|
|
@ -112,16 +112,8 @@ 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
|
||||
collection do
|
||||
get :edit
|
||||
post :update
|
||||
post :add
|
||||
delete :remove
|
||||
end
|
||||
member do
|
||||
post :update
|
||||
end
|
||||
resources :collaborators do
|
||||
get :find, :on => :collection
|
||||
end
|
||||
member do
|
||||
post :fork
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# -*- encoding : utf-8 -*-
|
||||
module Modules
|
||||
module Models
|
||||
module ActsLikeMember
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do |klass|
|
||||
scope :not_member_of, lambda { |item|
|
||||
where("
|
||||
#{klass.table_name}.id NOT IN (
|
||||
SELECT relations.object_id
|
||||
FROM relations
|
||||
WHERE (
|
||||
relations.object_type = '#{klass.to_s}'
|
||||
AND relations.target_type = '#{item.class.to_s}'
|
||||
AND relations.target_id = #{item.id}
|
||||
)
|
||||
)
|
||||
")
|
||||
}
|
||||
|
||||
scope :search_order, order("CHAR_LENGTH(uname) ASC")
|
||||
scope :without, lambda {|a| where("#{klass.table_name}.id NOT IN (?)", a)}
|
||||
scope :search, lambda {|q| where("#{klass.table_name}.uname ILIKE ?", "%#{q.strip}%")}
|
||||
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue