Generalize issues imports (#28234).

Extend import controller to support arbitrary imports based on Import subclasses. This way, we may add other kinds of imports, by providing some views and a custom import class. This may also be done from within plugins.

Patch by Gregor Schmidt.


git-svn-id: http://svn.redmine.org/redmine/trunk@18145 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Go MAEDA
2019-05-09 07:40:06 +00:00
parent bcc60805c9
commit b540046ed7
20 changed files with 121 additions and 53 deletions

View File

@@ -20,19 +20,20 @@
require 'csv'
class ImportsController < ApplicationController
menu_item :issues
before_action :find_import, :only => [:show, :settings, :mapping, :run]
before_action :authorize_global
before_action :authorize_import
layout :import_layout
helper :issues
helper :queries
def new
@import = import_type.new
end
def create
@import = IssueImport.new
@import = import_type.new
@import.user = User.current
@import.file = params[:file]
@import.set_default_settings
@@ -98,6 +99,14 @@ class ImportsController < ApplicationController
end
end
def current_menu(project)
if import_layout == 'admin'
nil
else
:application_menu
end
end
private
def find_import
@@ -123,4 +132,31 @@ class ImportsController < ApplicationController
def max_items_per_request
5
end
def import_layout
import_type && import_type.layout || 'base'
end
def menu_items
menu_item = import_type ? import_type.menu_item : nil
{ self.controller_name.to_sym => { :actions => {}, :default => menu_item } }
end
def authorize_import
return render_404 unless import_type
return render_403 unless import_type.authorized?(User.current)
end
def import_type
return @import_type if defined? @import_type
@import_type =
if @import
@import.class
else
type = Object.const_get(params[:type]) rescue nil
type && type < Import ? type : nil
end
end
end

View File

@@ -18,6 +18,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ImportsHelper
def import_title
l(:"label_import_#{import_partial_prefix}")
end
def import_partial_prefix
@import.class.name.sub('Import', '').underscore.pluralize
end
def options_for_mapping_select(import, field, options={})
tags = "".html_safe
blank_text = options[:required] ? "-- #{l(:actionview_instancetag_blank_option)} --" : "&nbsp;".html_safe

View File

@@ -37,6 +37,18 @@ class Import < ActiveRecord::Base
'%d-%m-%Y'
]
def self.menu_item
nil
end
def self.layout
'base'
end
def self.authorized?(user)
user.admin?
end
def initialize(*args)
super
self.settings ||= {}

View File

@@ -18,6 +18,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueImport < Import
def self.menu_item
:issues
end
def self.authorized?(user)
user.allowed_to?(:import_issues, nil, :global => true)
end
# Returns the objects that were imported
def saved_objects

View File

@@ -54,7 +54,7 @@
</p>
<% @custom_fields.each do |field| %>
<p>
<label for="import_mapping_cf_<% field.id %>"><%= field.name %></label>
<label for="import_mapping_cf_<%= field.id %>"><%= field.name %></label>
<%= mapping_select_tag @import, "cf_#{field.id}" %>
</p>
<% end %>

View File

@@ -0,0 +1,16 @@
<fieldset class="box tabular">
<legend><%= l(:label_fields_mapping) %></legend>
<div id="fields-mapping">
<%= render :partial => 'issues_fields_mapping' %>
</div>
</fieldset>
<%= javascript_tag do %>
$('#fields-mapping').on('change', '#import_mapping_project_id, #import_mapping_tracker', function(){
$.ajax({
url: '<%= import_mapping_path(@import, :format => 'js') %>',
type: 'post',
data: $('#import-form').serialize()
});
});
<% end %>

View File

@@ -0,0 +1 @@
$('#fields-mapping').html('<%= escape_javascript(render :partial => 'issues_fields_mapping') %>');

View File

@@ -0,0 +1,7 @@
<ul id="saved-items">
<% saved_objects.each do |issue| %>
<li><%= link_to_issue issue %></li>
<% end %>
</ul>
<p><%= link_to l(:label_issue_view_all), issues_path(:set_filter => 1, :status_id => '*', :issue_id => saved_objects.map(&:id).join(',')) %></p>

View File

@@ -0,0 +1,3 @@
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
<% end %>

View File

@@ -1,12 +1,7 @@
<h2><%= l(:label_import_issues) %></h2>
<h2><%= import_title %></h2>
<%= form_tag(import_mapping_path(@import), :id => "import-form") do %>
<fieldset class="box tabular">
<legend><%= l(:label_fields_mapping) %></legend>
<div id="fields-mapping">
<%= render :partial => 'fields_mapping' %>
</div>
</fieldset>
<%= render :partial => "#{import_partial_prefix}_mapping" %>
<div class="autoscroll">
<fieldset class="box">
@@ -28,25 +23,13 @@
</p>
<% end %>
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
<% end %>
<%= render :partial => "#{import_partial_prefix}_sidebar" %>
<%= javascript_tag do %>
$(document).ready(function() {
$('#fields-mapping').on('change', '#import_mapping_project_id, #import_mapping_tracker', function(){
$.ajax({
url: '<%= import_mapping_path(@import, :format => 'js') %>',
type: 'post',
data: $('#import-form').serialize()
});
});
$('#import-form').submit(function(){
$('#import-details').show().addClass('ajax-loading');
$('#import-progress').progressbar({value: 0, max: <%= @import.total_items || 0 %>});
});
});
<% end %>

