-class ArvadosBase < ActiveRecord::Base
- self.abstract_class = true
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class ArvadosBase
+ include ActiveModel::Validations
+ include ActiveModel::Conversion
+ include ActiveModel::Serialization
+ include ActiveModel::Dirty
+ include ActiveModel::AttributeAssignment
+ extend ActiveModel::Naming
+
+ Column = Struct.new("Column", :name)
+
attr_accessor :attribute_sortkey
+ attr_accessor :create_params
+
+ class Error < StandardError; end
+
+ module Type
+ class Hash < ActiveModel::Type::Value
+ def type
+ :hash
+ end
+
+ def default_value
+ {}
+ end
+
+ private
+ def cast_value(value)
+ (value.class == String) ? ::JSON.parse(value) : value
+ end
+ end
+
+ class Array < ActiveModel::Type::Value
+ def type
+ :array
+ end
+
+ def default_value
+ []
+ end
+
+ private
+ def cast_value(value)
+ (value.class == String) ? ::JSON.parse(value) : value
+ end
+ end
+ end
def self.arvados_api_client
ArvadosApiClient.new_or_current
end
end
- def initialize raw_params={}
- super self.class.permit_attribute_params(raw_params)
+ def initialize raw_params={}, create_params={}
+ self.class.permit_attribute_params(raw_params)
+ @create_params = create_params
@attribute_sortkey ||= {
'id' => nil,
'name' => '000',
'modified_by_client_uuid' => '203',
'uuid' => '999',
}
+ @loaded_attributes = {}
+ attributes = self.class.columns.map { |c| [c.name.to_sym, nil] }.to_h.merge(raw_params)
+ attributes.symbolize_keys.each do |name, value|
+ send("#{name}=", value)
+ end
end
def self.columns
- return @columns unless @columns.nil?
- @columns = []
+ @discovered_columns = [] if !defined?(@discovered_columns)
+ return @discovered_columns if @discovered_columns.andand.any?
@attribute_info ||= {}
schema = arvados_api_client.discovery[:schemas][self.to_s.to_sym]
- return @columns if schema.nil?
+ return @discovered_columns if schema.nil?
schema[:properties].each do |k, coldef|
case k
when :etag, :kind
else
if coldef[:type] == coldef[:type].downcase
# boolean, integer, etc.
- @columns << column(k, coldef[:type].to_sym)
+ @discovered_columns << column(k, coldef[:type])
else
# Hash, Array
- @columns << column(k, :text)
- serialize k, coldef[:type].constantize
+ @discovered_columns << column(k, coldef[:type], coldef[:type].constantize.new)
end
+ attr_reader k
@attribute_info[k] = coldef
end
end
- @columns
+ @discovered_columns
+ end
+
+ def new_record?
+ # dup method doesn't reset the uuid attr
+ @uuid.nil? || @new_record || false
+ end
+
+ def initialize_dup(other)
+ super
+ @new_record = true
+ @created_at = nil
end
def self.column(name, sql_type = nil, default = nil, null = true)
- ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
+ caster = case sql_type
+ when 'integer'
+ ActiveModel::Type::Integer
+ when 'string', 'text'
+ ActiveModel::Type::String
+ when 'float'
+ ActiveModel::Type::Float
+ when 'datetime'
+ ActiveModel::Type::DateTime
+ when 'boolean'
+ ActiveModel::Type::Boolean
+ when 'Hash'
+ ArvadosBase::Type::Hash
+ when 'Array'
+ ArvadosBase::Type::Array
+ when 'jsonb'
+ ArvadosBase::Type::Hash
+ else
+ raise ArvadosBase::Error.new("Type unknown: #{sql_type}")
+ end
+ define_method "#{name}=" do |val|
+ val = default if val.nil?
+ casted_value = caster.new.cast(val)
+ attribute_will_change!(name) if send(name) != casted_value
+ set_attribute_after_cast(name, casted_value)
+ end
+ Column.new(name.to_s)
+ end
+
+ def set_attribute_after_cast(name, casted_value)
+ instance_variable_set("@#{name}", casted_value)
+ end
+
+ def [](attr_name)
+ begin
+ send(attr_name)
+ rescue
+ Rails.logger.debug "BUG: access non-loaded attribute #{attr_name}"
+ nil
+ end
+ end
+
+ def []=(attr_name, attr_val)
+ send("#{attr_name}=", attr_val)
end
def self.attribute_info
new.private_reload(hash)
end
+ def self.find?(*args)
+ find(*args) rescue nil
+ end
+
def self.order(*args)
ArvadosResourceList.new(self).order(*args)
end
ArvadosResourceList.new(self).limit(*args)
end
+ def self.select(*args)
+ ArvadosResourceList.new(self).select(*args)
+ end
+
+ def self.with_count(*args)
+ ArvadosResourceList.new(self).with_count(*args)
+ end
+
+ def self.distinct(*args)
+ ArvadosResourceList.new(self).distinct(*args)
+ end
+
+ def self.include_trash(*args)
+ ArvadosResourceList.new(self).include_trash(*args)
+ end
+
+ def self.recursive(*args)
+ ArvadosResourceList.new(self).recursive(*args)
+ end
+
def self.eager(*args)
ArvadosResourceList.new(self).eager(*args)
end
- def self.all(*args)
- ArvadosResourceList.new(self).all(*args)
+ def self.all
+ ArvadosResourceList.new(self)
end
def self.permit_attribute_params raw_params
# The following permit! is necessary even with
# "ActionController::Parameters.permit_all_parameters = true",
# because permit_all does not permit nested attributes.
- ActionController::Parameters.new(raw_params).permit!
+ if !raw_params.is_a? ActionController::Parameters
+ raw_params = ActionController::Parameters.new(raw_params)
+ end
+ raw_params.permit!
+ end
+
+ def self.create raw_params={}, create_params={}
+ x = new(permit_attribute_params(raw_params), create_params)
+ x.save
+ x
end
- def self.create raw_params={}
- super(permit_attribute_params(raw_params))
+ def self.create! raw_params={}, create_params={}
+ x = new(permit_attribute_params(raw_params), create_params)
+ x.save!
+ x
+ end
+
+ def self.table_name
+ self.name.underscore.pluralize.downcase
end
def update_attributes raw_params={}
- super(self.class.permit_attribute_params(raw_params))
+ assign_attributes(self.class.permit_attribute_params(raw_params))
+ save
+ end
+
+ def update_attributes! raw_params={}
+ assign_attributes(self.class.permit_attribute_params(raw_params))
+ save!
end
def save
obdata = {}
self.class.columns.each do |col|
- obdata[col.name.to_sym] = self.send(col.name.to_sym)
+ # Non-nil serialized values must be sent because we can't tell
+ # whether they've changed. Other than that, any given attribute
+ # is either unchanged (in which case there's no need to send its
+ # old value in the update/create command) or has been added to
+ # #changed by ActiveRecord's #attr= method.
+ if changed.include? col.name or
+ ([Hash, Array].include?(attributes[col.name].class) and
+ @loaded_attributes[col.name])
+ obdata[col.name.to_sym] = self.send col.name
+ end
end
obdata.delete :id
postdata = { self.class.to_s.underscore => obdata }
obdata.delete :uuid
resp = arvados_api_client.api(self.class, '/' + uuid, postdata)
else
+ if @create_params
+ @create_params = @create_params.to_unsafe_hash if @create_params.is_a? ActionController::Parameters
+ postdata.merge!(@create_params)
+ end
resp = arvados_api_client.api(self.class, '', postdata)
end
return false if !resp[:etag] || !resp[:uuid]
end
end
+ changes_applied
@new_record = false
self
self.save or raise Exception.new("Save failed")
end
+ def persisted?
+ (!new_record? && !destroyed?) ? true : false
+ end
+
+ def destroyed?
+ !(new_record? || etag || uuid)
+ end
+
def destroy
if etag || uuid
postdata = { '_method' => 'DELETE' }
hash = arvados_api_client.api(self.class, '/' + uuid_or_hash)
end
hash.each do |k,v|
+ @loaded_attributes[k.to_s] = true
if self.respond_to?(k.to_s + '=')
self.send(k.to_s + '=', v)
else
end
end
@all_links = nil
+ changes_applied
@new_record = false
self
end
uuid
end
- def dup
- super.forget_uuid!
+ def initialize_copy orig
+ super
+ forget_uuid!
+ end
+
+ def attributes
+ kv = self.class.columns.collect {|c| c.name}.map {|key| [key, send(key)]}
+ kv.to_h
end
def attributes_for_display
end
def class_for_display
- self.class.to_s
+ self.class.to_s.underscore.humanize
+ end
+
+ def self.class_for_display
+ self.to_s.underscore.humanize
+ end
+
+ # Array of strings that are names of attributes that should be rendered as textile.
+ def textile_attributes
+ []
end
def self.creatable?
- current_user
+ current_user.andand.is_active && api_exists?(:create)
+ end
+
+ def self.goes_in_projects?
+ false
+ end
+
+ # can this class of object be copied into a project?
+ # override to false on indivudal model classes for which this should not be true
+ def self.copies_to_projects?
+ self.goes_in_projects?
end
def editable?
(current_user and current_user.is_active and
(current_user.is_admin or
current_user.uuid == self.owner_uuid or
- new_record?))
+ new_record? or
+ (respond_to?(:writable_by) ?
+ writable_by.include?(current_user.uuid) :
+ (ArvadosBase.find(owner_uuid).writable_by.include? current_user.uuid rescue false)))) or false
end
- def attribute_editable?(attr)
- if "created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at".index(attr.to_s)
+ def deletable?
+ editable?
+ end
+
+ def self.api_exists?(method)
+ arvados_api_client.discovery[:resources][self.to_s.underscore.pluralize.to_sym].andand[:methods].andand[method]
+ end
+
+ # Array of strings that are the names of attributes that can be edited
+ # with X-Editable.
+ def editable_attributes
+ self.class.columns.map(&:name) -
+ %w(created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at)
+ end
+
+ def attribute_editable?(attr, ever=nil)
+ if not editable_attributes.include?(attr.to_s)
false
elsif not (current_user.andand.is_active)
false
- elsif "uuid owner_uuid".index(attr.to_s) or current_user.is_admin
+ elsif attr == 'uuid'
current_user.is_admin
+ elsif ever
+ true
else
- current_user.uuid == self.owner_uuid or
- current_user.uuid == self.uuid or
- new_record?
+ editable?
end
end
if opts[:class].is_a? Class
return opts[:class]
end
- if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
+ if uuid.match(/^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/)
return Collection
end
resource_class = nil
- uuid.match /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/ do |re|
+ uuid.match(/^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/) do |re|
resource_class ||= arvados_api_client.
kind_class(self.uuid_infix_object_kind[re[1]])
end
if opts[:referring_object] and
opts[:referring_attr] and
- opts[:referring_attr].match /_uuid$/
+ opts[:referring_attr].match(/_uuid$/)
resource_class ||= arvados_api_client.
kind_class(opts[:referring_object].
attributes[opts[:referring_attr].
resource_class
end
- def friendly_link_name
- (name if self.respond_to? :name) || uuid
+ def resource_param_name
+ self.class.to_s.underscore
+ end
+
+ def friendly_link_name lookup=nil
+ (name if self.respond_to? :name) || default_name
end
def content_summary
friendly_link_name
end
+ def self.default_name
+ self.to_s.underscore.humanize
+ end
+
+ def controller
+ (self.class.to_s.pluralize + 'Controller').constantize
+ end
+
+ def controller_name
+ self.class.to_s.tableize
+ end
+
+ # Placeholder for name when name is missing or empty
+ def default_name
+ if self.respond_to? :name
+ "New #{class_for_display.downcase}"
+ else
+ uuid
+ end
+ end
+
+ def owner
+ ArvadosBase.find(owner_uuid) rescue nil
+ end
+
protected
def forget_uuid!