textmate rspec bundle (datamapper) helpers

I found that I often want to add debug outputs to the specs while working on them. Since I use textmate’s rspec bundle to run (focused) unit tests, I needed my debug messages to be html. Here’s what I came up with

http://gist.github.com/92470

dm-accepts_nested_attributes

I just pushed http://github.com/snusnu/dm-accepts_nested_attributes/tree/master

It’s probably not quite complete yet, but it’s already reasonably specced and functional.

254 examples, 0 failures
89.7%   3 file(s)   473 Lines   234 LOC


Current status:

  • belongs_to: create / destroy / update
  • has(1): create / update/ destroy
  • has(n): create / update / destroy
  • has(n, :through): create / update / destroy

Also, since I need this rather soon for my current (merb) project, I wrote it for 0.9.11 I definitely want it to be available for next branch (of course) but I cannot spend any time on it now, since from what I read, the datamapper next branch is not production ready yet.

Anyways, I think it’d be really cool to have this in datamapper! (a plugin is perfeclty fine, needn’t be core). The API is almost the same (one minor change for now - have a look at the README) like the one activerecord has, which should mean, that once rails3 comes out, multimodel forms are totally possible with datamapper as ORM.

As for merb, I don’t really know if it has support for nested model assignment in its form_for and fields_for helpers. I have to say though, that I don’t really care, since I don’t use these helpers, but write the html that my designer colleague wants to see.

Any help, ideas, discussions on irc/email, forks, patches, whatever … really appreciated!

UPDATE 12

This commit added initial support for transactions. Currently, every call to save gets wrapped inside a transaction. However,there is still some work to do to make this solid. It’s also totally not specced atm.

UPDATE 11

This commit added a hook to allow to sanitize nested model attributes that get passed to a resource. See this thread on the rails-core list for the motivation behind this new method. Currently the method doesn’t do anything, but can be overwritten to perform just the amount of sanitization that you would like to apply.

UPDATE 10

With this patch from Rupert Voelcker, it is now possible to access the nested attributes also after Resource#save was called. This means that if for example a controller’s create action needs to redirect to the new action because the resource couldn’t be saved, the values the user previously entered can be displayed on the new form, just as you would expect. Thx again Rupert!

UPDATE 9

This commit added (experimental) support for dm-validations. More details explaining the current status and TODO list can be found in my post to the datamapper mailinglist

UPDATE 8

Rupert Voelcker sent a patch that allows renamed has(n, :through, :class_name, :child_key, :parent_key) associations to work with dm-accepts_nested_attributes. Thx a lot!

UPDATE 7

This commit added a new helper method for many_to_one (belongs_to) and one_to_one (has 1) associations, that will return either the associated record (if present) or a new record of the associated class. This is somehow close to activerecords build_association, I guess, but it differs in that it won’t build a new record in case that one is already associated. For this reason, I named it get_#{association}. It isn’t available for _to_many associations, since datamapper already provides get and build methods on their association proxy.

After I added this method, I read through Ryan’s Scraps again, and got reminded of his cool trick to achieve the same thing I’m trying to do with view helpers (I mainly added this feature to support fields_for a nested attribute). I totally agree when he says that it’s actually a view concern to prepare the resource for form submission, and I’m now thinking that I will maybe remove this added feauture from dm-accepts_nested_attributes

UPDATE 6

This commit added the possibility to specify a reject_if proc that gets passed all attributes. If the proc returns true, the creation of a new associated entry will be rejected. Now, dm-accepts_nested_attributes supports the same features as activerecord nested attribute assignment (I think).

UPDATE 5

This commit added the possibility to update has(n, :through) associations via #{association_name}_attributes=

This means that all (current) specs pass and everything works so far :) Hooray!

UPDATE 4

This commit added the possibility to destroy has(n, :through) associations via #{association_name}_attributes=

 UPDATE 3

This commit added the possibility to create has(n, :through) associations via #{association_name}_attributes=

UPDATE 2

