diff --git a/app/controllers/api/v2/attachments_controller.rb b/app/controllers/api/v2/attachments_controller.rb new file mode 100644 index 00000000..b88bba1b --- /dev/null +++ b/app/controllers/api/v2/attachments_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module Api + module V2 + class AttachmentsController < RestfulController + def searchable_columns + [:file] + end + end + end +end diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb new file mode 100644 index 00000000..070431de --- /dev/null +++ b/app/controllers/attachments_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +class AttachmentsController < ApplicationController + before_action :set_attachment, only: [:destroy] + after_action :verify_authorized + + def create + @attachment = Attachment.new(attachment_params) + authorize @attachment + + respond_to do |format| + if @attachment.save + format.json { render json: @attachment, status: :created } + else + format.json { render json: @attachment.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @attachment.destroy + respond_to do |format| + format.json { head :no_content } + end + end + + private + + def set_attachment + @attachment = Attachment.find(params[:id]) + authorize @attachment + end + + def attachment_params + params.require(:attachment).permit(:id, :file, :attachable_id, :attachable_type) + end +end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 5dd49e8d..a18e0956 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -15,7 +15,9 @@ class Attachment < ApplicationRecord end } + validates :attachable, presence: true validates_attachment_content_type :file, content_type: Attachable.allowed_types + validates_attachment_size :file, in: 0.megabytes..5.megabytes def image? Attachable.image_types.include?(file.instance.file_content_type) diff --git a/app/models/concerns/attachable.rb b/app/models/concerns/attachable.rb index c4154bef..a3459131 100644 --- a/app/models/concerns/attachable.rb +++ b/app/models/concerns/attachable.rb @@ -33,7 +33,8 @@ module Attachable end def audio_types - ['audio/ogg', 'audio/mp3'] + # .ogg files might be labelled as video + ['audio/ogg', 'video/ogg', 'audio/mpeg', 'audio/wav', 'video/webm'] end def text_types diff --git a/app/models/topic.rb b/app/models/topic.rb index 5c7e3127..4f9c1587 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -69,10 +69,23 @@ class Topic < ApplicationRecord Pundit.policy_scope(user, maps).map(&:id) end + def attachments_json + attachments.map do |a| + { + id: a.id, + file_name: a.file_file_name, + content_type: a.file_content_type, + file_size: a.file_file_size, + url: a.file.url + } + end + end + def as_json(options = {}) super(methods: %i[user_name user_image collaborator_ids]) .merge(inmaps: inmaps(options[:user]), inmapsLinks: inmaps_links(options[:user]), - map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user])) + map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user]), + attachments: attachments_json) end def as_rdf diff --git a/app/policies/attachment_policy.rb b/app/policies/attachment_policy.rb new file mode 100644 index 00000000..2e21bfb0 --- /dev/null +++ b/app/policies/attachment_policy.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +class AttachmentPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope.where(attachable: TopicPolicy::Scope.new(user, Topic).resolve) + end + end + + def index? + true + end + + def create? + Pundit.policy(user, record.attachable).update? + end + + def destroy? + Pundit.policy(user, record.attachable).update? + end +end diff --git a/app/serializers/api/v2/attachment_serializer.rb b/app/serializers/api/v2/attachment_serializer.rb new file mode 100644 index 00000000..f227f152 --- /dev/null +++ b/app/serializers/api/v2/attachment_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +module Api + module V2 + class AttachmentSerializer < ApplicationSerializer + attributes :id, + :file, + :attachable_type, + :attachable_id, + :created_at, + :updated_at + end + end +end diff --git a/app/serializers/api/v2/topic_serializer.rb b/app/serializers/api/v2/topic_serializer.rb index aaf6a11a..a1443cc0 100644 --- a/app/serializers/api/v2/topic_serializer.rb +++ b/app/serializers/api/v2/topic_serializer.rb @@ -14,7 +14,8 @@ module Api def self.embeddable { user: {}, - metacode: {} + metacode: {}, + attachments: {} } end diff --git a/config/routes.rb b/config/routes.rb index eba0d5e1..1ef8643a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,8 @@ Metamaps::Application.routes.draw do root to: 'main#home', via: :get get 'request', to: 'main#requestinvite', as: :request + resources :attachments, only: [:create, :destroy], shallow: true + namespace :explore do get 'active' get 'featured' @@ -121,6 +123,7 @@ Metamaps::Application.routes.draw do namespace :api, path: '/api', default: { format: :json } do namespace :v2, path: '/v2' do + resources :attachments, only: %i[index show] resources :metacodes, only: %i[index show] resources :mappings, only: %i[index create show update destroy] resources :maps, only: %i[index create show update destroy] do diff --git a/doc/api/api.raml b/doc/api/api.raml index d051b099..73ae46bb 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -22,6 +22,7 @@ traits: searchable: !include traits/searchable.raml schemas: + attachment: !include schemas/_attachment.json map: !include schemas/_map.json mapping: !include schemas/_mapping.json metacode: !include schemas/_metacode.json @@ -35,6 +36,7 @@ schemas: # item: !include resourceTypes/item.raml # collection: !include resourceTypes/collection.raml +/attachments : !include api/attachments.raml /maps: !include apis/maps.raml /mappings: !include apis/mappings.raml /metacodes: !include apis/metacodes.raml diff --git a/doc/api/apis/attachments.raml b/doc/api/apis/attachments.raml new file mode 100644 index 00000000..50e2189e --- /dev/null +++ b/doc/api/apis/attachments.raml @@ -0,0 +1,16 @@ +get: + is: [ searchable: { searchFields: "file" }, orderable, pageable ] + securedBy: [ null, token, oauth_2_0 ] + responses: + 200: + body: + application/json: + example: !include ../examples/attachments.json +/{id}: + get: + securedBy: [ null, token, oauth_2_0 ] + responses: + 200: + body: + application/json: + example: !include ../examples/attachment.json diff --git a/doc/api/examples/attachment.json b/doc/api/examples/attachment.json new file mode 100644 index 00000000..e52ece3a --- /dev/null +++ b/doc/api/examples/attachment.json @@ -0,0 +1,10 @@ +{ + "data": { + "id": 1, + "file": "https://example.org/file.png", + "attachable_type": "Topic", + "attachable_id": 187, + "created_at": "2017-03-01T05:48:09.533Z", + "updated_at": "2017-03-01T05:48:09.533Z" + } +} diff --git a/doc/api/examples/attachments.json b/doc/api/examples/attachments.json new file mode 100644 index 00000000..c70b0112 --- /dev/null +++ b/doc/api/examples/attachments.json @@ -0,0 +1,28 @@ +{ + "data": [ + { + "id": 1, + "file": "https://example.org/file.png", + "attachable_type": "Topic", + "attachable_id": 187, + "created_at": "2017-03-01T05:48:09.533Z", + "updated_at": "2017-03-01T05:48:09.533Z" + }, + { + "id": 2, + "file": "https://example.org/file.docx", + "attachable_type": "Message", + "attachable_id": 1043, + "created_at": "2017-03-01T05:50:19.533Z", + "updated_at": "2017-03-01T05:50:19.533Z" + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 156, + "total_count": 312, + "per": 2 + } +} diff --git a/doc/api/schemas/_attachment.json b/doc/api/schemas/_attachment.json new file mode 100644 index 00000000..78f20517 --- /dev/null +++ b/doc/api/schemas/_attachment.json @@ -0,0 +1,34 @@ +{ + "name": "Attachment", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "file": { + "format": "uri", + "type": "string" + }, + "attachable_type": { + "pattern": "(Topic|Message)", + "type": "string" + }, + "attachable_id": { + "$ref": "_id.json" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + } + }, + "required": [ + "id", + "file", + "attachable_type", + "attachable_id", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/attachment.json b/doc/api/schemas/attachment.json new file mode 100644 index 00000000..13357371 --- /dev/null +++ b/doc/api/schemas/attachment.json @@ -0,0 +1,12 @@ +{ + "name": "Attachment Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_attachment.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/attachments.json b/doc/api/schemas/attachments.json new file mode 100644 index 00000000..5f09e609 --- /dev/null +++ b/doc/api/schemas/attachments.json @@ -0,0 +1,19 @@ +{ + "name": "Attachments", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_attachment.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/spec/api/v2/attachments_api_spec.rb b/spec/api/v2/attachments_api_spec.rb new file mode 100644 index 00000000..04687f0c --- /dev/null +++ b/spec/api/v2/attachments_api_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe 'topics API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:attachment) { create(:attachment) } + + it 'GET /api/v2/attachments' do + create_list(:attachment, 5) + get '/api/v2/attachments', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:attachments) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/attachments/:id' do + get "/api/v2/attachments/#{attachment.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:attachment) + expect(JSON.parse(response.body)['data']['id']).to eq attachment.id + end + + context 'RAML example' do + let(:resource) { get_json_example(:attachment) } + let(:collection) { get_json_example(:attachments) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:attachment) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:attachments) + end + end +end diff --git a/spec/factories/attachments.rb b/spec/factories/attachments.rb new file mode 100644 index 00000000..92a8ec74 --- /dev/null +++ b/spec/factories/attachments.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +FactoryGirl.define do + factory :attachment do + association :attachable, factory: :topic + end +end diff --git a/spec/policies/map_policy_spec.rb b/spec/policies/map_policy_spec.rb index 8ac8c793..878c0ef5 100644 --- a/spec/policies/map_policy_spec.rb +++ b/spec/policies/map_policy_spec.rb @@ -23,7 +23,7 @@ RSpec.describe MapPolicy, type: :policy do context 'private' do let(:map) { create(:map, permission: :private) } permissions :show?, :create?, :update?, :destroy? do - it 'permits access' do + it 'denies access' do expect(subject).to_not permit(nil, map) end end