Refactoring: Created generic UploadCache class, controller and routes to handle attachment upload.
This commit is contained in:
parent
f5b671729d
commit
c7f8045569
14 changed files with 419 additions and 132 deletions
|
@ -38,8 +38,7 @@ class App.UiElement.richtext
|
|||
# delete attachment from storage
|
||||
App.Ajax.request(
|
||||
type: 'DELETE'
|
||||
url: "#{App.Config.get('api_path')}/ticket_attachment_upload"
|
||||
data: JSON.stringify(id: id),
|
||||
url: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}/items/#{id}"
|
||||
processData: false
|
||||
)
|
||||
|
||||
|
@ -57,15 +56,15 @@ class App.UiElement.richtext
|
|||
@attachmentsHolder = item.find('.attachments')
|
||||
@cancelContainer = item.find('.js-cancel')
|
||||
|
||||
u = => html5Upload.initialize(
|
||||
uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
upload_initialize_callback = =>
|
||||
form_id = item.closest('form').find('[name=form_id]').val()
|
||||
html5Upload.initialize(
|
||||
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{form_id}"
|
||||
dropContainer: item.closest('form').get(0)
|
||||
cancelContainer: @cancelContainer
|
||||
inputField: item.find('input').get(0)
|
||||
maxSimultaneousUploads: 1,
|
||||
key: 'File'
|
||||
data:
|
||||
form_id: item.closest('form').find('[name=form_id]').val()
|
||||
onFileAdded: (file) =>
|
||||
|
||||
file.on(
|
||||
|
@ -107,9 +106,8 @@ class App.UiElement.richtext
|
|||
if parseInt(progress) >= 90
|
||||
@cancelContainer.addClass('hide')
|
||||
App.Log.debug 'UiElement.richtext', 'uploadProgress ', parseInt(progress)
|
||||
|
||||
)
|
||||
)
|
||||
App.Delay.set(u, 100, undefined, 'form_upload')
|
||||
App.Delay.set(upload_initialize_callback, 100, undefined, 'form_upload')
|
||||
|
||||
item
|
||||
|
|
|
@ -22,11 +22,10 @@ class App.UiElement.textarea
|
|||
|
||||
# only add upload item if html element exists
|
||||
if $('#' + fileUploaderId )[0]
|
||||
form_id = item.closest('form').find('[name=form_id]').val()
|
||||
$('#' + fileUploaderId ).fineUploader(
|
||||
request:
|
||||
endpoint: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
params:
|
||||
form_id: item.closest('form').find('[name=form_id]').val()
|
||||
endpoint: "#{App.Config.get('api_path')}/upload_caches/#{form_id}"
|
||||
text:
|
||||
uploadButton: App.Utils.icon('paperclip')
|
||||
template: '<div class="qq-uploader">' +
|
||||
|
|
|
@ -958,8 +958,7 @@ class App.TicketZoom extends App.Controller
|
|||
# reset/delete uploaded attachments
|
||||
App.Ajax.request(
|
||||
type: 'DELETE'
|
||||
url: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
data: JSON.stringify(form_id: @form_id)
|
||||
url: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}"
|
||||
processData: false
|
||||
)
|
||||
|
||||
|
|
|
@ -172,13 +172,11 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
})
|
||||
|
||||
html5Upload.initialize(
|
||||
uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}"
|
||||
dropContainer: @$('.article-add').get(0)
|
||||
cancelContainer: @cancelContainer
|
||||
inputField: @$('.article-attachment input').get(0)
|
||||
key: 'File'
|
||||
data:
|
||||
form_id: @form_id
|
||||
maxSimultaneousUploads: 1
|
||||
onFileAdded: (file) =>
|
||||
|
||||
|
@ -600,8 +598,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
# delete attachment from storage
|
||||
App.Ajax.request(
|
||||
type: 'DELETE'
|
||||
url: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
data: JSON.stringify(id: id)
|
||||
url: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}/items/#{id}"
|
||||
processData: false
|
||||
)
|
||||
|
||||
|
|
|
@ -55,10 +55,7 @@ module CreatesTicketArticles
|
|||
|
||||
# find attachments in upload cache
|
||||
if form_id
|
||||
article.attachments = Store.list(
|
||||
object: 'UploadCache',
|
||||
o_id: form_id,
|
||||
)
|
||||
article.attachments = UploadCache.new(form_id).attachments
|
||||
end
|
||||
|
||||
# set subtype of present
|
||||
|
@ -130,11 +127,8 @@ module CreatesTicketArticles
|
|||
.first { |taskbar| taskbar.persisted_form_id == form_id }
|
||||
&.update!(state: {})
|
||||
|
||||
# remove attachments from upload cache
|
||||
Store.remove(
|
||||
object: 'UploadCache',
|
||||
o_id: form_id,
|
||||
)
|
||||
# remove temporary attachment cache
|
||||
UploadCache.new(form_id).destroy
|
||||
|
||||
article
|
||||
end
|
||||
|
|
|
@ -154,66 +154,6 @@ class TicketArticlesController < ApplicationController
|
|||
raise Exceptions::NotAuthorized, 'Not authorized (admin permission required)!'
|
||||
end
|
||||
|
||||
# DELETE /ticket_attachment_upload
|
||||
def ticket_attachment_upload_delete
|
||||
|
||||
if params[:id].present?
|
||||
Store.remove_item(params[:id])
|
||||
render json: {
|
||||
success: true,
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
if params[:form_id].present?
|
||||
Store.remove(
|
||||
object: 'UploadCache',
|
||||
o_id: params[:form_id],
|
||||
)
|
||||
render json: {
|
||||
success: true,
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
render json: { message: 'No such id or form_id!' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
# POST /ticket_attachment_upload
|
||||
def ticket_attachment_upload_add
|
||||
|
||||
# store file
|
||||
file = params[:File]
|
||||
content_type = file.content_type
|
||||
if !content_type || content_type == 'application/octet-stream'
|
||||
content_type = if MIME::Types.type_for(file.original_filename).first
|
||||
MIME::Types.type_for(file.original_filename).first.content_type
|
||||
else
|
||||
'application/octet-stream'
|
||||
end
|
||||
end
|
||||
headers_store = {
|
||||
'Content-Type' => content_type
|
||||
}
|
||||
store = Store.add(
|
||||
object: 'UploadCache',
|
||||
o_id: params[:form_id],
|
||||
data: file.read,
|
||||
filename: file.original_filename,
|
||||
preferences: headers_store
|
||||
)
|
||||
|
||||
# return result
|
||||
render json: {
|
||||
success: true,
|
||||
data: {
|
||||
id: store.id,
|
||||
filename: file.original_filename,
|
||||
size: store.size,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# POST /ticket_attachment_upload_clone_by_article
|
||||
def ticket_attachment_upload_clone_by_article
|
||||
article = Ticket::Article.find(params[:article_id])
|
||||
|
|
58
app/controllers/upload_caches_controller.rb
Normal file
58
app/controllers/upload_caches_controller.rb
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class UploadCachesController < ApplicationController
|
||||
prepend_before_action :authentication_check
|
||||
|
||||
# POST /upload_caches/1
|
||||
def update
|
||||
file = params[:File]
|
||||
content_type = file.content_type
|
||||
if !content_type || content_type == 'application/octet-stream'
|
||||
mime_type = MIME::Types.type_for(file.original_filename).first
|
||||
content_type = mime_type&.content_type || 'application/octet-stream'
|
||||
end
|
||||
headers_store = {
|
||||
'Content-Type' => content_type
|
||||
}
|
||||
|
||||
store = cache.add(
|
||||
filename: file.original_filename,
|
||||
data: file.read,
|
||||
preferences: headers_store
|
||||
)
|
||||
|
||||
# return result
|
||||
render json: {
|
||||
success: true,
|
||||
data: {
|
||||
id: store.id, # TODO: rename?
|
||||
filename: file.original_filename,
|
||||
size: store.size,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# DELETE /upload_caches/1
|
||||
def destroy
|
||||
cache.destroy
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
}
|
||||
end
|
||||
|
||||
# DELETE /upload_caches/1/items/1
|
||||
def remove_item
|
||||
cache.remove_item(params[:store_id])
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cache
|
||||
UploadCache.new(params[:id])
|
||||
end
|
||||
end
|
|
@ -54,7 +54,7 @@ class Taskbar < ApplicationModel
|
|||
def attachments
|
||||
return [] if persisted_form_id.blank?
|
||||
|
||||
Store.list(object: 'UploadCache', o_id: persisted_form_id)
|
||||
UploadCache.new(persisted_form_id).attachments
|
||||
end
|
||||
|
||||
def add_attachments_to_attributes(attributes)
|
||||
|
|
|
@ -42,8 +42,6 @@ Zammad::Application.routes.draw do
|
|||
match api_path + '/ticket_articles/:id', to: 'ticket_articles#update', via: :put
|
||||
match api_path + '/ticket_articles/:id', to: 'ticket_articles#destroy', via: :delete
|
||||
match api_path + '/ticket_attachment/:ticket_id/:article_id/:id', to: 'ticket_articles#attachment', via: :get
|
||||
match api_path + '/ticket_attachment_upload', to: 'ticket_articles#ticket_attachment_upload_add', via: :post
|
||||
match api_path + '/ticket_attachment_upload', to: 'ticket_articles#ticket_attachment_upload_delete', via: :delete
|
||||
match api_path + '/ticket_attachment_upload_clone_by_article/:article_id', to: 'ticket_articles#ticket_attachment_upload_clone_by_article', via: :post
|
||||
match api_path + '/ticket_article_plain/:id', to: 'ticket_articles#article_plain', via: :get
|
||||
|
||||
|
|
9
config/routes/upload_cache.rb
Normal file
9
config/routes/upload_cache.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
# upload cache
|
||||
match api_path + '/upload_caches/:id', to: 'upload_caches#update', via: :post
|
||||
match api_path + '/upload_caches/:id', to: 'upload_caches#destroy', via: :delete
|
||||
match api_path + '/upload_caches/:id/items/:store_id', to: 'upload_caches#remove_item', via: :delete
|
||||
|
||||
end
|
98
lib/upload_cache.rb
Normal file
98
lib/upload_cache.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
# A wrapper class around Store that handles temporary attachment uploads
|
||||
# and provides an interface for those.
|
||||
class UploadCache
|
||||
|
||||
attr_reader :id
|
||||
|
||||
# Initializes a UploadCache for a given form_id.
|
||||
#
|
||||
# @example
|
||||
# cache = UploadCache.new(form_id)
|
||||
#
|
||||
# @return [UploadCache]
|
||||
def initialize(id)
|
||||
# conversion to Integer is required for proper Store#o_id comparsion
|
||||
@id = id.to_i
|
||||
end
|
||||
|
||||
# Adds a Store item with the given attributes for the form_id.
|
||||
#
|
||||
# @see Store#add
|
||||
#
|
||||
# @example
|
||||
# cache = UploadCache.new(form_id)
|
||||
# store = cache.add(
|
||||
# filename: file.original_filename,
|
||||
# data: file.read,
|
||||
# preferences: {
|
||||
# 'Content-Type' => 'application/octet-stream'
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# @return [Store] the created Store item
|
||||
def add(args)
|
||||
Store.add(
|
||||
args.merge(
|
||||
object: store_object,
|
||||
o_id: id,
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Provides all Store items associated to the form_id.
|
||||
#
|
||||
# @see Store#list
|
||||
#
|
||||
# @example
|
||||
# attachments = UploadCache.new(form_id).attachments
|
||||
#
|
||||
# @return [Array<Store>] an enumerator of Store items
|
||||
def attachments
|
||||
Store.list(
|
||||
object: store_object,
|
||||
o_id: id,
|
||||
)
|
||||
end
|
||||
|
||||
# Removes all Store items associated to the form_id.
|
||||
#
|
||||
# @see Store#remove
|
||||
#
|
||||
# @example
|
||||
# UploadCache.new(form_id).destroy
|
||||
#
|
||||
def destroy
|
||||
Store.remove(
|
||||
object: store_object,
|
||||
o_id: id,
|
||||
)
|
||||
end
|
||||
|
||||
# Removes all Store items associated to the form_id.
|
||||
#
|
||||
# @see Store#remove
|
||||
#
|
||||
# @example
|
||||
# UploadCache.new(form_id).remove_item(store_id)
|
||||
#
|
||||
# @raise [Exceptions::UnprocessableEntity] in cases where a Store item should get deleted that is not an UploadCache item
|
||||
#
|
||||
def remove_item(store_id = nil)
|
||||
store = Store.find(store_id)
|
||||
if store.o_id != id || store.store_object_id != store_object_id
|
||||
raise Exceptions::UnprocessableEntity, "Attempt to delete Store item #{store_id} that is not bound to UploadCache object"
|
||||
end
|
||||
|
||||
Store.remove_item(store_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def store_object
|
||||
self.class.name
|
||||
end
|
||||
|
||||
def store_object_id
|
||||
Store::Object.lookup(name: store_object).id
|
||||
end
|
||||
end
|
1
spec/fixtures/upload/hello_world.txt
vendored
Normal file
1
spec/fixtures/upload/hello_world.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Hello World!
|
114
spec/lib/upload_cache_spec.rb
Normal file
114
spec/lib/upload_cache_spec.rb
Normal file
|
@ -0,0 +1,114 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UploadCache do
|
||||
|
||||
let(:subject) { described_class.new(1337) }
|
||||
|
||||
# required for adding items to the Store
|
||||
before { UserInfo.current_user_id = 1 }
|
||||
|
||||
describe '#initialize' do
|
||||
|
||||
it 'converts given (form_)id to an Integer' do
|
||||
expect(described_class.new('1337').id).to eq(1337)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#add' do
|
||||
|
||||
it 'adds a Store item' do
|
||||
expect do
|
||||
subject.add(
|
||||
data: 'content_file3_normally_should_be_an_image',
|
||||
filename: 'some_file3.jpg',
|
||||
preferences: {
|
||||
'Content-Type' => 'image/jpeg',
|
||||
'Mime-Type' => 'image/jpeg',
|
||||
'Content-Disposition' => 'attached',
|
||||
},
|
||||
)
|
||||
end.to change { Store.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#attachments' do
|
||||
|
||||
before do
|
||||
subject.add(
|
||||
data: 'hello world',
|
||||
filename: 'some.txt',
|
||||
preferences: {
|
||||
'Content-Type' => 'text/plain',
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns all Store items' do
|
||||
attachments = subject.attachments
|
||||
|
||||
expect(attachments.count).to be(1)
|
||||
expect(attachments).to include(Store.last)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#destroy' do
|
||||
|
||||
before do
|
||||
subject.add(
|
||||
data: 'hello world',
|
||||
filename: 'some.txt',
|
||||
preferences: {
|
||||
'Content-Type' => 'text/plain',
|
||||
},
|
||||
)
|
||||
|
||||
subject.add(
|
||||
data: 'hello other world',
|
||||
filename: 'another_some.txt',
|
||||
preferences: {
|
||||
'Content-Type' => 'text/plain',
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes all added Store items' do
|
||||
expect { subject.destroy }.to change { Store.count }.by(-2)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#remove_item' do
|
||||
|
||||
before do
|
||||
subject.add(
|
||||
data: 'hello world',
|
||||
filename: 'some.txt',
|
||||
preferences: {
|
||||
'Content-Type' => 'text/plain',
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes the Store item matching the given ID' do
|
||||
expect { subject.remove_item(Store.last.id) }.to change { Store.count }.by(-1)
|
||||
end
|
||||
|
||||
it 'prevents removage of non UploadCache Store items' do
|
||||
|
||||
item = Store.add(
|
||||
object: 'Ticket',
|
||||
o_id: 1,
|
||||
data: "Can't touch this",
|
||||
filename: 'keep.txt',
|
||||
preferences: {
|
||||
'Content-Type' => 'text/plain',
|
||||
},
|
||||
)
|
||||
|
||||
expect { subject.remove_item(item.id) }.to raise_error(Exceptions::UnprocessableEntity)
|
||||
end
|
||||
|
||||
it 'fails for non existing UploadCache Store items' do
|
||||
expect { subject.remove_item(1337) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
82
spec/requests/upload_cache_spec.rb
Normal file
82
spec/requests/upload_cache_spec.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'UploadCache', type: :request do
|
||||
|
||||
let(:user) { create(:customer_user) }
|
||||
let(:form_id) { 1337 }
|
||||
let(:upload_cache) { UploadCache.new(form_id) }
|
||||
|
||||
# required for adding items to the Store
|
||||
before { UserInfo.current_user_id = 1 }
|
||||
|
||||
before { authenticated_as(user) }
|
||||
|
||||
describe '/upload_caches/:id' do
|
||||
|
||||
context 'for POST requests' do
|
||||
|
||||
it 'adds items to UploadCache' do
|
||||
params = {
|
||||
File: fixture_file_upload('upload/hello_world.txt', 'text/plain')
|
||||
}
|
||||
post "/api/v1/upload_caches/#{form_id}", params: params
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'detects Content-Type for binary uploads' do
|
||||
params = {
|
||||
File: fixture_file_upload('upload/hello_world.txt', 'application/octet-stream')
|
||||
}
|
||||
post "/api/v1/upload_caches/#{form_id}", params: params
|
||||
|
||||
expect(Store.last.preferences['Content-Type']).to eq('text/plain')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for DELETE requests' do
|
||||
|
||||
before do
|
||||
2.times do |iteration|
|
||||
upload_cache.add(
|
||||
data: "Can't touch this #{iteration}",
|
||||
filename: 'keep.txt',
|
||||
preferences: {
|
||||
'Content-Type' => 'text/plain',
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes all form_id UploadCache items' do
|
||||
expect do
|
||||
delete "/api/v1/upload_caches/#{form_id}", as: :json
|
||||
end.to change { upload_cache.attachments }.to([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '/upload_caches/:id/items/:store_id' do
|
||||
|
||||
context 'for DELETE requests' do
|
||||
|
||||
before do
|
||||
upload_cache.add(
|
||||
data: "Can't touch this",
|
||||
filename: 'keep.txt',
|
||||
preferences: {
|
||||
'Content-Type' => 'text/plain',
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes a UploadCache item by given store id' do
|
||||
|
||||
store_id = upload_cache.attachments.first.id
|
||||
delete "/api/v1/upload_caches/#{form_id}/items/#{store_id}", as: :json
|
||||
|
||||
expect(Store.exists?(store_id)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue