Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
atruskie committed Feb 12, 2025
2 parents 2fe7eff + cd593a4 commit f18d7bf
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 f18d7bf

Please sign in to comment.