Skip to content

Commit

Permalink
Verification API - closes #705
Browse files Browse the repository at this point in the history
add a verification model, factory, and spec
update user, audio event and tag models
update gems

verification api in progress:
- updates abilities
- adds permissions, requests specs
- adds model schema
- adds verification to spec creation_helper

add permission spec for verifications

update audio_event foreign key constraint on verification model to have cascade delete
update expected cascade delete behaviour in models/audio_event_spec.rb to pass test

add api spec for verification (shallow + nested)

fix cascade deletes model specs

fixes users request spec

updates spec/README.md

add verifications to AudioEventImportFile association scope

fix user account spec - users *can* now update their own profile via api due to previous change in #703

adds verification controller helpers
extend verification filter slightly + test

update verification migration with FK tags cascade delete
add required dependants to tags model
add cascade delete test on tag spec

change permitted params on verification update action

adds verification nested filterable

removes rails validation on verification; rely on database unique constraint
adds request spec for invalid request - duplicate verification

updates confirmation enum values: from 'true' to 'correct'; from 'false' to 'incorrect'
  • Loading branch information
andrew-1234 authored and root committed Feb 10, 2025
1 parent f8c5b2a commit 44303c4
Show file tree
Hide file tree
Showing 33 changed files with 855 additions and 83 deletions.
106 changes: 106 additions & 0 deletions app/controllers/verifications_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

# VerificationsController
class VerificationsController < ApplicationController
include Api::ControllerHelper

# GET /verifications
# GET /audio_recordings/:audio_recording_id/audio_events/:audio_event_id/verifications
def index
do_authorize_class
get_audio_event

@verifications, opts = Settings.api_response.response_advanced(
api_filter_params,
list_permissions,
Verification,
Verification.filter_settings
)
respond_index(opts)
end

# GET /verifications/:id
# GET /audio_recordings/:audio_recording_id/audio_events/:audio_event_id/verifications/:id
def show
do_load_resource
do_authorize_instance

respond_show
end

# GET /verifications/new
def new
do_new_resource
get_resource
do_set_attributes
do_authorize_instance

respond_new
end

# POST /verifications
def create
do_new_resource
do_set_attributes(verification_params)

do_authorize_instance

if @verification.save
respond_create_success(shallow_verification_url(@verification))
else
respond_change_fail
end
end

# PUT/PATCH /verifications/:id
def update
do_load_resource
do_authorize_instance

if @verification.update(verification_update_params)
respond_show
else
respond_change_fail
end
end

# Handled in Archivable
# DELETE /verifications/:id

# GET|POST /verifications/filter
# GET|POST /audio_recordings/:audio_recording_id/audio_events/:audio_event_id/verifications/filter
def filter
do_authorize_class
get_audio_event

filter_response, opts = Settings.api_response.response_advanced(
api_filter_params,
list_permissions,
Verification,
Verification.filter_settings
)
respond_filter(filter_response, opts)
end

private

def verification_params
params.require(:verification).permit(
:confirmed, :audio_event_id, :tag_id
)
end

def verification_update_params
params.require(:verification).permit(
:confirmed
)
end

def get_audio_event #rubocop:disable Naming/AccessorMethodName
@audio_event = AudioEvent.find(params[:audio_event_id]) if params&.key?(:audio_event_id)
end

def list_permissions
Access::ByPermission.audio_event_verifications(current_user, audio_event: @audio_event)
end
end
52 changes: 51 additions & 1 deletion app/models/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def initialize(user)
to_tag(user, is_guest)
to_tagging(user, is_guest)
to_user(user, is_guest)
to_verification(user, is_guest)

to_analysis(user, is_guest)
to_media(user, is_guest)
Expand Down Expand Up @@ -717,10 +718,11 @@ def to_tagging(user, is_guest)
end

def to_user(user, is_guest)
# admin only: :index, :edit, :update
# admin only: :index, :edit
# :edit and :update are the Admin interface for editing any user
# normal users edit their profile using devise/registrations#edit

# users can :update their own attributes on the user model via api
# users can only view their own:
can [:projects, :sites, :bookmarks, :audio_events, :audio_event_comments, :update], User, id: user.id

Expand Down Expand Up @@ -825,4 +827,52 @@ def to_response(user, is_guest)

# only admin can update or delete responses
end

def to_verification(user, _is_guest)
# admin can do anything, see #for_admin

# available to any user, including guest
can [:index, :filter, :new], Verification

# any user, including guest, with reader permissions on project can #show a verification
can [:show], Verification do |verification|
check_model(verification)
check_audio_event(user, verification.audio_event.audio_recording.site, verification.audio_event)
end

can [:create, :update], Verification do |verification|
check_model(verification)
Access::Core.check_orphan_site!(verification.audio_event.audio_recording.site)

has_writer_access = Access::Core.can_any?(
user, :writer, verification.audio_event.audio_recording.site.projects
)

# anyone with writer access can create a verification,
# but only users who created the verifications can update them
# (and only if they still have access to the project)
if verification.persisted?
has_writer_access && verification.creator_id == user&.id
else
has_writer_access
end
end

# 1. Are you an owner?
# 2. Are you a writer?
# -> 2.1. Are you the creator?
can [:destroy], Verification do |verification|
user_level = Access::Core.user_levels(user, verification.audio_event.audio_recording.site.projects)
next false if user_level.blank? || user_level.compact.blank?

actual_highest = Access::Core.highest(user_level)
if actual_highest == :owner
true
elsif actual_highest == :writer
verification.creator_id == user&.id
else
false
end
end
end
end
4 changes: 3 additions & 1 deletion app/models/audio_event_import_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
#
class AudioEventImportFile < ApplicationRecord
# associations
has_many :audio_events, -> { includes :taggings }, inverse_of: :audio_event_import_file, dependent: :destroy
has_many :audio_events, lambda {
includes [:taggings, :verifications]
}, inverse_of: :audio_event_import_file, dependent: :destroy

belongs_to :audio_event_import, inverse_of: :audio_event_import_files
belongs_to :analysis_jobs_item, inverse_of: :audio_event_import_files, optional: true
Expand Down
4 changes: 2 additions & 2 deletions app/models/tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ class Tag < ApplicationRecord
extend Enumerize

# relations
has_many :taggings, inverse_of: :tag
has_many :taggings, inverse_of: :tag, dependent: :destroy
has_many :audio_events, through: :taggings
has_many :tag_groups, inverse_of: :tag
has_many :verifications, inverse_of: :tag
has_many :verifications, inverse_of: :tag, dependent: :destroy

belongs_to :creator, class_name: 'User', inverse_of: :created_tags
belongs_to :updater, class_name: 'User', inverse_of: :updated_tags, optional: true
Expand Down
83 changes: 76 additions & 7 deletions app/models/verification.rb
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
#
# Foreign Keys
#
# fk_rails_... (audio_event_id => audio_events.id)
# fk_rails_... (audio_event_id => audio_events.id) ON DELETE => cascade
# fk_rails_... (creator_id => users.id)
# fk_rails_... (tag_id => tags.id)
# fk_rails_... (tag_id => tags.id) ON DELETE => cascade
# fk_rails_... (updater_id => users.id)
#
class Verification < ApplicationRecord
Expand All @@ -36,12 +36,9 @@ class Verification < ApplicationRecord
belongs_to :creator, class_name: 'User', inverse_of: :created_verifications
belongs_to :updater, class_name: 'User', inverse_of: :updated_verifications, optional: true

# A user can only have one verification per audio event and tag
validates :creator_id, uniqueness: { scope: [:audio_event_id, :tag_id] }

# Defines the possible values for confirmation
CONFIRMATION_TRUE = 'true'
CONFIRMATION_FALSE = 'false'
CONFIRMATION_TRUE = 'correct'
CONFIRMATION_FALSE = 'incorrect'
CONFIRMATION_UNSURE = 'unsure'
CONFIRMATION_SKIP = 'skip'

Expand Down Expand Up @@ -70,4 +67,76 @@ class Verification < ApplicationRecord
# @!method confirmed_skip!
# @return [void] sets the verification as skip
enum :confirmed, CONFIRMATION_ENUM, prefix: :confirmed, validate: true

def self.filter_settings
fields = [
:id, :confirmed, :audio_event_id, :tag_id, :creator_id,
:updater_id, :created_at, :updated_at
]

