Commit 875728b1184d4524bf96a6de47b329eba7645446

Authored by Dmitriy Zaporozhets
2 parents 77e3fab8 27ad8826

Merge branch 'feature/admin_project_transfer' of /home/git/repositories/gitlab/gitlabhq

@@ -17,6 +17,7 @@ v 6.3.0 @@ -17,6 +17,7 @@ v 6.3.0
17 - Fix 500 error for repos with newline in file name 17 - Fix 500 error for repos with newline in file name
18 - Extended html titles 18 - Extended html titles
19 - API: create/update repo files 19 - API: create/update repo files
  20 + - Admin can transfer project to any namespace
20 21
21 v 6.2.0 22 v 6.2.0
22 - Public project pages are now visible to everyone (files, issues, wik, etc.) 23 - Public project pages are now visible to everyone (files, issues, wik, etc.)
app/assets/javascripts/api.js.coffee
@@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
2 users_path: "/api/:version/users.json" 2 users_path: "/api/:version/users.json"
3 user_path: "/api/:version/users/:id.json" 3 user_path: "/api/:version/users/:id.json"
4 notes_path: "/api/:version/projects/:id/notes.json" 4 notes_path: "/api/:version/projects/:id/notes.json"
  5 + namespaces_path: "/api/:version/namespaces.json"
5 6
6 # Get 20 (depends on api) recent notes 7 # Get 20 (depends on api) recent notes
7 # and sort the ascending from oldest to newest 8 # and sort the ascending from oldest to newest
@@ -49,6 +50,20 @@ @@ -49,6 +50,20 @@
49 ).done (users) -> 50 ).done (users) ->
50 callback(users) 51 callback(users)
51 52
  53 + # Return namespaces list. Filtered by query
  54 + namespaces: (query, callback) ->
  55 + url = Api.buildUrl(Api.namespaces_path)
  56 +
  57 + $.ajax(
  58 + url: url
  59 + data:
  60 + private_token: gon.api_token
  61 + search: query
  62 + per_page: 20
  63 + dataType: "json"
  64 + ).done (namespaces) ->
  65 + callback(namespaces)
  66 +
52 buildUrl: (url) -> 67 buildUrl: (url) ->
53 url = gon.relative_url_root + url if gon.relative_url_root? 68 url = gon.relative_url_root + url if gon.relative_url_root?
54 return url.replace(':version', gon.api_version) 69 return url.replace(':version', gon.api_version)
app/assets/javascripts/namespace_select.js.coffee 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +$ ->
  2 + namespaceFormatResult = (namespace) ->
  3 + markup = "<div class='namespace-result'>"
  4 + markup += "<span class='namespace-kind'>" + namespace.kind + "</span>"
  5 + markup += "<span class='namespace-path'>" + namespace.path + "</span>"
  6 + markup += "</div>"
  7 + markup
  8 +
  9 + formatSelection = (namespace) ->
  10 + namespace.kind + ": " + namespace.path
  11 +
  12 + $('.ajax-namespace-select').each (i, select) ->
  13 + $(select).select2
  14 + placeholder: "Search for namespace"
  15 + multiple: $(select).hasClass('multiselect')
  16 + minimumInputLength: 0
  17 + query: (query) ->
  18 + Api.namespaces query.term, (namespaces) ->
  19 + data = { results: namespaces }
  20 + query.callback(data)
  21 +
  22 + dropdownCssClass: "ajax-namespace-dropdown"
  23 + formatResult: namespaceFormatResult
  24 + formatSelection: formatSelection
app/assets/stylesheets/common.scss
@@ -358,3 +358,33 @@ table { @@ -358,3 +358,33 @@ table {
358 background: #555; 358 background: #555;
359 color: #BBB; 359 color: #BBB;
360 } 360 }
  361 +
  362 +.ajax-users-select {
  363 + width: 400px;
  364 +
  365 + &.input-large {
  366 + width: 210px;
  367 + }
  368 +}
  369 +
  370 +.user-result {
  371 + .user-image {
  372 + float: left;
  373 + }
  374 + .user-name {
  375 + }
  376 + .user-username {
  377 + color: #999;
  378 + }
  379 +}
  380 +
  381 +.namespace-result {
  382 + .namespace-kind {
  383 + color: #AAA;
  384 + font-weight: normal;
  385 + }
  386 + .namespace-path {
  387 + margin-left: 10px;
  388 + font-weight: bolder;
  389 + }
  390 +}
