Commit a9dbdac0 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '8948-epics-presenter' into 'master'

Move code from EpicsHelper to a presenter

Closes #8948

See merge request gitlab-org/gitlab-ee!10849
parents b2f7e472 950d9ba2
# frozen_string_literal: true
module EpicsHelper
include EntityDateHelper
# rubocop: disable Metrics/AbcSize
def epic_show_app_data(epic, opts)
group = epic.group
todo = epic_pending_todo(epic)
epic_meta = {
epic_id: epic.id,
created: epic.created_at,
author: epic_author(epic, opts),
ancestors: epic_ancestors(epic.ancestors.inc_group),
todo_exists: todo.present?,
todo_path: group_todos_path(group),
start_date: epic.start_date,
start_date_is_fixed: epic.start_date_is_fixed?,
start_date_fixed: epic.start_date_fixed,
start_date_from_milestones: epic.start_date_from_milestones,
start_date_sourcing_milestone_title: epic.start_date_sourcing_milestone&.title,
start_date_sourcing_milestone_dates: {
start_date: epic.start_date_sourcing_milestone&.start_date,
due_date: epic.start_date_sourcing_milestone&.due_date
},
due_date: epic.due_date,
due_date_is_fixed: epic.due_date_is_fixed?,
due_date_fixed: epic.due_date_fixed,
due_date_from_milestones: epic.due_date_from_milestones,
due_date_sourcing_milestone_title: epic.due_date_sourcing_milestone&.title,
due_date_sourcing_milestone_dates: {
start_date: epic.due_date_sourcing_milestone&.start_date,
due_date: epic.due_date_sourcing_milestone&.due_date
},
lock_version: epic.lock_version,
end_date: epic.end_date,
state: epic.state,
namespace: group.path,
labels_path: group_labels_path(group, format: :json, only_group_labels: true, include_ancestor_groups: true),
toggle_subscription_path: toggle_subscription_group_epic_path(group, epic),
labels_web_url: group_labels_path(group),
epics_web_url: group_epics_path(group),
scoped_labels: group.feature_available?(:scoped_labels),
scoped_labels_documentation_link: help_page_path('user/project/labels.md', anchor: 'scoped-labels')
}
epic_meta[:todo_delete_path] = dashboard_todo_path(todo) if todo.present?
participants = UserSerializer.new.represent(epic.participants)
initial = opts[:initial].merge(labels: epic.labels,
participants: participants,
subscribed: epic.subscribed?(current_user))
# TODO: Remove from `namespace` to epics_web_url
# from below as it is already included in `epic_meta`
{
initial: initial.to_json,
meta: epic_meta.to_json,
namespace: group.path,
labels_path: group_labels_path(group, format: :json, only_group_labels: true, include_ancestor_groups: true),
toggle_subscription_path: toggle_subscription_group_epic_path(group, epic),
labels_web_url: group_labels_path(group),
epics_web_url: group_epics_path(group)
}
end
# rubocop: enable Metrics/AbcSize
def epic_pending_todo(epic)
current_user.pending_todo_for(epic) if current_user
end
def epic_author(epic, opts)
{
name: epic.author.name,
url: user_path(epic.author),
username: "@#{epic.author.username}",
src: opts[:author_icon]
}
end
def epic_ancestors(epics)
epics.map do |epic|
{
id: epic.id,
title: epic.title,
url: epic_path(epic),
state: epic.state,
human_readable_end_date: epic.end_date&.to_s(:medium),
human_readable_timestamp: remaining_days_in_words(epic.end_date, epic.start_date)
}
end
def epic_show_app_data(epic)
EpicPresenter.new(epic, current_user: current_user).show_data(author_icon: avatar_icon_for_user(epic.author), base_data: issuable_initial_data(epic))
end
def epic_endpoint_query_params(opts)
opts[:data] ||= {}
opts[:data][:endpoint_query_params] = {
only_group_labels: true,
include_ancestor_groups: true,
include_descendant_groups: true
only_group_labels: true,
include_ancestor_groups: true,
include_descendant_groups: true
}.to_json
opts
......
# frozen_string_literal: true
class EpicPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include EntityDateHelper
presents :epic
def show_data(base_data: {}, author_icon: nil)
{
initial: initial_data.merge(base_data).to_json,
meta: meta_data(author_icon).to_json
}
end
private
def initial_data
{
labels: epic.labels,
participants: participants,
subscribed: epic.subscribed?(current_user)
}
end
def meta_data(author_icon)
{}.tap do |hash|
hash.merge!(base_attributes(author_icon))
hash.merge!(endpoints)
hash.merge!(start_dates)
hash.merge!(due_dates)
end
end
def base_attributes(author_icon)
{
epic_id: epic.id,
created: epic.created_at,
author: epic_author(author_icon),
ancestors: epic_ancestors(epic.ancestors.inc_group),
todo_exists: epic_pending_todo.present?,
todo_path: group_todos_path(group),
lock_version: epic.lock_version,
state: epic.state,
scoped_labels: group.feature_available?(:scoped_labels)
}
end
def endpoints
paths = {
namespace: group.path,
labels_path: group_labels_path(group, format: :json, only_group_labels: true, include_ancestor_groups: true),
toggle_subscription_path: toggle_subscription_group_epic_path(group, epic),
labels_web_url: group_labels_path(group),
epics_web_url: group_epics_path(group),
scoped_labels_documentation_link: help_page_path('user/project/labels.md', anchor: 'scoped-labels')
}
paths[:todo_delete_path] = dashboard_todo_path(epic_pending_todo) if epic_pending_todo.present?
paths
end
def start_dates
{
start_date: epic.start_date,
start_date_is_fixed: epic.start_date_is_fixed?,
start_date_fixed: epic.start_date_fixed,
start_date_from_milestones: epic.start_date_from_milestones,
start_date_sourcing_milestone_title: epic.start_date_sourcing_milestone&.title,
start_date_sourcing_milestone_dates: {
start_date: epic.start_date_sourcing_milestone&.start_date,
due_date: epic.start_date_sourcing_milestone&.due_date
}
}
end
def due_dates
{
due_date: epic.due_date,
due_date_is_fixed: epic.due_date_is_fixed?,
due_date_fixed: epic.due_date_fixed,
due_date_from_milestones: epic.due_date_from_milestones,
due_date_sourcing_milestone_title: epic.due_date_sourcing_milestone&.title,
due_date_sourcing_milestone_dates: {
start_date: epic.due_date_sourcing_milestone&.start_date,
due_date: epic.due_date_sourcing_milestone&.due_date
}
}
end
def participants
UserEntity.represent(epic.participants)
end
def epic_pending_todo
current_user.pending_todo_for(epic) if current_user
end
def epic_author(author_icon)
{
name: epic.author.name,
url: user_path(epic.author),
username: "@#{epic.author.username}",
src: author_icon
}
end
def epic_ancestors(epics)
epics.map do |epic|
{
id: epic.id,
title: epic.title,
url: epic_path(epic),
state: epic.state,
human_readable_end_date: epic.end_date&.to_s(:medium),
human_readable_timestamp: remaining_days_in_words(epic.end_date, epic.start_date)
}
end
end
end
......@@ -11,7 +11,7 @@
- page_card_attributes @epic.card_attributes
#epic-app-root{ data: epic_show_app_data(@epic, author_icon: avatar_icon_for_user(@epic.author), initial: issuable_initial_data(@epic)) }
#epic-app-root{ data: epic_show_app_data(@epic) }
.content-block.content-block-small.emoji-list-container.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @epic, inline: true
......
{
"additionalProperties": false,
"type": "object",
"properties": {
"labels": {},
"participants": {},
"subscribed": {}
}
}
{
"additionalProperties": false,
"type": "object",
"required": ["epic_id", "created", "author", "ancestors", "todo_exists", "todo_path", "lock_version",
"state", "namespace", "labels_path", "toggle_subscription_path", "labels_web_url", "epics_web_url",
"scoped_labels", "scoped_labels_documentation_link", "start_date", "start_date_is_fixed", "start_date_fixed",
"start_date_from_milestones", "start_date_sourcing_milestone_title", "start_date_sourcing_milestone_dates",
"due_date", "due_date_is_fixed", "due_date_fixed",
"due_date_from_milestones", "due_date_sourcing_milestone_title", "due_date_sourcing_milestone_dates"],
"properties": {
"epic_id": {
"type": "integer"
},
"created": {
"type": "string"
},
"author": {
"type": [
"object",
"null"
],
"required": [
"name",
"url",
"username",
"src"
],
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
},
"username": {
"type": "string"
},
"src": {
"type": "string"
}
}
},
"ancestors": {
"items": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"state": {
"type": "string"
},
"human_readable_end_date": {
"type": "string"
},
"human_readable_timestamp": {
"type": "string"
}
},
"type": "array"
},
"todo_exists": {
"type": "boolean"
},
"todo_path": {
"type": "string"
},
"lock_version": {
"type": [
"integer",
null
]
},
"state": {
"type": "string"
},
"namespace": {
"type": "string"
},
"labels_path": {
"type": "string"
},
"toggle_subscription_path": {
"type": "string"
},
"labels_web_url": {
"type": "string"
},
"epics_web_url": {
"type": "string"
},
"scoped_labels": {
"type": "boolean"
},
"scoped_labels_documentation_link": {
"type": "string"
},
"start_date": {
"type": [
"string",
null
]
},
"start_date_is_fixed": {
"type": "boolean"
},
"start_date_fixed": {
"type": [
"string",
null
]
},
"start_date_from_milestones": {
"type": [
"string",
null
]
},
"start_date_sourcing_milestone_title": {
"type": "string"
},
"start_date_sourcing_milestone_dates": {
"type": [
"object",
"null"
],
"required": [
"start_date",
"due_date"
],
"properties": {
"start_date": {
"type": [
"string",
null
]
},
"due_date": {
"type": [
"string",
null
]
}
}
},
"due_date": {
"type": [
"string",
null
]
},
"due_date_is_fixed": {
"type": "boolean"
},
"due_date_fixed": {
"type": [
"string",
null
]
},
"due_date_from_milestones": {
"type": [
"string",
null
]
},
"due_date_sourcing_milestone_title": {
"type": "string"
},
"due_date_sourcing_milestone_dates": {
"type": [
"object",
"null"
],
"required": [
"start_date",
"due_date"
],
"properties": {
"start_date": {
"type": [
"string",
null
]
},
"due_date": {
"type": [
"string",
null
]
}
}
}
}
}
require 'spec_helper'
describe EpicsHelper do
describe EpicsHelper, type: :helper do
include ApplicationHelper
describe '#epic_show_app_data' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:milestone1) { create(:milestone, title: 'make me a sandwich', start_date: '2010-01-01', due_date: '2019-12-31') }
let(:milestone2) { create(:milestone, title: 'make me a pizza', start_date: '2020-01-01', due_date: '2029-12-31') }
let(:parent_epic) { create(:epic, group: group, start_date: Date.new(2000, 1, 10), due_date: Date.new(2000, 1, 20)) }
let!(:epic) do
create(
:epic,
group: group,
author: user,
start_date_sourcing_milestone: milestone1,
start_date: Date.new(2000, 1, 1),
due_date_sourcing_milestone: milestone2,
due_date: Date.new(2000, 1, 2),
parent: parent_epic
)
end
before do
allow(helper).to receive(:current_user).and_return(user)
stub_licensed_features(epics: true)
describe '#epic_endpoint_query_params' do
let(:endpoint_data) do
{
only_group_labels: true,
include_ancestor_groups: true,
include_descendant_groups: true
}
end
it 'returns the correct json' do
data = helper.epic_show_app_data(epic, initial: {}, author_icon: 'icon_path')
meta_data = JSON.parse(data[:meta])
it 'includes Epic specific options in JSON format' do
opts = epic_endpoint_query_params({})
expected_keys = %i(initial meta namespace labels_path toggle_subscription_path labels_web_url epics_web_url)
expect(data.keys).to match_array(expected_keys)
expect(meta_data.keys).to match_array(%w[
epic_id created author todo_exists todo_path start_date ancestors
start_date_is_fixed start_date_fixed start_date_from_milestones
start_date_sourcing_milestone_title start_date_sourcing_milestone_dates
due_date due_date_is_fixed due_date_fixed due_date_from_milestones due_date_sourcing_milestone_title
due_date_sourcing_milestone_dates end_date state namespace labels_path toggle_subscription_path
labels_web_url epics_web_url lock_version scoped_labels scoped_labels_documentation_link
])
expect(meta_data['author']).to eq({
'name' => user.name,
'url' => "/#{user.username}",
'username' => "@#{user.username}",
'src' => 'icon_path'
})
expect(meta_data['start_date']).to eq('2000-01-01')
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone1.title)
expect(meta_data['start_date_sourcing_milestone_dates']['start_date']).to eq(milestone1.start_date.to_s)
expect(meta_data['start_date_sourcing_milestone_dates']['due_date']).to eq(milestone1.due_date.to_s)
expect(meta_data['due_date']).to eq('2000-01-02')
expect(meta_data['due_date_sourcing_milestone_title']).to eq(milestone2.title)
expect(meta_data['due_date_sourcing_milestone_dates']['start_date']).to eq(milestone2.start_date.to_s)
expect(meta_data['due_date_sourcing_milestone_dates']['due_date']).to eq(milestone2.due_date.to_s)
expect(opts[:data][:endpoint_query_params]).to eq(endpoint_data.to_json)
end
it 'returns a list of epic ancestors', :nested_groups do
data = helper.epic_show_app_data(epic, initial: {}, author_icon: 'icon_path')
meta_data = JSON.parse(data[:meta])
it 'includes data provided in param' do
opts = epic_endpoint_query_params(data: { default_param: true })
expect(meta_data['ancestors']).to eq([{
'id' => parent_epic.id,
'title' => parent_epic.title,
'url' => "/groups/#{group.full_path}/-/epics/#{parent_epic.iid}",
'state' => 'opened',
'human_readable_end_date' => 'Jan 20, 2000',
'human_readable_timestamp' => '<strong>Past due</strong>'
}])
expect(opts[:data]).to eq({ default_param: true }.merge(endpoint_query_params: endpoint_data.to_json))
end
end
it 'avoids N+1 database queries', :nested_groups do
group1 = create(:group)
group2 = create(:group, parent: group1)
epic1 = create(:epic, group: group1)
epic2 = create(:epic, group: group1, parent: epic1)
epic3 = create(:epic, group: group2, parent: epic2)
control_count = ActiveRecord::QueryRecorder.new { helper.epic_show_app_data(epic2, initial: {}) }
expect { helper.epic_show_app_data(epic3, initial: {}) }.not_to exceed_query_limit(control_count)
describe '#epic_state_dropdown_link' do
it 'returns the active link when selected state is same as the link' do
expect(helper.epic_state_dropdown_link(:opened, :opened))
.to eq('<a class="is-active" href="?state=opened">Open epics</a>')
end
context 'when a user can update an epic' do
let(:milestone) { create(:milestone, title: 'make me a sandwich') }
let!(:epic) do
create(
:epic,
author: user,
start_date_sourcing_milestone: milestone1,
start_date: Date.new(2000, 1, 1),
due_date_sourcing_milestone: milestone2,
due_date: Date.new(2000, 1, 2)
)
end
before do
epic.group.add_developer(user)
end
it 'returns extra date fields' do
data = helper.epic_show_app_data(epic, initial: {}, author_icon: 'icon_path')
meta_data = JSON.parse(data[:meta])
expect(meta_data.keys).to match_array(%w[
epic_id created author todo_exists todo_path start_date ancestors
start_date_is_fixed start_date_fixed start_date_from_milestones
start_date_sourcing_milestone_title start_date_sourcing_milestone_dates
due_date due_date_is_fixed due_date_fixed due_date_from_milestones due_date_sourcing_milestone_title
due_date_sourcing_milestone_dates end_date state namespace labels_path toggle_subscription_path
labels_web_url epics_web_url lock_version scoped_labels scoped_labels_documentation_link
])
expect(meta_data['start_date']).to eq('2000-01-01')
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone1.title)
expect(meta_data['start_date_sourcing_milestone_dates']['start_date']).to eq(milestone1.start_date.to_s)
expect(meta_data['start_date_sourcing_milestone_dates']['due_date']).to eq(milestone1.due_date.to_s)
expect(meta_data['due_date']).to eq('2000-01-02')
expect(meta_data['due_date_sourcing_milestone_title']).to eq(milestone2.title)
expect(meta_data['due_date_sourcing_milestone_dates']['start_date']).to eq(milestone2.start_date.to_s)
expect(meta_data['due_date_sourcing_milestone_dates']['due_date']).to eq(milestone2.due_date.to_s)
end
it 'returns the non-active link when selected state is different from the link' do
expect(helper.epic_state_dropdown_link(:opened, :closed))
.to eq('<a class="" href="?state=opened">Open epics</a>')
end
end
describe '#epic_endpoint_query_params' do
it 'includes Epic specific options in JSON format' do
opts = epic_endpoint_query_params({})
describe '#epic_state_title' do
it 'returns "Open" when the state is opened' do
expect(epic_state_title(:opened)).to eq('Open epics')
end
expected = "{\"only_group_labels\":true,\"include_ancestor_groups\":true,\"include_descendant_groups\":true}"
expect(opts[:data][:endpoint_query_params]).to eq(expected)
it 'returns humanized string when the state is other than opened' do
expect(epic_state_title(:some_other_state)).to eq('Some other state epics')
end
end
end
......@@ -38,13 +38,11 @@ describe 'Epics (JavaScript fixtures)' do
clean_frontend_fixtures('epic/')
end
describe EpicsHelper, '(JavaScript fixtures)', type: :helper do
before do
allow(helper).to receive(:current_user).and_return(user)
end
describe EpicPresenter, '(JavaScript fixtures)', type: :presenter do
it 'epic/mock_meta.json' do |example|
result = helper.epic_show_app_data(epic, initial: {}, author_icon: 'icon_path')
presenter = EpicPresenter.new(epic, current_user: user)
result = presenter.show_data(base_data: {}, author_icon: 'icon_path')
store_frontend_fixture(result.to_json, example.description)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe EpicPresenter do
include UsersHelper
describe '#show_data' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:milestone1) { create(:milestone, title: 'make me a sandwich', start_date: '2010-01-01', due_date: '2019-12-31') }
let(:milestone2) { create(:milestone, title: 'make me a pizza', start_date: '2020-01-01', due_date: '2029-12-31') }
let(:parent_epic) { create(:epic, group: group, start_date: Date.new(2000, 1, 10), due_date: Date.new(2000, 1, 20)) }
let(:epic) do
create(
:epic,
group: group,
author: user,
start_date_sourcing_milestone: milestone1,
start_date: Date.new(2000, 1, 1),
due_date_sourcing_milestone: milestone2,
due_date: Date.new(2000, 1, 2),
parent: parent_epic
)
end
let(:presenter) { described_class.new(epic, current_user: user) }
before do
stub_licensed_features(epics: true)
end
it 'has correct keys' do
expect(presenter.show_data.keys).to match_array([:initial, :meta])
end
it 'returns the correct json schema for epic initial data' do
data = presenter.show_data(author_icon: 'icon_path')
expect(data[:initial]).to match_schema('epic_initial_data', dir: 'ee')
end
it 'returns the correct json schema for epic meta data' do
data = presenter.show_data(author_icon: 'icon_path')
expect(data[:meta]).to match_schema('epic_meta_data', dir: 'ee')
end
it 'avoids N+1 database queries', :nested_groups do
group1 = create(:group)
group2 = create(:group, parent: group1)
epic1 = create(:epic, group: group1)
epic2 = create(:epic, group: group1, parent: epic1)
create(:epic, group: group2, parent: epic2)
control_count = ActiveRecord::QueryRecorder.new { presenter.show_data }
expect { presenter.show_data }.not_to exceed_query_limit(control_count)
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