{
valid_fields: fields,
render_fields: fields,
text_fields: [],
new_spec_fields: lambda { |_user|
{
confirmed: nil,
audio_event_id: nil,
tag_id: nil
}
},
controller: :verifications,
action: :filter,
defaults: {
order_by: :created_at,
direction: :desc
},
valid_associations: [
{
join: AudioEvent,
on: Verification.arel_table[:audio_event_id].eq(AudioEvent.arel_table[:id]),
available: true,
associations: [
{
join: AudioRecording,
on: AudioEvent.arel_table[:audio_recording_id].eq(AudioRecording.arel_table[:id]),
available: true
}
]
},
{
join: Tag,
on: Verification.arel_table[:tag_id].eq(Tag.arel_table[:id]),
available: true
}
]
}
end

def self.schema
{
type: 'object',
additionalProperties: false,
properties: {
id: Api::Schema.id,
confirmed: {
type: 'string',
enum: CONFIRMATION_ENUM.values
},
audio_event_id: Api::Schema.id(read_only: false),
tag_id: Api::Schema.id(read_only: false),
**Api::Schema.updater_and_creator_user_stamps
},
required: [
:id,
:confirmed,
:audio_event_id,
:tag_id,
:creator_id,
:created_at,
:updater_id,
:updated_at
]
}.freeze
end
end
11 changes: 11 additions & 0 deletions app/modules/access/by_permission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ def audio_event_comments(user, levels: Access::Core.levels, audio_event: nil)
end
end

# Get all verifications for which this user has these access levels.
# @param [User] user
# @param [Symbol, Array<Symbol>] levels
# @param [AudioEvent] audio_event
# @return [ActiveRecord::Relation] verifications
def audio_event_verifications(user, levels: Access::Core.levels, audio_event: nil)
query = Verification.joins(audio_event: [{ audio_recording: [:site] }])
query = query.where(audio_event_id: audio_event.id) if audio_event
permission_sites(user, levels, query)
end

# Get all saved searches for which this user has these access levels.
# @param [User] user
# @param [Symbol, Array<Symbol>] levels
Expand Down
2 changes: 1 addition & 1 deletion app/modules/api/controller_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Api
module ControllerHelper
extend ActiveSupport::Concern

# based on https://codelation.com/blog/rails-restful-api-just-add-water
# based on https://codelation.com/restful-rails-api-just-add-water/
private

# The singular name for the resource class based on the controller
Expand Down
3 changes: 2 additions & 1 deletion app/modules/api/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ module Schema
audio_event_import: AudioEventImport.schema,
audio_event_import_file: AudioEventImportFile.schema,
audio_event: AudioEvent.schema,
provenance: Provenance.schema
provenance: Provenance.schema,
verification: Verification.schema
},
parameters: {
'archived-parameter': archived_parameter
Expand Down
7 changes: 7 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ def matches?(request)
end
resources :tags, only: [:index], defaults: { format: 'json' }
resources :taggings, except: [:edit], defaults: { format: 'json' }
resources :verifications, only: [:index, :show], defaults: { format: 'json' }, concerns: [:filterable]
end
end

Expand All @@ -760,6 +761,12 @@ def matches?(request)
# API tags
resources :tags, only: [:index, :show, :create, :new], defaults: { format: 'json' }, concerns: [:filterable]

# API verifications
resources :verifications, except: [:edit],
as: 'shallow_verifications',
defaults: { format: 'json' },
concerns: [:filterable]

# API audio_event create
resources :audio_events, only: [], defaults: { format: 'json' }, concerns: [:filterable] do
resources :audio_event_comments, except: [:edit], defaults: { format: 'json' }, path: :comments, as: :comments,
Expand Down
6 changes: 3 additions & 3 deletions db/migrate/20250120064731_create_verifications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

class CreateVerifications < ActiveRecord::Migration[7.2]
def change
create_enum :confirmation, ['true', 'false', 'unsure', 'skip']
create_enum :confirmation, ['correct', 'incorrect', 'unsure', 'skip']

create_table :verifications do |t|
t.references :audio_event, null: false, foreign_key: true
t.references :tag, null: false, foreign_key: true
t.references :audio_event, null: false, foreign_key: { on_delete: :cascade }
t.references :tag, null: false, foreign_key: { on_delete: :cascade }
t.column :creator_id, :integer, null: false
t.column :updater_id, :integer
t.column :confirmed, :confirmation, null: false
Expand Down
Loading

0 comments on commit 44303c4

Please sign in to comment.