Commit 186c51b5 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '321625-epic_boards-redirect' into 'master'

Redirect to the last visited epic board

See merge request gitlab-org/gitlab!60720
parents a09996b3 6354cfdf
...@@ -19,7 +19,7 @@ module BoardsActions ...@@ -19,7 +19,7 @@ module BoardsActions
def show def show
# Add / update the board in the recent visits table # Add / update the board in the recent visits table
Boards::Visits::CreateService.new(parent, current_user).execute(board) if request.format.html? board_visit_service.new(parent, current_user).execute(board) if request.format.html?
respond_with_board respond_with_board
end end
...@@ -52,6 +52,10 @@ module BoardsActions ...@@ -52,6 +52,10 @@ module BoardsActions
board_klass.to_type board_klass.to_type
end end
def board_visit_service
Boards::Visits::CreateService
end
def serializer def serializer
BoardSerializer.new(current_user: current_user) BoardSerializer.new(current_user: current_user)
end end
......
...@@ -2,27 +2,19 @@ ...@@ -2,27 +2,19 @@
# Tracks which boards in a specific group a user has visited # Tracks which boards in a specific group a user has visited
class BoardGroupRecentVisit < ApplicationRecord class BoardGroupRecentVisit < ApplicationRecord
include BoardRecentVisit
belongs_to :user belongs_to :user
belongs_to :group belongs_to :group
belongs_to :board belongs_to :board
validates :user, presence: true validates :user, presence: true
validates :group, presence: true validates :group, presence: true
validates :board, presence: true validates :board, presence: true
scope :by_user_group, -> (user, group) { where(user: user, group: group) } scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
def self.visited!(user, board)
visit = find_or_create_by(user: user, group: board.group, board: board)
visit.touch if visit.updated_at < Time.current
rescue ActiveRecord::RecordNotUnique
retry
end
def self.latest(user, group, count: nil)
visits = by_user_group(user, group).order(updated_at: :desc)
visits = visits.preload(:board) if count && count > 1
visits.first(count) def self.board_parent_relation
:group
end end
end end
...@@ -2,27 +2,19 @@ ...@@ -2,27 +2,19 @@
# Tracks which boards in a specific project a user has visited # Tracks which boards in a specific project a user has visited
class BoardProjectRecentVisit < ApplicationRecord class BoardProjectRecentVisit < ApplicationRecord
include BoardRecentVisit
belongs_to :user belongs_to :user
belongs_to :project belongs_to :project
belongs_to :board belongs_to :board
validates :user, presence: true validates :user, presence: true
validates :project, presence: true validates :project, presence: true
validates :board, presence: true validates :board, presence: true
scope :by_user_project, -> (user, project) { where(user: user, project: project) } scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
def self.visited!(user, board)
visit = find_or_create_by(user: user, project: board.project, board: board)
visit.touch if visit.updated_at < Time.current
rescue ActiveRecord::RecordNotUnique
retry
end
def self.latest(user, project, count: nil)
visits = by_user_project(user, project).order(updated_at: :desc)
visits = visits.preload(:board) if count && count > 1
visits.first(count) def self.board_parent_relation
:project
end end
end end
# frozen_string_literal: true
module BoardRecentVisit
extend ActiveSupport::Concern
class_methods do
def visited!(user, board)
find_or_create_by(
"user" => user,
board_parent_relation => board.resource_parent,
board_relation => board
).tap do |visit|
visit.touch
end
rescue ActiveRecord::RecordNotUnique
retry
end
def latest(user, parent, count: nil)
visits = by_user_parent(user, parent).order(updated_at: :desc)
visits = visits.preload(board_relation)
visits.first(count)
end
def board_relation
:board
end
def board_parent_relation
raise NotImplementedError
end
end
end
...@@ -5,13 +5,17 @@ module Boards ...@@ -5,13 +5,17 @@ module Boards
class CreateService < Boards::BaseService class CreateService < Boards::BaseService
def execute(board) def execute(board)
return unless current_user && Gitlab::Database.read_write? return unless current_user && Gitlab::Database.read_write?
return unless board.is_a?(Board) # other board types do not support board visits yet return unless board
if parent.is_a?(Group) model.visited!(current_user, board)
BoardGroupRecentVisit.visited!(current_user, board) end
else
BoardProjectRecentVisit.visited!(current_user, board) private
end
def model
return BoardGroupRecentVisit if parent.is_a?(Group)
BoardProjectRecentVisit
end end
end end
end end
......
---
title: Redirect to the last visited epic board
merge_request: 60720
author:
type: added
# frozen_string_literal: true
class AddEpicBoardRecentVisitsTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
def up
with_lock_retries do
create_table :boards_epic_board_recent_visits do |t|
t.references :user, index: true, null: false, foreign_key: { on_delete: :cascade }
t.references :epic_board, index: true, foreign_key: { to_table: :boards_epic_boards, on_delete: :cascade }, null: false
t.references :group, index: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }, null: false
t.timestamps_with_timezone null: false
end
end
end
def down
with_lock_retries do
drop_table :boards_epic_board_recent_visits
end
end
end
# frozen_string_literal: true
class AddIndexToEpicBoardRecentVisits < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_epic_board_recent_visits_on_user_group_and_board'
disable_ddl_transaction!
def up
add_concurrent_index :boards_epic_board_recent_visits,
[:user_id, :group_id, :epic_board_id],
name: INDEX_NAME,
unique: true
end
def down
remove_concurrent_index_by_name :boards_epic_board_recent_visits, INDEX_NAME
end
end
c92824844732aad7277fc8b50bac0bf6919ad0a8d72e73b4ec3b89eafc085b7d
\ No newline at end of file
ae3c8336cb25efa7d23357a6777c0656dbe1a216efb5d4edcf923d1128f7e1e3
\ No newline at end of file
...@@ -10057,6 +10057,24 @@ CREATE SEQUENCE boards_epic_board_positions_id_seq ...@@ -10057,6 +10057,24 @@ CREATE SEQUENCE boards_epic_board_positions_id_seq
ALTER SEQUENCE boards_epic_board_positions_id_seq OWNED BY boards_epic_board_positions.id; ALTER SEQUENCE boards_epic_board_positions_id_seq OWNED BY boards_epic_board_positions.id;
CREATE TABLE boards_epic_board_recent_visits (
id bigint NOT NULL,
user_id bigint NOT NULL,
epic_board_id bigint NOT NULL,
group_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE boards_epic_board_recent_visits_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE boards_epic_board_recent_visits_id_seq OWNED BY boards_epic_board_recent_visits.id;
CREATE TABLE boards_epic_boards ( CREATE TABLE boards_epic_boards (
id bigint NOT NULL, id bigint NOT NULL,
hide_backlog_list boolean DEFAULT false NOT NULL, hide_backlog_list boolean DEFAULT false NOT NULL,
...@@ -19353,6 +19371,8 @@ ALTER TABLE ONLY boards_epic_board_labels ALTER COLUMN id SET DEFAULT nextval('b ...@@ -19353,6 +19371,8 @@ ALTER TABLE ONLY boards_epic_board_labels ALTER COLUMN id SET DEFAULT nextval('b
ALTER TABLE ONLY boards_epic_board_positions ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_positions_id_seq'::regclass); ALTER TABLE ONLY boards_epic_board_positions ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_positions_id_seq'::regclass);
ALTER TABLE ONLY boards_epic_board_recent_visits ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_recent_visits_id_seq'::regclass);
ALTER TABLE ONLY boards_epic_boards ALTER COLUMN id SET DEFAULT nextval('boards_epic_boards_id_seq'::regclass); ALTER TABLE ONLY boards_epic_boards ALTER COLUMN id SET DEFAULT nextval('boards_epic_boards_id_seq'::regclass);
ALTER TABLE ONLY boards_epic_list_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_list_user_preferences_id_seq'::regclass); ALTER TABLE ONLY boards_epic_list_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_list_user_preferences_id_seq'::regclass);
...@@ -20468,6 +20488,9 @@ ALTER TABLE ONLY boards_epic_board_labels ...@@ -20468,6 +20488,9 @@ ALTER TABLE ONLY boards_epic_board_labels
ALTER TABLE ONLY boards_epic_board_positions ALTER TABLE ONLY boards_epic_board_positions
ADD CONSTRAINT boards_epic_board_positions_pkey PRIMARY KEY (id); ADD CONSTRAINT boards_epic_board_positions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY boards_epic_board_recent_visits
ADD CONSTRAINT boards_epic_board_recent_visits_pkey PRIMARY KEY (id);
ALTER TABLE ONLY boards_epic_boards ALTER TABLE ONLY boards_epic_boards
ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id); ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id);
...@@ -22293,6 +22316,12 @@ CREATE INDEX index_boards_epic_board_positions_on_epic_id ON boards_epic_board_p ...@@ -22293,6 +22316,12 @@ CREATE INDEX index_boards_epic_board_positions_on_epic_id ON boards_epic_board_p
CREATE INDEX index_boards_epic_board_positions_on_scoped_relative_position ON boards_epic_board_positions USING btree (epic_board_id, epic_id, relative_position); CREATE INDEX index_boards_epic_board_positions_on_scoped_relative_position ON boards_epic_board_positions USING btree (epic_board_id, epic_id, relative_position);
CREATE INDEX index_boards_epic_board_recent_visits_on_epic_board_id ON boards_epic_board_recent_visits USING btree (epic_board_id);
CREATE INDEX index_boards_epic_board_recent_visits_on_group_id ON boards_epic_board_recent_visits USING btree (group_id);
CREATE INDEX index_boards_epic_board_recent_visits_on_user_id ON boards_epic_board_recent_visits USING btree (user_id);
CREATE INDEX index_boards_epic_boards_on_group_id ON boards_epic_boards USING btree (group_id); CREATE INDEX index_boards_epic_boards_on_group_id ON boards_epic_boards USING btree (group_id);
CREATE INDEX index_boards_epic_list_user_preferences_on_epic_list_id ON boards_epic_list_user_preferences USING btree (epic_list_id); CREATE INDEX index_boards_epic_list_user_preferences_on_epic_list_id ON boards_epic_list_user_preferences USING btree (epic_list_id);
...@@ -22857,6 +22886,8 @@ CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING ...@@ -22857,6 +22886,8 @@ CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING
CREATE UNIQUE INDEX index_epic_board_list_preferences_on_user_and_list ON boards_epic_list_user_preferences USING btree (user_id, epic_list_id); CREATE UNIQUE INDEX index_epic_board_list_preferences_on_user_and_list ON boards_epic_list_user_preferences USING btree (user_id, epic_list_id);
CREATE UNIQUE INDEX index_epic_board_recent_visits_on_user_group_and_board ON boards_epic_board_recent_visits USING btree (user_id, group_id, epic_board_id);
CREATE INDEX index_epic_issues_on_epic_id ON epic_issues USING btree (epic_id); CREATE INDEX index_epic_issues_on_epic_id ON epic_issues USING btree (epic_id);
CREATE INDEX index_epic_issues_on_epic_id_and_issue_id ON epic_issues USING btree (epic_id, issue_id); CREATE INDEX index_epic_issues_on_epic_id_and_issue_id ON epic_issues USING btree (epic_id, issue_id);
...@@ -26626,6 +26657,9 @@ ALTER TABLE ONLY packages_rubygems_metadata ...@@ -26626,6 +26657,9 @@ ALTER TABLE ONLY packages_rubygems_metadata
ALTER TABLE ONLY packages_pypi_metadata ALTER TABLE ONLY packages_pypi_metadata
ADD CONSTRAINT fk_rails_9698717cdd FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_9698717cdd FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
ALTER TABLE ONLY boards_epic_board_recent_visits
ADD CONSTRAINT fk_rails_96c2c18642 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY packages_dependency_links ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
...@@ -26890,6 +26924,9 @@ ALTER TABLE ONLY pages_deployments ...@@ -26890,6 +26924,9 @@ ALTER TABLE ONLY pages_deployments
ALTER TABLE ONLY merge_request_user_mentions ALTER TABLE ONLY merge_request_user_mentions
ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY boards_epic_board_recent_visits
ADD CONSTRAINT fk_rails_c4dcba4a3e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_job_artifacts ALTER TABLE ONLY ci_job_artifacts
ADD CONSTRAINT fk_rails_c5137cb2c1 FOREIGN KEY (job_id) REFERENCES ci_builds(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_c5137cb2c1 FOREIGN KEY (job_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
...@@ -27067,6 +27104,9 @@ ALTER TABLE ONLY draft_notes ...@@ -27067,6 +27104,9 @@ ALTER TABLE ONLY draft_notes
ALTER TABLE ONLY namespace_package_settings ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT fk_rails_e773444769 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_e773444769 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY boards_epic_board_recent_visits
ADD CONSTRAINT fk_rails_e77911cf03 FOREIGN KEY (epic_board_id) REFERENCES boards_epic_boards(id) ON DELETE CASCADE;
ALTER TABLE ONLY dast_site_tokens ALTER TABLE ONLY dast_site_tokens
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
...@@ -6,6 +6,7 @@ class Groups::EpicBoardsController < Groups::ApplicationController ...@@ -6,6 +6,7 @@ class Groups::EpicBoardsController < Groups::ApplicationController
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
before_action :redirect_to_recent_board, only: [:index]
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do before_action do
push_frontend_feature_flag(:epic_boards, group, default_enabled: :yaml) push_frontend_feature_flag(:epic_boards, group, default_enabled: :yaml)
...@@ -18,6 +19,21 @@ class Groups::EpicBoardsController < Groups::ApplicationController ...@@ -18,6 +19,21 @@ class Groups::EpicBoardsController < Groups::ApplicationController
private private
def redirect_to_recent_board
return if request.format.json? || !latest_visited_board
redirect_to group_epic_board_path(group, latest_visited_board.epic_board)
end
def latest_visited_board
@latest_visited_board ||= Boards::EpicBoardsVisitsFinder.new(parent, current_user).latest
end
override :board_visit_service
def board_visit_service
Boards::EpicBoards::Visits::CreateService
end
def board_klass def board_klass
::Boards::EpicBoard ::Boards::EpicBoard
end end
......
# frozen_string_literal: true
module Boards
class EpicBoardsVisitsFinder < VisitsFinder
def recent_visit_model
Boards::EpicBoardRecentVisit
end
end
end
...@@ -5,6 +5,7 @@ module Boards ...@@ -5,6 +5,7 @@ module Boards
belongs_to :group, optional: false, inverse_of: :epic_boards belongs_to :group, optional: false, inverse_of: :epic_boards
has_many :epic_board_labels, foreign_key: :epic_board_id, inverse_of: :epic_board has_many :epic_board_labels, foreign_key: :epic_board_id, inverse_of: :epic_board
has_many :epic_board_positions, foreign_key: :epic_board_id, inverse_of: :epic_board has_many :epic_board_positions, foreign_key: :epic_board_id, inverse_of: :epic_board
has_many :epic_board_recent_visits, foreign_key: :epic_board_id, inverse_of: :epic_board
has_many :epic_lists, -> { ordered }, foreign_key: :epic_board_id, inverse_of: :epic_board has_many :epic_lists, -> { ordered }, foreign_key: :epic_board_id, inverse_of: :epic_board
has_many :labels, through: :epic_board_labels has_many :labels, through: :epic_board_labels
......
# frozen_string_literal: true
module Boards
class EpicBoardRecentVisit < ApplicationRecord
include BoardRecentVisit
belongs_to :user, optional: false, inverse_of: :epic_board_recent_visits
belongs_to :group, optional: false, inverse_of: :epic_board_recent_visits
belongs_to :epic_board, optional: false, inverse_of: :epic_board_recent_visits
validates :user, presence: true
validates :group, presence: true
validates :epic_board, presence: true
scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
def self.board_relation
:epic_board
end
def self.board_parent_relation
:group
end
end
end
...@@ -55,6 +55,8 @@ module EE ...@@ -55,6 +55,8 @@ module EE
has_one :group_wiki_repository has_one :group_wiki_repository
has_many :repository_storage_moves, class_name: 'Groups::RepositoryStorageMove', inverse_of: :container has_many :repository_storage_moves, class_name: 'Groups::RepositoryStorageMove', inverse_of: :container
has_many :epic_board_recent_visits, class_name: 'Boards::EpicBoardRecentVisit', inverse_of: :group
belongs_to :file_template_project, class_name: "Project" belongs_to :file_template_project, class_name: "Project"
belongs_to :push_rule, inverse_of: :group belongs_to :push_rule, inverse_of: :group
......
...@@ -45,6 +45,7 @@ module EE ...@@ -45,6 +45,7 @@ module EE
has_many :vulnerability_feedback, foreign_key: :author_id, class_name: 'Vulnerabilities::Feedback' has_many :vulnerability_feedback, foreign_key: :author_id, class_name: 'Vulnerabilities::Feedback'
has_many :commented_vulnerability_feedback, foreign_key: :comment_author_id, class_name: 'Vulnerabilities::Feedback' has_many :commented_vulnerability_feedback, foreign_key: :comment_author_id, class_name: 'Vulnerabilities::Feedback'
has_many :boards_epic_user_preferences, class_name: 'Boards::EpicUserPreference', inverse_of: :user has_many :boards_epic_user_preferences, class_name: 'Boards::EpicUserPreference', inverse_of: :user
has_many :epic_board_recent_visits, class_name: 'Boards::EpicBoardRecentVisit', inverse_of: :user
has_many :approvals, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent has_many :approvals, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :approvers, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent has_many :approvers, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
......
# frozen_string_literal: true
module Boards
module EpicBoards
module Visits
class CreateService < ::Boards::Visits::CreateService
extend ::Gitlab::Utils::Override
private
override :model
def model
Boards::EpicBoardRecentVisit
end
end
end
end
end
...@@ -41,6 +41,22 @@ RSpec.describe Groups::EpicBoardsController do ...@@ -41,6 +41,22 @@ RSpec.describe Groups::EpicBoardsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
context 'with multiple boards' do
let(:boards) { create_list(:epic_board, 3, group: group) }
before do
visit_board(boards[2], Time.current + 1.minute)
visit_board(boards[0], Time.current + 2.minutes)
visit_board(boards[1], Time.current + 5.minutes)
end
it 'redirects to latest visited board' do
list_boards
expect(response).to redirect_to(group_epic_board_path(group, boards[1]))
end
end
end end
context 'with unauthorized user' do context 'with unauthorized user' do
...@@ -87,6 +103,10 @@ RSpec.describe Groups::EpicBoardsController do ...@@ -87,6 +103,10 @@ RSpec.describe Groups::EpicBoardsController do
def list_boards(format: :html) def list_boards(format: :html)
get :index, params: { group_id: group }, format: format get :index, params: { group_id: group }, format: format
end end
def visit_board(epic_board, time)
create(:epic_board_recent_visit, group: group, epic_board: epic_board, user: user, updated_at: time)
end
end end
describe 'GET show' do describe 'GET show' do
...@@ -114,8 +134,6 @@ RSpec.describe Groups::EpicBoardsController do ...@@ -114,8 +134,6 @@ RSpec.describe Groups::EpicBoardsController do
context 'when format is HTML' do context 'when format is HTML' do
it 'renders template' do it 'renders template' do
# epic board visits not supported yet
# https://gitlab.com/gitlab-org/gitlab/-/issues/321625
expect { read_board board: board }.not_to change(BoardGroupRecentVisit, :count) expect { read_board board: board }.not_to change(BoardGroupRecentVisit, :count)
expect(response).to render_template :show expect(response).to render_template :show
...@@ -141,14 +159,24 @@ RSpec.describe Groups::EpicBoardsController do ...@@ -141,14 +159,24 @@ RSpec.describe Groups::EpicBoardsController do
group.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) group.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end end
it 'does not save visit for unsigned user' do context 'when user is signed out' do
sign_out(user) it 'does not save visit' do
sign_out(user)
# epic board visits not supported yet expect { read_board board: board }.not_to change(Boards::EpicBoardRecentVisit, :count)
expect { read_board board: board }.not_to change(BoardGroupRecentVisit, :count)
expect(response).to render_template :show expect(response).to render_template :show
expect(response.media_type).to eq 'text/html' expect(response.media_type).to eq 'text/html'
end
end
context 'when user is signed in' do
it 'saves the visit' do
expect { read_board board: board }.to change(Boards::EpicBoardRecentVisit, :count)
expect(response).to render_template :show
expect(response.media_type).to eq 'text/html'
end
end end
end end
end end
......
# frozen_string_literal: true
FactoryBot.define do
factory :epic_board_recent_visit, class: 'Boards::EpicBoardRecentVisit' do
user
group
epic_board
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Boards::EpicBoardRecentVisit do
let_it_be(:board_parent) { create(:group) }
let_it_be(:board) { create(:epic_board, group: board_parent) }
describe 'associations' do
it { is_expected.to belong_to(:epic_board).required.inverse_of(:epic_board_recent_visits) }
it { is_expected.to belong_to(:group).required.inverse_of(:epic_board_recent_visits) }
it { is_expected.to belong_to(:user).required.inverse_of(:epic_board_recent_visits) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:group) }
it { is_expected.to validate_presence_of(:epic_board) }
end
it_behaves_like 'boards recent visit' do
let_it_be(:board_parent_relation) { :group }
let_it_be(:board_relation) { :epic_board }
let_it_be(:visit_relation) { :epic_board_recent_visit }
end
end
...@@ -7,6 +7,7 @@ RSpec.describe Boards::EpicBoard do ...@@ -7,6 +7,7 @@ RSpec.describe Boards::EpicBoard do
it { is_expected.to belong_to(:group).required.inverse_of(:epic_boards) } it { is_expected.to belong_to(:group).required.inverse_of(:epic_boards) }
it { is_expected.to have_many(:epic_board_labels).inverse_of(:epic_board) } it { is_expected.to have_many(:epic_board_labels).inverse_of(:epic_board) }
it { is_expected.to have_many(:epic_board_positions).inverse_of(:epic_board) } it { is_expected.to have_many(:epic_board_positions).inverse_of(:epic_board) }
it { is_expected.to have_many(:epic_board_recent_visits).inverse_of(:epic_board) }
it { is_expected.to have_many(:epic_lists).order(list_type: :asc, position: :asc).inverse_of(:epic_board) } it { is_expected.to have_many(:epic_lists).order(list_type: :asc, position: :asc).inverse_of(:epic_board) }
end end
......
...@@ -30,6 +30,7 @@ RSpec.describe Group do ...@@ -30,6 +30,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:repository_storage_moves) } it { is_expected.to have_many(:repository_storage_moves) }
it { is_expected.to have_many(:iterations) } it { is_expected.to have_many(:iterations) }
it { is_expected.to have_many(:iterations_cadences) } it { is_expected.to have_many(:iterations_cadences) }
it { is_expected.to have_many(:epic_board_recent_visits).inverse_of(:group) }
it_behaves_like 'model with wiki' do it_behaves_like 'model with wiki' do
let(:container) { create(:group, :nested, :wiki_repo) } let(:container) { create(:group, :nested, :wiki_repo) }
......
...@@ -31,6 +31,7 @@ RSpec.describe User do ...@@ -31,6 +31,7 @@ RSpec.describe User do
it { is_expected.to have_many(:oncall_participants).class_name('IncidentManagement::OncallParticipant') } it { is_expected.to have_many(:oncall_participants).class_name('IncidentManagement::OncallParticipant') }
it { is_expected.to have_many(:oncall_rotations).class_name('IncidentManagement::OncallRotation').through(:oncall_participants) } it { is_expected.to have_many(:oncall_rotations).class_name('IncidentManagement::OncallRotation').through(:oncall_participants) }
it { is_expected.to have_many(:oncall_schedules).class_name('IncidentManagement::OncallSchedule').through(:oncall_rotations) } it { is_expected.to have_many(:oncall_schedules).class_name('IncidentManagement::OncallSchedule').through(:oncall_rotations) }
it { is_expected.to have_many(:epic_board_recent_visits).inverse_of(:user) }
end end
describe 'nested attributes' do describe 'nested attributes' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Boards::EpicBoards::Visits::CreateService do
describe '#execute' do
let_it_be(:group) { create(:group) }
let_it_be(:board) { create(:epic_board, group: group) }
let_it_be(:model) { Boards::EpicBoardRecentVisit }
context 'with epic board' do
it_behaves_like 'boards recent visit create service'
end
end
end
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe BoardGroupRecentVisit do RSpec.describe BoardGroupRecentVisit do
let(:user) { create(:user) } let_it_be(:board_parent) { create(:group) }
let(:group) { create(:group) } let_it_be(:board) { create(:board, group: board_parent) }
let(:board) { create(:board, group: group) }
describe 'relationships' do describe 'relationships' do
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
...@@ -19,56 +18,9 @@ RSpec.describe BoardGroupRecentVisit do ...@@ -19,56 +18,9 @@ RSpec.describe BoardGroupRecentVisit do
it { is_expected.to validate_presence_of(:board) } it { is_expected.to validate_presence_of(:board) }
end end
describe '#visited' do it_behaves_like 'boards recent visit' do
it 'creates a visit if one does not exists' do let_it_be(:board_relation) { :board }
expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) let_it_be(:board_parent_relation) { :group }
end let_it_be(:visit_relation) { :board_group_recent_visit }
shared_examples 'was visited previously' do
let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago }
it 'updates the timestamp' do
freeze_time do
described_class.visited!(user, board)
expect(described_class.count).to eq 1
expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
end
end
end
it_behaves_like 'was visited previously'
context 'when we try to create a visit that is not unique' do
before do
expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
expect(described_class).to receive(:find_or_create_by).and_return(visit)
end
it_behaves_like 'was visited previously'
end
end
describe '#latest' do
def create_visit(time)
create :board_group_recent_visit, group: group, user: user, updated_at: time
end
it 'returns the most recent visited' do
create_visit(7.days.ago)
create_visit(5.days.ago)
recent = create_visit(1.day.ago)
expect(described_class.latest(user, group)).to eq recent
end
it 'returns last 3 visited boards' do
create_visit(7.days.ago)
visit1 = create_visit(3.days.ago)
visit2 = create_visit(2.days.ago)
visit3 = create_visit(5.days.ago)
expect(described_class.latest(user, group, count: 3)).to eq([visit2, visit1, visit3])
end
end end
end end
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe BoardProjectRecentVisit do RSpec.describe BoardProjectRecentVisit do
let(:user) { create(:user) } let_it_be(:board_parent) { create(:project) }
let(:project) { create(:project) } let_it_be(:board) { create(:board, project: board_parent) }
let(:board) { create(:board, project: project) }
describe 'relationships' do describe 'relationships' do
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
...@@ -19,56 +18,9 @@ RSpec.describe BoardProjectRecentVisit do ...@@ -19,56 +18,9 @@ RSpec.describe BoardProjectRecentVisit do
it { is_expected.to validate_presence_of(:board) } it { is_expected.to validate_presence_of(:board) }
end end
describe '#visited' do it_behaves_like 'boards recent visit' do
it 'creates a visit if one does not exists' do let_it_be(:board_relation) { :board }
expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) let_it_be(:board_parent_relation) { :project }
end let_it_be(:visit_relation) { :board_project_recent_visit }
shared_examples 'was visited previously' do
let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago }
it 'updates the timestamp' do
freeze_time do
described_class.visited!(user, board)
expect(described_class.count).to eq 1
expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
end
end
end
it_behaves_like 'was visited previously'
context 'when we try to create a visit that is not unique' do
before do
expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
expect(described_class).to receive(:find_or_create_by).and_return(visit)
end
it_behaves_like 'was visited previously'
end
end
describe '#latest' do
def create_visit(time)
create :board_project_recent_visit, project: project, user: user, updated_at: time
end
it 'returns the most recent visited' do
create_visit(7.days.ago)
create_visit(5.days.ago)
recent = create_visit(1.day.ago)
expect(described_class.latest(user, project)).to eq recent
end
it 'returns last 3 visited boards' do
create_visit(7.days.ago)
visit1 = create_visit(3.days.ago)
visit2 = create_visit(2.days.ago)
visit3 = create_visit(5.days.ago)
expect(described_class.latest(user, project, count: 3)).to eq([visit2, visit1, visit3])
end
end end
end end
...@@ -7,47 +7,20 @@ RSpec.describe Boards::Visits::CreateService do ...@@ -7,47 +7,20 @@ RSpec.describe Boards::Visits::CreateService do
let(:user) { create(:user) } let(:user) { create(:user) }
context 'when a project board' do context 'when a project board' do
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:project_board) { create(:board, project: project) } let_it_be(:board) { create(:board, project: project) }
subject(:service) { described_class.new(project_board.resource_parent, user) } let_it_be(:model) { BoardProjectRecentVisit }
it 'returns nil when there is no user' do it_behaves_like 'boards recent visit create service'
service.current_user = nil
expect(service.execute(project_board)).to eq nil
end
it 'returns nil when database is read-only' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect(service.execute(project_board)).to eq nil
end
it 'records the visit' do
expect(BoardProjectRecentVisit).to receive(:visited!).once
service.execute(project_board)
end
end end
context 'when a group board' do context 'when a group board' do
let(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:group_board) { create(:board, group: group) } let_it_be(:board) { create(:board, group: group) }
let_it_be(:model) { BoardGroupRecentVisit }
subject(:service) { described_class.new(group_board.resource_parent, user) }
it 'returns nil when there is no user' do
service.current_user = nil
expect(service.execute(group_board)).to eq nil
end
it 'records the visit' do
expect(BoardGroupRecentVisit).to receive(:visited!).once
service.execute(group_board) it_behaves_like 'boards recent visit create service'
end
end end
end end
end end
# frozen_string_literal: true
RSpec.shared_examples 'boards recent visit' do
let_it_be(:user) { create(:user) }
describe '#visited' do
it 'creates a visit if one does not exists' do
expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
end
shared_examples 'was visited previously' do
let_it_be(:visit) do
create(visit_relation,
board_parent_relation => board_parent,
board_relation => board,
user: user,
updated_at: 7.days.ago
)
end
it 'updates the timestamp' do
freeze_time do
described_class.visited!(user, board)
expect(described_class.count).to eq 1
expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
end
end
end
it_behaves_like 'was visited previously'
context 'when we try to create a visit that is not unique' do
before do
expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
expect(described_class).to receive(:find_or_create_by).and_return(visit)
end
it_behaves_like 'was visited previously'
end
end
describe '#latest' do
def create_visit(time)
create(visit_relation, board_parent_relation => board_parent, user: user, updated_at: time)
end
it 'returns the most recent visited' do
create_visit(7.days.ago)
create_visit(5.days.ago)
recent = create_visit(1.day.ago)
expect(described_class.latest(user, board_parent)).to eq recent
end
it 'returns last 3 visited boards' do
create_visit(7.days.ago)
visit1 = create_visit(3.days.ago)
visit2 = create_visit(2.days.ago)
visit3 = create_visit(5.days.ago)
expect(described_class.latest(user, board_parent, count: 3)).to eq([visit2, visit1, visit3])
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'boards recent visit create service' do
let_it_be(:user) { create(:user) }
subject(:service) { described_class.new(board.resource_parent, user) }
it 'returns nil when there is no user' do
service.current_user = nil
expect(service.execute(board)).to be_nil
end
it 'returns nil when database is read only' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect(service.execute(board)).to be_nil
end
it 'records the visit' do
expect(model).to receive(:visited!).once
service.execute(board)
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