app/assets/stylesheets/selects.scss
1 -.ajax-users-select {  
2 - width: 400px;  
3 -  
4 - &.input-large {  
5 - width: 210px;  
6 - }  
7 -}  
8 -  
9 -.user-result {  
10 - .user-image {  
11 - float: left;  
12 - }  
13 - .user-name {  
14 - }  
15 - .user-username {  
16 - color: #999;  
17 - }  
18 -}  
19 -  
20 /** Chosen.js selectbox style override **/ 1 /** Chosen.js selectbox style override **/
21 .chosen-container { 2 .chosen-container {
22 min-width: 100px; 3 min-width: 100px;
app/controllers/admin/projects_controller.rb
1 class Admin::ProjectsController < Admin::ApplicationController 1 class Admin::ProjectsController < Admin::ApplicationController
2 - before_filter :project, only: [:edit, :show, :update, :destroy, :team_update] 2 + before_filter :project, only: [:show, :transfer]
  3 + before_filter :group, only: [:show, :transfer]
  4 + before_filter :repository, only: [:show, :transfer]
3 5
4 def index 6 def index
5 owner_id = params[:owner_id] 7 owner_id = params[:owner_id]
@@ -14,8 +16,16 @@ class Admin::ProjectsController &lt; Admin::ApplicationController @@ -14,8 +16,16 @@ class Admin::ProjectsController &lt; Admin::ApplicationController
14 end 16 end
15 17
16 def show 18 def show
17 - @repository = @project.repository  
18 - @group = @project.group 19 + end
  20 +
  21 + def transfer
  22 + result = ::Projects::TransferContext.new(@project, current_user, project: params).execute(:admin)
  23 +
  24 + if result
  25 + redirect_to [:admin, @project]
  26 + else
  27 + render :show
  28 + end
19 end 29 end
20 30
21 protected 31 protected
@@ -26,4 +36,12 @@ class Admin::ProjectsController &lt; Admin::ApplicationController @@ -26,4 +36,12 @@ class Admin::ProjectsController &lt; Admin::ApplicationController
26 @project = Project.find_with_namespace(id) 36 @project = Project.find_with_namespace(id)
27 @project || render_404 37 @project || render_404
28 end 38 end
  39 +
  40 + def group
  41 + @group ||= project.group
  42 + end
  43 +
  44 + def repository
  45 + @repository ||= project.repository
  46 + end
29 end 47 end
app/helpers/namespaces_helper.rb
@@ -16,4 +16,13 @@ module NamespacesHelper @@ -16,4 +16,13 @@ module NamespacesHelper
16 16
17 grouped_options_for_select(options, selected) 17 grouped_options_for_select(options, selected)
18 end 18 end
  19 +
  20 + def namespace_select_tag(id, opts = {})
  21 + css_class = "ajax-namespace-select "
  22 + css_class << "multiselect " if opts[:multiple]
  23 + css_class << (opts[:class] || '')
  24 + value = opts[:selected] || ''
  25 +
  26 + hidden_field_tag(id, value, class: css_class)
  27 + end
19 end 28 end
app/models/namespace.rb
@@ -87,4 +87,8 @@ class Namespace &lt; ActiveRecord::Base @@ -87,4 +87,8 @@ class Namespace &lt; ActiveRecord::Base
87 def send_update_instructions 87 def send_update_instructions
88 projects.each(&:send_move_instructions) 88 projects.each(&:send_move_instructions)
89 end 89 end
  90 +
  91 + def kind
  92 + type == 'Group' ? 'group' : 'user'
  93 + end
90 end 94 end
app/views/admin/projects/show.html.haml
@@ -74,6 +74,23 @@ @@ -74,6 +74,23 @@
74 %span.cgreen 74 %span.cgreen
75 %i.icon-lock 75 %i.icon-lock
76 Private 76 Private
  77 + .ui-box
  78 + .title
  79 + Transfer project
  80 + .ui-box-body
  81 + = form_for @project, url: transfer_admin_project_path(@project), method: :put do |f|
  82 + .control-group
  83 + = f.label :namespace_id, "Namespace"
  84 + .controls
  85 + = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large'
  86 +
  87 + .control-group
  88 + .controls
  89 + = f.submit 'Transfer', class: 'btn btn-primary'
  90 +
  91 +
  92 +
  93 +
77 .span6 94 .span6
78 - if @group 95 - if @group
79 .ui-box 96 .ui-box
config/routes.rb
@@ -89,7 +89,13 @@ Gitlab::Application.routes.draw do @@ -89,7 +89,13 @@ Gitlab::Application.routes.draw do
89 resources :broadcast_messages, only: [:index, :create, :destroy] 89 resources :broadcast_messages, only: [:index, :create, :destroy]
90 resource :logs, only: [:show] 90 resource :logs, only: [:show]
91 resource :background_jobs, controller: 'background_jobs', only: [:show] 91 resource :background_jobs, controller: 'background_jobs', only: [:show]
92 - resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, only: [:index, :show] 92 +
  93 + resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, only: [:index, :show] do
  94 + member do
  95 + put :transfer
  96 + end
  97 + end
  98 +
93 root to: "dashboard#index" 99 root to: "dashboard#index"
94 end 100 end
95 101
lib/api/api.rb
@@ -40,5 +40,6 @@ module API @@ -40,5 +40,6 @@ module API
40 mount ProjectHooks 40 mount ProjectHooks
41 mount Services 41 mount Services
42 mount Files 42 mount Files
  43 + mount Namespaces
43 end 44 end
44 end 45 end
lib/api/entities.rb
@@ -136,5 +136,9 @@ module API @@ -136,5 +136,9 @@ module API
136 expose :target_id, :target_type, :author_id 136 expose :target_id, :target_type, :author_id
137 expose :data, :target_title 137 expose :data, :target_title
138 end 138 end
  139 +
  140 + class Namespace < Grape::Entity
  141 + expose :id, :path, :kind
  142 + end
139 end 143 end
140 end 144 end
lib/api/namespaces.rb 0 → 100644
@@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
  1 +module API
  2 + # namespaces API
  3 + class Namespaces < Grape::API
  4 + before {
  5 + authenticate!
  6 + authenticated_as_admin!
  7 + }
  8 +
  9 + resource :namespaces do
  10 + # Get a namespaces list
  11 + #
  12 + # Example Request:
  13 + # GET /namespaces
  14 + get do
  15 + @namespaces = Namespace.scoped
  16 + @namespaces = @namespaces.search(params[:search]) if params[:search].present?
  17 + @namespaces = paginate @namespaces
  18 +
  19 + present @namespaces, with: Entities::Namespace
  20 + end
  21 + end
  22 + end
  23 +end
spec/requests/api/namespaces_spec.rb 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +require 'spec_helper'
  2 +
  3 +describe API::API do
  4 + include ApiHelpers
  5 + before(:each) { ActiveRecord::Base.observers.enable(:user_observer) }
  6 + after(:each) { ActiveRecord::Base.observers.disable(:user_observer) }
  7 +
  8 + let(:admin) { create(:admin) }
  9 + let!(:group1) { create(:group) }
  10 + let!(:group2) { create(:group) }
  11 +
  12 + describe "GET /namespaces" do
  13 + context "when unauthenticated" do
  14 + it "should return authentication error" do
  15 + get api("/namespaces")
  16 + response.status.should == 401
  17 + end
  18 + end
  19 +
  20 + context "when authenticated as admin" do
  21 + it "admin: should return an array of all namespaces" do
  22 + get api("/namespaces", admin)
  23 + response.status.should == 200
  24 + json_response.should be_an Array
  25 +
  26 + # Admin namespace + 2 group namespaces
  27 + json_response.length.should == 3
  28 + end
  29 + end
  30 + end
  31 +end