<commit_before>ATTACHMENT_METADATA_FIELDS = [
  :title,
  :source,
  :description,
  :creator,
  :attribution,
  :subject,
  :license,
  :spatial,
]

module AttachableWithMetadata 

  module ClassMethods
    
    def attaches_with_metadata(*fields)
      
      # Set up standard attachment
      attaches *fields
      
      # Add metadata methods
      fields.map(&:to_s).each do |attachment_field|        

        before_save :"update_#{attachment_field}_metadata", 
                    :if => :"#{attachment_field}_metadata_has_changed?"

        ATTACHMENT_METADATA_FIELDS.each do |metadata_field|
          
          field_name = "#{attachment_field}_#{metadata_field}"

          define_method(field_name) do
            if instance_variable_get("@#{field_name}").nil?
              instance_variable_set "@#{field_name}", send(attachment_field)[metadata_field.to_s]
            end
            instance_variable_get("@#{field_name}")
          end
          
          case metadata_field
          when :spatial
            define_method("#{field_name}=") do |value|
              # Auto-parse string assignment
              if value.is_a?(String)
                 spatial = ['lat', 'lng'].zip(value.split(','))
                 value = Hash[spatial]
              end
              # Store
              if instance_variable_get("@#{field_name}") != value
                instance_variable_set("@#{attachment_field}_metadata_has_changed", true)
                instance_variable_set("@#{field_name}", value)
              end
            end
          when :subject
            define_method("#{field_name}=") do |value|
              # Auto-parse string assignment
              if value.is_a?(String)
                value = value.split(',')
              end
              # Store
              if instance_variable_get("@#{field_name}") != value
                instance_variable_set("@#{attachment_field}_metadata_has_changed", true)
                instance_variable_set("@#{field_name}", value)
              end
            end
          else
            define_method("#{field_name}=") do |value|
              if instance_variable_get("@#{field_name}") != value
                instance_variable_set("@#{attachment_field}_metadata_has_changed", true)
                instance_variable_set("@#{field_name}", value)
              end
            end
          end
          
        end
        
        define_method("#{attachment_field}_metadata_hash") do
          # Get all fields
          Hash[ATTACHMENT_METADATA_FIELDS.map do |metadata_field|
            [metadata_field, instance_variable_get("@#{attachment_field}_#{metadata_field}")]
          end]
        end

        define_method("#{attachment_field}_metadata_has_changed?") do
          instance_variable_get("@#{attachment_field}_metadata_has_changed") || false
        end
        
        define_method("update_#{attachment_field}_metadata") do
          raise ApiClientNotPresent unless Attachable.asset_api_client
          begin
            asset_id = send(attachment_field)['id'].split('/').last
            response = Attachable.asset_api_client.update_asset(asset_id,
                         send(:"#{attachment_field}_metadata_hash"))
            # Clear metadata tracking flag
            instance_variable_set("@#{attachment_field}_metadata_has_changed", false)
            # Explicit nil return so we don't return false form line above
            nil
          rescue StandardError
            errors.add("#{attachment_field}_id".to_sym, "could not be uploaded")
          end
        end
        
        define_method("upload_#{attachment_field}") do
          raise ApiClientNotPresent unless Attachable.asset_api_client
          begin
            data = {:file => instance_variable_get("@#{attachment_field}_file")}
            data.merge!(send(:"#{attachment_field}_metadata_hash")) if send(:"#{attachment_field}_metadata_has_changed?")
            response = Attachable.asset_api_client.create_asset(data)
            self.send("#{attachment_field}_id=", response.id.match(/\/([^\/]+)\z/) {|m| m[1] })
            # Clear metadata tracking flag
            instance_variable_set("@#{attachment_field}_metadata_has_changed", false)
            # Explicit nil return so we don't return false form line above
            nil
          rescue StandardError
            errors.add("#{attachment_field}_id".to_sym, "could not be uploaded")
          end
        end

      end
    end<commit_msg>DRY up assignment methods, and explain things with some comments<commit_after>ATTACHMENT_METADATA_FIELDS = [
  :title,
  :source,
  :description,
  :creator,
  :attribution,
  :subject,
  :license,
  :spatial,
]

