Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

362 add and edit attributes in sample types #2032

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
cfd7789
Add sample metadata updater module
kdp-cloud Jul 24, 2024
133718f
Add Sample Metadata upater job
kdp-cloud Jul 24, 2024
9c5aec7
Add after_action for updating the sample json metadata
kdp-cloud Jul 24, 2024
90ac8b4
Skip if sample metadata keys correspond with sample attribute titles
kdp-cloud Jul 24, 2024
037521b
Remove add new attribute constraint
kdp-cloud Jul 24, 2024
caeab80
Don't start job if there are samples
kdp-cloud Jul 24, 2024
8ce597b
typo
kdp-cloud Jul 24, 2024
d78ea79
Make unit editable
kdp-cloud Jul 24, 2024
fd18c8b
Fix sample types controller test
kdp-cloud Jul 24, 2024
36e355a
test for adding new attribute
kdp-cloud Jul 24, 2024
f894152
linting
kdp-cloud Oct 10, 2024
fd3a532
Add UpdateSampleMetadataTest
kdp-cloud Oct 10, 2024
a631ae3
Add test
kdp-cloud Oct 15, 2024
662fbdf
Fix the Sample metadata updater
kdp-cloud Oct 15, 2024
f4a59ac
Check user
kdp-cloud Oct 16, 2024
8b20eaf
Change `allow_name_change?`
kdp-cloud Oct 16, 2024
4a079c1
Add explaination
kdp-cloud Oct 16, 2024
2547489
Fix test
kdp-cloud Oct 16, 2024
928ffc4
Change existing title constraints test
kdp-cloud Oct 16, 2024
d44fafc
Add test for changing name of attribute.
kdp-cloud Oct 21, 2024
3cb7b13
Allow adding optional attribute to existing sample type
kdp-cloud Oct 21, 2024
fc0a0b0
Adjusted error message if attribute.new_record?
kdp-cloud Oct 22, 2024
675a39b
Restructure constraints
kdp-cloud Oct 22, 2024
190cd76
Put job in with_current_user block
kdp-cloud Oct 22, 2024
60763a1
Split test in optional and mandatory test
kdp-cloud Oct 22, 2024
d107a7b
check if samples instead of returning false
kdp-cloud Oct 22, 2024
07da6a1
test should fail if samples
kdp-cloud Oct 22, 2024
2dadb38
Remove `allow_name_change?` completely
kdp-cloud Oct 24, 2024
ed0d6bd
Revert deletion of `allow_new_attrbute?`
kdp-cloud Oct 28, 2024
520beb0
Update sample metadata in batches
kdp-cloud Oct 28, 2024
79ac49d
Add lock to cache
kdp-cloud Oct 28, 2024
45daffd
Update in batches
kdp-cloud Oct 28, 2024
acb2d44
Add caching for locking record
kdp-cloud Oct 28, 2024
6f3c382
Simplify
kdp-cloud Oct 29, 2024
fea3836
Tests
kdp-cloud Oct 29, 2024
9216b4f
Remove redundant begin block
kdp-cloud Oct 29, 2024
cde9a25
Lock sample during update job
kdp-cloud Oct 29, 2024
3d36b85
Move warning to show page
kdp-cloud Oct 29, 2024
d46e749
Move before_action up
kdp-cloud Oct 29, 2024
faa938a
Prevent adding samples to a locked sample type
kdp-cloud Oct 29, 2024
0d92bb4
Fix failing test
kdp-cloud Oct 29, 2024
3159cc3
Remove database lock
kdp-cloud Oct 30, 2024
6421eed
Use Taks instead of DelayedJob
kdp-cloud Oct 30, 2024
9bcbd7e
Fix tests
kdp-cloud Oct 30, 2024
b9b5235
Fix tests
kdp-cloud Oct 30, 2024
f68b44e
Cleaning up
kdp-cloud Nov 13, 2024
48e94de
use `state_allows_xxx?` instead of `can_xxx?`
kdp-cloud Nov 13, 2024
5d5ae6b
Enqueue `SampleTypeUpdateJob` after performing `UpdateSampleMetadataJob`
kdp-cloud Nov 13, 2024
7b6a600
Prevent saving each sample at update.
kdp-cloud Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/controllers/sample_types_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ class SampleTypesController < ApplicationController

