diff --git a/.rubocop.yml b/.rubocop.yml index c2370187b..a6b7784fa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ inherit_from: .rubocop_todo.yml -require: +plugins: - rubocop-factory_bot - rubocop-capybara - rubocop-rails diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3195873c1..fbba7cede 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -314,7 +314,7 @@ RSpec/MultipleExpectations: # Offense count: 138 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: - Max: 14 + Max: 20 # Offense count: 133 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 08e8aa4bd..8ca7f956e 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -29,7 +29,7 @@ class ActivitiesController < DataController def new @activity = Activity.new( - owner: current_member, + owner: current_member, due_date: Date.today ) if params[:garden_id] diff --git a/app/controllers/api/v1/activities_controller.rb b/app/controllers/api/v1/activities_controller.rb index a79f9c778..eaeb4c085 100644 --- a/app/controllers/api/v1/activities_controller.rb +++ b/app/controllers/api/v1/activities_controller.rb @@ -2,8 +2,6 @@ module Api module V1 - # This controller is intentionally empty. - # The `jsonapi-resources` gem provides the necessary actions. class ActivitiesController < BaseController end end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 84eb42dcc..bf69de691 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -4,6 +4,40 @@ module Api module V1 class BaseController < JSONAPI::ResourceController abstract + protect_from_forgery with: :null_session + before_action :authenticate_member_from_token! + before_action :enforce_member_for_write_operations!, only: %i(create update destroy) + rescue_from CanCan::AccessDenied do + head :forbidden + end + + def context + { + current_user: current_user, + current_ability: current_ability, + controller: self, + action: params[:action] + } + end + + private + + attr_reader :current_user + + def enforce_member_for_write_operations! + head :unauthorized unless current_user + end + + def authenticate_member_from_token! + authenticate_with_http_token do |token, _options| + auth = Authentication.find_by(token: token, provider: 'api') + if auth.present? + @current_user = auth.member + + return true + end + end + end end end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 2bb8b1764..88c569c83 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -6,7 +6,7 @@ class RegistrationsController < Devise::RegistrationsController prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect with recaptcha. def edit - @flickr_auth = current_member.auth('flickr') + @flickr_auth = current_member.auth('flickr') render "edit" end @@ -38,6 +38,12 @@ class RegistrationsController < Devise::RegistrationsController end end + def regenerate_api_token + current_member.regenerate_api_token + set_flash_message :notice, :api_token_regenerated + redirect_to edit_member_registration_path + '#apps' + end + def destroy if @member.valid_password?(params.require(:member)[:current_password]) @member.discard diff --git a/app/models/activity.rb b/app/models/activity.rb index cbdfbbf89..30bde60e1 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -30,4 +30,20 @@ class Activity < ApplicationRecord def to_s name end + + def garden_name + garden&.name + end + + def garden_slug + garden&.slug + end + + def planting_name + planting&.crop&.name + end + + def planting_slug + planting&.crop&.slug + end end diff --git a/app/models/member.rb b/app/models/member.rb index 3c9bda05d..9ef9ca90c 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -24,6 +24,20 @@ class Member < ApplicationRecord has_many :notifications, foreign_key: 'recipient_id', inverse_of: :recipient has_many :sent_notifications, foreign_key: 'sender_id', inverse_of: :sender, class_name: "Notification" has_many :authentications, dependent: :destroy + has_one :api_token, -> { where(provider: 'api') }, class_name: 'Authentication', dependent: :destroy + + def api_token? + api_token.present? + end + + def regenerate_api_token + api_token.destroy if api_token? + create_api_token( + provider: 'api', + uid: id, + token: SecureRandom.hex(16) + ) + end has_many :photos, inverse_of: :owner has_many :likes, dependent: :destroy diff --git a/app/resources/api/v1/activity_resource.rb b/app/resources/api/v1/activity_resource.rb index 72256146b..5803a7686 100644 --- a/app/resources/api/v1/activity_resource.rb +++ b/app/resources/api/v1/activity_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class ActivityResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_one :garden diff --git a/app/resources/api/v1/crop_resource.rb b/app/resources/api/v1/crop_resource.rb index 789884392..ed4cac67c 100644 --- a/app/resources/api/v1/crop_resource.rb +++ b/app/resources/api/v1/crop_resource.rb @@ -3,8 +3,7 @@ module Api module V1 class CropResource < BaseResource - immutable - + immutable # TODO: Re-evaluate this later filter :approval_status, default: 'approved' has_many :plantings diff --git a/app/resources/api/v1/garden_resource.rb b/app/resources/api/v1/garden_resource.rb index 47dcd7858..cc94847a3 100644 --- a/app/resources/api/v1/garden_resource.rb +++ b/app/resources/api/v1/garden_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class GardenResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_many :plantings diff --git a/app/resources/api/v1/harvest_resource.rb b/app/resources/api/v1/harvest_resource.rb index 2013390a6..8f086dc55 100644 --- a/app/resources/api/v1/harvest_resource.rb +++ b/app/resources/api/v1/harvest_resource.rb @@ -3,11 +3,17 @@ module Api module V1 class HarvestResource < BaseResource - immutable + before_save do + @model.owner = context[:current_user] + @model.crop_id = @model.planting.crop_id if @model.planting_id + @model.harvested_at = Time.zone.now if @model.harvested_at.blank? + @model.plant_part = PlantPart.first + end has_one :crop has_one :planting has_one :owner, class_name: 'Member' + # has_one :plant_part has_many :photos attribute :harvested_at diff --git a/app/resources/api/v1/photo_resource.rb b/app/resources/api/v1/photo_resource.rb index 6da294cd8..4f6c08223 100644 --- a/app/resources/api/v1/photo_resource.rb +++ b/app/resources/api/v1/photo_resource.rb @@ -3,7 +3,10 @@ module Api module V1 class PhotoResource < BaseResource - immutable + immutable # TODO: Re-evaluate this. + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_many :plantings diff --git a/app/resources/api/v1/planting_resource.rb b/app/resources/api/v1/planting_resource.rb index 8a5bd4659..3ab2c4fc8 100644 --- a/app/resources/api/v1/planting_resource.rb +++ b/app/resources/api/v1/planting_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class PlantingResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :garden has_one :crop diff --git a/app/resources/api/v1/seed_resource.rb b/app/resources/api/v1/seed_resource.rb index dc1016cd9..9c69e493a 100644 --- a/app/resources/api/v1/seed_resource.rb +++ b/app/resources/api/v1/seed_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class SeedResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_one :crop diff --git a/app/resources/base_resource.rb b/app/resources/base_resource.rb index 46de0ce53..b85ba45e1 100644 --- a/app/resources/base_resource.rb +++ b/app/resources/base_resource.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true class BaseResource < JSONAPI::Resource - immutable abstract + + [:create, :update, :remove].each do |action| + set_callback action, :before, :authorize + end + + # Check authorisation for write operations. + # NOTE: At a later time, we may require API tokens for READ operations. + def authorize + # context[:action] is simply context[:controller].params[:action] + context[:current_ability].authorize! context[:action].to_sym, @model + end end diff --git a/app/views/devise/registrations/_edit_apps.html.haml b/app/views/devise/registrations/_edit_apps.html.haml index 12e267bb0..24fb472e4 100644 --- a/app/views/devise/registrations/_edit_apps.html.haml +++ b/app/views/devise/registrations/_edit_apps.html.haml @@ -15,3 +15,16 @@ method: :delete, class: "remove btn btn-danger" - else = link_to 'Connect to Flickr', '/members/auth/flickr', class: 'btn' + %hr + .row + .col-md-12 + %p + = image_tag "icons/post.svg", size: "32x32", alt: 'API logo' + - if current_member.api_token? + Your API token is + %code= current_member.api_token.token + = link_to "Regenerate", regenerate_api_token_path, + data: { confirm: "Are you sure? Your old token will stop working immediately." }, + method: :post, class: "remove btn btn-danger" + - else + = link_to 'Generate API Token', regenerate_api_token_path, method: :post, class: 'btn btn-primary' diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index 21a6d0635..dc5a10f55 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -89,11 +89,11 @@ - else .col-md-12 %p Nothing is currently planned here. - - if @finished_activities&.size&.positive? - %h2 Finished activities for planting - .index-cards - - @finished_activities.each do |activity| - = render "activities/card", activity: activity + - if @finished_activities&.size&.positive? + %h2 Finished activities for planting + .index-cards + - @finished_activities.each do |activity| + = render "activities/card", activity: activity .col-md-4.col-xs-12 = render @planting.crop diff --git a/config/initializers/jsonapi_resources.rb b/config/initializers/jsonapi_resources.rb index 8cf1a906b..7f0c57c29 100644 --- a/config/initializers/jsonapi_resources.rb +++ b/config/initializers/jsonapi_resources.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true - +class UnauthorisedError < JSONAPI::Error +end JSONAPI.configure do |config| # built in paginators are :none, :offset, :paged config.default_paginator = :offset config.default_page_size = 10 config.maximum_page_size = 100 + config.exception_class_whitelist = [CanCan::AccessDenied, UnauthorisedError] end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 3fa1481e0..ffd459cf5 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -54,6 +54,7 @@ en: You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address. destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' + api_token_regenerated: 'Your API token has been regenerated.' unlocks: send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' unlocked: 'Your account has been unlocked successfully. Please sign in to continue.' diff --git a/config/routes.rb b/config/routes.rb index ed85d2111..cd6b2af52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,7 @@ Rails.application.routes.draw do } devise_scope :member do get '/members/unsubscribe/:message' => 'members#unsubscribe', as: 'unsubscribe_member' + post '/members/regenerate_api_token' => 'registrations#regenerate_api_token', as: 'regenerate_api_token' end match '/members/:id/finish_signup' => 'members#finish_signup', via: %i(get patch), as: :finish_signup diff --git a/lib/tasks/wikidata.rake b/lib/tasks/wikidata.rake index e1f117a67..8ce31b942 100644 --- a/lib/tasks/wikidata.rake +++ b/lib/tasks/wikidata.rake @@ -36,21 +36,21 @@ namespace :wikidata do aliases = wikidata_data['entities'][wikidata_id]['aliases'] aliases.each do |lang, values| values.each do |value| - unless AlternateName.exists?(name: value['value'], language: lang, crop: crop) - AlternateName.create!( - name: value['value'], - language: lang, - crop: crop, - creator: creator - ) - puts " Added alternate name: #{value['value']} (#{lang})" - end + next if AlternateName.exists?(name: value['value'], language: lang, crop: crop) + + AlternateName.create!( + name: value['value'], + language: lang, + crop: crop, + creator: creator + ) + puts " Added alternate name: #{value['value']} (#{lang})" end end else puts " Could not find Wikidata ID for #{crop.name}" end - rescue => e + rescue StandardError => e puts " Error processing crop #{crop.name}: #{e.message}" end end diff --git a/spec/features/members/token_management_spec.rb b/spec/features/members/token_management_spec.rb new file mode 100644 index 000000000..6fcc77366 --- /dev/null +++ b/spec/features/members/token_management_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "member token management", :js do + include_context 'signed in member' + + before do + visit edit_member_registration_path + click_on "Apps" + end + + it "can generate an API token" do + expect(page).to have_no_content("Your API token is") + click_on "Generate API Token" + expect(page).to have_content("Your API token is") + member.reload + expect(member.api_token).to be_present + end + + context "with an existing token" do + before do + member.regenerate_api_token + visit edit_member_registration_path + click_on "Apps" + end + + it "can regenerate an API token" do + old_token = member.api_token.token + expect(page).to have_content("Your API token is") + accept_confirm do + click_on "Regenerate" + end + expect(page).to have_content("Your API token is") + expect(member.reload.api_token.token).not_to eq(old_token) + end + end +end diff --git a/spec/requests/api/v1/activities_request_spec.rb b/spec/requests/api/v1/activities_request_spec.rb index 553c39856..0eb5e98a8 100644 --- a/spec/requests/api/v1/activities_request_spec.rb +++ b/spec/requests/api/v1/activities_request_spec.rb @@ -23,34 +23,34 @@ RSpec.describe 'Activities', type: :request do it 'filters by owner' do get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by garden' do - get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:) + get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by planting' do - get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:) + get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by category' do - get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:) + get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(2) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) - expect(subject['data'][1]['id']).to eq(activity2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(subject['data'][1]['id']).to eq(activity2.id.to_s) end end end diff --git a/spec/requests/api/v1/gardens_request_spec.rb b/spec/requests/api/v1/gardens_request_spec.rb index 344ea74d1..de3906db8 100644 --- a/spec/requests/api/v1/gardens_request_spec.rb +++ b/spec/requests/api/v1/gardens_request_spec.rb @@ -52,18 +52,19 @@ RSpec.describe 'Gardens', type: :request do context 'filtering' do let!(:garden2) { FactoryBot.create(:garden, active: false, garden_type: FactoryBot.create(:garden_type)) } + pending 'filters by active' do get('/api/v1/gardens?filter[active]=true', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(garden.id.to_s) end it 'filters by garden_type' do get("/api/v1/gardens?filter[garden_type]=#{garden2.garden_type.id}", params: {}, headers:) - - expect(response.status).to eq 200 + + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(garden2.id.to_s) end @@ -71,27 +72,116 @@ RSpec.describe 'Gardens', type: :request do it 'filters by owner' do get("/api/v1/gardens?filter[owner_id]=#{garden2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(2) expect(subject['data'][1]['id']).to eq(garden2.id.to_s) end end - it '#create' do - expect do - post '/api/v1/gardens', params: { 'garden' => { 'name' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:garden_params) do + { + data: { + type: 'gardens', + attributes: { + name: 'My API Garden' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/gardens', params: garden_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/gardens', params: garden_params, headers: auth_headers + expect(response).to have_http_status(:created) + expect(member.gardens.count).to eq(2) # 1 from after_create callback, 1 from api + end end - it '#update' do - expect do - post "/api/v1/gardens/#{garden.id}", params: { 'garden' => { 'name' => 'can i modify this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:garden) { create(:garden, owner: member) } + let(:other_member_garden) { create(:garden) } + let(:update_params) do + { + data: { + type: 'gardens', + id: garden.id.to_s, + attributes: { + name: 'An updated garden' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own garden' do + patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: auth_headers + expect(response).to have_http_status(:ok) + expect(garden.reload.name).to eq('An updated garden') + end + + it 'returns 403 Forbidden for another member\'s garden' do + update_params_for_other = { + data: { + type: 'gardens', + id: other_member_garden.id.to_s, + attributes: { + name: 'An updated garden' + } + } + }.to_json + patch "/api/v1/gardens/#{other_member_garden.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/gardens/#{garden.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:garden) { create(:garden, owner: member) } + let(:other_member_garden) { create(:garden) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/gardens/#{garden.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own garden' do + delete "/api/v1/gardens/#{garden.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: garden.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s garden' do + delete "/api/v1/gardens/#{other_member_garden.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end end diff --git a/spec/requests/api/v1/harvests_request_spec.rb b/spec/requests/api/v1/harvests_request_spec.rb index f49b7b88f..38d6777c1 100644 --- a/spec/requests/api/v1/harvests_request_spec.rb +++ b/spec/requests/api/v1/harvests_request_spec.rb @@ -78,6 +78,7 @@ RSpec.describe 'Harvests', type: :request do context 'filtering' do let!(:harvest2) { FactoryBot.create(:harvest, planting: create(:planting)) } + it 'filters by crop' do get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers:) expect(subject['data'].size).to eq(1) @@ -87,47 +88,141 @@ RSpec.describe 'Harvests', type: :request do it 'filters by planting' do get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end it 'filters by plant_part' do - get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:) + get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end it 'filters by owner' do - get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:) + get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end end - it '#create' do - expect do - put '/api/v1/harvests', headers:, params: { - 'harvest' => { 'description' => 'can i make this' } - } - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:planting) { create(:planting, owner: member) } + let(:plant_part) { create(:plant_part) } + let(:harvest_params) do + { + data: { + type: 'harvests', + attributes: { + description: 'My API harvests' + }, + relationships: { + planting: { data: { type: 'plantings', id: planting.id } } + # plant_part: { data: { type: 'plant_parts', id: plant_part.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/harvests', params: harvest_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/harvests', params: harvest_params, headers: auth_headers + + expect(response).to have_http_status(:created) + expect(member.harvests.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/harvests/#{harvest.id}", headers:, params: { - 'harvest' => { 'description' => 'can i modify this' } - } - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:harvest) { create(:harvest, owner: member) } + let(:other_member_harvest) { create(:harvest) } + let(:update_params) do + { + data: { + type: 'harvests', + id: harvest.id.to_s, + attributes: { + description: 'An updated harvest' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own harvest' do + patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: auth_headers + + expect(response).to have_http_status(:ok) + expect(harvest.reload.description).to eq('An updated harvest') + end + + it 'returns 403 Forbidden for another member\'s harvest' do + update_params_for_other = { + data: { + type: 'harvests', + id: other_member_harvest.id.to_s, + attributes: { + description: 'An updated harvest' + } + } + }.to_json + patch "/api/v1/harvests/#{other_member_harvest.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/harvests/#{harvest.id}", headers:, params: {} - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:harvest) { create(:harvest, owner: member) } + let(:other_member_harvest) { create(:harvest) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/harvests/#{harvest.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own harvest' do + delete "/api/v1/harvests/#{harvest.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: harvest.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s harvest' do + delete "/api/v1/harvests/#{other_member_harvest.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end end diff --git a/spec/requests/api/v1/plantings_request_spec.rb b/spec/requests/api/v1/plantings_request_spec.rb index 5a406afe9..7d334d539 100644 --- a/spec/requests/api/v1/plantings_request_spec.rb +++ b/spec/requests/api/v1/plantings_request_spec.rb @@ -95,24 +95,119 @@ RSpec.describe 'Plantings', type: :request do expect(subject['data']).to eq(planting_encoded_as_json_api) end - it '#create' do - expect do - post '/api/v1/plantings', params: { 'planting' => { 'description' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:garden) { create(:garden, owner: member) } + let(:planting_params) do + { + data: { + type: 'plantings', + attributes: { + description: 'My API plantings' + }, + relationships: { + crop: { data: { type: 'crops', id: crop.id } }, + garden: { data: { type: 'gardens', id: garden.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/plantings', params: planting_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/plantings', params: planting_params, headers: auth_headers + + expect(response).to have_http_status(:created) + expect(member.plantings.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/plantings/#{planting.id}", headers:, params: { - 'planting' => { 'description' => 'can i modify this' } - } - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:planting) { create(:planting, owner: member) } + let(:other_member_planting) { create(:planting) } + let(:update_params) do + { + data: { + type: 'plantings', + id: planting.id.to_s, + attributes: { + description: 'An updated planting' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own planting' do + patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: auth_headers + + expect(response).to have_http_status(:ok) + expect(planting.reload.description).to eq('An updated planting') + end + + it 'returns 403 Forbidden for another member\'s planting' do + update_params_for_other = { + data: { + type: 'plantings', + id: other_member_planting.id.to_s, + attributes: { + description: 'An updated planting' + } + } + }.to_json + patch "/api/v1/plantings/#{other_member_planting.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/plantings/#{planting.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:planting) { create(:planting, owner: member) } + let(:other_member_planting) { create(:planting) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/plantings/#{planting.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own planting' do + delete "/api/v1/plantings/#{planting.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: planting.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s planting' do + delete "/api/v1/plantings/#{other_member_planting.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end describe "by member/owner" do @@ -144,6 +239,7 @@ RSpec.describe 'Plantings', type: :request do context 'filtering' do let!(:planting2) { FactoryBot.create(:planting, failed: true, sunniness: 'shade') } let!(:perennial_planting) { FactoryBot.create(:planting, crop: FactoryBot.create(:crop, perennial: true)) } + it 'filters by failed' do get('/api/v1/plantings?filter[failed]=true', params: {}, headers:) expect(subject['data'].size).to eq(1) @@ -151,25 +247,25 @@ RSpec.describe 'Plantings', type: :request do end it 'filters by sunniness' do - get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:) - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(planting2.id.to_s) + get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(planting2.id.to_s) end it 'filters by perennial' do - get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:) + get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s) end it 'filters by active' do - get('/api/v1/plantings?filter[active]=true', params: {}, headers:) + get('/api/v1/plantings?filter[active]=true', params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(2) - expect(subject['data'][0]['id']).to eq(planting.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(planting.id.to_s) end end end diff --git a/spec/requests/api/v1/seeds_request_spec.rb b/spec/requests/api/v1/seeds_request_spec.rb index 00ee04684..ea30c7924 100644 --- a/spec/requests/api/v1/seeds_request_spec.rb +++ b/spec/requests/api/v1/seeds_request_spec.rb @@ -61,39 +61,136 @@ RSpec.describe 'Seeds', type: :request do it { expect(subject['data']).to eq(seed_encoded_as_json_api) } end - it '#create' do - expect do - post '/api/v1/seeds', params: { 'seed' => { 'name' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:seed_params) do + { + data: { + type: 'seeds', + attributes: { + description: 'My API seeds' + }, + relationships: { + crop: { data: { type: 'crops', id: crop.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/seeds', params: seed_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/seeds', params: seed_params, headers: auth_headers + expect(response).to have_http_status(:created) + expect(member.seeds.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/seeds/#{seed.id}", params: { 'seed' => { 'name' => 'can i modify this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:seed) { create(:seed, owner: member, crop: crop) } + let(:other_member_seed) { create(:seed) } + let(:update_params) do + { + data: { + type: 'seeds', + id: seed.id.to_s, + attributes: { + description: 'An updated seed' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own seed' do + patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: auth_headers + expect(response).to have_http_status(:ok) + expect(seed.reload.description).to eq('An updated seed') + end + + it 'returns 403 Forbidden for another member\'s seed' do + update_params_for_other = { + data: { + type: 'seeds', + id: other_member_seed.id.to_s, + attributes: { + description: 'An updated seed' + } + } + }.to_json + patch "/api/v1/seeds/#{other_member_seed.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/seeds/#{seed.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let!(:seed) { create(:seed, owner: member, crop: crop) } + let(:other_member_seed) { create(:seed) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/seeds/#{seed.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own seed' do + delete "/api/v1/seeds/#{seed.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Seed.find_by(id: seed.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s seed' do + delete "/api/v1/seeds/#{other_member_seed.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end context 'filtering' do - let!(:seed2) { FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') } + let!(:seed2) do + FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') + end + it 'filters by crop' do get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end - it 'filters by tradable_to' do get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -101,7 +198,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by organic' do get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -109,7 +206,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by gmo' do get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -117,7 +214,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by heirloom' do get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -125,7 +222,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by owner' do get("/api/v1/seeds?filter[owner_id]=#{seed2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end diff --git a/swagger/v1/swagger.json b/swagger/v1/swagger.json index 9ac938a56..64531dc66 100644 --- a/swagger/v1/swagger.json +++ b/swagger/v1/swagger.json @@ -1,114 +1,388 @@ { "swagger": "2.0", "info": { - "title": "API V1", - "version": "V1" -}, - "basePath" : "/api/v1", + "title": "API V1", + "version": "V1" + }, + "basePath": "/api/v1", "paths": { - "/crops": { - "get": { - "summary": "crops List", - "tags": [ - "crops" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,name,en_wikipedia_url,perennial,median_lifespan,median_days_to_first_harvest,median_days_to_last_harvest", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[approval_status]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false + "/crops": { + "get": { + "summary": "crops List", + "tags": [ + "crops" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,name,en_wikipedia_url,perennial,median_lifespan,median_days_to_first_harvest,median_days_to_last_harvest", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[approval_status]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, + "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "x-nullable": false, + "description": "Name" + }, + "en_wikipedia_url": { + "type": "string", + "x-nullable": true, + "description": "Wikipedia URL (English)" + }, + "perennial": { + "type": "boolean", + "x-nullable": true, + "description": "Is the item perennial? (A plant that lives more than two years)" + }, + "median_lifespan": { + "type": "integer", + "x-nullable": true, + "description": "Median lifespan" + }, + "median_days_to_first_harvest": { + "type": "integer", + "x-nullable": true, + "description": "Median days to first harvest" + }, + "median_days_to_last_harvest": { + "type": "integer", + "x-nullable": true, + "description": "Median days to last harvest" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "seeds": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "parent": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" + } + } + }, + "description": "Data" + }, + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } + } } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + } + }, + "/crops/{id}": { + "get": { + "summary": "crops Detail", + "tags": [ + "crops" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -261,381 +535,309 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/crops/{id}": { - "get": { - "summary": "crops Detail", - "tags": [ - "crops" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/gardens": { + "get": { + "summary": "gardens List", + "tags": [ + "gardens" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,name", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "x-nullable": false, + "description": "Name" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "name": { - "type": "string", - "x-nullable": false, - "description": "Name" - }, - "en_wikipedia_url": { - "type": "string", - "x-nullable": true, - "description": "Wikipedia URL (English)" - }, - "perennial": { - "type": "boolean", - "x-nullable": true, - "description": "Is the item perennial? (A plant that lives more than two years)" - }, - "median_lifespan": { - "type": "integer", - "x-nullable": true, - "description": "Median lifespan" - }, - "median_days_to_first_harvest": { - "type": "integer", - "x-nullable": true, - "description": "Median days to first harvest" - }, - "median_days_to_last_harvest": { - "type": "integer", - "x-nullable": true, - "description": "Median days to last harvest" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "seeds": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "parent": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/gardens": { - "get": { - "summary": "gardens List", - "tags": [ - "gardens" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,name", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/gardens/{id}": { + "get": { + "summary": "gardens Detail", + "tags": [ + "gardens" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -723,337 +925,396 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/gardens/{id}": { - "get": { - "summary": "gardens Detail", - "tags": [ - "gardens" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/members": { + "get": { + "summary": "members List", + "tags": [ + "members" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,login_name,slug", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[login_name]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[slug]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "name": { - "type": "string", - "x-nullable": false, - "description": "Name" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" }, - "plantings": { + "attributes": { "type": "object", "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" + "login_name": { + "type": "string", + "x-nullable": true, + "description": "Login name" + }, + "slug": { + "type": "string", + "x-nullable": true, + "description": "Slug" } }, - "description": "Related model" + "description": "Attributes" }, - "photos": { + "relationships": { "type": "object", "properties": { - "links": { + "gardens": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "seeds": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" } }, - "description": "Related model" + "description": "Associate data" } - }, - "description": "Associate data" - } + } + }, + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/members": { - "get": { - "summary": "members List", - "tags": [ - "members" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,login_name,slug", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[login_name]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[slug]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/members/{id}": { + "get": { + "summary": "members Detail", + "tags": [ + "members" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -1186,375 +1447,368 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/members/{id}": { - "get": { - "summary": "members Detail", - "tags": [ - "members" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/harvests": { + "get": { + "summary": "harvests List", + "tags": [ + "harvests" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,harvested_at,description,unit,weight_quantity,weight_unit,si_weight", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "harvested_at": { + "type": "string", + "x-nullable": true, + "description": "Harvested date time" + }, + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "unit": { + "type": "string", + "x-nullable": true, + "description": "Unit" + }, + "weight_quantity": { + "type": "string", + "x-nullable": true, + "description": "Weight/Quanitity" + }, + "weight_unit": { + "type": "string", + "x-nullable": true, + "description": "Weight Unit" + }, + "si_weight": { + "type": "string", + "x-nullable": true, + "description": "SI Weight" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "crop": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "planting": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "login_name": { - "type": "string", - "x-nullable": true, - "description": "Login name" - }, - "slug": { - "type": "string", - "x-nullable": true, - "description": "Slug" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "gardens": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "seeds": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/harvests": { - "get": { - "summary": "harvests List", - "tags": [ - "harvests" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,harvested_at,description,unit,weight_quantity,weight_unit,si_weight", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/harvests/{id}": { + "get": { + "summary": "harvests Detail", + "tags": [ + "harvests" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -1571,7 +1825,7 @@ "harvested_at": { "type": "string", "x-nullable": true, - "description": "Harvested date time" + "description": "Harvested datetime" }, "description": { "type": "string", @@ -1586,7 +1840,7 @@ "weight_quantity": { "type": "string", "x-nullable": true, - "description": "Weight/Quanitity" + "description": "Weight/Quantity" }, "weight_unit": { "type": "string", @@ -1687,354 +1941,315 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/harvests/{id}": { - "get": { - "summary": "harvests Detail", - "tags": [ - "harvests" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/seeds": { + "get": { + "summary": "seeds List", + "tags": [ + "seeds" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,description,quantity,plant_before,tradable_to,days_until_maturity_min,days_until_maturity_max,organic,gmo,heirloom", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "quantity": { + "type": "integer", + "x-nullable": true, + "description": "Quanitity" + }, + "plant_before": { + "type": "string", + "x-nullable": true, + "description": "Plant before" + }, + "tradable_to": { + "type": "string", + "x-nullable": true, + "description": "Tradeable to" + }, + "days_until_maturity_min": { + "type": "integer", + "x-nullable": true, + "description": "Days until maturity (min)" + }, + "days_until_maturity_max": { + "type": "integer", + "x-nullable": true, + "description": "Days until maturity (max)" + }, + "organic": { + "type": "string", + "x-nullable": true, + "description": "Organic" + }, + "gmo": { + "type": "string", + "x-nullable": true, + "description": "GMO" + }, + "heirloom": { + "type": "string", + "x-nullable": true, + "description": "Heirloom" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "crop": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "harvested_at": { - "type": "string", - "x-nullable": true, - "description": "Harvested datetime" - }, - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" - }, - "unit": { - "type": "string", - "x-nullable": true, - "description": "Unit" - }, - "weight_quantity": { - "type": "string", - "x-nullable": true, - "description": "Weight/Quantity" - }, - "weight_unit": { - "type": "string", - "x-nullable": true, - "description": "Weight Unit" - }, - "si_weight": { - "type": "string", - "x-nullable": true, - "description": "SI Weight" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "crop": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "planting": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/seeds": { - "get": { - "summary": "seeds List", - "tags": [ - "seeds" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,description,quantity,plant_before,tradable_to,days_until_maturity_min,days_until_maturity_max,organic,gmo,heirloom", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/seeds/{id}": { + "get": { + "summary": "seeds Detail", + "tags": [ + "seeds" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -2056,7 +2271,7 @@ "quantity": { "type": "integer", "x-nullable": true, - "description": "Quanitity" + "description": "Quanity" }, "plant_before": { "type": "string", @@ -2066,7 +2281,7 @@ "tradable_to": { "type": "string", "x-nullable": true, - "description": "Tradeable to" + "description": "Tradable to" }, "days_until_maturity_min": { "type": "integer", @@ -2142,371 +2357,420 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/seeds/{id}": { - "get": { - "summary": "seeds Detail", - "tags": [ - "seeds" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/plantings": { + "get": { + "summary": "plantings List", + "tags": [ + "plantings" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,slug,planted_at,finished,finished_at,quantity,description,sunniness,planted_from", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[slug]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[crop]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[planted_from]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[garden]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[owner]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[finished]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" }, - "quantity": { - "type": "integer", - "x-nullable": true, - "description": "Quanity" - }, - "plant_before": { - "type": "string", - "x-nullable": true, - "description": "Plant before" - }, - "tradable_to": { - "type": "string", - "x-nullable": true, - "description": "Tradable to" - }, - "days_until_maturity_min": { - "type": "integer", - "x-nullable": true, - "description": "Days until maturity (min)" - }, - "days_until_maturity_max": { - "type": "integer", - "x-nullable": true, - "description": "Days until maturity (max)" - }, - "organic": { - "type": "string", - "x-nullable": true, - "description": "Organic" - }, - "gmo": { - "type": "string", - "x-nullable": true, - "description": "GMO" - }, - "heirloom": { - "type": "string", - "x-nullable": true, - "description": "Heirloom" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { + "attributes": { "type": "object", "properties": { - "links": { + "slug": { + "type": "string", + "x-nullable": true, + "description": "Slug" + }, + "planted_at": { + "type": "string", + "x-nullable": true, + "description": "Planted at" + }, + "finished": { + "type": "boolean", + "x-nullable": false, + "description": "Finished?" + }, + "finished_at": { + "type": "string", + "x-nullable": true, + "description": "Finished at" + }, + "quantity": { + "type": "integer", + "x-nullable": true, + "description": "Quanity" + }, + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "sunniness": { + "type": "string", + "x-nullable": true, + "description": "Sunniness" + }, + "planted_from": { + "type": "string", + "x-nullable": true, + "description": "Planted from" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "garden": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "crop": { - "type": "object", - "properties": { - "links": { + "description": "Related model" + }, + "crop": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" } }, - "description": "Related model" + "description": "Associate data" } - }, - "description": "Associate data" - } + } + }, + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/plantings": { - "get": { - "summary": "plantings List", - "tags": [ - "plantings" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,slug,planted_at,finished,finished_at,quantity,description,sunniness,planted_from", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[slug]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[crop]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[planted_from]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[garden]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[owner]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[finished]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/plantings/{id}": { + "get": { + "summary": "plantings Detail", + "tags": [ + "plantings" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -2543,7 +2807,7 @@ "quantity": { "type": "integer", "x-nullable": true, - "description": "Quanity" + "description": "Quantity" }, "description": { "type": "string", @@ -2649,378 +2913,363 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/plantings/{id}": { - "get": { - "summary": "plantings Detail", - "tags": [ - "plantings" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/photos": { + "get": { + "summary": "photos List", + "tags": [ + "photos" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,thumbnail_url,fullsize_url,license_name,link_url,title", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "thumbnail_url": { + "type": "string", + "x-nullable": false, + "description": "Thumbnail URL" + }, + "fullsize_url": { + "type": "string", + "x-nullable": false, + "description": "Full-size URL" + }, + "license_name": { + "type": "string", + "x-nullable": false, + "description": "License name" + }, + "link_url": { + "type": "string", + "x-nullable": false, + "description": "Link URL" + }, + "title": { + "type": "string", + "x-nullable": false, + "description": "Title" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "gardens": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "slug": { - "type": "string", - "x-nullable": true, - "description": "Slug" - }, - "planted_at": { - "type": "string", - "x-nullable": true, - "description": "Planted at" - }, - "finished": { - "type": "boolean", - "x-nullable": false, - "description": "Finished?" - }, - "finished_at": { - "type": "string", - "x-nullable": true, - "description": "Finished at" - }, - "quantity": { - "type": "integer", - "x-nullable": true, - "description": "Quantity" - }, - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" - }, - "sunniness": { - "type": "string", - "x-nullable": true, - "description": "Sunniness" - }, - "planted_from": { - "type": "string", - "x-nullable": true, - "description": "Planted from" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "garden": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "crop": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/photos": { - "get": { - "summary": "photos List", - "tags": [ - "photos" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,thumbnail_url,fullsize_url,license_name,link_url,title", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/photos/{id}": { + "get": { + "summary": "photos Detail", + "tags": [ + "photos" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -3148,266 +3397,17 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "/photos/{id}": { - "get": { - "summary": "photos Detail", - "tags": [ - "photos" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "thumbnail_url": { - "type": "string", - "x-nullable": false, - "description": "Thumbnail URL" - }, - "fullsize_url": { - "type": "string", - "x-nullable": false, - "description": "Full-size URL" - }, - "license_name": { - "type": "string", - "x-nullable": false, - "description": "License name" - }, - "link_url": { - "type": "string", - "x-nullable": false, - "description": "Link URL" - }, - "title": { - "type": "string", - "x-nullable": false, - "description": "Title" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "gardens": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } - }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } } } -} -} +} \ No newline at end of file