Commit 6d9b901d authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'issue_212330_2' into 'master'

Move export issues feature to core

See merge request gitlab-org/gitlab!28703
parents 5cdaf0ec 9492ef31
......@@ -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,9 +131,22 @@ module Issuable
strip_attributes :title
def self.locking_enabled?
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
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
......@@ -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