Skip to content

Commit

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

Verification API - closes #705

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 Andrew Schwenke committed Feb 10, 2025
1 parent 1b1fbbd commit aa77668
Show file tree
Hide file tree
Showing 34 changed files with 1,095 additions and 47 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
1 change: 1 addition & 0 deletions app/models/audio_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class AudioEvent < ApplicationRecord
belongs_to :updater, class_name: 'User', inverse_of: :updated_audio_events, optional: true
belongs_to :deleter, class_name: 'User', inverse_of: :deleted_audio_events, optional: true
has_many :comments, class_name: 'AudioEventComment', inverse_of: :audio_event, dependent: :delete_all
has_many :verifications, inverse_of: :audio_event, dependent: :destroy

# AT 2021: disabled. Nested associations are extremely complex,
# and as far as we are aware, they are not used anywhere in production
Expand Down
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
6 changes: 4 additions & 2 deletions app/models/tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ 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, 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 Expand Up @@ -72,7 +74,7 @@ def type_of_tag=(new_type_of_tag)
#Tag.joins(:taggings).select('tags.*, count(tag_id) as "tag_count"').group(:tag_id).order(' tag_count desc')

def taxonomic_enforced
if type_of_tag == 'common_name' || type_of_tag == 'species_name'
if ['common_name', 'species_name'].include?(type_of_tag)
errors.add(:is_taxonomic, "must be true for #{type_of_tag}") unless is_taxonomic
elsif is_taxonomic
errors.add(:is_taxonomic, "must be false for #{type_of_tag}")
Expand Down
3 changes: 3 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ def login

has_one :statistics, class_name: Statistics::UserStatistics.name

has_many :created_verifications, class_name: 'Verification', foreign_key: :creator_id, inverse_of: :creator
has_many :updated_verifications, class_name: 'Verification', foreign_key: :updator_id, inverse_of: :updater

# scopes
scope :users, -> { where(roles_mask: 2) }
scope(:recently_seen,
Expand Down
142 changes: 142 additions & 0 deletions app/models/verification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# frozen_string_literal: true

# A Verification represents a user's confirmation that a tag is correctly
# applied to an audio event.
# @see (#AudioEvent) and (#Tag) for more information on these models.
#
# == Schema Information
#
# Table name: verifications
#
# id :bigint not null, primary key
# confirmed :enum not null
# created_at :datetime not null
# updated_at :datetime not null
# audio_event_id :bigint not null
# creator_id :integer not null
# tag_id :bigint not null
# updater_id :integer
#
# Indexes
#
# idx_on_audio_event_id_tag_id_creator_id_f944f25f20 (audio_event_id,tag_id,creator_id) UNIQUE
# index_verifications_on_audio_event_id (audio_event_id)
# index_verifications_on_tag_id (tag_id)
#
# Foreign Keys
#
# fk_rails_... (audio_event_id => audio_events.id) ON DELETE => cascade
# fk_rails_... (creator_id => users.id)
# fk_rails_... (tag_id => tags.id) ON DELETE => cascade
# fk_rails_... (updater_id => users.id)
#
class Verification < ApplicationRecord
belongs_to :audio_event, inverse_of: :verifications
belongs_to :tag, inverse_of: :verifications
belongs_to :creator, class_name: 'User', inverse_of: :created_verifications
belongs_to :updater, class_name: 'User', inverse_of: :updated_verifications, optional: true

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

# @type [Hash{String => String}]
CONFIRMATION_ENUM = {
CONFIRMATION_TRUE => CONFIRMATION_TRUE,
CONFIRMATION_FALSE => CONFIRMATION_FALSE,
CONFIRMATION_UNSURE => CONFIRMATION_UNSURE,
CONFIRMATION_SKIP => CONFIRMATION_SKIP
}.freeze

# @!method confirmed_true?
# @return [Boolean] true if the verification is confirmed as true
# @!method confirmed_true!
# @return [void] sets the verification as confirmed true
# @!method confirmed_false?
# @return [Boolean] true if the verification is confirmed as false
# @!method confirmed_false!
# @return [void] sets the verification as confirmed false
# @!method confirmed_unsure?
# @return [Boolean] true if the verification is marked as unsure
# @!method confirmed_unsure!
# @return [void] sets the verification as unsure
# @!method confirmed_skip?
# @return [Boolean] true if the verification is marked as skip
# @!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
Loading

0 comments on commit aa77668

Please sign in to comment.