Commit e865eea7 authored by Max Woolf's avatar Max Woolf

Merge branch '346171-add-url-text-to-issuable-metric-images' into 'master'

Add url_text column to issuable metric images, and allow updates to url and url_text

See merge request gitlab-org/gitlab!78430
parents 3736bc11 5f3e7c13
# frozen_string_literal: true
class AddUrlTextToIssuableMetricImages < Gitlab::Database::Migration[1.0]
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20220118020026_add_url_text_limit_to_issuable_metric_images
def change
add_column :issuable_metric_images, :url_text, :text
end
# rubocop:enable Migration/AddLimitToTextColumns
end
# frozen_string_literal: true
class AddUrlTextLimitToIssuableMetricImages < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_text_limit :issuable_metric_images, :url_text, 128
end
def down
remove_text_limit :issuable_metric_images, :url_text
end
end
6a73f49306de7c799a39afa3ac1f761840860833a96f1a91cf992c9a3ebfef9b
\ No newline at end of file
77374c81456f235d3afeb45cdda14552e1ef8047de5aaa3f5bb0a82e4aebe849
\ No newline at end of file
...@@ -15353,6 +15353,8 @@ CREATE TABLE issuable_metric_images ( ...@@ -15353,6 +15353,8 @@ CREATE TABLE issuable_metric_images (
file_store smallint, file_store smallint,
file text NOT NULL, file text NOT NULL,
url text, url text,
url_text text,
CONSTRAINT check_3bc6d47661 CHECK ((char_length(url_text) <= 128)),
CONSTRAINT check_5b3011e234 CHECK ((char_length(url) <= 255)), CONSTRAINT check_5b3011e234 CHECK ((char_length(url) <= 255)),
CONSTRAINT check_7ed527062f CHECK ((char_length(file) <= 255)) CONSTRAINT check_7ed527062f CHECK ((char_length(file) <= 255))
); );
...@@ -2428,10 +2428,11 @@ POST /projects/:id/issues/:issue_iid/metric_images ...@@ -2428,10 +2428,11 @@ POST /projects/:id/issues/:issue_iid/metric_images
| `issue_iid` | integer | yes | The internal ID of a project's issue | | `issue_iid` | integer | yes | The internal ID of a project's issue |
| `file` | file | yes | The image file to be uploaded | | `file` | file | yes | The image file to be uploaded |
| `url` | string | no | The URL to view more metric information | | `url` | string | no | The URL to view more metric information |
| `url_text` | string | no | A description of the image or URL |
```shell ```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --form 'file=@/path/to/file.png' \ curl --header "PRIVATE-TOKEN: <your_access_token>" --form 'file=@/path/to/file.png' \
--form 'url=http://example.com' "https://gitlab.example.com/api/v4/projects/5/issues/93/metric_images" --form 'url=http://example.com' --form 'url_text=Example website' "https://gitlab.example.com/api/v4/projects/5/issues/93/metric_images"
``` ```
Example response: Example response:
...@@ -2442,7 +2443,8 @@ Example response: ...@@ -2442,7 +2443,8 @@ Example response:
"created_at": "2020-11-13T00:06:18.084Z", "created_at": "2020-11-13T00:06:18.084Z",
"filename": "file.png", "filename": "file.png",
"file_path": "/uploads/-/system/issuable_metric_image/file/23/file.png", "file_path": "/uploads/-/system/issuable_metric_image/file/23/file.png",
"url": "http://example.com" "url": "http://example.com",
"url_text": "Example website"
} }
``` ```
...@@ -2484,6 +2486,39 @@ Example response: ...@@ -2484,6 +2486,39 @@ Example response:
] ]
``` ```
## Update metric image
Available only for Incident issues.
```plaintext
PUT /projects/:id/issues/:issue_iid/metric_images/:image_id
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `image_id` | integer | yes | The ID of the image |
| `url` | string | no | The URL to view more metric information |
| `url_text` | string | no | A description of the image or URL |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request PUT --form 'url=http://example.com' --form 'url_text=Example website' "https://gitlab.example.com/api/v4/projects/5/issues/93/metric_images/1"
```
Example response:
```json
{
"id": 23,
"created_at": "2020-11-13T00:06:18.084Z",
"filename": "file.png",
"file_path": "/uploads/-/system/issuable_metric_image/file/23/file.png",
"url": "http://example.com",
"url_text": "Example website"
}
```
## Delete metric image ## Delete metric image
Available only for Incident issues. Available only for Incident issues.
......
...@@ -15,6 +15,7 @@ class IssuableMetricImage < ApplicationRecord ...@@ -15,6 +15,7 @@ class IssuableMetricImage < ApplicationRecord
validates :file, presence: true validates :file, presence: true
validate :validate_file_is_image validate :validate_file_is_image
validates :url, length: { maximum: 255 }, public_url: { allow_blank: true } validates :url, length: { maximum: 255 }, public_url: { allow_blank: true }
validates :url_text, length: { maximum: 128 }
scope :order_created_at_asc, -> { order(created_at: :asc) } scope :order_created_at_asc, -> { order(created_at: :asc) }
......
...@@ -31,11 +31,13 @@ module EE ...@@ -31,11 +31,13 @@ module EE
end end
rule { is_author | can?(:create_issue) & can?(:update_issue) }.policy do rule { is_author | can?(:create_issue) & can?(:update_issue) }.policy do
enable :update_issuable_metric_image
enable :destroy_issuable_metric_image enable :destroy_issuable_metric_image
end end
rule { ~is_project_member }.policy do rule { ~is_project_member }.policy do
prevent :upload_issuable_metric_image prevent :upload_issuable_metric_image
prevent :update_issuable_metric_image
prevent :destroy_issuable_metric_image prevent :destroy_issuable_metric_image
end end
end end
......
...@@ -10,6 +10,7 @@ module IncidentManagement ...@@ -10,6 +10,7 @@ module IncidentManagement
@project = issuable&.project @project = issuable&.project
@file = params.fetch(:file) @file = params.fetch(:file)
@url = params.fetch(:url, nil) @url = params.fetch(:url, nil)
@url_text = params.fetch(:url_text, nil)
end end
def execute def execute
...@@ -22,7 +23,7 @@ module IncidentManagement ...@@ -22,7 +23,7 @@ module IncidentManagement
ServiceResponse.error(message: e.message) ServiceResponse.error(message: e.message)
end end
attr_reader :issuable, :project, :file, :url, :metric attr_reader :issuable, :project, :file, :url, :url_text, :metric
private private
...@@ -30,7 +31,8 @@ module IncidentManagement ...@@ -30,7 +31,8 @@ module IncidentManagement
@metric = IssuableMetricImage.create!( @metric = IssuableMetricImage.create!(
issue: issuable, issue: issuable,
file: file, file: file,
url: url url: url,
url_text: url_text
) )
end end
......
...@@ -5,7 +5,7 @@ module EE ...@@ -5,7 +5,7 @@ module EE
module Entities module Entities
class IssuableMetricImage < Grape::Entity class IssuableMetricImage < Grape::Entity
expose :id, :created_at expose :id, :created_at
expose :filename, :file_path, :url expose :filename, :file_path, :url, :url_text
end end
end end
end end
......
...@@ -34,6 +34,7 @@ module EE ...@@ -34,6 +34,7 @@ module EE
params do params do
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The image file to be uploaded' requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The image file to be uploaded'
optional :url, type: String, desc: 'The url to view more metric info' optional :url, type: String, desc: 'The url to view more metric info'
optional :url_text, type: String, desc: 'A description of the image or URL'
end end
post do post do
require_gitlab_workhorse! require_gitlab_workhorse!
...@@ -44,7 +45,7 @@ module EE ...@@ -44,7 +45,7 @@ module EE
upload = ::IncidentManagement::Incidents::UploadMetricService.new( upload = ::IncidentManagement::Incidents::UploadMetricService.new(
issue, issue,
current_user, current_user,
params.slice(:file, :url) params.slice(:file, :url, :url_text)
).execute ).execute
if upload.success? if upload.success?
...@@ -71,6 +72,30 @@ module EE ...@@ -71,6 +72,30 @@ module EE
end end
end end
desc 'Update a metric image for an issue' do
success Entities::IssuableMetricImage
end
params do
requires :metric_image_id, type: Integer, desc: 'The ID of metric image'
optional :url, type: String, desc: 'The url to view more metric info'
optional :url_text, type: String, desc: 'A description of the image or URL'
end
put ':metric_image_id' do
issue = find_project_issue(params[:issue_iid])
authorize!(:update_issuable_metric_image, issue)
metric_image = issue.metric_images.find_by_id(params[:metric_image_id])
render_api_error!('Metric image not found', 404) unless metric_image
if metric_image&.update(params.slice(:url, :url_text))
present metric_image, with: Entities::IssuableMetricImage, current_user: current_user, project: user_project
else
render_api_error!('Metric image could not be updated', 400)
end
end
desc 'Remove a metric image for an issue' do desc 'Remove a metric image for an issue' do
success Entities::IssuableMetricImage success Entities::IssuableMetricImage
end end
......
...@@ -15,6 +15,7 @@ RSpec.describe IssuableMetricImage do ...@@ -15,6 +15,7 @@ RSpec.describe IssuableMetricImage do
it { is_expected.not_to allow_value(txt_file).for(:file) } it { is_expected.not_to allow_value(txt_file).for(:file) }
it { is_expected.to allow_value(img_file).for(:file) } it { is_expected.to allow_value(img_file).for(:file) }
it { is_expected.to validate_length_of(:url_text).is_at_most(128) }
describe 'url' do describe 'url' do
it { is_expected.not_to allow_value('test').for(:url) } it { is_expected.not_to allow_value('test').for(:url) }
......
...@@ -30,19 +30,19 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -30,19 +30,19 @@ RSpec.describe IssuablePolicy, models: true do
it 'disallows non-members from creating and deleting metric images' do it 'disallows non-members from creating and deleting metric images' do
expect(permissions(non_member, issue)).to be_allowed(:read_issuable_metric_image) expect(permissions(non_member, issue)).to be_allowed(:read_issuable_metric_image)
expect(permissions(non_member, issue)).to be_disallowed(:upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(non_member, issue)).to be_disallowed(:upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
end end
it 'allows guests to read, create metric images, and delete them in their own issues' do it 'allows guests to read, create metric images, and delete them in their own issues' do
expect(permissions(guest, issue)).to be_allowed(:read_issuable_metric_image) expect(permissions(guest, issue)).to be_allowed(:read_issuable_metric_image)
expect(permissions(guest, issue)).to be_disallowed(:upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(guest, issue)).to be_disallowed(:upload_issuable_metric_image, :destroy_issuable_metric_image)
expect(permissions(guest, guest_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(guest, guest_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
end end
it 'allows reporters to create and delete metric images' do it 'allows reporters to create and delete metric images' do
expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
end end
context 'Timeline events' do context 'Timeline events' do
...@@ -79,19 +79,19 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -79,19 +79,19 @@ RSpec.describe IssuablePolicy, models: true do
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
it 'disallows non-members from creating and deleting metric images' do it 'disallows non-members from creating and deleting metric images' do
expect(permissions(non_member, issue)).to be_disallowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(non_member, issue)).to be_disallowed(:read_issuable_metric_image, :upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
end end
it 'allows guests to read metric images, and create + delete in their own issues' do it 'allows guests to read metric images, and create + delete in their own issues' do
expect(permissions(guest, issue)).to be_allowed(:read_issuable_metric_image) expect(permissions(guest, issue)).to be_allowed(:read_issuable_metric_image)
expect(permissions(guest, issue)).to be_disallowed(:upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(guest, issue)).to be_disallowed(:upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
expect(permissions(guest, guest_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(guest, guest_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
end end
it 'allows reporters to create and delete metric images' do it 'allows reporters to create and delete metric images' do
expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :destroy_issuable_metric_image) expect(permissions(reporter, reporter_issue)).to be_allowed(:read_issuable_metric_image, :upload_issuable_metric_image, :update_issuable_metric_image, :destroy_issuable_metric_image)
end end
context 'Timeline events' do context 'Timeline events' do
......
...@@ -583,8 +583,9 @@ RSpec.describe API::Issues, :mailer do ...@@ -583,8 +583,9 @@ RSpec.describe API::Issues, :mailer do
let(:file) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:file) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:file_name) { 'rails_sample.jpg' } let(:file_name) { 'rails_sample.jpg' }
let(:url) { 'http://gitlab.com' } let(:url) { 'http://gitlab.com' }
let(:url_text) { 'GitLab' }
let(:params) { { url: url } } let(:params) { { url: url, url_text: url_text } }
subject do subject do
workhorse_finalize( workhorse_finalize(
...@@ -604,6 +605,7 @@ RSpec.describe API::Issues, :mailer do ...@@ -604,6 +605,7 @@ RSpec.describe API::Issues, :mailer do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(json_response['filename']).to eq(file_name) expect(json_response['filename']).to eq(file_name)
expect(json_response['url']).to eq(url) expect(json_response['url']).to eq(url)
expect(json_response['url_text']).to eq(url_text)
expect(json_response['file_path']).to match(%r{/uploads/-/system/issuable_metric_image/file/[\d+]/#{file_name}}) expect(json_response['file_path']).to match(%r{/uploads/-/system/issuable_metric_image/file/[\d+]/#{file_name}})
expect(json_response['created_at']).not_to be_nil expect(json_response['created_at']).not_to be_nil
expect(json_response['id']).not_to be_nil expect(json_response['id']).not_to be_nil
...@@ -701,7 +703,8 @@ RSpec.describe API::Issues, :mailer do ...@@ -701,7 +703,8 @@ RSpec.describe API::Issues, :mailer do
created_at: image.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), created_at: image.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
filename: image.filename, filename: image.filename,
file_path: image.file_path, file_path: image.file_path,
url: image.url url: image.url,
url_text: nil
}.with_indifferent_access }.with_indifferent_access
) )
end end
...@@ -736,6 +739,104 @@ RSpec.describe API::Issues, :mailer do ...@@ -736,6 +739,104 @@ RSpec.describe API::Issues, :mailer do
end end
end end
describe 'PUT /projects/:id/issues/:issue_iid/metric_images/:metric_image_id' do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
end
let!(:image) { create(:issuable_metric_image, issue: issue) }
let(:params) { { url: 'http://test.example.com', url_text: 'Example website 123' } }
subject { put api("/projects/#{project.id}/issues/#{issue.iid}/metric_images/#{image.id}", user2), params: params }
shared_examples 'can_update_metric_image' do
it 'can update the metric images' do
subject
expect(response).to have_gitlab_http_status(:success)
expect(json_response['url']).to eq(params[:url])
expect(json_response['url_text']).to eq(params[:url_text])
end
end
shared_examples 'unauthorized_update' do
it 'cannot delete the metric image' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
expect(image.reload).to eq(image)
end
end
shared_examples 'not_found' do
it 'cannot delete the metric image' do
subject
expect(response).to have_gitlab_http_status(:not_found)
expect(image.reload).to eq(image)
end
end
where(:user_role, :own_issue, :issue_confidential, :expected_status) do
:not_member | false | false | :unauthorized_update
:not_member | true | false | :unauthorized_update
:not_member | true | true | :unauthorized_update
:guest | false | true | :not_found
:guest | false | false | :unauthorized_update
:guest | true | false | :can_update_metric_image
:reporter | true | false | :can_update_metric_image
:reporter | false | false | :can_update_metric_image
end
with_them do
before do
stub_licensed_features(incident_metric_upload: true)
project.send("add_#{user_role}", user2) unless user_role == :not_member
end
let!(:issue) do
author = own_issue ? user2 : user
confidential = issue_confidential
create(:incident, project: project, confidential: confidential, author: author)
end
it_behaves_like "#{params[:expected_status]}"
end
context 'user has access' do
let(:issue) { create(:incident, project: project) }
before do
project.add_reporter(user2)
end
context 'metric image not found' do
subject { delete api("/projects/#{project.id}/issues/#{issue.iid}/metric_images/#{non_existing_record_id}", user2) }
it 'returns an error' do
subject
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('Metric image not found')
end
end
context 'metric image cannot be updated' do
let(:params) { { url_text: 'something_long' * 100 } }
it 'returns an error' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Metric image could not be updated')
end
end
end
end
describe 'DELETE /projects/:id/issues/:issue_iid/metric_images/:metric_image_id' do describe 'DELETE /projects/:id/issues/:issue_iid/metric_images/:metric_image_id' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
......
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