Commit 80bd6f78 authored by Stan Hu's avatar Stan Hu

Merge branch '33042-persist-zoom-meetings-added-to-issues-in-the-database-2' into 'master'

Switch quick actions to use Zoom Meetings table and model

See merge request gitlab-org/gitlab!18620
parents a1eb38e3 4529c73f
......@@ -281,10 +281,7 @@ module IssuablesHelper
}
data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
zoom_links = Gitlab::ZoomLinkExtractor.new(issuable.description).links
data[:zoomMeetingUrl] = zoom_links.last if zoom_links.any?
data[:zoomMeetingUrl] = ZoomMeeting.canonical_meeting_url(issuable) if issuable.is_a?(Issue)
if parent.is_a?(Group)
data[:groupPath] = parent.path
......
......@@ -40,6 +40,7 @@ class Issue < ApplicationRecord
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
has_many :zoom_meetings
validates :project, presence: true
......
......@@ -14,4 +14,13 @@ class ZoomMeeting < ApplicationRecord
scope :added_to_issue, -> { where(issue_status: :added) }
scope :removed_from_issue, -> { where(issue_status: :removed) }
scope :canonical, -> (issue) { where(issue: issue).added_to_issue }
def self.canonical_meeting(issue)
canonical(issue)&.take
end
def self.canonical_meeting_url(issue)
canonical_meeting(issue)&.url
end
end
......@@ -61,8 +61,6 @@ module Issues
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
ZoomNotesService.new(issue, project, current_user, old_description: old_associations[:description]).execute
end
def handle_task_changes(issuable)
......
......@@ -6,32 +6,37 @@ module Issues
super(issue.project, user)
@issue = issue
@added_meeting = ZoomMeeting.canonical_meeting(@issue)
end
def add_link(link)
if can_add_link? && (link = parse_link(link))
track_meeting_added_event
success(_('Zoom meeting added'), append_to_description(link))
begin
add_zoom_meeting(link)
success(_('Zoom meeting added'))
rescue ActiveRecord::RecordNotUnique
error(_('Failed to add a Zoom meeting'))
end
else
error(_('Failed to add a Zoom meeting'))
end
end
def can_add_link?
can? && !link_in_issue_description?
end
def remove_link
if can_remove_link?
track_meeting_removed_event
success(_('Zoom meeting removed'), remove_from_description)
remove_zoom_meeting
success(_('Zoom meeting removed'))
else
error(_('Failed to remove a Zoom meeting'))
end
end
def can_add_link?
can_update_issue? && !@added_meeting
end
def can_remove_link?
can? && link_in_issue_description?
can_update_issue? && !!@added_meeting
end
def parse_link(link)
......@@ -42,10 +47,6 @@ module Issues
attr_reader :issue
def issue_description
issue.description || ''
end
def track_meeting_added_event
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
end
......@@ -54,39 +55,33 @@ module Issues
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
end
def success(message, description)
ServiceResponse
.success(message: message, payload: { description: description })
end
def error(message)
ServiceResponse.error(message: message)
def add_zoom_meeting(link)
ZoomMeeting.create(
issue: @issue,
project: @issue.project,
issue_status: :added,
url: link
)
track_meeting_added_event
SystemNoteService.zoom_link_added(@issue, @project, current_user)
end
def append_to_description(link)
"#{issue_description}\n\n#{link}"
def remove_zoom_meeting
@added_meeting.update(issue_status: :removed)
track_meeting_removed_event
SystemNoteService.zoom_link_removed(@issue, @project, current_user)
end
def remove_from_description
link = parse_link(issue_description)
return issue_description unless link
issue_description.delete_suffix(link).rstrip
def success(message)
ServiceResponse.success(message: message)
end
def link_in_issue_description?
link = extract_link_from_issue_description
return unless link
Gitlab::ZoomLinkExtractor.new(link).match?
end
def extract_link_from_issue_description
issue_description[/(\S+)\z/, 1]
def error(message)
ServiceResponse.error(message: message)
end
def can?
current_user.can?(:update_issue, project)
def can_update_issue?
can?(current_user, :update_issue, project)
end
end
end
# frozen_string_literal: true
class ZoomNotesService
def initialize(issue, project, current_user, old_description: nil)
@issue = issue
@project = project
@current_user = current_user
@old_description = old_description
end
def execute
return if @issue.description == @old_description
if zoom_link_added?
zoom_link_added_notification
elsif zoom_link_removed?
zoom_link_removed_notification
end
end
private
def zoom_link_added?
has_zoom_link?(@issue.description) && !has_zoom_link?(@old_description)
end
def zoom_link_removed?
!has_zoom_link?(@issue.description) && has_zoom_link?(@old_description)
end
def has_zoom_link?(text)
Gitlab::ZoomLinkExtractor.new(text).match?
end
def zoom_link_added_notification
SystemNoteService.zoom_link_added(@issue, @project, @current_user)
end
def zoom_link_removed_notification
SystemNoteService.zoom_link_removed(@issue, @project, @current_user)
end
end
---
title: Store Zoom URLs in a table rather than in the issue description
merge_request: 18620
author:
type: changed
......@@ -28,6 +28,7 @@ tree:
- label:
- :priorities
- :issue_assignees
- :zoom_meetings
- snippets:
- :award_emoji
- notes:
......
......@@ -174,18 +174,14 @@ module Gitlab
params '<Zoom URL>'
types Issue
condition do
zoom_link_service.can_add_link?
@zoom_service = zoom_link_service
@zoom_service.can_add_link?
end
parse_params do |link|
zoom_link_service.parse_link(link)
@zoom_service.parse_link(link)
end
command :zoom do |link|
result = zoom_link_service.add_link(link)
if result.success?
@updates[:description] = result.payload[:description]
end
result = @zoom_service.add_link(link)
@execution_message[:zoom] = result.message
end
......@@ -194,15 +190,11 @@ module Gitlab
execution_message _('Zoom meeting removed')
types Issue
condition do
zoom_link_service.can_remove_link?
@zoom_service = zoom_link_service
@zoom_service.can_remove_link?
end
command :remove_zoom do
result = zoom_link_service.remove_link
if result.success?
@updates[:description] = result.payload[:description]
end
result = @zoom_service.remove_link
@execution_message[:remove_zoom] = result.message
end
......
......@@ -92,19 +92,6 @@ describe "User creates issue" do
.and have_content(label_titles.first)
end
end
context "with Zoom link" do
it "adds Zoom button" do
issue_title = "Issue containing Zoom meeting link"
zoom_url = "https://gitlab.zoom.us/j/123456789"
fill_in("Title", with: issue_title)
fill_in("Description", with: zoom_url)
click_button("Submit issue")
expect(page).to have_link('Join Zoom meeting', href: zoom_url)
end
end
end
context "when signed in as user with special characters in their name" do
......
......@@ -80,6 +80,17 @@
"issue_id": 40
}
],
"zoom_meetings": [
{
"id": 1,
"project_id": 5,
"issue_id": 40,
"url": "https://zoom.us/j/123456789",
"issue_status": 1,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z"
}
],
"milestone": {
"id": 1,
"title": "test milestone",
......
......@@ -203,42 +203,53 @@ describe IssuablesHelper do
end
describe '#zoomMeetingUrl in issue' do
let(:issue) { create(:issue, author: user, description: description) }
let(:issue) { create(:issue, author: user) }
before do
assign(:project, issue.project)
end
context 'no zoom links in the issue description' do
let(:description) { 'issue text' }
it 'does not set zoomMeetingUrl' do
expect(helper.issuable_initial_data(issue))
.not_to include(:zoomMeetingUrl)
shared_examples 'sets zoomMeetingUrl to nil' do
specify do
expect(helper.issuable_initial_data(issue)[:zoomMeetingUrl])
.to be_nil
end
end
context 'no zoom links in the issue description if it has link but not a zoom link' do
let(:description) { 'issue text https://stackoverflow.com/questions/22' }
context 'with no "added" zoom mettings' do
it_behaves_like 'sets zoomMeetingUrl to nil'
context 'with multiple removed meetings' do
before do
create(:zoom_meeting, issue: issue, issue_status: :removed)
create(:zoom_meeting, issue: issue, issue_status: :removed)
end
it 'does not set zoomMeetingUrl' do
expect(helper.issuable_initial_data(issue))
.not_to include(:zoomMeetingUrl)
it_behaves_like 'sets zoomMeetingUrl to nil'
end
end
context 'with two zoom links in description' do
let(:description) do
<<~TEXT
issue text and
zoom call on https://zoom.us/j/123456789 this url
and new zoom url https://zoom.us/s/lastone and some more text
TEXT
context 'with "added" zoom meeting' do
before do
create(:zoom_meeting, issue: issue)
end
it 'sets zoomMeetingUrl value to the last url' do
expect(helper.issuable_initial_data(issue))
.to include(zoomMeetingUrl: 'https://zoom.us/s/lastone')
shared_examples 'sets zoomMeetingUrl to canonical meeting url' do
specify do
expect(helper.issuable_initial_data(issue))
.to include(zoomMeetingUrl: 'https://zoom.us/j/123456789')
end
end
it_behaves_like 'sets zoomMeetingUrl to canonical meeting url'
context 'with muliple "removed" zoom meetings' do
before do
create(:zoom_meeting, issue: issue, issue_status: :removed)
create(:zoom_meeting, issue: issue, issue_status: :removed)
end
it_behaves_like 'sets zoomMeetingUrl to canonical meeting url'
end
end
end
......
......@@ -29,6 +29,7 @@ issues:
- prometheus_alerts
- prometheus_alert_events
- self_managed_prometheus_alert_events
- zoom_meetings
events:
- author
- project
......@@ -529,4 +530,6 @@ versions: &version
- issue
- designs
- actions
zoom_meetings:
- issue
design_versions: *version
......@@ -211,6 +211,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(CustomIssueTrackerService.first).not_to be_nil
end
it 'restores zoom meetings' do
meetings = @project.issues.first.zoom_meetings
expect(meetings.count).to eq(1)
expect(meetings.first.url).to eq('https://zoom.us/j/123456789')
end
context 'Merge requests' do
it 'always has the new project as a target' do
expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project)
......
......@@ -753,3 +753,11 @@ DesignManagement::Version:
- sha
- issue_id
- author_id
ZoomMeeting:
- id
- issue_id
- project_id
- issue_status
- url
- created_at
- updated_at
......@@ -187,7 +187,6 @@ describe Issues::UpdateService, :mailer do
it 'creates system note about issue reassign' do
note = find_note('assigned to')
expect(note).not_to be_nil
expect(note.note).to include "assigned to #{user2.to_reference}"
end
......@@ -202,14 +201,12 @@ describe Issues::UpdateService, :mailer do
it 'creates system note about title change' do
note = find_note('changed title')
expect(note).not_to be_nil
expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
it 'creates system note about discussion lock' do
note = find_note('locked this issue')
expect(note).not_to be_nil
expect(note.note).to eq 'locked this issue'
end
end
......@@ -221,20 +218,10 @@ describe Issues::UpdateService, :mailer do
note = find_note('changed the description')
expect(note).not_to be_nil
expect(note.note).to eq('changed the description')
end
end
it 'creates zoom_link_added system note when a zoom link is added to the description' do
update_issue(description: 'Changed description https://zoom.us/j/5873603787')
note = find_note('added a Zoom call')
expect(note).not_to be_nil
expect(note.note).to eq('added a Zoom call to this issue')
end
context 'when issue turns confidential' do
let(:opts) do
{
......@@ -252,7 +239,6 @@ describe Issues::UpdateService, :mailer do
note = find_note('made the issue confidential')
expect(note).not_to be_nil
expect(note.note).to eq 'made the issue confidential'
end
......
......@@ -14,27 +14,16 @@ describe Issues::ZoomLinkService do
project.add_reporter(user)
end
shared_context 'with Zoom link' do
shared_context '"added" Zoom meeting' do
before do
issue.update!(description: "Description\n\n#{zoom_link}")
create(:zoom_meeting, issue: issue)
end
end
shared_context 'with Zoom link not at the end' do
shared_context '"removed" zoom meetings' do
before do
issue.update!(description: "Description with #{zoom_link} some where")
end
end
shared_context 'without Zoom link' do
before do
issue.update!(description: "Description\n\nhttp://example.com")
end
end
shared_context 'without issue description' do
before do
issue.update!(description: nil)
create(:zoom_meeting, issue: issue, issue_status: :removed)
create(:zoom_meeting, issue: issue, issue_status: :removed)
end
end
......@@ -45,11 +34,10 @@ describe Issues::ZoomLinkService do
end
describe '#add_link' do
shared_examples 'can add link' do
it 'appends the link to issue description' do
shared_examples 'can add meeting' do
it 'appends the new meeting to zoom_meetings' do
expect(result).to be_success
expect(result.payload[:description])
.to eq("#{issue.description}\n\n#{zoom_link}")
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(zoom_link)
end
it 'tracks the add event' do
......@@ -57,55 +45,63 @@ describe Issues::ZoomLinkService do
.with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
result
end
it 'creates a zoom_link_added notification' do
expect(SystemNoteService).to receive(:zoom_link_added).with(issue, project, user)
expect(SystemNoteService).not_to receive(:zoom_link_removed)
result
end
end
shared_examples 'cannot add link' do
it 'cannot add the link' do
shared_examples 'cannot add meeting' do
it 'cannot add the meeting' do
expect(result).to be_error
expect(result.message).to eq('Failed to add a Zoom meeting')
end
it 'creates no notification' do
expect(SystemNoteService).not_to receive(:zoom_link_added)
expect(SystemNoteService).not_to receive(:zoom_link_removed)
result
end
end
subject(:result) { service.add_link(zoom_link) }
context 'without Zoom link in the issue description' do
include_context 'without Zoom link'
include_examples 'can add link'
context 'without existing Zoom meeting' do
include_examples 'can add meeting'
context 'with invalid Zoom link' do
context 'with invalid Zoom url' do
let(:zoom_link) { 'https://not-zoom.link' }
include_examples 'cannot add link'
include_examples 'cannot add meeting'
end
context 'with insufficient permissions' do
include_context 'insufficient permissions'
include_examples 'cannot add link'
include_examples 'cannot add meeting'
end
end
context 'with Zoom link in the issue description' do
include_context 'with Zoom link'
include_examples 'cannot add link'
context 'with "added" Zoom meeting' do
include_context '"added" Zoom meeting'
include_examples 'cannot add meeting'
end
context 'but not at the end' do
include_context 'with Zoom link not at the end'
include_examples 'can add link'
context 'with "added" Zoom meeting and race condition' do
include_context '"added" Zoom meeting'
before do
allow(service).to receive(:can_add_link?).and_return(true)
end
end
context 'without issue description' do
include_context 'without issue description'
include_examples 'can add link'
include_examples 'cannot add meeting'
end
end
describe '#can_add_link?' do
subject { service.can_add_link? }
context 'without Zoom link in the issue description' do
include_context 'without Zoom link'
context 'without "added" zoom meeting' do
it { is_expected.to eq(true) }
context 'with insufficient permissions' do
......@@ -115,81 +111,93 @@ describe Issues::ZoomLinkService do
end
end
context 'with Zoom link in the issue description' do
include_context 'with Zoom link'
context 'with Zoom meeting in the issue description' do
include_context '"added" Zoom meeting'
it { is_expected.to eq(false) }
end
end
describe '#remove_link' do
shared_examples 'cannot remove link' do
it 'cannot remove the link' do
shared_examples 'cannot remove meeting' do
it 'cannot remove the meeting' do
expect(result).to be_error
expect(result.message).to eq('Failed to remove a Zoom meeting')
end
end
subject(:result) { service.remove_link }
it 'creates no notification' do
expect(SystemNoteService).not_to receive(:zoom_link_added)
expect(SystemNoteService).not_to receive(:zoom_link_removed)
result
end
end
context 'with Zoom link in the issue description' do
include_context 'with Zoom link'
shared_examples 'can remove meeting' do
it 'creates no notification' do
expect(SystemNoteService).not_to receive(:zoom_link_added).with(issue, project, user)
expect(SystemNoteService).to receive(:zoom_link_removed)
result
end
it 'removes the link from the issue description' do
it 'can remove the meeting' do
expect(result).to be_success
expect(result.payload[:description])
.to eq(issue.description.delete_suffix("\n\n#{zoom_link}"))
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(nil)
end
it 'tracks the remove event' do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
.with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
result
end
end
context 'with insufficient permissions' do
include_context 'insufficient permissions'
include_examples 'cannot remove link'
end
subject(:result) { service.remove_link }
context 'but not at the end' do
include_context 'with Zoom link not at the end'
include_examples 'cannot remove link'
context 'with Zoom meeting' do
include_context '"added" Zoom meeting'
context 'removes the link' do
include_examples 'can remove meeting'
end
end
context 'without Zoom link in the issue description' do
include_context 'without Zoom link'
include_examples 'cannot remove link'
context 'with insufficient permissions' do
include_context 'insufficient permissions'
include_examples 'cannot remove meeting'
end
end
context 'without issue description' do
include_context 'without issue description'
include_examples 'cannot remove link'
context 'without "added" Zoom meeting' do
include_context '"removed" zoom meetings'
include_examples 'cannot remove meeting'
end
end
describe '#can_remove_link?' do
subject { service.can_remove_link? }
context 'with Zoom link in the issue description' do
include_context 'with Zoom link'
context 'without Zoom meeting' do
it { is_expected.to eq(false) }
end
context 'with only "removed" zoom meetings' do
include_context '"removed" zoom meetings'
it { is_expected.to eq(false) }
end
context 'with "added" Zoom meeting' do
include_context '"added" Zoom meeting'
it { is_expected.to eq(true) }
context 'with "removed" zoom meetings' do
include_context '"removed" zoom meetings'
it { is_expected.to eq(true) }
end
context 'with insufficient permissions' do
include_context 'insufficient permissions'
it { is_expected.to eq(false) }
end
end
context 'without Zoom link in the issue description' do
include_context 'without Zoom link'
it { is_expected.to eq(false) }
end
end
describe '#parse_link' do
......
# frozen_string_literal: true
require 'spec_helper'
describe ZoomNotesService do
describe '#execute' do
let(:issue) { OpenStruct.new(description: description) }
let(:project) { Object.new }
let(:user) { Object.new }
let(:description) { 'an issue description' }
let(:old_description) { nil }
subject { described_class.new(issue, project, user, old_description: old_description) }
shared_examples 'no notifications' do
it "doesn't create notifications" do
expect(SystemNoteService).not_to receive(:zoom_link_added)
expect(SystemNoteService).not_to receive(:zoom_link_removed)
subject.execute
end
end
it_behaves_like 'no notifications'
context 'when the zoom link exists in both description and old_description' do
let(:description) { 'a changed issue description https://zoom.us/j/123' }
let(:old_description) { 'an issue description https://zoom.us/j/123' }
it_behaves_like 'no notifications'
end
context "when the zoom link doesn't exist in both description and old_description" do
let(:description) { 'a changed issue description' }
let(:old_description) { 'an issue description' }
it_behaves_like 'no notifications'
end
context 'when description == old_description' do
let(:old_description) { 'an issue description' }
it_behaves_like 'no notifications'
end
context 'when the description contains a zoom link and old_description is nil' do
let(:description) { 'a changed issue description https://zoom.us/j/123' }
it 'creates a zoom_link_added notification' do
expect(SystemNoteService).to receive(:zoom_link_added).with(issue, project, user)
expect(SystemNoteService).not_to receive(:zoom_link_removed)
subject.execute
end
end
context 'when the zoom link has been added to the description' do
let(:description) { 'a changed issue description https://zoom.us/j/123' }
let(:old_description) { 'an issue description' }
it 'creates a zoom_link_added notification' do
expect(SystemNoteService).to receive(:zoom_link_added).with(issue, project, user)
expect(SystemNoteService).not_to receive(:zoom_link_removed)
subject.execute
end
end
context 'when the zoom link has been removed from the description' do
let(:description) { 'a changed issue description' }
let(:old_description) { 'an issue description https://zoom.us/j/123' }
it 'creates a zoom_link_removed notification' do
expect(SystemNoteService).not_to receive(:zoom_link_added).with(issue, project, user)
expect(SystemNoteService).to receive(:zoom_link_removed)
subject.execute
end
end
end
end
......@@ -2,22 +2,19 @@
shared_examples 'zoom quick actions' do
let(:zoom_link) { 'https://zoom.us/j/123456789' }
let(:existing_zoom_link) { 'https://zoom.us/j/123456780' }
let(:invalid_zoom_link) { 'https://invalid-zoom' }
before do
issue.update!(description: description)
end
describe '/zoom' do
shared_examples 'skip silently' do
it 'skip addition silently' do
it 'skips addition silently' do
add_note("/zoom #{zoom_link}")
wait_for_requests
expect(page).not_to have_content('Zoom meeting added')
expect(page).not_to have_content('Failed to add a Zoom meeting')
expect(issue.reload.description).to eq(description)
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).not_to eq(zoom_link)
end
end
......@@ -28,13 +25,11 @@ shared_examples 'zoom quick actions' do
wait_for_requests
expect(page).to have_content('Zoom meeting added')
expect(issue.reload.description).to end_with(zoom_link)
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to eq(zoom_link)
end
end
context 'without issue description' do
let(:description) { nil }
context 'without zoom_meetings' do
include_examples 'success'
it 'cannot add invalid zoom link' do
......@@ -47,14 +42,18 @@ shared_examples 'zoom quick actions' do
end
end
context 'with Zoom link not at the end of the issue description' do
let(:description) { "A link #{zoom_link} not at the end" }
context 'with "removed" zoom meeting' do
before do
create(:zoom_meeting, issue_status: :removed, url: existing_zoom_link, issue: issue)
end
include_examples 'success'
end
context 'with Zoom link at end of the issue description' do
let(:description) { "Text\n#{zoom_link}" }
context 'with "added" zoom meeting' do
before do
create(:zoom_meeting, issue_status: :added, url: existing_zoom_link, issue: issue)
end
include_examples 'skip silently'
end
......@@ -62,19 +61,19 @@ shared_examples 'zoom quick actions' do
describe '/remove_zoom' do
shared_examples 'skip silently' do
it 'skip removal silently' do
it 'skips removal silently' do
add_note('/remove_zoom')
wait_for_requests
expect(page).not_to have_content('Zoom meeting removed')
expect(page).not_to have_content('Failed to remove a Zoom meeting')
expect(issue.reload.description).to eq(description)
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
end
end
context 'with Zoom link in the description' do
let(:description) { "Text with #{zoom_link}\n\n\n#{zoom_link}" }
context 'with added zoom meeting' do
let!(:added_zoom_meeting) { create(:zoom_meeting, url: zoom_link, issue: issue, issue_status: :added) }
it 'removes last Zoom link' do
add_note('/remove_zoom')
......@@ -82,14 +81,8 @@ shared_examples 'zoom quick actions' do
wait_for_requests
expect(page).to have_content('Zoom meeting removed')
expect(issue.reload.description).to eq("Text with #{zoom_link}")
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
end
end
context 'with a Zoom link not at the end of the description' do
let(:description) { "A link #{zoom_link} not at the end" }
include_examples 'skip silently'
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