It seems to me that this commit fixes the issue mentioned in UPDATE 1 since the new code should behave the same as dm-core-0.9.11 does if the association to be saved does not accept_nested_attributes

UPDATE 1

In order to get update working for belongs_to, has(1) and has(n), I commented out a line of dm-core-0.9.11 code which doesn’t seem to have too many effects. I ran the specs for 0.9.11 with this change incorporated, and mostly mocks(!) that expect the method call that i commented, fail. However, there is also one stack level too deep error Now, I don’t really know if this means that something else is broken really badly now, or if this can be worked around. The new failing dm-core specs after this commit are:

1)
SystemStackError in ‘DataMapper::Associations one to many associations #<< should add the correct number of elements if they are created’
stack level too deep
/Users/snusnu/projects/github/dm-core/lib/dm-core/repository.rb:41:in `scope’
/Users/snusnu/projects/github/dm-core/lib/dm-core.rb:181:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/support/kernel.rb:6:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/relationship.rb:172:in `with_repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/one_to_many.rb:298:in `save_resource’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/one_to_many.rb:210:in `save’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/one_to_many.rb:309:in `send’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/one_to_many.rb:309:in `method_missing’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/one_to_many.rb:210:in `save’
/Users/snusnu/projects/github/dm-core/lib/dm-core/resource.rb:301:in `hookable__save_nan_before_advised’
/Users/snusnu/projects/github/dm-core/lib/dm-core/resource.rb:301:in `each’
/Users/snusnu/projects/github/dm-core/lib/dm-core/resource.rb:301:in `hookable__save_nan_before_advised’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/many_to_one.rb:64:in `save’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/relationship.rb:172:in `with_repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/support/kernel.rb:6:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core.rb:181:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/repository.rb:44:in `scope’

……………………

2)
Spec::Mocks::MockExpectationError in ‘DataMapper::Associations::ManyToOne::Proxy#save when the parent is not a new record should not save the parent’
#<ManyToOneSpec::Parent:0×1159388> expected :save with (no args) 0 times, but received it once
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/many_to_one.rb:64:in `save’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/relationship.rb:172:in `with_repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/support/kernel.rb:6:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core.rb:181:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/repository.rb:44:in `scope’
/Users/snusnu/projects/github/dm-core/lib/dm-core.rb:181:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/support/kernel.rb:6:in `repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/relationship.rb:172:in `with_repository’
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/many_to_one.rb:63:in `save’
/Users/snusnu/projects/github/dm-core/spec/integration/associations/many_to_one_spec.rb:116:

11)
Spec::Mocks::MockExpectationError in ‘DataMapper::Associations::ManyToOne::Proxy#save when the parent is not a new record should not save the parent’
Mock ‘relationship’ received unexpected message :with_repository with (#<Spec::Mocks::Mock:0×217d426 @name=”parent”>)
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/many_to_one.rb:63:in `save’
/Users/snusnu/projects/github/dm-core/spec/unit/associations/many_to_one_spec.rb:91:

12)
Spec::Mocks::MockExpectationError in ‘DataMapper::Associations::ManyToOne::Proxy#save when the parent is not a new record should return true’
Mock ‘relationship’ received unexpected message :with_repository with (#<Spec::Mocks::Mock:0×216d9cc @name=”parent”>)
/Users/snusnu/projects/github/dm-core/lib/dm-core/associations/many_to_one.rb:63:in `save’
/Users/snusnu/projects/github/dm-core/spec/unit/associations/many_to_one_spec.rb:95:

13)
Spec::Mocks::MockExpectationError in ‘DataMapper::Associations::ManyToOne::Proxy#save when the parent is a new record should save the parent’
Mock ‘parent’ expected :new_record? with (no args) once, but received it 0 times
/Users/snusnu/projects/github/dm-core/spec/unit/associations/many_to_one_spec.rb:101:

14)
Spec::Mocks::MockExpectationError in ‘DataMapper::Associations::ManyToOne::Proxy#save when the parent is a new record should return the result of the save’
Mock ‘parent’ expected :new_record? with (no args) once, but received it 0 times
/Users/snusnu/projects/github/dm-core/spec/unit/associations/many_to_one_spec.rb:101:

datamapper debug output in Textmate RSpec Bundle

# stick this in spec_helper.rb
# this will append a <br /> to every logged message, which produces
# nicely formatted DataMapper debug outputs in Textmate's RSpec Bundle's output

module DataMapper
  class TextmateRspecLogger < Logger
    def prep_msg(message, level)
      "#{super}<br />"
    end
  end
end

# comment this for spec runs where you don't need to see logs
DataMapper::TextmateRspecLogger.new(STDOUT, :debug)

case foo.class might not work like you expect

irb(main):001:0> class Foo; end
=> nil
irb(main):002:0> class Bar; end
=> nil
irb(main):003:0> f = Foo.new
=> #
irb(main):004:0> case f.class
irb(main):005:1> when Foo then puts “f.class = Foo”
irb(main):006:1> when Bar then puts “f.class = Bar”
irb(main):007:1> else
irb(main):008:1*   puts “no match found”
irb(main):009:1> end
no match found
=> nil
irb(main):010:0> case f.class.name
irb(main):011:1> when “Foo” then puts “f.class.name = ‘Foo’”
irb(main):012:1> when “Bar” then puts “f.class.name = ‘Bar’”
irb(main):013:1> else
irb(main):014:1*   puts “no match found”
irb(main):015:1> end
f.class.name = ‘Foo’
=> nil

dm-is-cloneable

I just pushed dm-is-cloneable.

A DataMapper plugin that adds the ability to clone any model. As this alone wouldn’t really be a big deal, it is also possible to specifiy a 1:n relation with so called clone_specs. In these, the attributes_to_clone are persisted, thus leaving clients with the choice of selecting predefined sets of attributes to clone. Currently, associated objects don’t get cloned, although this might change if I need this in the future.

Client code looks something like this:


require 'rubygems'

gem 'dm-core',         '=0.9.11'
gem 'dm-validations',  '=0.9.11'
gem 'dm-is-remixable', '=0.9.11'
gem 'dm-is-cloneable', '=0.0.1'

require 'dm-core'
require 'dm-validations'
require 'dm-is-remixable'
require 'dm-is-cloneable'

DataMapper::Logger.new(STDOUT, :debug)

DataMapper.setup(:default, 'sqlite3:memory:')

class Item

  include DataMapper::Resource

  property :id,             Serial
  property :master_item_id, Integer # add this property if backlinks are desired

  property :name,           String
  property :description,    String

  is :cloneable

  # adds the following api to this class
  #
  # # attributes_to_clone can be one of
  # # <:all|String|Array[String|Symbol]|ItemCloneSpec>
  # Item#clone_resource(nr_of_clones, attributes_to_clone = :all)
  # Item#master_resource
  # Item#cloned_resources
  # Item#has_backlinks_to_master?
  # Item#has_clone_specs?

end

dm-is-remixable and overwritten property accessors

I just stumbled across some weirdness in dm-is-remixable. Seems like overwritten property writers work, whereas overwritten property readers don’t … hmm …

merb-helpers should produce valid html id attributes

A Ticket for this issue can be found here

# stick this in init.rb's before_app_loads block
# until this issue maybe gets resolved in merb-helpers
module Merb::Helpers::Form::Builder
  class Form < Base
    def update_unbound_controls(attrs, type)
      if attrs[:name] && !attrs[:id]
        # this makes sure that the '[]' characters which are not valid
        # in html ids, don't get copied over. furthermore, the trailing '_'
        # character makes sure that this id stays unique (particularly
        # necessary for additionally generated hidden inputs as they
        # are generated for checkboxes for example). the 'to_s' call
        # is necessary in order for the merb testsuite to run. obviously
        # there are some tests that use the Symbol :truez as a name
        # attribute, and since Symbol#gsub is private, tests fail if the
        # name attr isn't always converted to a String before use.
        attrs.merge!(:id => attrs[:name].to_s.gsub(/(\[|\])/, '_'))
      end
      case type
      when "text", "radio", "password", "hidden", "checkbox", "file"
        add_css_class(attrs, type)
      end
      super
    end
  end
end

working with UTC and datamapper


  # stick this in the before_app_loads block in init.rb
  # if this maybe finds its way into dm-more, this wouldn't be necessary anymore

  module DataMapper
    module Types

      class UtcDateTime < DataMapper::Type

        primitive DateTime

        def self.load(v, property)
          if v.nil?
            nil
          elsif v.is_a?(DateTime)
            ::DateTime.new(v.year, v.month, v.day, v.hour, v.min, v.sec, 0)
          else
            raise ArgumentError.new("+value+ must be nil or a DateTime")
          end
        end

        def self.dump(v, property)
          if v.nil?
            nil
          elsif v.is_a?(String)
            Time.parse(v).utc.to_datetime
          elsif v.is_a?(DateTime)
            Time.parse(v.to_s).utc.to_datetime
          else
            raise ArgumentError.new("+value+ must be nil or a String or a DateTime")
          end
        end

      end # class UtcDateTime

      UTCDateTime = UtcDateTime

    end # module Types
  end # module DataMapper

  module DataMapper
    module Timestamp

      def set_timestamps
        return unless dirty? || new_record?
        TIMESTAMP_PROPERTIES.each do |name,(_type,proc)|
          if model.properties.has_property?(name)
            self.send("#{name}=", proc.call(self, model.properties[name]))
          end
        end
      end

      def utc_timestamped?
        self.class.utc_timestamped?
      end

      module ClassMethods

        def timestamps(*names)
          raise ArgumentError, 'You need to pass at least one argument' if names.empty?

          # if the last element in names is a Hash:
          # extract this hash and look for a :utc key
          opts = names.last.is_a?(Hash) ? names.pop : nil
          utc_possible = names.include?(:created_at) || names.include?(:updated_at)
          @utc = opts && opts[:utc] && utc_possible

          names.each do |name|
            case name
              when *TIMESTAMP_PROPERTIES.keys
                type = TIMESTAMP_PROPERTIES[name].first
                property name, type, :nullable => false, :auto_validation => false

                if type == DateTime && @utc # UTC makes no sense for Date
                  define_method "#{name}=", UTC::PROPERTY_WRITER.call(name, type)
                  define_method "#{name}",  UTC::PROPERTY_READER.call(name, type)
                end

              when :at
                timestamps(:created_at, :updated_at, :utc => @utc)
              when :on
                timestamps(:created_on, :updated_on) # UTC makes no sense for Date
              else
                raise InvalidTimestampName, "Invalid timestamp property name '#{name}'"
            end
          end
        end

        def utc_timestamped?
          !!@utc
        end

      end

      module UTC
        PROPERTY_WRITER = lambda { |name, type|
          typecast = "to_#{type.name.downcase}"
          lambda { |dt| attribute_set(name, Time.parse(dt.to_s).utc.send(typecast)) }
        }
        PROPERTY_READER = lambda { |name, type|
          lambda {
            return nil unless dt = attribute_get(name)
            if type == Date
              Date.new(dt.year, dt.month, dt.day, 0)
            elsif type == DateTime
              DateTime.new(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec, 0)
            else
              nil
            end
          }
        }
      end

    end
  end

I HATE WORKING WITH DATE AND DATETIME AND TIME

seriously, i do ….

ruby method calls with heredoc parameter and block

This is taken from here and serves mainly as a reminder for myself.

  dbh.execute(<<-STMT) { |sth|
      select distinct customer, business_unit_id, business_unit_key_name
       from problem_ticket_lz
       order by customer
    STMT
    sth.fetch { |row|
      print "#{row[1]}\t#{row[0]}\t#{row[2]}\n"
    }
  }

« older

newer »