mirror of
https://github.com/redmine/redmine.git
synced 2025-10-26 07:46:17 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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)} --" : " ".html_safe
|
||||
|
||||
@@ -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 ||= {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
16
app/views/imports/_issues_mapping.html.erb
Normal file
16
app/views/imports/_issues_mapping.html.erb
Normal 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 %>
|
||||
1
app/views/imports/_issues_mapping.js.erb
Normal file
1
app/views/imports/_issues_mapping.js.erb
Normal file
@@ -0,0 +1 @@
|
||||
$('#fields-mapping').html('<%= escape_javascript(render :partial => 'issues_fields_mapping') %>');
|
||||
7
app/views/imports/_issues_saved_objects.html.erb
Normal file
7
app/views/imports/_issues_saved_objects.html.erb
Normal 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>
|
||||
3
app/views/imports/_issues_sidebar.html.erb
Normal file
3
app/views/imports/_issues_sidebar.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'issues/sidebar' %>
|
||||
<% end %>
|
||||
@@ -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 %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
$('#fields-mapping').html('<%= escape_javascript(render :partial => 'fields_mapping') %>');
|
||||
<%= render :partial => "#{import_partial_prefix}_mapping" %>
|
||||
|
||||
@@ -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 + " »".html_safe, :name => nil %></p>
|
||||
<% end %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'issues/sidebar' %>
|
||||
<% end %>
|
||||
<%= render :partial => "#{import_partial_prefix}_sidebar" %>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 + " »".html_safe, :name => nil %></p>
|
||||
<% end %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'issues/sidebar' %>
|
||||
<% end %>
|
||||
<%= render :partial => "#{import_partial_prefix}_sidebar" %>
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user