before_action :samples_enabled?
before_action :check_no_created_samples, only: [:destroy]
before_action :check_if_locked, only: %i[edit manage manage_update update]
before_action :find_and_authorize_requested_item, except: %i[create batch_upload index new template_details]
before_action :find_sample_type, only: %i[batch_upload template_details]
before_action :check_isa_json_compliance, only: %i[edit update manage manage_update]
before_action :find_assets, only: [:index]
before_action :auth_to_create, only: %i[new create]
before_action :project_membership_required, only: %i[create new select filter_for_select]
before_action :old_attributes, only: %i[update]

after_action :update_sample_json_metadata, only: :update
kdp-cloud marked this conversation as resolved.
Show resolved Hide resolved

api_actions :index, :show, :create, :update, :destroy

Expand Down Expand Up @@ -201,4 +204,35 @@ def check_no_created_samples
redirect_to @sample_type
end
end

def old_attributes
return if @sample_type.sample_attributes.blank?

@old_sample_type_attributes = @sample_type.sample_attributes.map { |attr| { id: attr.id, title: attr.title } }
end

def update_sample_json_metadata
return if @sample_type.samples.blank? || @old_sample_type_attributes.blank?

attribute_changes = @sample_type.sample_attributes.map do |attr|
old_attr = @old_sample_type_attributes.detect { |oa| oa[:id] == attr.id }
next if old_attr.nil?

{ id: attr.id, old_title: old_attr[:title], new_title: attr.title } unless old_attr[:title] == attr.title
end.compact
return if attribute_changes.blank?

UpdateSampleMetadataJob.perform_later(@sample_type, @current_user, attribute_changes)
end

def check_if_locked
@sample_type ||= SampleType.find(params[:id])
@sample_type.reload
return unless @sample_type&.locked?

error_message = 'This sample type is locked and cannot be edited right now.'
flash[:error] = error_message
@sample_type.errors.add(:base, error_message)
redirect_to @sample_type
end
end
11 changes: 11 additions & 0 deletions app/controllers/samples_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class SamplesController < ApplicationController
before_action :samples_enabled?
before_action :find_index_assets, only: :index
before_action :find_and_authorize_requested_item, except: [:index, :new, :create, :preview]
before_action :check_if_locked_sample_type, only: %i[edit new create update]
before_action :templates_enabled?, only: [:query, :query_form]

before_action :auth_to_create, only: %i[new create batch_create]
Expand Down Expand Up @@ -371,4 +372,14 @@ def templates_enabled?
redirect_to select_sample_types_path
end
end

def check_if_locked_sample_type
return unless params[:sample_type_id]

sample_type = SampleType.find(params[:sample_type_id])
return unless sample_type&.locked?

flash[:error] = 'This sample type is locked. You cannot edit the sample.'
redirect_to sample_types_path(sample_type)
end
end
22 changes: 22 additions & 0 deletions app/jobs/update_sample_metadata_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class UpdateSampleMetadataJob < TaskJob
queue_with_priority 1
queue_as QueueNames::SAMPLES

after_perform do |job|
SampleTypeUpdateJob.perform_later(job.arguments.first, false)
end

def perform(sample_type, user, attribute_changes = [])
@sample_type = sample_type
@user = user
@attribute_changes = attribute_changes

Seek::Samples::SampleMetadataUpdater.new(@sample_type, @user, @attribute_changes).update_metadata
kdp-cloud marked this conversation as resolved.
Show resolved Hide resolved
end

def task
arguments[0].sample_metadata_update_task
end
end
13 changes: 7 additions & 6 deletions app/models/sample.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Sample < ApplicationRecord

validates_with SampleAttributeValidator
validate :validate_added_linked_sample_permissions
validate :check_if_locked_sample_type, on: %i[create update]

before_validation :set_title_to_title_attribute_value
before_validation :update_sample_resource_links
Expand Down Expand Up @@ -241,12 +242,12 @@ def validate_added_linked_sample_permissions
return if $authorization_checks_disabled
return if linked_samples.empty?
previous_linked_samples = []
unless new_record?
previous_linked_samples = Sample.find(id).referenced_samples
end
previous_linked_samples = Sample.find(id).referenced_samples unless new_record?
additions = linked_samples - previous_linked_samples
if additions.detect { |sample| !sample.can_view? }
errors.add(:linked_samples, 'includes a new private sample')
end
errors.add(:linked_samples, 'includes a new private sample') if additions.detect { |sample| !sample.can_view? }
end

