Commit e8879bc4 authored by Mario de la Ossa's avatar Mario de la Ossa

Move Milestone methods to Timebox

Move all methods Milestone and Sprint will share to Timebox
parent 6891abfe
# frozen_string_literal: true
module Timebox
extend ActiveSupport::Concern
include AtomicInternalId
include CacheMarkdownField
include IidRoutes
include StripAttribute
included do
alias_method :timebox_id, :id
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
validates :title, presence: true
validate :uniqueness_of_title, if: :title_changed?
validate :timebox_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
validate :dates_within_4_digits
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project
belongs_to :group
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :for_projects, -> { where(group: nil).includes(:project) }
scope :for_projects_and_groups, -> (projects, groups) do
projects = projects.compact if projects.is_a? Array
projects = [] if projects.nil?
groups = groups.compact if groups.is_a? Array
groups = [] if groups.nil?
where(project_id: projects).or(where(group_id: groups))
end
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date)
.where('due_date is NULL or due_date >= ?', start_date)
end
strip_attributes :title
alias_attribute :name, :title
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
def timebox_name
model_name.singular
end
def group_timebox?
group_id.present?
end
def project_timebox?
project_id.present?
end
def safe_title
title.to_slug.normalize.to_s
end
def resource_parent
group || project
end
def to_ability_name
model_name.singular
end
def merge_requests_enabled?
if group_timebox?
# Assume that groups have at least one project with merge requests enabled.
# Otherwise, we would need to load all of the projects from the database.
true
elsif project_timebox?
project&.merge_requests_enabled?
end
end
private
# Timebox titles must be unique across project and group timeboxes
def uniqueness_of_title
if project
relation = self.class.for_projects_and_groups([project_id], [project.group&.id])
elsif group
relation = self.class.for_projects_and_groups(group.projects.select(:id), [group.id])
end
title_exists = relation.find_by_title(title)
errors.add(:title, _("already being used for another group or project %{timebox_name}.") % { timebox_name: timebox_name }) if title_exists
end
# Timebox should be either a project timebox or a group timebox
def timebox_type_check
if group_id && project_id
field = project_id_changed? ? :project_id : :group_id
errors.add(field, _("%{timebox_name} should belong either to a project or a group.") % { timebox_name: timebox_name })
end
end
def start_date_should_be_less_than_due_date
if due_date <= start_date
errors.add(:due_date, _("must be greater than start date"))
end
end
def dates_within_4_digits
if start_date && start_date > Date.new(9999, 12, 31)
errors.add(:start_date, _("date must not be after 9999-12-31"))
end
if due_date && due_date > Date.new(9999, 12, 31)
errors.add(:due_date, _("date must not be after 9999-12-31"))
end
end
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
end
......@@ -11,7 +11,7 @@ class GlobalMilestone
delegate :title, :state, :due_date, :start_date, :participants, :project,
:group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
:milestoneish_id, :resource_parent, :releases, to: :milestone
:timebox_id, :milestoneish_id, :resource_parent, :releases, to: :milestone
def to_hash
{
......
......@@ -15,12 +15,9 @@ class Milestone < ApplicationRecord
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
include AtomicInternalId
include IidRoutes
include Sortable
include Referable
include StripAttribute
include Timebox
include Milestoneish
include FromUnion
include Importable
......@@ -28,61 +25,21 @@ class Milestone < ApplicationRecord
prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project
belongs_to :group
has_many :milestone_releases
has_many :releases, through: :milestone_releases
has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, track_if: -> { !importing? }, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :for_projects, -> { where(group: nil).includes(:project) }
scope :started, -> { active.where('milestones.start_date <= CURRENT_DATE') }
scope :for_projects_and_groups, -> (projects, groups) do
projects = projects.compact if projects.is_a? Array
projects = [] if projects.nil?
groups = groups.compact if groups.is_a? Array
groups = [] if groups.nil?
where(project_id: projects).or(where(group_id: groups))
end
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date)
.where('due_date is NULL or due_date >= ?', start_date)
end
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
validates :title, presence: true
validate :uniqueness_of_title, if: :title_changed?
validate :milestone_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
validate :dates_within_4_digits
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
strip_attributes :title
state_machine :state, initial: :active do
event :close do
transition active: :closed
......@@ -97,8 +54,6 @@ class Milestone < ApplicationRecord
state :active
end
alias_attribute :name, :title
class << self
# Searches for milestones with a matching title or description.
#
......@@ -220,7 +175,7 @@ class Milestone < ApplicationRecord
end
##
# Returns the String necessary to reference this Milestone in Markdown. Group
# Returns the String necessary to reference a Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project
# references.
#
......@@ -248,10 +203,6 @@ class Milestone < ApplicationRecord
self.class.reference_prefix + self.title
end
def milestoneish_id
id
end
def for_display
self
end
......@@ -264,62 +215,16 @@ class Milestone < ApplicationRecord
nil
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
def safe_title
title.to_slug.normalize.to_s
end
def resource_parent
group || project
end
def to_ability_name
model_name.singular
end
def group_milestone?
group_id.present?
end
def project_milestone?
project_id.present?
end
def merge_requests_enabled?
if group_milestone?
# Assume that groups have at least one project with merge requests enabled.
# Otherwise, we would need to load all of the projects from the database.
true
elsif project_milestone?
project&.merge_requests_enabled?
end
end
# TODO: remove after all code paths use `timebox_id`
# https://gitlab.com/gitlab-org/gitlab/-/issues/215688
alias_method :milestoneish_id, :timebox_id
# TODO: remove after all code paths use (group|project)_timebox?
# https://gitlab.com/gitlab-org/gitlab/-/issues/215690
alias_method :group_milestone?, :group_timebox?
alias_method :project_milestone?, :project_timebox?
private
# Milestone titles must be unique across project milestones and group milestones
def uniqueness_of_title
if project
relation = Milestone.for_projects_and_groups([project_id], [project.group&.id])
elsif group
relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id])
end
title_exists = relation.find_by_title(title)
errors.add(:title, _("already being used for another group or project milestone.")) if title_exists
end
# Milestone should be either a project milestone or a group milestone
def milestone_type_check
if group_id && project_id
field = project_id_changed? ? :project_id : :group_id
errors.add(field, _("milestone should belong either to a project or a group."))
end
end
def milestone_format_reference(format = :iid)
raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
......@@ -334,26 +239,6 @@ class Milestone < ApplicationRecord
end
end
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
def start_date_should_be_less_than_due_date
if due_date <= start_date
errors.add(:due_date, _("must be greater than start date"))
end
end
def dates_within_4_digits
if start_date && start_date > Date.new(9999, 12, 31)
errors.add(:start_date, _("date must not be after 9999-12-31"))
end
if due_date && due_date > Date.new(9999, 12, 31)
errors.add(:due_date, _("date must not be after 9999-12-31"))
end
end
def issues_finder_params
{ project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end
......
# frozen_string_literal: true
class Sprint < ApplicationRecord
include Timebox
STATE_ID_MAP = {
active: 1,
closed: 2
......@@ -16,4 +18,17 @@ class Sprint < ApplicationRecord
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.sprints&.maximum(:iid) }
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.sprints&.maximum(:iid) }
state_machine :state, initial: :active do
event :close do
transition active: :closed
end
event :activate do
transition closed: :active
end
state :active, value: Sprint::STATE_ID_MAP[:active]
state :closed, value: Sprint::STATE_ID_MAP[:closed]
end
end
- milestone = local_assigns[:milestone]
- burndown = burndown_chart(milestone)
- warning = data_warning_for(burndown)
- burndown_endpoint = milestone.group_milestone? ? api_v4_groups_milestones_burndown_events_path(id: milestone.group.id, milestone_id: milestone.id) : api_v4_projects_milestones_burndown_events_path(id: milestone.project.id, milestone_id: milestone.milestoneish_id)
- burndown_endpoint = milestone.group_milestone? ? api_v4_groups_milestones_burndown_events_path(id: milestone.group.id, milestone_id: milestone.id) : api_v4_projects_milestones_burndown_events_path(id: milestone.project.id, milestone_id: milestone.timebox_id)
= warning
......
......@@ -516,6 +516,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
msgid "%{timebox_name} should belong either to a project or a group."
msgstr ""
msgid "%{title} %{operator} %{threshold}"
msgstr ""
......@@ -24473,7 +24476,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
msgid "already being used for another group or project milestone."
msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
......@@ -25086,9 +25089,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
msgid "milestone should belong either to a project or a group."
msgstr ""
msgid "missing"
msgstr ""
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
describe Milestone do
it_behaves_like 'a timebox', :milestone
describe 'MilestoneStruct#serializable_hash' do
let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) }
......@@ -15,69 +17,11 @@ describe Milestone do
end
end
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:milestone, project: build(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :milestones }
end
end
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:milestone, project: nil, group: build(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) { :milestones }
end
end
end
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
end
describe 'start_date' do
it 'adds an error when start_date is greater then due_date' do
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(milestone).not_to be_valid
expect(milestone.errors[:due_date]).to include("must be greater than start date")
end
it 'adds an error when start_date is greater than 9999-12-31' do
milestone = build(:milestone, start_date: Date.new(10000, 1, 1))
expect(milestone).not_to be_valid
expect(milestone.errors[:start_date]).to include("date must not be after 9999-12-31")
end
end
describe 'due_date' do
it 'adds an error when due_date is greater than 9999-12-31' do
milestone = build(:milestone, due_date: Date.new(10000, 1, 1))
expect(milestone).not_to be_valid
expect(milestone.errors[:due_date]).to include("date must not be after 9999-12-31")
end
end
describe 'title' do
it { is_expected.to validate_presence_of(:title) }
it 'is invalid if title would be empty after sanitation' do
milestone = build(:milestone, project: project, title: '<img src=x onerror=prompt(1)>')
expect(milestone).not_to be_valid
expect(milestone.errors[:title]).to include("can't be blank")
end
end
describe 'milestone_releases' do
let(:milestone) { build(:milestone, project: project) }
......@@ -99,8 +43,6 @@ describe Milestone do
end
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:releases) }
it { is_expected.to have_many(:milestone_releases) }
end
......@@ -110,87 +52,6 @@ describe Milestone do
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
describe "#title" do
let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do
expect(milestone.title).to eq("foo & bar -> 2.2")
end
end
describe '#merge_requests_enabled?' do
context "per project" do
it "is true for projects with MRs enabled" do
project = create(:project, :merge_requests_enabled)
milestone = create(:milestone, project: project)
expect(milestone.merge_requests_enabled?).to be(true)
end
it "is false for projects with MRs disabled" do
project = create(:project, :repository_enabled, :merge_requests_disabled)
milestone = create(:milestone, project: project)
expect(milestone.merge_requests_enabled?).to be(false)
end
it "is false for projects with repository disabled" do
project = create(:project, :repository_disabled)
milestone = create(:milestone, project: project)
expect(milestone.merge_requests_enabled?).to be(false)
end
end
context "per group" do
let(:group) { create(:group) }
let(:milestone) { create(:milestone, group: group) }
it "is always true for groups, for performance reasons" do
expect(milestone.merge_requests_enabled?).to be(true)
end
end
end
describe "unique milestone title" do
context "per project" do
it "does not accept the same title in a project twice" do
new_milestone = described_class.new(project: milestone.project, title: milestone.title)
expect(new_milestone).not_to be_valid
end
it "accepts the same title in another project" do
project = create(:project)
new_milestone = described_class.new(project: project, title: milestone.title)
expect(new_milestone).to be_valid
end
end
context "per group" do
let(:group) { create(:group) }
let(:milestone) { create(:milestone, group: group) }
before do
project.update(group: group)
end
it "does not accept the same title in a group twice" do
new_milestone = described_class.new(group: group, title: milestone.title)
expect(new_milestone).not_to be_valid
end
it "does not accept the same title of a child project milestone" do
create(:milestone, project: group.projects.first)
new_milestone = described_class.new(group: group, title: milestone.title)
expect(new_milestone).not_to be_valid
end
end
end
describe '.predefined_id?' do
it 'returns true for a predefined Milestone ID' do
expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true
......
......@@ -3,39 +3,12 @@
require 'spec_helper'
describe Sprint do
it_behaves_like 'a timebox', :sprint
describe "#iid" do
let!(:project) { create(:project) }
let!(:group) { create(:group) }
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:sprint, project: build(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) {:sprints }
end
end
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:sprint, project: nil, group: build(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) {:sprints }
end
end
end
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:group) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:merge_requests) }
end
describe "#iid" do
it "is properly scoped on project and group" do
sprint1 = create(:sprint, project: project)
sprint2 = create(:sprint, project: project)
......
# frozen_string_literal: true
RSpec.shared_examples 'a timebox' do |timebox_type|
let(:project) { create(:project, :public) }
let(:group) { create(:group) }
let(:timebox) { create(timebox_type, project: project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(timebox_type, project: build(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) {timebox_table_name }
end
end
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(timebox_type, project: nil, group: build(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) {timebox_table_name }
end
end
end
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
end
describe 'start_date' do
it 'adds an error when start_date is greater then due_date' do
timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(timebox).not_to be_valid
expect(timebox.errors[:due_date]).to include("must be greater than start date")
end
it 'adds an error when start_date is greater than 9999-12-31' do
timebox = build(timebox_type, start_date: Date.new(10000, 1, 1))
expect(timebox).not_to be_valid
expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31")
end
end
describe 'due_date' do
it 'adds an error when due_date is greater than 9999-12-31' do
timebox = build(timebox_type, due_date: Date.new(10000, 1, 1))
expect(timebox).not_to be_valid
expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31")
end
end
describe 'title' do
it { is_expected.to validate_presence_of(:title) }
it 'is invalid if title would be empty after sanitation' do
timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>')
expect(timebox).not_to be_valid
expect(timebox.errors[:title]).to include("can't be blank")
end
end
describe '#timebox_type_check' do
it 'is invalid if it has both project_id and group_id' do
timebox = build(timebox_type, group: group)
timebox.project = project
expect(timebox).not_to be_valid
expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.")
end
end
describe "#uniqueness_of_title" do
context "per project" do
it "does not accept the same title in a project twice" do
new_timebox = described_class.new(project: timebox.project, title: timebox.title)
expect(new_timebox).not_to be_valid
end
it "accepts the same title in another project" do
project = create(:project)
new_timebox = described_class.new(project: project, title: timebox.title)
expect(new_timebox).to be_valid
end
end
context "per group" do
let(:timebox) { create(timebox_type, group: group) }
before do
project.update(group: group)
end
it "does not accept the same title in a group twice" do
new_timebox = described_class.new(group: group, title: timebox.title)
expect(new_timebox).not_to be_valid
end
it "does not accept the same title of a child project timebox" do
create(timebox_type, project: group.projects.first)
new_timebox = described_class.new(group: group, title: timebox.title)
expect(new_timebox).not_to be_valid
end
end
end
end
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:group) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:labels) }
end
describe '#timebox_name' do
it 'returns the name of the model' do
expect(timebox.timebox_name).to eq(timebox_type.to_s)
end
end
describe '#project_timebox?' do
context 'when project_id is present' do
it 'returns true' do
expect(timebox.project_timebox?).to be_truthy
end
end
context 'when project_id is not present' do
let(:timebox) { build(timebox_type, group: group) }
it 'returns false' do
expect(timebox.project_timebox?).to be_falsey
end
end
end
describe '#group_timebox?' do
context 'when group_id is present' do
let(:timebox) { build(timebox_type, group: group) }
it 'returns true' do
expect(timebox.group_timebox?).to be_truthy
end
end
context 'when group_id is not present' do
it 'returns false' do
expect(timebox.group_timebox?).to be_falsey
end
end
end
describe '#safe_title' do
let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
it 'normalizes the title for use as a slug' do
expect(timebox.safe_title).to eq('foo-bar-22')
end
end
describe '#resource_parent' do
context 'when group is present' do
let(:timebox) { build(timebox_type, group: group) }
it 'returns the group' do
expect(timebox.resource_parent).to eq(group)
end
end
context 'when project is present' do
it 'returns the project' do
expect(timebox.resource_parent).to eq(project)
end
end
end
describe "#title" do
let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do
expect(timebox.title).to eq("foo & bar -> 2.2")
end
end
describe '#merge_requests_enabled?' do
context "per project" do
it "is true for projects with MRs enabled" do
project = create(:project, :merge_requests_enabled)
timebox = create(timebox_type, project: project)
expect(timebox.merge_requests_enabled?).to be_truthy
end
it "is false for projects with MRs disabled" do
project = create(:project, :repository_enabled, :merge_requests_disabled)
timebox = create(timebox_type, project: project)
expect(timebox.merge_requests_enabled?).to be_falsey
end
it "is false for projects with repository disabled" do
project = create(:project, :repository_disabled)
timebox = create(timebox_type, project: project)
expect(timebox.merge_requests_enabled?).to be_falsey
end
end
context "per group" do
let(:timebox) { create(timebox_type, group: group) }
it "is always true for groups, for performance reasons" do
expect(timebox.merge_requests_enabled?).to be_truthy
end
end
end
it_behaves_like 'within_timeframe scope' do
let_it_be(:now) { Time.now }
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:resource_1) { create(timebox_type, project: project, start_date: now - 1.day, due_date: now + 1.day) }
let_it_be(:resource_2) { create(timebox_type, project: project, start_date: now + 2.days, due_date: now + 3.days) }
let_it_be(:resource_3) { create(timebox_type, project: project, due_date: now) }
let_it_be(:resource_4) { create(timebox_type, project: project, start_date: now) }
end
describe '#to_ability_name' do
it 'returns timebox' do
timebox = build(timebox_type)
expect(timebox.to_ability_name).to eq(timebox_type.to_s)
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