Commit 875728b1184d4524bf96a6de47b329eba7645446
Exists in
master
and in
4 other branches
Merge branch 'feature/admin_project_transfer' of /home/git/repositories/gitlab/gitlabhq
Showing
14 changed files
with
187 additions
and
23 deletions
Show diff stats
CHANGELOG
@@ -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) |
@@ -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 < Admin::ApplicationController | @@ -14,8 +16,16 @@ class Admin::ProjectsController < 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 < Admin::ApplicationController | @@ -26,4 +36,12 @@ class Admin::ProjectsController < 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 < ActiveRecord::Base | @@ -87,4 +87,8 @@ class Namespace < 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
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 |
@@ -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 |
@@ -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 |