def check_if_locked_sample_type
errors.add(:sample_type, 'is locked') if sample_type&.locked?
end
end
5 changes: 2 additions & 3 deletions app/models/sample_attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ def force_required_when_is_title

def validate_against_editing_constraints
c = sample_type.editing_constraints
error_message = "cannot be changed (#{title_was})" # Use pre-change title in error message.

errors.add(:title, error_message) if title_changed? && !c.allow_name_change?(self)
attr_title = self.new_record? ? title : title_was
error_message = "cannot be changed (#{attr_title})" # Use pre-change title in error message.

unless c.allow_required?(self)
errors.add(:is_title, error_message) if is_title_changed?
Expand Down
28 changes: 23 additions & 5 deletions app/models/sample_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class SampleType < ApplicationRecord
:validate_attribute_title_unique,
:validate_attribute_accessor_names_unique,
:validate_title_is_not_type_of_seek_sample_multi,
:validate_against_editing_constraints
:validate_against_editing_constraints,
:validate_sample_type_is_not_locked
validates :projects, presence: true, projects: { self: true }

accepts_nested_attributes_for :sample_attributes, allow_destroy: true
Expand All @@ -63,6 +64,7 @@ class SampleType < ApplicationRecord

has_annotation_type :sample_type_tag, method_name: :tags

has_task :sample_metadata_update
def investigations
return [] if studies.empty? && assays.empty?

Expand Down Expand Up @@ -110,6 +112,10 @@ def is_isa_json_compliant?
(studies.any? || assays.any?) && has_only_isa_json_compliant_investigations && !isa_template.nil?
end

def locked?
sample_metadata_update_task&.in_progress?
end

def validate_value?(attribute_name, value)
attribute = sample_attributes.detect { |attr| attr.title == attribute_name }
raise UnknownAttributeException, "Unknown attribute #{attribute_name}" unless attribute
Expand Down Expand Up @@ -156,6 +162,18 @@ def self.can_create?
can && (!Seek::Config.project_admin_sample_type_restriction || User.current_user.is_admin_or_project_administrator?)
end

def state_allows_edit?(*args)
super && !locked?
end

def state_allows_manage?(*args)
super && !locked?
end

def state_allows_delete?(*args)
super && !locked?
end

def can_delete?(user = User.current_user)
# Users should be able to delete an ISA JSON compliant sample type that has linked sample attributes,
# as long as it's ISA JSON compliant.
Expand Down Expand Up @@ -244,13 +262,13 @@ def validate_against_editing_constraints
if a.marked_for_destruction? && !c.allow_attribute_removal?(a)
errors.add(:sample_attributes, "cannot be removed, there are existing samples using this attribute (#{a.title})")
end

if a.new_record? && !c.allow_new_attribute?
errors.add(:sample_attributes, "cannot be added, new attributes are not allowed (#{a.title})")
end
end
end

def validate_sample_type_is_not_locked
errors.add(:base, 'This sample type is locked and cannot be edited right now.') if locked?
end

def attribute_search_terms
attribute_titles
end
Expand Down
2 changes: 1 addition & 1 deletion app/views/sample_types/_buttons.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<%= button_link_to("View samples", 'view_samples', sample_type_samples_path(sample_type)) %>
<% end %>

<% if Sample.can_create? %>
<% if Sample.can_create? && !sample_type.locked? %>
<%= button_link_to("Create sample", 'new', new_sample_path(sample_type_id:sample_type.id)) %>
<% end %>

Expand Down
2 changes: 1 addition & 1 deletion app/views/sample_types/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
display_isa_tag: false } %>
<% end %>

<% unless @sample_type.uploaded_template? || !@sample_type.editing_constraints.allow_new_attribute? %>
<% unless @sample_type.uploaded_template? %>
<tr id="add-attribute-row">
<td colspan="6">
<%= button_link_to('Add new attribute', 'add', '#', id: 'add-attribute') %>
Expand Down
5 changes: 2 additions & 3 deletions app/views/sample_types/_sample_attribute_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<% allow_required = constraints.allow_required?(sample_attribute) %>
<% allow_attribute_removal = constraints.allow_attribute_removal?(sample_attribute) %>
<% allow_type_change = constraints.allow_type_change?(sample_attribute) %>
<% allow_name_change = constraints.allow_name_change?(sample_attribute) %>
<% seek_sample_multi = sample_attribute&.seek_sample_multi? %>
<% link_to_self = sample_attribute && sample_attribute.deferred_link_to_self %>
<% display_isa_tag ||= false %>
Expand All @@ -43,7 +42,7 @@
<td>

<%= text_field_tag "#{field_name_prefix}[title]", title, class: 'form-control',
placeholder: 'Attribute name', disabled: !allow_name_change, data: { attr: "title" } %>
placeholder: 'Attribute name', data: { attr: "title" } %>
</td>

<td class="text-center" title="<%= required_title_text %>">
Expand Down Expand Up @@ -132,7 +131,7 @@
unit_id),
include_blank: true,
class: 'form-control',
disabled: !allow_type_change, data: { attr: "unit" } %>
data: { attr: "unit" } %>
</td>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<%= render :partial => "assets/resource_list_item", :object => sample_type %>
</div>
</div>
<% if Sample.can_create? %>
<% if Sample.can_create? && !sample_type.locked? %>
<div class="col-sm-2">
<%= link_to "New sample", new_sample_path(:sample_type_id => sample_type.id, project_ids: params[:project_ids]), class: "btn btn-primary" %>
</div>
Expand Down
6 changes: 5 additions & 1 deletion app/views/sample_types/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<%= render partial: 'general/item_title', locals: { item: @sample_type, buttons_partial: 'buttons' } %>

<% if @sample_type.locked? %>
<div id="locked-sample-type-warning" class="alert alert-danger">
<strong>Warning!</strong> This sample type is being edited by a background process and cannot be edited right now.
</div>
<% end %>
<%= render partial: 'general/show_page_tab_definitions' %>

<div class="tab-content">
Expand Down
39 changes: 39 additions & 0 deletions lib/seek/samples/sample_metadata_updater.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Seek
module Samples

class SampleMetadataUpdateException < StandardError; end

# Class to handle the updating of sample metadata after updating Sample type
class SampleMetadataUpdater
def initialize(sample_type, user, attribute_changes)
@sample_type = sample_type
@user = user
@attribute_change_maps = attribute_changes
end

def update_metadata
return if @attribute_change_maps.blank? || @sample_type.blank? || @user.nil? || @sample_type.samples.blank?
return unless @sample_type.locked?

User.with_current_user(@user) do
@sample_type.samples.in_batches(of: 1000).each do |batch|
batch.each do |sample|
metadata = JSON.parse(sample.json_metadata)
# Update the metadata keys with the new attribute titles
@attribute_change_maps.each do |change|
metadata[change[:new_title]] = metadata.delete(change[:old_title])
end
sample.update_column(:json_metadata, metadata.to_json)
end
end
end
end

def raise_sample_metadata_update_exception
raise SampleMetadataUpdateException.new('An unexpected error occurred while updating the sample metadata')
end
end
end
end
34 changes: 10 additions & 24 deletions lib/seek/samples/sample_type_editing_constraints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ def samples?
# if attr is nil, indicates a new attribute. required is not allowed if there are already samples
def allow_required?(attr)
if attr.is_a?(SampleAttribute)
return true if attr.new_record?
return true if attr.new_record? && @sample_type.new_record?
return false if inherited?(attr)

attr = attr.accessor_name
end
if attr
!blanks?(attr)
else
!samples?
end

return !samples? unless attr
return !blanks?(attr) if samples?

true
end

# an attribute could be removed if all are currently blank
Expand All @@ -49,26 +49,12 @@ def allow_attribute_removal?(attr)
end
end

# whether a new attribtue can be created
# This method was left in so it can be changed in the future
# Currently, it always returns true
# see https://github.com/seek4science/seek/pull/2032#discussion_r1813137258
def allow_new_attribute?
!samples?
true
end

# whether the name of the attribute can be changed
def allow_name_change?(attr)
if attr.is_a?(SampleAttribute)
return true if attr.new_record?
return false if inherited?(attr)

attr = attr.accessor_name
end
if attr
!samples?
else
true
end
end

# whether the type for the attribute can be changed
def allow_type_change?(attr)
if attr.is_a?(SampleAttribute)
Expand Down
Loading