Module: Copyable::CopyableExtension::ClassMethods

Defined in:
lib/copyable/copyable_extension.rb

Instance Method Summary collapse

Instance Method Details

#copyable(&block) ⇒ Object

Use this copyable declaration in an ActiveRecord model to instruct the model how to copy itself. This declaration will create a create_copy! method that follows the instructions in the copyable declaration.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/copyable/copyable_extension.rb', line 14

def copyable(&block)

  begin
    model_class = self
    # raise an error if the copyable declaration is stated incorrectly
    SyntaxChecker.check!(model_class, block)
    # "execute" the copyable declaration, which basically saves the
    # information listed in the declaration for later use by the
    # create_copy! method
    main = Declarations::Main.new
    main.execute(block)
  rescue => e
    # if suppressing schema errors, don't raise an error, but also don't define a create_copy! method since it would have broken behavior
    return if (e.is_a?(Copyable::ColumnError) || e.is_a?(Copyable::AssociationError) || e.is_a?(ActiveRecord::StatementInvalid)) && Copyable.config.suppress_schema_errors == true
    raise
  end

  # define a create_copy! method for use on model objects
  define_method(:create_copy!) do |options={}|

    # raise an error if passed invalid options
    OptionChecker.check!(options)

    # we basically wrap the method in a lambda to help us manage
    # running it in a transaction
    do_the_copy = lambda do |options|
      new_model = nil
      begin
        # start by disabling all callbacks and observers (except for
        # validation callbacks and observers)
        ModelHooks.disable!(model_class)
        # rename self for clarity
        original_model = self
        # create a brand new, empty model
        new_model = model_class.new
        # fill in each column of this brand new model according to the
        # instructions given in the copyable declaration
        column_overrides = options[:override] || {}
        # merge with global override hash if exists
        column_overrides = column_overrides.merge(options[:global_override]) if options[:global_override]
        Declarations::Columns.execute(main.column_list, original_model, new_model, column_overrides)
        # save that sucker!
        Copyable::Saver.save!(new_model, options[:skip_validations])
        # tell the registry that we've created a new model (the registry
        # helps keep us from creating another copy of a model we've
        # already copied)
        CopyRegistry.register(original_model, new_model)
        # for this brand new model, visit all of the associated models,
        # making new copies according to the instructions in the copyable
        # declaration

        skip_associations = options[:skip_associations] || []
        Declarations::Associations.execute(main.association_list, original_model, new_model, options[:global_override], options[:skip_validations], skip_associations)
        # run the after_copy block if it exists
        Declarations::AfterCopy.execute(main.after_copy_block, original_model, new_model)
      ensure
        # it's critically important to re-enable the callbacks and
        # observers or they will stay disabled for future web
        # requests
        ModelHooks.reenable!(model_class)
      end
      # it's polite to return the newly created model
      new_model
    end


    # create_copy! can end up calling itself (by copying associations).
    # There is some behavior that we want to be slightly different if
    # create_copy! is called from within another create_copy! call.
    # (This means that any create_copy! call in copyable's internal
    # code should pass { __called_recursively: true } to create_copy!.
    if options[:__called_recursively]
      do_the_copy.call(options)
    else
      # Imagine the case where you have a model hierarchy such as
      # a Book that has many Sections that has many Pages.
      #
      # When @book.create_copy! is called, the CopyRegistry will keep
      # track of all of the copied models, making sure no model is
      # re-duplicated (such as in an unusual case where book sections
      # actually overlapped, and therefore two different sections
      # contained some of the same pages--you wouldn't want to re-copy
      # the pages).
      #
      # If we don't clear the registry before we start @book.create_copy!,
      # then we can't do this:
      #
      #    copy1 = @book.create_copy!
      #    copy2 = @book.create_copy!
      #
      # since when copying copy2, the CopyRegistry will remember the
      # Sections and Pages that it copied for copy1 and therefore
      # they won't get recopied.  So we have to clear the CopyRegistry's
      # memory each time before create_copy! is called.
      CopyRegistry.clear
      # Nested transactions can end up swallowing ActiveRecord::Rollback
      # errors in surprising ways.  create_copy! can eventually call
      # create_copy! when copying associated objects, which can result
      # in nested transactions.  We use this option to avoid the nesting.
      ActiveRecord::Base.transaction do
        do_the_copy.call(options)
      end
    end
  end
end