Commit 9492ef31 authored by Felipe Artur's avatar Felipe Artur

Move export issues feature to core

Export issues feature is being open sourced
because we allow importing on core but not export.
parent 6381f19a
......@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
include RecordUserLastActivity
def issue_except_actions
%i[index calendar new create bulk_update import_csv]
%i[index calendar new create bulk_update import_csv export_csv]
end
def set_issuables_index_only_actions
......@@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
prepend_before_action :authenticate_user!, only: [:new]
prepend_before_action :authenticate_user!, only: [:new, :export_csv]
# designs is only applicable to EE, but defining a prepend_before_action in EE code would overwrite this
prepend_before_action :store_uri, only: [:new, :show, :designs]
......@@ -189,6 +189,13 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def export_csv
ExportCsvWorker.perform_async(current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
index_path = project_issues_path(project)
redirect_to(index_path, notice: "Your CSV export has started. It will be emailed to #{current_user.notification_email} when complete.")
end
def import_csv
if uploader = UploadService.new(project, params[:file]).execute
ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id) # rubocop:disable CodeReuse/Worker
......
......@@ -91,6 +91,20 @@ module Emails
end
end
def issues_csv_email(user, project, csv_data, export_status)
@project = project
@issues_count = export_status.fetch(:rows_expected)
@written_count = export_status.fetch(:rows_written)
@truncated = export_status.fetch(:truncated)
filename = "#{project.full_path.parameterize}_issues_#{Date.today.iso8601}.csv"
attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
mail(to: user.notification_email_for(@project.group), subject: subject("Exported issues")) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
end
private
def setup_issue_mail(issue_id, recipient_id, closed_via: nil)
......
......@@ -80,6 +80,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.import_issues_csv_email(user.id, project.id, { success: 3, errors: [5, 6, 7], valid_file: true })
end
def issues_csv_email
Notify.issues_csv_email(user, project, '1997,Ford,E350', { truncated: false, rows_expected: 3, rows_written: 3 }).message
end
def closed_merge_request_email
Notify.closed_merge_request_email(user.id, issue.id, user.id).message
end
......
......@@ -131,8 +131,21 @@ module Issuable
strip_attributes :title
def self.locking_enabled?
false
class << self
def labels_hash
issue_labels = Hash.new { |h, k| h[k] = [] }
relation = unscoped.where(id: self.select(:id)).eager_load(:labels)
relation.pluck(:id, 'labels.title').each do |issue_id, label|
issue_labels[issue_id] << label if label.present?
end
issue_labels
end
def locking_enabled?
false
end
end
# We want to use optimistic lock for cases when only title or description are involved
......@@ -478,5 +491,4 @@ module Issuable
end
end
Issuable.prepend_if_ee('EE::Issuable') # rubocop: disable Cop/InjectEnterpriseEditionModule
Issuable::ClassMethods.prepend_if_ee('EE::Issuable::ClassMethods')
Issuable.prepend_if_ee('EE::Issuable')
......@@ -27,12 +27,16 @@ module Issues
# rubocop: disable CodeReuse/ActiveRecord
def csv_builder
@csv_builder ||=
CsvBuilder.new(@issues.preload(:author, :assignees, :timelogs, :epic), header_to_value_hash)
CsvBuilder.new(@issues.preload(associations_to_preload), header_to_value_hash)
end
# rubocop: enable CodeReuse/ActiveRecord
private
def associations_to_preload
%i(author assignees timelogs)
end
def header_to_value_hash
{
'Issue ID' => 'iid',
......@@ -55,12 +59,7 @@ module Issues
'Labels' => -> (issue) { issue_labels(issue) },
'Time Estimate' => ->(issue) { issue.time_estimate.to_s(:csv) },
'Time Spent' => -> (issue) { issue_time_spent(issue) }
}.tap do |hash|
if project.group&.feature_available?(:epics)
hash['Epic ID'] = -> (issue) { issue.epic&.id }
hash['Epic Title'] = -> (issue) { issue.epic&.title }
end
end
}
end
def issue_labels(issue)
......@@ -74,3 +73,5 @@ module Issues
# rubocop: enable CodeReuse/ActiveRecord
end
end
Issues::ExportCsvService.prepend_if_ee('EE::Issues::ExportCsvService')
-# haml-lint:disable NoPlainNodes
%p{ style: 'font-size:18px; text-align:center; line-height:30px;' }
Your CSV export of #{ pluralize(@written_count, 'issue') } from project
%a{ href: project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;" }
......
......@@ -8,7 +8,7 @@
.btn-group
- if show_export_button
= render_if_exists 'projects/issues/export_csv/button'
= render 'projects/issues/export_csv/button'
- if show_import_button
= render 'projects/issues/import_csv/button'
......@@ -23,7 +23,7 @@
id: "new_issue_link"
- if show_export_button
= render_if_exists 'projects/issues/export_csv/modal'
= render 'projects/issues/export_csv/modal'
- if show_import_button
= render 'projects/issues/import_csv/modal'
-# haml-lint:disable NoPlainNodes
- if current_user
.issues-export-modal.modal
.modal-dialog
......@@ -7,7 +8,8 @@
= _('Export issues')
.svg-content.import-export-svg-container
= image_tag 'illustrations/export-import.svg', alt: _('Import/Export illustration'), class: 'illustration'
%a.close{ href: '#', 'data-dismiss' => 'modal' } ×
%a.close{ href: '#', 'data-dismiss' => 'modal' }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
.modal-body
.modal-subheader
= icon('check', { class: 'checkmark' })
......@@ -18,5 +20,3 @@
= _('The CSV export will be created in the background. Once finished, it will be sent to <strong>%{email}</strong> in an attachment.').html_safe % { email: @current_user.notification_email }
.modal-footer
= link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn btn-success float-left', title: _('Export issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" }
- elsif show_promotions?
= render 'shared/promotions/promote_csv_export'
......@@ -1046,6 +1046,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: export_csv
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
:idempotent:
- :name: file_hook
:feature_category: :integrations
:has_external_dependencies:
......
---
title: Move export issues feature to core
merge_request: 28703
author:
type: added
......@@ -18,5 +18,6 @@ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
collection do
post :bulk_update
post :import_csv
post :export_csv
end
end
# Export Issues to CSV
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/releases/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/releases/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
> - Moved to GitLab Core in GitLab 12.10.
Issues can be exported as CSV from GitLab and are sent to your default notification email as an attachment.
......
......@@ -9,19 +9,13 @@ module EE
prepended do
include DescriptionDiffActions
# Specifying before_action :authenticate_user! multiple times
# doesn't work, since the last filter will override the previous
# ones.
alias_method :export_csv_authenticate_user!, :authenticate_user!
before_action :export_csv_authenticate_user!, only: [:export_csv]
before_action :check_service_desk_available!, only: [:service_desk]
before_action :whitelist_query_limiting_ee, only: [:update]
end
override :issue_except_actions
def issue_except_actions
super + %i[export_csv service_desk]
super + %i[service_desk]
end
override :set_issuables_index_only_actions
......@@ -34,13 +28,6 @@ module EE
@users.push(::User.support_bot) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def export_csv
ExportCsvWorker.perform_async(current_user.id, project.id, finder_options.to_h)
index_path = project_issues_path(project)
redirect_to(index_path, notice: "Your CSV export has started. It will be emailed to #{current_user.notification_email} when complete.")
end
private
def issue_params_attributes
......
......@@ -10,7 +10,6 @@ module EE
# See https://gitlab.com/gitlab-org/gitlab/issues/7846
prepended do
include ::Emails::AdminNotification
include ::Emails::CsvExport
include ::Emails::ServiceDesk
include ::Emails::Epics
include ::Emails::Reviews
......
......@@ -12,10 +12,6 @@ module EE
::Notify.add_merge_request_approver_email(user.id, merge_request.id, user.id).message
end
def issues_csv_email
::Notify.issues_csv_email(user, project, '1997,Ford,E350', { truncated: false, rows_expected: 3, rows_written: 3 }).message
end
def approved_merge_request_email
::Notify.approved_merge_request_email(user.id, merge_request.id, approver.id).message
end
......
# frozen_string_literal: true
module Emails
module CsvExport
def issues_csv_email(user, project, csv_data, export_status)
@project = project
@issues_count = export_status.fetch(:rows_expected)
@written_count = export_status.fetch(:rows_written)
@truncated = export_status.fetch(:truncated)
filename = "#{project.full_path.parameterize}_issues_#{Date.today.iso8601}.csv"
attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
mail(to: user.notification_email_for(@project.group), subject: subject("Exported issues")) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
end
end
end
......@@ -5,19 +5,6 @@ module EE
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
class_methods do
def labels_hash
issue_labels = Hash.new { |h, k| h[k] = [] }
relation = unscoped.where(id: self.select(:id)).eager_load(:labels)
relation.pluck(:id, 'labels.title').each do |issue_id, label|
issue_labels[issue_id] << label if label.present?
end
issue_labels
end
end
def supports_epic?
is_a?(Issue) && project.group
end
......
# frozen_string_literal: true
module EE
module Issues
module ExportCsvService
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
override :associations_to_preload
def associations_to_preload
return super unless epics_available?
super << :epic
end
override :header_to_value_hash
def header_to_value_hash
return super unless epics_available?
super.merge({
'Epic ID' => -> (issue) { issue.epic&.id },
'Epic Title' => -> (issue) { issue.epic&.title }
})
end
def epics_available?
strong_memoize(:epics_available) do
project.group&.feature_available?(:epics)
end
end
end
end
end
.issues-export-modal.modal.promotion-modal
.modal-dialog
.modal-content
.modal-header
%a.close{ href: '#', 'data-dismiss' => 'modal' } ×
.modal-body.center
%div
.svg-container
= custom_icon('icon_export_issues')
.user-callout-copy
%h4
Export issues with GitLab Enterprise Edition.
%p
Export Issues to CSV enables you and your team to export all the data collected from issues into a comma-separated values (CSV) file, which stores tabular data in plain text.
= link_to 'Read more', help_page_path('user/project/issues/csv_export.html'), target: '_blank'
= render 'shared/promotions/promotion_link_project'
......@@ -521,13 +521,6 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: export_csv
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
:idempotent:
- :name: ldap_group_sync
:feature_category: :authentication_and_authorization
:has_external_dependencies: true
......
......@@ -8,7 +8,6 @@ resources :issues, only: [], constraints: { id: /\d+/ } do
end
collection do
post :export_csv
get :service_desk
end
......
......@@ -7,65 +7,6 @@ describe Projects::IssuesController do
let(:project) { create(:project_empty_repo, :public, namespace: namespace) }
let(:user) { create(:user) }
describe 'POST export_csv' do
let(:viewer) { user }
let(:issue) { create(:issue, project: project) }
before do
project.add_developer(user)
sign_in(viewer) if viewer
allow(License).to receive(:feature_available?).and_call_original
end
def request_csv
post :export_csv, params: { namespace_id: project.namespace.to_param, project_id: project.to_param }
end
context 'globally licensed' do
it 'allows CSV export' do
expect(ExportCsvWorker).to receive(:perform_async).with(viewer.id, project.id, anything)
request_csv
expect(response).to redirect_to(project_issues_path(project))
expect(response.flash[:notice]).to match(/\AYour CSV export has started/i)
end
context 'anonymous user' do
let(:project) { create(:project_empty_repo, :public, namespace: namespace) }
let(:viewer) { nil }
it 'redirects to the sign in page' do
request_csv
expect(ExportCsvWorker).not_to receive(:perform_async)
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'licensed by namespace' do
let(:namespace) { create(:group, :private) }
let!(:gitlab_subscriptions) { create(:gitlab_subscription, :bronze, namespace: namespace) }
let(:project) { create(:project, namespace: namespace) }
before do
stub_application_setting(check_namespace_plan: true)
end
it 'allows CSV export' do
expect(ExportCsvWorker).to receive(:perform_async).with(viewer.id, project.id, anything)
request_csv
expect(response).to redirect_to(project_issues_path(project))
expect(response.flash[:notice]).to match(/\AYour CSV export has started/i)
end
end
end
describe 'licensed features' do
let(:project) { create(:project, group: namespace) }
let(:user) { create(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
require 'email_spec'
describe Emails::CsvExport do
include EmailSpec::Matchers
include_context 'gitlab email notification'
it 'adds email methods to Notify' do
subject.instance_methods.each do |email_method|
expect(Notify).to be_respond_to(email_method)
end
end
describe 'csv export email' do
let(:user) { create(:user) }
let(:empty_project) { create(:project, path: 'myproject') }
let(:export_status) { { truncated: false, rows_expected: 3, rows_written: 3 } }
subject { Notify.issues_csv_email(user, empty_project, "dummy content", export_status) }
let(:attachment) { subject.attachments.first }
it 'attachment has csv mime type' do
expect(attachment.mime_type).to eq 'text/csv'
end
it 'generates a useful filename' do
expect(attachment.filename).to include(Date.today.year.to_s)
expect(attachment.filename).to include('issues')
expect(attachment.filename).to include('myproject')
expect(attachment.filename).to end_with('.csv')
end
it 'mentions number of issues and project name' do
expect(subject).to have_content '3'
expect(subject).to have_content empty_project.name
end
it "doesn't need to mention truncation by default" do
expect(subject).not_to have_content 'truncated'
end
context 'when truncated' do
let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } }
it 'mentions that the csv has been truncated' do
expect(subject).to have_content 'truncated'
end
it 'mentions the number of issues written and expected' do
expect(subject).to have_content '10 of 12 issues'
end
end
end
end
......@@ -22,40 +22,6 @@ describe EE::Issuable do
end
end
describe '.labels_hash' do
let(:feature_label) { create(:label, title: 'Feature') }
let(:second_label) { create(:label, title: 'Second Label') }
let!(:issues) { create_list(:labeled_issue, 3, labels: [feature_label, second_label]) }
let(:issue_id) { issues.first.id }
it 'maps issue ids to labels titles' do
expect(Issue.labels_hash[issue_id]).to include('Feature')
end
it 'works on relations filtered by multiple labels' do
relation = Issue.with_label(['Feature', 'Second Label'])
expect(relation.labels_hash[issue_id]).to include('Feature', 'Second Label')
end
# This tests the workaround for the lack of a NOT NULL constraint in
# label_links.label_id:
# https://gitlab.com/gitlab-org/gitlab/issues/197307
context 'with a NULL label ID in the link' do
let(:issue) { create(:labeled_issue, labels: [feature_label, second_label]) }
before do
label_link = issue.label_links.find_by(label_id: second_label.id)
label_link.label_id = nil
label_link.save(validate: false)
end
it 'filters out bad labels' do
expect(Issue.where(id: issue.id).labels_hash[issue.id]).to match_array(['Feature'])
end
end
end
describe '#matches_cross_reference_regex?' do
context "epic description with long path string" do
let(:mentionable) { build(:epic, description: "/a" * 50000) }
......
......@@ -7,146 +7,14 @@ describe Issues::ExportCsvService do
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let!(:issue) { create(:issue, project: project, author: user) }
let!(:bad_issue) { create(:issue, project: project, author: user) }
let!(:issue2) { create(:issue, project: project, author: user) }
let(:subject) { described_class.new(Issue.all, project) }
it 'renders csv to string' do
expect(subject.csv_data).to be_a String
end
describe '#email' do
it 'emails csv' do
expect { subject.email(user) }.to change(ActionMailer::Base.deliveries, :count)
end
it 'renders with a target filesize' do
expect(subject.csv_builder).to receive(:render).with(described_class::TARGET_FILESIZE)
subject.email(user)
end
end
def csv
CSV.parse(subject.csv_data, headers: true)
end
context 'includes' do
let(:milestone) { create(:milestone, title: 'v1.0', project: project) }
let(:idea_label) { create(:label, project: project, title: 'Idea') }
let(:feature_label) { create(:label, project: project, title: 'Feature') }
before do
# Creating a timelog touches the updated_at timestamp of issue,
# so create these first.
issue.timelogs.create(time_spent: 360, user: user)
issue.timelogs.create(time_spent: 200, user: user)
issue.update!(milestone: milestone,
assignees: [user],
description: 'Issue with details',
state: :opened,
due_date: DateTime.new(2014, 3, 2),
created_at: DateTime.new(2015, 4, 3, 2, 1, 0),
updated_at: DateTime.new(2016, 5, 4, 3, 2, 1),
closed_at: DateTime.new(2017, 6, 5, 4, 3, 2),
weight: 4,
discussion_locked: true,
labels: [feature_label, idea_label],
time_estimate: 72000)
end
it 'includes the columns required for import' do
expect(csv.headers).to include('Title', 'Description')
end
specify 'iid' do
expect(csv[0]['Issue ID']).to eq issue.iid.to_s
end
specify 'url' do
expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/)
end
specify 'title' do
expect(csv[0]['Title']).to eq issue.title
end
specify 'state' do
expect(csv[0]['State']).to eq 'Open'
end
specify 'description' do
expect(csv[0]['Description']).to eq issue.description
expect(csv[1]['Description']).to eq nil
end
specify 'author name' do
expect(csv[0]['Author']).to eq issue.author_name
end
specify 'author username' do
expect(csv[0]['Author Username']).to eq issue.author.username
end
specify 'assignee name' do
expect(csv[0]['Assignee']).to eq user.name
expect(csv[1]['Assignee']).to eq ''
end
specify 'assignee username' do
expect(csv[0]['Assignee Username']).to eq user.username
expect(csv[1]['Assignee Username']).to eq ''
end
specify 'confidential' do
expect(csv[0]['Confidential']).to eq 'No'
end
specify 'milestone' do
expect(csv[0]['Milestone']).to eq issue.milestone.title
expect(csv[1]['Milestone']).to eq nil
end
specify 'labels' do
expect(csv[0]['Labels']).to eq 'Feature,Idea'
expect(csv[1]['Labels']).to eq nil
end
specify 'due_date' do
expect(csv[0]['Due Date']).to eq '2014-03-02'
expect(csv[1]['Due Date']).to eq nil
end
specify 'created_at' do
expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00'
end
specify 'updated_at' do
expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
end
specify 'closed_at' do
expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
expect(csv[1]['Closed At (UTC)']).to eq nil
end
specify 'discussion_locked' do
expect(csv[0]['Locked']).to eq 'Yes'
end
specify 'weight' do
expect(csv[0]['Weight']).to eq '4'
end
specify 'time estimate' do
expect(csv[0]['Time Estimate']).to eq '72000'
expect(csv[1]['Time Estimate']).to eq '0'
end
specify 'time spent' do
expect(csv[0]['Time Spent']).to eq '560'
expect(csv[1]['Time Spent']).to eq '0'
end
context 'handling epics' do
let(:epic) { create(:epic, group: group) }
......@@ -176,25 +44,5 @@ describe Issues::ExportCsvService do
end
end
end
context 'with issues filtered by labels and project' do
let(:subject) do
described_class.new(
IssuesFinder.new(user,
project_id: project.id,
label_name: %w(Idea Feature)).execute, project)
end
it 'returns only filtered objects' do
expect(csv.count).to eq(1)
expect(csv[0]['Issue ID']).to eq issue.iid.to_s
end
end
end
context 'with minimal details' do
it 'renders labels as nil' do
expect(csv[0]['Labels']).to eq nil
end
end
end
......@@ -17,11 +17,11 @@ module QA
element :issuable_weight
end
view 'ee/app/views/projects/issues/export_csv/_button.html.haml' do
view 'app/views/projects/issues/export_csv/_button.html.haml' do
element :export_as_csv_button
end
view 'ee/app/views/projects/issues/export_csv/_modal.html.haml' do
view 'app/views/projects/issues/export_csv/_modal.html.haml' do
element :export_issues_button
element :export_issues_modal
end
......
......@@ -1427,6 +1427,45 @@ describe Projects::IssuesController do
end
end
describe 'POST export_csv' do
let(:viewer) { user }
let(:issue) { create(:issue, project: project) }
before do
project.add_developer(user)
end
def request_csv
post :export_csv, params: { namespace_id: project.namespace.to_param, project_id: project.to_param }
end
context 'when logged in' do
before do
sign_in(viewer)
end
it 'allows CSV export' do
expect(ExportCsvWorker).to receive(:perform_async).with(viewer.id, project.id, anything)
request_csv
expect(response).to redirect_to(project_issues_path(project))
expect(response.flash[:notice]).to match(/\AYour CSV export has started/i)
end
end
context 'when not logged in' do
let(:project) { create(:project_empty_repo, :public) }
it 'redirects to the sign in page' do
request_csv
expect(ExportCsvWorker).not_to receive(:perform_async)
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe 'GET #discussions' do
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
......
......@@ -6,6 +6,12 @@ require 'email_spec'
describe Emails::Issues do
include EmailSpec::Matchers
it 'adds email methods to Notify' do
subject.instance_methods.each do |email_method|
expect(Notify).to be_respond_to(email_method)
end
end
describe "#import_issues_csv_email" do
let(:user) { create(:user) }
let(:project) { create(:project) }
......@@ -39,4 +45,47 @@ describe Emails::Issues do
it_behaves_like 'appearance header and footer not enabled'
end
end
describe '#issues_csv_email' do
let(:user) { create(:user) }
let(:empty_project) { create(:project, path: 'myproject') }
let(:export_status) { { truncated: false, rows_expected: 3, rows_written: 3 } }
let(:attachment) { subject.attachments.first }
subject { Notify.issues_csv_email(user, empty_project, "dummy content", export_status) }
include_context 'gitlab email notification'
it 'attachment has csv mime type' do
expect(attachment.mime_type).to eq 'text/csv'
end
it 'generates a useful filename' do
expect(attachment.filename).to include(Date.today.year.to_s)
expect(attachment.filename).to include('issues')
expect(attachment.filename).to include('myproject')
expect(attachment.filename).to end_with('.csv')
end
it 'mentions number of issues and project name' do
expect(subject).to have_content '3'
expect(subject).to have_content empty_project.name
end
it "doesn't need to mention truncation by default" do
expect(subject).not_to have_content 'truncated'
end
context 'when truncated' do
let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } }
it 'mentions that the csv has been truncated' do
expect(subject).to have_content 'truncated'
end
it 'mentions the number of issues written and expected' do
expect(subject).to have_content '10 of 12 issues'
end
end
end
end
......@@ -496,6 +496,40 @@ describe Issuable do
end
end
describe '.labels_hash' do
let(:feature_label) { create(:label, title: 'Feature') }
let(:second_label) { create(:label, title: 'Second Label') }
let!(:issues) { create_list(:labeled_issue, 3, labels: [feature_label, second_label]) }
let(:issue_id) { issues.first.id }
it 'maps issue ids to labels titles' do
expect(Issue.labels_hash[issue_id]).to include('Feature')
end
it 'works on relations filtered by multiple labels' do
relation = Issue.with_label(['Feature', 'Second Label'])
expect(relation.labels_hash[issue_id]).to include('Feature', 'Second Label')
end
# This tests the workaround for the lack of a NOT NULL constraint in
# label_links.label_id:
# https://gitlab.com/gitlab-org/gitlab/issues/197307
context 'with a NULL label ID in the link' do
let(:issue) { create(:labeled_issue, labels: [feature_label, second_label]) }
before do
label_link = issue.label_links.find_by(label_id: second_label.id)
label_link.label_id = nil
label_link.save(validate: false)
end
it 'filters out bad labels' do
expect(Issue.where(id: issue.id).labels_hash[issue.id]).to match_array(['Feature'])
end
end
end
describe '#user_notes_count' do
let(:project) { create(:project) }
let(:issue1) { create(:issue, project: project) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Issues::ExportCsvService do
let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let!(:issue) { create(:issue, project: project, author: user) }
let!(:bad_issue) { create(:issue, project: project, author: user) }
let(:subject) { described_class.new(Issue.all, project) }
it 'renders csv to string' do
expect(subject.csv_data).to be_a String
end
describe '#email' do
it 'emails csv' do
expect { subject.email(user) }.to change(ActionMailer::Base.deliveries, :count)
end
it 'renders with a target filesize' do
expect(subject.csv_builder).to receive(:render).with(described_class::TARGET_FILESIZE)
subject.email(user)
end
end
def csv
CSV.parse(subject.csv_data, headers: true)
end
context 'includes' do
let(:milestone) { create(:milestone, title: 'v1.0', project: project) }
let(:idea_label) { create(:label, project: project, title: 'Idea') }
let(:feature_label) { create(:label, project: project, title: 'Feature') }
before do
# Creating a timelog touches the updated_at timestamp of issue,
# so create these first.
issue.timelogs.create(time_spent: 360, user: user)
issue.timelogs.create(time_spent: 200, user: user)
issue.update!(milestone: milestone,
assignees: [user],
description: 'Issue with details',
state: :opened,
due_date: DateTime.new(2014, 3, 2),
created_at: DateTime.new(2015, 4, 3, 2, 1, 0),
updated_at: DateTime.new(2016, 5, 4, 3, 2, 1),
closed_at: DateTime.new(2017, 6, 5, 4, 3, 2),
weight: 4,
discussion_locked: true,
labels: [feature_label, idea_label],
time_estimate: 72000)
end
it 'includes the columns required for import' do
expect(csv.headers).to include('Title', 'Description')
end
specify 'iid' do
expect(csv[0]['Issue ID']).to eq issue.iid.to_s
end
specify 'url' do
expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/)
end
specify 'title' do
expect(csv[0]['Title']).to eq issue.title
end
specify 'state' do
expect(csv[0]['State']).to eq 'Open'
end
specify 'description' do
expect(csv[0]['Description']).to eq issue.description
expect(csv[1]['Description']).to eq nil
end
specify 'author name' do
expect(csv[0]['Author']).to eq issue.author_name
end
specify 'author username' do
expect(csv[0]['Author Username']).to eq issue.author.username
end
specify 'assignee name' do
expect(csv[0]['Assignee']).to eq user.name
expect(csv[1]['Assignee']).to eq ''
end
specify 'assignee username' do
expect(csv[0]['Assignee Username']).to eq user.username
expect(csv[1]['Assignee Username']).to eq ''
end
specify 'confidential' do
expect(csv[0]['Confidential']).to eq 'No'
end
specify 'milestone' do
expect(csv[0]['Milestone']).to eq issue.milestone.title
expect(csv[1]['Milestone']).to eq nil
end
specify 'labels' do
expect(csv[0]['Labels']).to eq 'Feature,Idea'
expect(csv[1]['Labels']).to eq nil
end
specify 'due_date' do
expect(csv[0]['Due Date']).to eq '2014-03-02'
expect(csv[1]['Due Date']).to eq nil
end
specify 'created_at' do
expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00'
end
specify 'updated_at' do
expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
end
specify 'closed_at' do
expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
expect(csv[1]['Closed At (UTC)']).to eq nil
end
specify 'discussion_locked' do
expect(csv[0]['Locked']).to eq 'Yes'
end
specify 'weight' do
expect(csv[0]['Weight']).to eq '4'
end
specify 'time estimate' do
expect(csv[0]['Time Estimate']).to eq '72000'
expect(csv[1]['Time Estimate']).to eq '0'
end
specify 'time spent' do
expect(csv[0]['Time Spent']).to eq '560'
expect(csv[1]['Time Spent']).to eq '0'
end
context 'with issues filtered by labels and project' do
let(:subject) do
described_class.new(
IssuesFinder.new(user,
project_id: project.id,
label_name: %w(Idea Feature)).execute, project)
end
it 'returns only filtered objects' do
expect(csv.count).to eq(1)
expect(csv[0]['Issue ID']).to eq issue.iid.to_s
end
end
end
context 'with minimal details' do
it 'renders labels as nil' do
expect(csv[0]['Labels']).to eq nil
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment