14988: Adds missing create!() method to ArvadosBase.
[arvados.git] / apps / workbench / app / models / arvados_base.rb
index f19d47435ae979564c94cf9117109ff44fe5acdf..9925d46128410ca0730572cbdec6622d1adb3069 100644 (file)
@@ -1,8 +1,54 @@
-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
@@ -31,7 +77,7 @@ class ArvadosBase < ActiveRecord::Base
   end
 
   def initialize raw_params={}, create_params={}
-    super self.class.permit_attribute_params(raw_params)
+    self.class.permit_attribute_params(raw_params)
     @create_params = create_params
     @attribute_sortkey ||= {
       'id' => nil,
@@ -53,14 +99,19 @@ class ArvadosBase < ActiveRecord::Base
       'modified_by_client_uuid' => '203',
       'uuid' => '999',
     }
-  end
+    @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 if @columns.andand.any?
-    @columns = []
+    return @discovered_columns if @discovered_columns.andand.any?
+    @discovered_columns = []
     @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
@@ -68,20 +119,65 @@ class ArvadosBase < ActiveRecord::Base
       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)
+          serialize k, coldef[:type].constantize
         end
+        attr_reader k
         @attribute_info[k] = coldef
       end
     end
-    @columns
+    @discovered_columns
+  end
+
+  def new_record?
+    (uuid == nil) ? true : false
   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
+              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
+      nil
+    end
+  end
+
+  def []=(attr_name, attr_val)
+    send("#{attr_name}=", attr_val)
   end
 
   def self.attribute_info
@@ -135,6 +231,22 @@ class ArvadosBase < ActiveRecord::Base
     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
@@ -151,24 +263,50 @@ class ArvadosBase < ActiveRecord::Base
     # 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 = super(permit_attribute_params(raw_params))
-    x.create_params = create_params
+    x = new(permit_attribute_params(raw_params), create_params)
+    x.save
+    x
+  end
+
+  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|
-      unless self.send(col.name.to_sym).nil? and !self.changed.include?(col.name)
-          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
@@ -194,6 +332,7 @@ class ArvadosBase < ActiveRecord::Base
       end
     end
 
+    changes_applied
     @new_record = false
 
     self
@@ -203,6 +342,14 @@ class ArvadosBase < ActiveRecord::Base
     self.save or raise Exception.new("Save failed")
   end
 
+  def persisted?
+    (!new_record? && !destroyed?) ? true : false
+  end
+
+  def destroyed?
+    !(etag || uuid)
+  end
+
   def destroy
     if etag || uuid
       postdata = { '_method' => 'DELETE' }
@@ -262,6 +409,7 @@ class ArvadosBase < ActiveRecord::Base
       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
@@ -276,6 +424,7 @@ class ArvadosBase < ActiveRecord::Base
       end
     end
     @all_links = nil
+    changes_applied
     @new_record = false
     self
   end
@@ -289,6 +438,11 @@ class ArvadosBase < ActiveRecord::Base
     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
     self.attributes.reject { |k,v|
       attribute_sortkey.has_key?(k) and !attribute_sortkey[k]
@@ -311,7 +465,7 @@ class ArvadosBase < ActiveRecord::Base
   end
 
   def self.creatable?
-    current_user.andand.is_active
+    current_user.andand.is_active && api_exists?(:create)
   end
 
   def self.goes_in_projects?
@@ -334,6 +488,14 @@ class ArvadosBase < ActiveRecord::Base
        (ArvadosBase.find(owner_uuid).writable_by.include? current_user.uuid rescue false)))) or false
   end
 
+  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