module AttachableWithMetadata 

  module ClassMethods
    
    def attaches_with_metadata(*fields)
      
      # Set up standard attachment
      attaches *fields
      
      # Add metadata methods
      fields.map(&:to_s).each do |attachment_field|        

        before_save :"update_#{attachment_field}_metadata", 
                    :if => :"#{attachment_field}_metadata_has_changed?"

        ATTACHMENT_METADATA_FIELDS.each do |metadata_field|
          
          # Work out the field name. For instance, 'image_description'
          field_name = "#{attachment_field}_#{metadata_field}"
          
          # Field 'get'
          # Loads from asset manager API if not set
          define_method(field_name) do
            if instance_variable_get("@#{field_name}").nil?
              instance_variable_set "@#{field_name}", send(attachment_field)[metadata_field.to_s]
            end
            instance_variable_get("@#{field_name}")
          end
          
          # Field 'set'
          # Sets up assignment methods for all fields
          # 'spatial' and 'subject' fields have special handling for assigning comma-separated
          # strings. Ideally this code should be higher up in a controller, but I couldn't
          # find the best place for it without digging into a whole load of new code.
          # Maybe later.
          case metadata_field
          when :spatial
            define_method("#{field_name}=") do |value|
              # Auto-parse string assignment - this would be better in a controller
              if value.is_a?(String)
                 spatial = ['lat', 'lng'].zip(value.split(','))
                 value = Hash[spatial]
              end
              # Store
              send("store_#{field_name}", value)
            end
          when :subject
            define_method("#{field_name}=") do |value|
              # Auto-parse string assignment - this would be better in a controller
              if value.is_a?(String)
                value = value.split(',')
              end
              # Store
              send("store_#{field_name}", value)
            end
          else
            define_method("#{field_name}=") do |value|
              send("store_#{field_name}", value)
            end
          end
          
          # Private store method, shared between all setters.
          define_method("store_#{field_name}") do |value|
            if instance_variable_get("@#{field_name}") != value
              # Update metadata change tracker
              instance_variable_set("@#{attachment_field}_metadata_has_changed", true)
              # Set instance var
              instance_variable_set("@#{field_name}", value)
            end
          end
          private "store_#{field_name}".to_sym
          
        end
        
        # Get all metadata fields as a single hash, for sending to the API calls
        define_method("#{attachment_field}_metadata_hash") do
          # Get all fields
          Hash[ATTACHMENT_METADATA_FIELDS.map do |metadata_field|
            [metadata_field, instance_variable_get("@#{attachment_field}_#{metadata_field}")]
          end]
        end

        # Get whether any metadata fields have changed
        define_method("#{attachment_field}_metadata_has_changed?") do
          instance_variable_get("@#{attachment_field}_metadata_has_changed") || false
        end
        
        # If we're updating an existing asset, write the metadata to the API
        define_method("update_#{attachment_field}_metadata") do
          raise ApiClientNotPresent unless Attachable.asset_api_client
          begin
            # Get the asset ID. Is in URL form, so we need to split it
            asset_id = send(attachment_field)['id'].split('/').last
            # Update data in asset manager API.
            response = Attachable.asset_api_client.update_asset(asset_id,
                         send(:"#{attachment_field}_metadata_hash"))
            # Clear metadata tracking flag
            instance_variable_set("@#{attachment_field}_metadata_has_changed", false)
            # Explicit nil return so we don't return false form line above
            nil
          rescue StandardError
            errors.add("#{attachment_field}_id".to_sym, "could not be uploaded")
          end
        end
        
        # Overloaded method from Attachable
        # We override the original, so that when we upload a new asset, we also
        # set metadata fields
        define_method("upload_#{attachment_field}") do
          raise ApiClientNotPresent unless Attachable.asset_api_client
          begin
            # Build data hash, with file object and metadata all together
            data = {:file => instance_variable_get("@#{attachment_field}_file")}
            data.merge!(send(:"#{attachment_field}_metadata_hash")) if send(:"#{attachment_field}_metadata_has_changed?")
            # Create asset in asset manager API
            response = Attachable.asset_api_client.create_asset(data)
            self.send("#{attachment_field}_id=", response.id.match(/\/([^\/]+)\z/) {|m| m[1] })
            # Clear metadata tracking flag
            instance_variable_set("@#{attachment_field}_metadata_has_changed", false)
            # Explicit nil return so we don't return false form line above
            nil
          rescue StandardError
            errors.add("#{attachment_field}_id".to_sym, "could not be uploaded")
          end
        end

      end
    end
{'fraction_non_alphanumeric': 0.09453369905956113, 'fraction_numerical': 0.00019592476489028212, 'mean_word_length': 2.110603290676417, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3816571', 'n_tokens_mistral': 2677, 'n_tokens_neox': 2689, 'n_words': 622}