View File

@@ -1 +1 @@
$('#fields-mapping').html('<%= escape_javascript(render :partial => 'fields_mapping') %>');
<%= render :partial => "#{import_partial_prefix}_mapping" %>

View File

@@ -1,6 +1,7 @@
<h2><%= l(:label_import_issues) %></h2>
<h2><%= import_title %></h2>
<%= form_tag(imports_path, :multipart => true) do %>
<%= hidden_field_tag 'type', @import.type %>
<fieldset class="box">
<legend><%= l(:label_select_file_to_import) %> (CSV)</legend>
<p>
@@ -10,6 +11,4 @@
<p><%= submit_tag l(:label_next).html_safe + " &#187;".html_safe, :name => nil %></p>
<% end %>
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
<% end %>
<%= render :partial => "#{import_partial_prefix}_sidebar" %>

View File

@@ -1,12 +1,10 @@
<h2><%= l(:label_import_issues) %></h2>
<h2><%= import_title %></h2>
<div id="import-details">
<div id="import-progress"><div id="progress-label">0 / <%= @import.total_items.to_i %></div></div>
</div>
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
<% end %>
<%= render :partial => "#{import_partial_prefix}_sidebar" %>
<%= javascript_tag do %>
$(document).ready(function() {

View File

@@ -1,4 +1,4 @@
<h2><%= l(:label_import_issues) %></h2>
<h2><%= import_title %></h2>
<%= form_tag(import_settings_path(@import), :id => "import-form") do %>
<fieldset class="box tabular">
@@ -25,6 +25,4 @@
<p><%= submit_tag l(:label_next).html_safe + " &#187;".html_safe, :name => nil %></p>
<% end %>
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
<% end %>
<%= render :partial => "#{import_partial_prefix}_sidebar" %>

View File

@@ -1,15 +1,9 @@
<h2><%= l(:label_import_issues) %></h2>
<h2><%= import_title %></h2>
<% if @import.saved_items.count > 0 %>
<p><%= l(:notice_import_finished, :count => @import.saved_items.count) %>:</p>
<ul id="saved-items">
<% @import.saved_objects.each do |issue| %>
<li><%= link_to_issue issue %></li>
<% end %>
</ul>
<p><%= link_to l(:label_issue_view_all), issues_path(:set_filter => 1, :status_id => '*', :issue_id => @import.saved_objects.map(&:id).join(',')) %></p>
<%= render :partial => "#{import_partial_prefix}_saved_objects", :locals => { saved_objects: @import.saved_objects } %>
<% end %>
<% if @import.unsaved_items.count > 0 %>
@@ -33,6 +27,4 @@
</table>
<% end %>
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
<% end %>
<%= render :partial => "#{import_partial_prefix}_sidebar" %>

View File

@@ -64,7 +64,7 @@ Rails.application.routes.draw do
get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
get '/issues/imports/new', :to => 'imports#new', :as => 'new_issues_import'
get '/issues/imports/new', :to => 'imports#new', :defaults => { :type => 'IssueImport' }, :as => 'new_issues_import'
post '/imports', :to => 'imports#create', :as => 'imports'
get '/imports/:id', :to => 'imports#show', :as => 'import'
match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings'

View File

@@ -118,7 +118,7 @@ Redmine::AccessControl.map do |map|
map.permission :view_issue_watchers, {}, :read => true
map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
map.permission :delete_issue_watchers, {:watchers => :destroy}
map.permission :import_issues, {:imports => [:new, :create, :settings, :mapping, :run, :show]}
map.permission :import_issues, {}
# Issue categories
map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
end

View File

@@ -44,7 +44,7 @@ class ImportsControllerTest < Redmine::ControllerTest
end
def test_new_should_display_the_upload_form
get :new
get :new, :params => { :type => 'IssueImport' }
assert_response :success
assert_select 'input[name=?]', 'file'
end
@@ -52,6 +52,7 @@ class ImportsControllerTest < Redmine::ControllerTest
def test_create_should_save_the_file
import = new_record(Import) do
post :create, :params => {
:type => 'IssueImport',
:file => uploaded_test_file('import_issues.csv', 'text/csv')
}
assert_response 302

View File

@@ -21,7 +21,8 @@ require File.expand_path('../../../test_helper', __FILE__)
class RoutingImportsTest < Redmine::RoutingTest
def test_imports
should_route 'GET /issues/imports/new' => 'imports#new'
should_route 'GET /issues/imports/new' => 'imports#new', :type => 'IssueImport'
should_route 'POST /imports' => 'imports#create'
should_route 'GET /imports/4ae6bc' => 'imports#show', :id => '4ae6bc'

View File

@@ -41,6 +41,12 @@ class IssueImportTest < ActiveSupport::TestCase
set_language_if_valid 'en'
end
def test_authorized
assert IssueImport.authorized?(User.find(1)) # admins
assert IssueImport.authorized?(User.find(2)) # has import_issues permission
assert !IssueImport.authorized?(User.find(3)) # does not have permission
end
def test_create_versions_should_create_missing_versions
import = generate_import_with_mapping
import.mapping.merge!('fixed_version' => '9', 'create_versions' => '1')