diff --git a/app/assets/stylesheets/design/custom.scss b/app/assets/stylesheets/design/custom.scss index d5ef96d57..dbd61fe82 100644 --- a/app/assets/stylesheets/design/custom.scss +++ b/app/assets/stylesheets/design/custom.scss @@ -781,4 +781,36 @@ div#git_help_data { div#git_help_data p { padding-bottom: 5px; -} \ No newline at end of file +} + +// for bootstrap +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; + filter: alpha(opacity=40); + cursor: pointer; +} + +.modal { + margin: -150px 0 0 -280px; +} + +#forkModal.modal .btn.btn-primary { + width: 100%; +} + +.center { + text-align: center; +} diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index bd3b6b3a6..8f9a949ad 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -26,9 +26,10 @@ class ProjectsController < ApplicationController @project = Project.new params[:project] @project.owner = choose_owner @who_owns = (@project.owner_type == 'User' ? :me : :group) + authorize! :update, @project.owner if @project.owner.class == Group if @project.save - flash[:notice] = t('flash.project.saved') + flash[:notice] = t('flash.project.saved') redirect_to @project else flash[:error] = t('flash.project.save_error') @@ -56,7 +57,9 @@ class ProjectsController < ApplicationController end def fork - if forked = @project.fork(current_user) and forked.valid? + owner = (Group.find params[:group] if params[:group].present?) || current_user + authorize! :update, owner if owner.class == Group + if forked = @project.fork(owner) and forked.valid? redirect_to forked, :notice => t("flash.project.forked") else flash[:warning] = t("flash.project.fork_error") diff --git a/app/models/ability.rb b/app/models/ability.rb index 6820d6d97..3c9db3a8d 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -57,6 +57,7 @@ class Ability can(:write, Project) {|project| local_writer? project} # for grack can([:update, :sections, :manage_collaborators], Project) {|project| local_admin? project} can(:fork, Project) {|project| can? :read, project} + can(:fork_to_group, Project) {|project| project.owner_type == 'Group' and can? :update, project.owner} can(:destroy, Project) {|project| owner? project} can(:destroy, Project) {|project| project.owner_type == 'Group' and project.owner.objects.exists?(:object_type => 'User', :object_id => user.id, :role => 'admin')} can :remove_user, Project diff --git a/app/views/git/shared/_fork.html.haml b/app/views/git/shared/_fork.html.haml index 899a711d0..b491b3dfe 100644 --- a/app/views/git/shared/_fork.html.haml +++ b/app/views/git/shared/_fork.html.haml @@ -1,4 +1,29 @@ -- if can? :fork, @project - .r#fork-and-edit= link_to t('layout.projects.fork_and_edit'), fork_project_path(@project), :method => :post, :confirm => t("layout.confirm"), :class => 'button' +- if Group.can_own_project(current_user).present? + .r#fork-and-edit= link_to t('layout.projects.fork_and_edit'), '#forkModal', :class => 'button', 'data-toggle' => 'modal' - if can? :create, @project.build_lists.new .r{:style => "display: block"}= link_to t('layout.projects.new_build_list'), new_project_build_list_path(@project), :class => 'button' + + #forkModal.modal{:style => 'display: none;'} + .modal-header + %a.close{"data-dismiss" => "modal"} × + %h3=t 'layout.projects.fork_modal_header' + .modal-footer + - if current_user.projects.exists? :name => @project.name + %p.center + =t 'layout.projects.already_exists' + =link_to "#{current_user.uname}/#{@project.name}", project_path(current_user.projects.by_name(@project.name).first.id) + - else + = form_for @project, :url => fork_project_path(@project), :html => { :class => :form, :multipart => true, :method => :post } do |f| + =f.submit t('layout.projects.fork_to', :to => current_user.uname), :class => 'btn btn-primary' + - Group.can_own_project(current_user).each do |group| + .modal-footer + - if group.projects.exists? :name => @project.name + %p.center + =t 'layout.projects.already_exists' + =link_to "#{group.uname}/#{@project.name} (#{t 'activerecord.models.group'})", project_path(group.projects.by_name(@project.name).first.id) + - else + = form_for @project, :url => fork_project_path(@project), :html => { :class => :form, :multipart => true, :method => :post } do |f| + = hidden_field_tag :group, group.id + =f.submit t('layout.projects.fork_to', :to => "#{group.uname} (#{t 'activerecord.models.group'})"), :class => 'btn btn-primary' +- else + .r#fork-and-edit= link_to t('layout.projects.fork_and_edit'), fork_project_path(@project), :method => :post, :confirm => t("layout.confirm"), :class => 'button' diff --git a/config/locales/models/project.en.yml b/config/locales/models/project.en.yml index 11f4df34b..1d38c6b45 100644 --- a/config/locales/models/project.en.yml +++ b/config/locales/models/project.en.yml @@ -4,6 +4,9 @@ en: add: Add edit: Settings fork_and_edit: Fork + fork_to: Fork to %{to} + fork_modal_header: Where do you want to fork this project? + already_exists: Project already exists list: List list_header: Projects edit_header: Edit project diff --git a/config/locales/models/project.ru.yml b/config/locales/models/project.ru.yml index ae9a75c7a..219437767 100644 --- a/config/locales/models/project.ru.yml +++ b/config/locales/models/project.ru.yml @@ -4,6 +4,9 @@ ru: add: Добавить edit: Настройки fork_and_edit: Клонировать + fork_to: Клонировать в %{to} + fork_modal_header: Куда Вы хотите клонировать проект? + already_exists: Проект уже существует list: Список list_header: Проекты edit_header: Редактировать проект diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index de19f9852..eceef48a5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -2,17 +2,17 @@ require 'spec_helper' describe ProjectsController do - - before(:each) do + + before(:each) do stub_rsync_methods @project = FactoryGirl.create(:project) @another_user = FactoryGirl.create(:user) @create_params = {:project => {:name => 'pro'}} @update_params = {:project => {:name => 'pro2'}} - end + end - context 'for guest' do + context 'for guest' do it 'should not be able to perform index action' do get :index response.should redirect_to(new_user_session_path) @@ -25,10 +25,10 @@ describe ProjectsController do end context 'for admin' do - before(:each) do - @admin = FactoryGirl.create(:admin) - set_session_for(@admin) - end + before(:each) do + @admin = FactoryGirl.create(:admin) + set_session_for(@admin) + end it_should_behave_like 'projects user with admin rights' it_should_behave_like 'projects user with reader rights' @@ -44,12 +44,12 @@ describe ProjectsController do end context 'for owner user' do - before(:each) do - @user = FactoryGirl.create(:user) - set_session_for(@user) - @project.update_attribute(:owner, @user) - @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'admin') - end + before(:each) do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.update_attribute(:owner, @user) + @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'admin') + end it_should_behave_like 'projects user with admin rights' it_should_behave_like 'user with rights to view projects' @@ -67,30 +67,49 @@ describe ProjectsController do post :fork, :id => @project.id response.should redirect_to(forbidden_path) end + end context 'for reader user' do - before(:each) do - @user = FactoryGirl.create(:user) - set_session_for(@user) - @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'reader') - end + before(:each) do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'reader') + end it_should_behave_like 'projects user with reader rights' end context 'for writer user' do - before(:each) do - @user = FactoryGirl.create(:user) - set_session_for(@user) - @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'writer') - end + before(:each) do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'writer') + end it_should_behave_like 'projects user with reader rights' + + it 'should not be able to create project to other group' do + group = FactoryGirl.create(:group) + post :create, @create_params.merge({:who_owns => 'group', :owner_id => group.id}) + response.should redirect_to(forbidden_path) + end + + it 'should not be able to fork project to other group' do + group = FactoryGirl.create(:group) + post :fork, :id => @project.id, :group => group.id + response.should redirect_to(forbidden_path) + end + + it 'should be able to fork project to group' do + group = FactoryGirl.create(:group) + group.objects.create(:object_type => 'User', :object_id => @user.id, :role => 'admin') + post :fork, :id => @project.id, :group => group.id + response.should redirect_to(project_path(group.projects.first.id)) + end end context 'search projects' do - before(:each) do @admin = FactoryGirl.create(:admin) @project1 = FactoryGirl.create(:project, :name => 'perl-debug') @@ -103,4 +122,14 @@ describe ProjectsController do assigns(:projects).should eq([@project2, @project1]) end end + + context 'for other user' do + it 'should not be able to fork hidden project' do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.update_attribute(:visibility, 'hidden') + post :fork, :id => @project.id + response.should redirect_to(forbidden_path) + end + end end diff --git a/vendor/assets/javascripts/bootstrap-modal.js b/vendor/assets/javascripts/bootstrap-modal.js new file mode 100644 index 000000000..e92970627 --- /dev/null +++ b/vendor/assets/javascripts/bootstrap-modal.js @@ -0,0 +1,210 @@ +/* ========================================================= + * bootstrap-modal.js v2.0.2 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function( $ ){ + + "use strict" + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function ( content, options ) { + this.options = options + this.$element = $(content) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + + if (this.isShown) return + + $('body').addClass('modal-open') + + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function ( e ) { + e && e.preventDefault() + + if (!this.isShown) return + + var that = this + this.isShown = false + + $('body').removeClass('modal-open') + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + hideModal.call(that) + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal( that ) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('