ruote-extras 0.9.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/openwfe/extras/engine/db_persisted_engine.rb +100 -0
 - data/lib/openwfe/extras/expool/dberrorjournal.rb +189 -0
 - data/lib/openwfe/extras/expool/dbexpstorage.rb +353 -0
 - data/lib/openwfe/extras/listeners/sqslisteners.rb +146 -0
 - data/lib/openwfe/extras/misc/activityfeed.rb +264 -0
 - data/lib/openwfe/extras/misc/basecamp.rb +485 -0
 - data/lib/openwfe/extras/participants/activeparticipants.rb +739 -0
 - data/lib/openwfe/extras/participants/atomfeed_participants.rb +174 -0
 - data/lib/openwfe/extras/participants/atompub_participants.rb +268 -0
 - data/lib/openwfe/extras/participants/basecamp_participants.rb +87 -0
 - data/lib/openwfe/extras/participants/csvparticipants.rb +127 -0
 - data/lib/openwfe/extras/participants/sqsparticipants.rb +125 -0
 - data/lib/openwfe/extras/participants/twitterparticipants.rb +176 -0
 - metadata +70 -0
 
| 
         @@ -0,0 +1,146 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #
         
     | 
| 
      
 2 
     | 
    
         
            +
            #--
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Copyright (c) 2007-2008, John Mettraux, OpenWFE.org
         
     | 
| 
      
 4 
     | 
    
         
            +
            # All rights reserved.
         
     | 
| 
      
 5 
     | 
    
         
            +
            # 
         
     | 
| 
      
 6 
     | 
    
         
            +
            # Redistribution and use in source and binary forms, with or without 
         
     | 
| 
      
 7 
     | 
    
         
            +
            # modification, are permitted provided that the following conditions are met:
         
     | 
| 
      
 8 
     | 
    
         
            +
            # 
         
     | 
| 
      
 9 
     | 
    
         
            +
            # . Redistributions of source code must retain the above copyright notice, this
         
     | 
| 
      
 10 
     | 
    
         
            +
            #   list of conditions and the following disclaimer.  
         
     | 
| 
      
 11 
     | 
    
         
            +
            # 
         
     | 
| 
      
 12 
     | 
    
         
            +
            # . Redistributions in binary form must reproduce the above copyright notice, 
         
     | 
| 
      
 13 
     | 
    
         
            +
            #   this list of conditions and the following disclaimer in the documentation 
         
     | 
| 
      
 14 
     | 
    
         
            +
            #   and/or other materials provided with the distribution.
         
     | 
| 
      
 15 
     | 
    
         
            +
            # 
         
     | 
| 
      
 16 
     | 
    
         
            +
            # . Neither the name of the "OpenWFE" nor the names of its contributors may be
         
     | 
| 
      
 17 
     | 
    
         
            +
            #   used to endorse or promote products derived from this software without
         
     | 
| 
      
 18 
     | 
    
         
            +
            #   specific prior written permission.
         
     | 
| 
      
 19 
     | 
    
         
            +
            # 
         
     | 
| 
      
 20 
     | 
    
         
            +
            # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
         
     | 
| 
      
 21 
     | 
    
         
            +
            # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
         
     | 
| 
      
 22 
     | 
    
         
            +
            # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
         
     | 
| 
      
 23 
     | 
    
         
            +
            # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
         
     | 
| 
      
 24 
     | 
    
         
            +
            # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
         
     | 
| 
      
 25 
     | 
    
         
            +
            # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
         
     | 
| 
      
 26 
     | 
    
         
            +
            # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
         
     | 
| 
      
 27 
     | 
    
         
            +
            # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
         
     | 
| 
      
 28 
     | 
    
         
            +
            # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
         
     | 
| 
      
 29 
     | 
    
         
            +
            # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
         
     | 
| 
      
 30 
     | 
    
         
            +
            # POSSIBILITY OF SUCH DAMAGE.
         
     | 
| 
      
 31 
     | 
    
         
            +
            #++
         
     | 
| 
      
 32 
     | 
    
         
            +
            #
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
            #
         
     | 
| 
      
 35 
     | 
    
         
            +
            # "made in Japan"
         
     | 
| 
      
 36 
     | 
    
         
            +
            #
         
     | 
| 
      
 37 
     | 
    
         
            +
            # John Mettraux at openwfe.org
         
     | 
| 
      
 38 
     | 
    
         
            +
            #
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            require 'yaml'
         
     | 
| 
      
 41 
     | 
    
         
            +
            require 'base64'
         
     | 
| 
      
 42 
     | 
    
         
            +
            require 'monitor'
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
            require 'openwfe/service'
         
     | 
| 
      
 45 
     | 
    
         
            +
            require 'openwfe/listeners/listener'
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
            #require 'rubygems'
         
     | 
| 
      
 48 
     | 
    
         
            +
            require 'rufus/sqs' # gem 'rufus-sqs'
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            module OpenWFE
         
     | 
| 
      
 52 
     | 
    
         
            +
            module Extras
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                #
         
     | 
| 
      
 55 
     | 
    
         
            +
                # Polls an Amazon SQS queue for workitems
         
     | 
| 
      
 56 
     | 
    
         
            +
                #
         
     | 
| 
      
 57 
     | 
    
         
            +
                # Workitems can be instances of InFlowWorkItem or LaunchItem.
         
     | 
| 
      
 58 
     | 
    
         
            +
                #
         
     | 
| 
      
 59 
     | 
    
         
            +
                #     require 'openwfe/extras/listeners/sqslisteners'
         
     | 
| 
      
 60 
     | 
    
         
            +
                #
         
     | 
| 
      
 61 
     | 
    
         
            +
                #     ql = OpenWFE::SqsListener("workqueue1", engine.application_context)
         
     | 
| 
      
 62 
     | 
    
         
            +
                #
         
     | 
| 
      
 63 
     | 
    
         
            +
                #     engine.add_workitem_listener(ql, "2m30s")
         
     | 
| 
      
 64 
     | 
    
         
            +
                #         #
         
     | 
| 
      
 65 
     | 
    
         
            +
                #         # thus, the engine will poll our "workqueue1" SQS queue
         
     | 
| 
      
 66 
     | 
    
         
            +
                #         # every 2 minutes and 30 seconds
         
     | 
| 
      
 67 
     | 
    
         
            +
                #
         
     | 
| 
      
 68 
     | 
    
         
            +
                class SqsListener < Service
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                    include MonitorMixin
         
     | 
| 
      
 71 
     | 
    
         
            +
                    include WorkItemListener
         
     | 
| 
      
 72 
     | 
    
         
            +
                    include Rufus::Schedulable
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    #
         
     | 
| 
      
 75 
     | 
    
         
            +
                    # The name of the Amazon SQS whom this listener cares for
         
     | 
| 
      
 76 
     | 
    
         
            +
                    #
         
     | 
| 
      
 77 
     | 
    
         
            +
                    attr_reader :queue_name
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                    def initialize (queue_name, application_context)
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                        @queue_name = queue_name.to_s
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                        service_name = "#{self.class}::#{@queue_name}"
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                        super service_name, application_context
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                        linfo { "new() queue is '#{@queue_name}'" }
         
     | 
| 
      
 88 
     | 
    
         
            +
                    end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                    #
         
     | 
| 
      
 91 
     | 
    
         
            +
                    # polls the SQS for incoming messages
         
     | 
| 
      
 92 
     | 
    
         
            +
                    #
         
     | 
| 
      
 93 
     | 
    
         
            +
                    def trigger (params)
         
     | 
| 
      
 94 
     | 
    
         
            +
                        synchronize do
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                            ldebug { "trigger()" }
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                            qs = Rufus::SQS::QueueService.new
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                            qs.create_queue @queue_name
         
     | 
| 
      
 101 
     | 
    
         
            +
                                # just to be sure it is there
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                            while true
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                                l = qs.get_messages(
         
     | 
| 
      
 106 
     | 
    
         
            +
                                    @queue_name, :timeout => 0, :count => 255)
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                                break if l.length < 1
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                                l.each do |msg|
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                                    o = decode_object msg
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                                    handle_item o
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                                    msg.delete
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                                    ldebug do 
         
     | 
| 
      
 119 
     | 
    
         
            +
                                        "trigger() " +
         
     | 
| 
      
 120 
     | 
    
         
            +
                                        "handled successfully msg #{msg.message_id}" 
         
     | 
| 
      
 121 
     | 
    
         
            +
                                    end
         
     | 
| 
      
 122 
     | 
    
         
            +
                                end
         
     | 
| 
      
 123 
     | 
    
         
            +
                            end
         
     | 
| 
      
 124 
     | 
    
         
            +
                        end
         
     | 
| 
      
 125 
     | 
    
         
            +
                    end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                    #
         
     | 
| 
      
 128 
     | 
    
         
            +
                    # Extracts a workitem from the message's body.
         
     | 
| 
      
 129 
     | 
    
         
            +
                    #
         
     | 
| 
      
 130 
     | 
    
         
            +
                    # By default, this listeners assumes the workitem is stored in
         
     | 
| 
      
 131 
     | 
    
         
            +
                    # its "hash form" (not directly as a Ruby InFlowWorkItem instance).
         
     | 
| 
      
 132 
     | 
    
         
            +
                    #
         
     | 
| 
      
 133 
     | 
    
         
            +
                    # LaunchItem instances (as hash as well) are also accepted.
         
     | 
| 
      
 134 
     | 
    
         
            +
                    #
         
     | 
| 
      
 135 
     | 
    
         
            +
                    def decode_object (message)
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                        o = Base64.decode64 message.message_body
         
     | 
| 
      
 138 
     | 
    
         
            +
                        o = YAML.load o
         
     | 
| 
      
 139 
     | 
    
         
            +
                        o = OpenWFE::workitem_from_h o
         
     | 
| 
      
 140 
     | 
    
         
            +
                        o
         
     | 
| 
      
 141 
     | 
    
         
            +
                    end
         
     | 
| 
      
 142 
     | 
    
         
            +
                end
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
            end
         
     | 
| 
      
 145 
     | 
    
         
            +
            end
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
         @@ -0,0 +1,264 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #
         
     | 
| 
      
 2 
     | 
    
         
            +
            #--
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Copyright (c) 2007-2008, John Mettraux, OpenWFE.org
         
     | 
| 
      
 4 
     | 
    
         
            +
            # All rights reserved.
         
     | 
| 
      
 5 
     | 
    
         
            +
            # 
         
     | 
| 
      
 6 
     | 
    
         
            +
            # Redistribution and use in source and binary forms, with or without 
         
     | 
| 
      
 7 
     | 
    
         
            +
            # modification, are permitted provided that the following conditions are met:
         
     | 
| 
      
 8 
     | 
    
         
            +
            # 
         
     | 
| 
      
 9 
     | 
    
         
            +
            # . Redistributions of source code must retain the above copyright notice, this
         
     | 
| 
      
 10 
     | 
    
         
            +
            #   list of conditions and the following disclaimer.  
         
     | 
| 
      
 11 
     | 
    
         
            +
            # 
         
     | 
| 
      
 12 
     | 
    
         
            +
            # . Redistributions in binary form must reproduce the above copyright notice, 
         
     | 
| 
      
 13 
     | 
    
         
            +
            #   this list of conditions and the following disclaimer in the documentation 
         
     | 
| 
      
 14 
     | 
    
         
            +
            #   and/or other materials provided with the distribution.
         
     | 
| 
      
 15 
     | 
    
         
            +
            # 
         
     | 
| 
      
 16 
     | 
    
         
            +
            # . Neither the name of the "OpenWFE" nor the names of its contributors may be
         
     | 
| 
      
 17 
     | 
    
         
            +
            #   used to endorse or promote products derived from this software without
         
     | 
| 
      
 18 
     | 
    
         
            +
            #   specific prior written permission.
         
     | 
| 
      
 19 
     | 
    
         
            +
            # 
         
     | 
| 
      
 20 
     | 
    
         
            +
            # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
         
     | 
| 
      
 21 
     | 
    
         
            +
            # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
         
     | 
| 
      
 22 
     | 
    
         
            +
            # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
         
     | 
| 
      
 23 
     | 
    
         
            +
            # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
         
     | 
| 
      
 24 
     | 
    
         
            +
            # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
         
     | 
| 
      
 25 
     | 
    
         
            +
            # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
         
     | 
| 
      
 26 
     | 
    
         
            +
            # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
         
     | 
| 
      
 27 
     | 
    
         
            +
            # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
         
     | 
| 
      
 28 
     | 
    
         
            +
            # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
         
     | 
| 
      
 29 
     | 
    
         
            +
            # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
         
     | 
| 
      
 30 
     | 
    
         
            +
            # POSSIBILITY OF SUCH DAMAGE.
         
     | 
| 
      
 31 
     | 
    
         
            +
            #++
         
     | 
| 
      
 32 
     | 
    
         
            +
            #
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
            #
         
     | 
| 
      
 35 
     | 
    
         
            +
            # "made in Japan"
         
     | 
| 
      
 36 
     | 
    
         
            +
            #
         
     | 
| 
      
 37 
     | 
    
         
            +
            # John Mettraux at openwfe.org
         
     | 
| 
      
 38 
     | 
    
         
            +
            #
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            #
         
     | 
| 
      
 41 
     | 
    
         
            +
            # this participant requires atom-tools from
         
     | 
| 
      
 42 
     | 
    
         
            +
            #
         
     | 
| 
      
 43 
     | 
    
         
            +
            # http://code.necronomicorp.com/trac/atom-tools 
         
     | 
| 
      
 44 
     | 
    
         
            +
            #
         
     | 
| 
      
 45 
     | 
    
         
            +
            # atom-tools' license is X11/MIT
         
     | 
| 
      
 46 
     | 
    
         
            +
            #
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            #require 'monitor'
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            #require 'rubygems'
         
     | 
| 
      
 51 
     | 
    
         
            +
            require 'atom/collection' # gem 'atoom-tools'
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            require 'openwfe/service'
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            module OpenWFE::Extras
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                #
         
     | 
| 
      
 60 
     | 
    
         
            +
                # A workitem event as kept
         
     | 
| 
      
 61 
     | 
    
         
            +
                #
         
     | 
| 
      
 62 
     | 
    
         
            +
                class Entry
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    attr_accessor :id
         
     | 
| 
      
 65 
     | 
    
         
            +
                    attr_accessor :updated
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                    attr_accessor :participant_name
         
     | 
| 
      
 68 
     | 
    
         
            +
                    attr_accessor :upon
         
     | 
| 
      
 69 
     | 
    
         
            +
                    attr_accessor :workitem
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    def initialize
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                        @update = Time.now
         
     | 
| 
      
 74 
     | 
    
         
            +
                    end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                    def as_atom_entry
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                        e = Atom::Entry.new
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                        e.id = @id
         
     | 
| 
      
 81 
     | 
    
         
            +
                        e.updated = @updated
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                        e
         
     | 
| 
      
 84 
     | 
    
         
            +
                    end
         
     | 
| 
      
 85 
     | 
    
         
            +
                end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                #
         
     | 
| 
      
 88 
     | 
    
         
            +
                # This feed registers as an observer to the ParticipantMap.
         
     | 
| 
      
 89 
     | 
    
         
            +
                # Each time a workitem is delivered to a participant or comes back from/for
         
     | 
| 
      
 90 
     | 
    
         
            +
                # it, an AtomEntry is generated.
         
     | 
| 
      
 91 
     | 
    
         
            +
                # The get_feed() method produces an atom feed of participant activity.
         
     | 
| 
      
 92 
     | 
    
         
            +
                #
         
     | 
| 
      
 93 
     | 
    
         
            +
                class ActivityFeedService
         
     | 
| 
      
 94 
     | 
    
         
            +
                    #include MonitorMixin
         
     | 
| 
      
 95 
     | 
    
         
            +
                    include OpenWFE::ServiceMixin
         
     | 
| 
      
 96 
     | 
    
         
            +
                    include OpenWFE::OwfeServiceLocator 
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                    attr_accessor :max_item_count
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    #
         
     | 
| 
      
 103 
     | 
    
         
            +
                    # This service is generally tied to an engine by doing :
         
     | 
| 
      
 104 
     | 
    
         
            +
                    #
         
     | 
| 
      
 105 
     | 
    
         
            +
                    #     engine.init_service 'activityFeed', ActivityFeedService
         
     | 
| 
      
 106 
     | 
    
         
            +
                    #
         
     | 
| 
      
 107 
     | 
    
         
            +
                    # The init_service() will take care of calling the constructor
         
     | 
| 
      
 108 
     | 
    
         
            +
                    # implemented here.
         
     | 
| 
      
 109 
     | 
    
         
            +
                    #
         
     | 
| 
      
 110 
     | 
    
         
            +
                    def initialize (service_name, application_context)
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                        super()
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                        service_init service_name, application_context
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                        @entries = []
         
     | 
| 
      
 117 
     | 
    
         
            +
                        @max_item_count = 100
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                        get_participant_map.add_observer ".*", self
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                    #
         
     | 
| 
      
 123 
     | 
    
         
            +
                    # This is the method call by the expression pool each time a
         
     | 
| 
      
 124 
     | 
    
         
            +
                    # workitem reaches a participant.
         
     | 
| 
      
 125 
     | 
    
         
            +
                    #
         
     | 
| 
      
 126 
     | 
    
         
            +
                    def call (channel, *args)
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                        #ldebug "call() c '#{channel}' entries count : #{@entries.size}"
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                        e = Entry.new
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
                        e.participant_name = channel
         
     | 
| 
      
 133 
     | 
    
         
            +
                        e.upon = args[0]
         
     | 
| 
      
 134 
     | 
    
         
            +
                        e.workitem = args[1].dup
         
     | 
| 
      
 135 
     | 
    
         
            +
                        e.updated = Time.now
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                        e.id = \
         
     | 
| 
      
 138 
     | 
    
         
            +
                            "#{e.workitem.participant_name} - #{e.upon} " +
         
     | 
| 
      
 139 
     | 
    
         
            +
                            "#{e.workitem.fei.workflow_instance_id}--" +
         
     | 
| 
      
 140 
     | 
    
         
            +
                            "#{e.workitem.fei.expression_id}"
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                        @entries << e
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                        while @entries.length > @max_item_count
         
     | 
| 
      
 145 
     | 
    
         
            +
                            @entries.delete_at 0
         
     | 
| 
      
 146 
     | 
    
         
            +
                        end
         
     | 
| 
      
 147 
     | 
    
         
            +
                    end
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
                    #
         
     | 
| 
      
 150 
     | 
    
         
            +
                    # Returns an Atom feed of all the workitem activity for the 
         
     | 
| 
      
 151 
     | 
    
         
            +
                    # participants whose name matches the given regular expression.
         
     | 
| 
      
 152 
     | 
    
         
            +
                    #
         
     | 
| 
      
 153 
     | 
    
         
            +
                    # Options :
         
     | 
| 
      
 154 
     | 
    
         
            +
                    #
         
     | 
| 
      
 155 
     | 
    
         
            +
                    # [:upon]       can be set to either nil, :apply, :reply. :apply states
         
     | 
| 
      
 156 
     | 
    
         
            +
                    #               that the returned feed should only contain entries about
         
     | 
| 
      
 157 
     | 
    
         
            +
                    #               participant getting applied, :reply only about 
         
     | 
| 
      
 158 
     | 
    
         
            +
                    #               participant replying.
         
     | 
| 
      
 159 
     | 
    
         
            +
                    # [:feed_uri]   the URI for the feed. Defaults to 
         
     | 
| 
      
 160 
     | 
    
         
            +
                    #               'http://localhost/feed'
         
     | 
| 
      
 161 
     | 
    
         
            +
                    # [:feed_title] the title for the feed. Defaults to 
         
     | 
| 
      
 162 
     | 
    
         
            +
                    #               'OpenWFEru engine activity feed'
         
     | 
| 
      
 163 
     | 
    
         
            +
                    # [:format]     yaml|text|json
         
     | 
| 
      
 164 
     | 
    
         
            +
                    #
         
     | 
| 
      
 165 
     | 
    
         
            +
                    #
         
     | 
| 
      
 166 
     | 
    
         
            +
                    def get_feed (participant_regex, options={})
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
                        participant_regex = Regexp.compile(participant_regex) \
         
     | 
| 
      
 169 
     | 
    
         
            +
                            if participant_regex.is_a?(String)
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                        upon = options[:upon]
         
     | 
| 
      
 172 
     | 
    
         
            +
                        feed_uri = options[:feed_uri] || "http://localhost/feed"
         
     | 
| 
      
 173 
     | 
    
         
            +
                        title = options[:feed_title] || "OpenWFEru engine activity feed"
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
                        feed = Atom::Feed.new feed_uri
         
     | 
| 
      
 176 
     | 
    
         
            +
                        feed.title = title
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                        format = options[:format]
         
     | 
| 
      
 179 
     | 
    
         
            +
                        format = validate_format(format)
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                        @entries.each do |e|
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
                            next unless participant_regex.match(e.participant_name)
         
     | 
| 
      
 184 
     | 
    
         
            +
                            next if upon and upon != e.upon
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
                            feed.updated = e.updated \
         
     | 
| 
      
 187 
     | 
    
         
            +
                                if feed.updated == nil or e.updated > feed.updated
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
                            feed << as_atom_entry(format, e)
         
     | 
| 
      
 190 
     | 
    
         
            +
                        end
         
     | 
| 
      
 191 
     | 
    
         
            +
             
     | 
| 
      
 192 
     | 
    
         
            +
                        feed
         
     | 
| 
      
 193 
     | 
    
         
            +
                    end
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
                    protected
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
                        #
         
     | 
| 
      
 198 
     | 
    
         
            +
                        # Makes sure the required 'render' is valid. Returns it
         
     | 
| 
      
 199 
     | 
    
         
            +
                        # as a Symbol.
         
     | 
| 
      
 200 
     | 
    
         
            +
                        #
         
     | 
| 
      
 201 
     | 
    
         
            +
                        def validate_format (f)
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
                            f = "as_#{f}"
         
     | 
| 
      
 204 
     | 
    
         
            +
                            return :as_yaml unless methods.include?(f)
         
     | 
| 
      
 205 
     | 
    
         
            +
                            f.to_sym
         
     | 
| 
      
 206 
     | 
    
         
            +
                        end
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                        def as_atom_entry (render, entry)
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
                            send(
         
     | 
| 
      
 211 
     | 
    
         
            +
                                render, 
         
     | 
| 
      
 212 
     | 
    
         
            +
                                entry.as_atom_entry, 
         
     | 
| 
      
 213 
     | 
    
         
            +
                                entry.participant_name, 
         
     | 
| 
      
 214 
     | 
    
         
            +
                                entry.upon, 
         
     | 
| 
      
 215 
     | 
    
         
            +
                                entry.workitem)
         
     | 
| 
      
 216 
     | 
    
         
            +
                        end
         
     | 
| 
      
 217 
     | 
    
         
            +
             
     | 
| 
      
 218 
     | 
    
         
            +
                        #
         
     | 
| 
      
 219 
     | 
    
         
            +
                        # A basic rendition of a workitem as a YAML string
         
     | 
| 
      
 220 
     | 
    
         
            +
                        #
         
     | 
| 
      
 221 
     | 
    
         
            +
                        def as_yaml (atom_entry, participant_name, upon, workitem)
         
     | 
| 
      
 222 
     | 
    
         
            +
             
     | 
| 
      
 223 
     | 
    
         
            +
                            atom_entry.title = "#{participant_name} - #{upon}"
         
     | 
| 
      
 224 
     | 
    
         
            +
                            atom_entry.content = workitem.to_yaml
         
     | 
| 
      
 225 
     | 
    
         
            +
                            atom_entry.content['type'] = "text/plain"
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
      
 227 
     | 
    
         
            +
                            atom_entry
         
     | 
| 
      
 228 
     | 
    
         
            +
                        end
         
     | 
| 
      
 229 
     | 
    
         
            +
             
     | 
| 
      
 230 
     | 
    
         
            +
                        #class Atom::Content
         
     | 
| 
      
 231 
     | 
    
         
            +
                        #    def convert_contents e
         
     | 
| 
      
 232 
     | 
    
         
            +
                        #        REXML::CData.new(@content.to_s)
         
     | 
| 
      
 233 
     | 
    
         
            +
                        #    end
         
     | 
| 
      
 234 
     | 
    
         
            +
                        #end
         
     | 
| 
      
 235 
     | 
    
         
            +
             
     | 
| 
      
 236 
     | 
    
         
            +
                        #
         
     | 
| 
      
 237 
     | 
    
         
            +
                        # A basic rendition of a workitem as text.
         
     | 
| 
      
 238 
     | 
    
         
            +
                        #
         
     | 
| 
      
 239 
     | 
    
         
            +
                        def as_text (atom_entry, participant_name, upon, workitem)
         
     | 
| 
      
 240 
     | 
    
         
            +
             
     | 
| 
      
 241 
     | 
    
         
            +
                            atom_entry.title = "#{participant_name} - #{upon}"
         
     | 
| 
      
 242 
     | 
    
         
            +
             
     | 
| 
      
 243 
     | 
    
         
            +
                            atom_entry.content = workitem.to_s
         
     | 
| 
      
 244 
     | 
    
         
            +
                            atom_entry.content['type'] = "text/plain"
         
     | 
| 
      
 245 
     | 
    
         
            +
             
     | 
| 
      
 246 
     | 
    
         
            +
                            atom_entry
         
     | 
| 
      
 247 
     | 
    
         
            +
                        end
         
     | 
| 
      
 248 
     | 
    
         
            +
             
     | 
| 
      
 249 
     | 
    
         
            +
                        #
         
     | 
| 
      
 250 
     | 
    
         
            +
                        # Renders the workitem as a JSON string.
         
     | 
| 
      
 251 
     | 
    
         
            +
                        #
         
     | 
| 
      
 252 
     | 
    
         
            +
                        def as_json (atom_entry, participant_name, upon, workitem)
         
     | 
| 
      
 253 
     | 
    
         
            +
             
     | 
| 
      
 254 
     | 
    
         
            +
                            atom_entry.title = "#{participant_name} - #{upon}"
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
                            atom_entry.content = workitem.to_json
         
     | 
| 
      
 257 
     | 
    
         
            +
                            atom_entry.content['type'] = "text/plain"
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
                            atom_entry
         
     | 
| 
      
 260 
     | 
    
         
            +
                        end
         
     | 
| 
      
 261 
     | 
    
         
            +
                end
         
     | 
| 
      
 262 
     | 
    
         
            +
             
     | 
| 
      
 263 
     | 
    
         
            +
            end
         
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
         @@ -0,0 +1,485 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
             
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # This code was written by 37signals.com
         
     | 
| 
      
 4 
     | 
    
         
            +
            #
         
     | 
| 
      
 5 
     | 
    
         
            +
            # The original is at : 
         
     | 
| 
      
 6 
     | 
    
         
            +
            # http://developer.37signals.com/basecamp/basecamp.rb
         
     | 
| 
      
 7 
     | 
    
         
            +
            #
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            require 'net/https'
         
     | 
| 
      
 10 
     | 
    
         
            +
            require 'yaml'
         
     | 
| 
      
 11 
     | 
    
         
            +
            require 'date'
         
     | 
| 
      
 12 
     | 
    
         
            +
            require 'time'
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            #require 'rubygems'
         
     | 
| 
      
 15 
     | 
    
         
            +
            gem 'xml-simple'; require 'xmlsimple'
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            # An interface to the Basecamp web-services API. Usage is straightforward:
         
     | 
| 
      
 18 
     | 
    
         
            +
            #
         
     | 
| 
      
 19 
     | 
    
         
            +
            #   session = Basecamp.new('your.basecamp.com', 'username', 'password')
         
     | 
| 
      
 20 
     | 
    
         
            +
            #   puts "projects: #{session.projects.length}"
         
     | 
| 
      
 21 
     | 
    
         
            +
            class Basecamp #:nodoc:
         
     | 
| 
      
 22 
     | 
    
         
            +
              
         
     | 
| 
      
 23 
     | 
    
         
            +
              # A wrapper to encapsulate the data returned by Basecamp, for easier access.
         
     | 
| 
      
 24 
     | 
    
         
            +
              class Record #:nodoc:
         
     | 
| 
      
 25 
     | 
    
         
            +
                attr_reader :type
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def initialize(type, hash)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @type = type
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @hash = hash
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def [](name)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  name = dashify(name)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  case @hash[name]
         
     | 
| 
      
 35 
     | 
    
         
            +
                    when Hash then 
         
     | 
| 
      
 36 
     | 
    
         
            +
                      @hash[name] = (@hash[name].keys.length == 1 && Array === @hash[name].values.first) ?
         
     | 
| 
      
 37 
     | 
    
         
            +
                        @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) } :
         
     | 
| 
      
 38 
     | 
    
         
            +
                        Record.new(name, @hash[name])
         
     | 
| 
      
 39 
     | 
    
         
            +
                    else @hash[name]
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def id
         
     | 
| 
      
 44 
     | 
    
         
            +
                  @hash["id"]
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                def attributes
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @hash.keys
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                def respond_to?(sym)
         
     | 
| 
      
 52 
     | 
    
         
            +
                  super || @hash.has_key?(dashify(sym))
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                def method_missing(sym, *args)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  if args.empty? && !block_given? && respond_to?(sym)
         
     | 
| 
      
 57 
     | 
    
         
            +
                    self[sym]
         
     | 
| 
      
 58 
     | 
    
         
            +
                  else
         
     | 
| 
      
 59 
     | 
    
         
            +
                    super
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 64 
     | 
    
         
            +
                  "\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                def inspect
         
     | 
| 
      
 68 
     | 
    
         
            +
                  to_s
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                private
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                  def dashify(name)
         
     | 
| 
      
 74 
     | 
    
         
            +
                    name.to_s.tr("_", "-")
         
     | 
| 
      
 75 
     | 
    
         
            +
                  end
         
     | 
| 
      
 76 
     | 
    
         
            +
              end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
              # A wrapper to represent a file that should be uploaded. This is used so that
         
     | 
| 
      
 79 
     | 
    
         
            +
              # the form/multi-part encoder knows when to encode a field as a file, versus
         
     | 
| 
      
 80 
     | 
    
         
            +
              # when to encode it as a simple field.
         
     | 
| 
      
 81 
     | 
    
         
            +
              class FileUpload #:nodoc:
         
     | 
| 
      
 82 
     | 
    
         
            +
                attr_reader :filename, :content
         
     | 
| 
      
 83 
     | 
    
         
            +
                
         
     | 
| 
      
 84 
     | 
    
         
            +
                def initialize(filename, content)
         
     | 
| 
      
 85 
     | 
    
         
            +
                  @filename = filename
         
     | 
| 
      
 86 
     | 
    
         
            +
                  @content = content
         
     | 
| 
      
 87 
     | 
    
         
            +
                end
         
     | 
| 
      
 88 
     | 
    
         
            +
              end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
              attr_accessor :use_xml
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
              # Connects
         
     | 
| 
      
 93 
     | 
    
         
            +
              def initialize(url, user_name, password, use_ssl = false)
         
     | 
| 
      
 94 
     | 
    
         
            +
                @use_xml = false
         
     | 
| 
      
 95 
     | 
    
         
            +
                @user_name, @password = user_name, password
         
     | 
| 
      
 96 
     | 
    
         
            +
                connect!(url, use_ssl)
         
     | 
| 
      
 97 
     | 
    
         
            +
              end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
              # Return the list of all accessible projects.
         
     | 
| 
      
 100 
     | 
    
         
            +
              def projects
         
     | 
| 
      
 101 
     | 
    
         
            +
                records "project", "/project/list"
         
     | 
| 
      
 102 
     | 
    
         
            +
              end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
              # Returns the list of message categories for the given project
         
     | 
| 
      
 105 
     | 
    
         
            +
              def message_categories(project_id)
         
     | 
| 
      
 106 
     | 
    
         
            +
                records "post-category", "/projects/#{project_id}/post_categories"
         
     | 
| 
      
 107 
     | 
    
         
            +
              end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
              # Returns the list of file categories for the given project
         
     | 
| 
      
 110 
     | 
    
         
            +
              def file_categories(project_id)
         
     | 
| 
      
 111 
     | 
    
         
            +
                records "attachment-category", "/projects/#{project_id}/attachment_categories"
         
     | 
| 
      
 112 
     | 
    
         
            +
              end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
              # Return information for the company with the given id
         
     | 
| 
      
 115 
     | 
    
         
            +
              def company(id)
         
     | 
| 
      
 116 
     | 
    
         
            +
                record "/contacts/company/#{id}"
         
     | 
| 
      
 117 
     | 
    
         
            +
              end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
              # Return an array of the people in the given company. If the project-id is
         
     | 
| 
      
 120 
     | 
    
         
            +
              # given, only people who have access to the given project will be returned.
         
     | 
| 
      
 121 
     | 
    
         
            +
              def people(company_id, project_id=nil)
         
     | 
| 
      
 122 
     | 
    
         
            +
                url = project_id ? "/projects/#{project_id}" : ""
         
     | 
| 
      
 123 
     | 
    
         
            +
                url << "/contacts/people/#{company_id}"
         
     | 
| 
      
 124 
     | 
    
         
            +
                records "person", url
         
     | 
| 
      
 125 
     | 
    
         
            +
              end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
              # Return information about the person with the given id
         
     | 
| 
      
 128 
     | 
    
         
            +
              def person(id)
         
     | 
| 
      
 129 
     | 
    
         
            +
                record "/contacts/person/#{id}"
         
     | 
| 
      
 130 
     | 
    
         
            +
              end
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
              # Return information about the message(s) with the given id(s). The API
         
     | 
| 
      
 133 
     | 
    
         
            +
              # limits you to requesting 25 messages at a time, so if you need to get more
         
     | 
| 
      
 134 
     | 
    
         
            +
              # than that, you'll need to do it in multiple requests.
         
     | 
| 
      
 135 
     | 
    
         
            +
              def message(*ids)
         
     | 
| 
      
 136 
     | 
    
         
            +
                result = records("post", "/msg/get/#{ids.join(",")}")
         
     | 
| 
      
 137 
     | 
    
         
            +
                result.length == 1 ? result.first : result
         
     | 
| 
      
 138 
     | 
    
         
            +
              end
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
              # Returns a summary of all messages in the given project (and category, if
         
     | 
| 
      
 141 
     | 
    
         
            +
              # specified). The summary is simply the title and category of the message,
         
     | 
| 
      
 142 
     | 
    
         
            +
              # as well as the number of attachments (if any).
         
     | 
| 
      
 143 
     | 
    
         
            +
              def message_list(project_id, category_id=nil)
         
     | 
| 
      
 144 
     | 
    
         
            +
                url = "/projects/#{project_id}/msg"
         
     | 
| 
      
 145 
     | 
    
         
            +
                url << "/cat/#{category_id}" if category_id
         
     | 
| 
      
 146 
     | 
    
         
            +
                url << "/archive"
         
     | 
| 
      
 147 
     | 
    
         
            +
                
         
     | 
| 
      
 148 
     | 
    
         
            +
                records "post", url
         
     | 
| 
      
 149 
     | 
    
         
            +
              end
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
              # Create a new message in the given project. The +message+ parameter should
         
     | 
| 
      
 152 
     | 
    
         
            +
              # be a hash. The +email_to+ parameter must be an array of person-id's that
         
     | 
| 
      
 153 
     | 
    
         
            +
              # should be notified of the post.
         
     | 
| 
      
 154 
     | 
    
         
            +
              #
         
     | 
| 
      
 155 
     | 
    
         
            +
              # If you want to add attachments to the message, the +attachments+ parameter
         
     | 
| 
      
 156 
     | 
    
         
            +
              # should be an array of hashes, where each has has a :name key (optional),
         
     | 
| 
      
 157 
     | 
    
         
            +
              # and a :file key (required). The :file key must refer to a Basecamp::FileUpload
         
     | 
| 
      
 158 
     | 
    
         
            +
              # instance.
         
     | 
| 
      
 159 
     | 
    
         
            +
              #
         
     | 
| 
      
 160 
     | 
    
         
            +
              #   msg = session.post_message(158141,
         
     | 
| 
      
 161 
     | 
    
         
            +
              #      { :title => "Requirements",
         
     | 
| 
      
 162 
     | 
    
         
            +
              #        :body => "Here are the requirements documents you asked for.",
         
     | 
| 
      
 163 
     | 
    
         
            +
              #        :category_id => 2301121 },
         
     | 
| 
      
 164 
     | 
    
         
            +
              #      [john.id, martha.id],
         
     | 
| 
      
 165 
     | 
    
         
            +
              #      [ { :name => "Primary Requirements",
         
     | 
| 
      
 166 
     | 
    
         
            +
              #          :file => Basecamp::FileUpload.new('primary.doc", File.read('primary.doc')) },
         
     | 
| 
      
 167 
     | 
    
         
            +
              #        { :file => Basecamp::FileUpload.new('other.doc', File.read('other.doc')) } ])
         
     | 
| 
      
 168 
     | 
    
         
            +
              def post_message(project_id, message, notify=[], attachments=[])
         
     | 
| 
      
 169 
     | 
    
         
            +
                prepare_attachments(attachments)
         
     | 
| 
      
 170 
     | 
    
         
            +
                record "/projects/#{project_id}/msg/create",
         
     | 
| 
      
 171 
     | 
    
         
            +
                  :post => message,
         
     | 
| 
      
 172 
     | 
    
         
            +
                  :notify => notify,
         
     | 
| 
      
 173 
     | 
    
         
            +
                  :attachments => attachments
         
     | 
| 
      
 174 
     | 
    
         
            +
              end
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
              # Edit the message with the given id. The +message+ parameter should
         
     | 
| 
      
 177 
     | 
    
         
            +
              # be a hash. The +email_to+ parameter must be an array of person-id's that
         
     | 
| 
      
 178 
     | 
    
         
            +
              # should be notified of the post.
         
     | 
| 
      
 179 
     | 
    
         
            +
              #
         
     | 
| 
      
 180 
     | 
    
         
            +
              # The +attachments+ parameter, if used, should be the same as described for
         
     | 
| 
      
 181 
     | 
    
         
            +
              # #post_message.
         
     | 
| 
      
 182 
     | 
    
         
            +
              def update_message(id, message, notify=[], attachments=[])
         
     | 
| 
      
 183 
     | 
    
         
            +
                prepare_attachments(attachments)
         
     | 
| 
      
 184 
     | 
    
         
            +
                record "/msg/update/#{id}",
         
     | 
| 
      
 185 
     | 
    
         
            +
                  :post => message,
         
     | 
| 
      
 186 
     | 
    
         
            +
                  :notify => notify,
         
     | 
| 
      
 187 
     | 
    
         
            +
                  :attachments => attachments
         
     | 
| 
      
 188 
     | 
    
         
            +
              end
         
     | 
| 
      
 189 
     | 
    
         
            +
             
     | 
| 
      
 190 
     | 
    
         
            +
              # Deletes the message with the given id, and returns it.
         
     | 
| 
      
 191 
     | 
    
         
            +
              def delete_message(id)
         
     | 
| 
      
 192 
     | 
    
         
            +
                record "/msg/delete/#{id}"
         
     | 
| 
      
 193 
     | 
    
         
            +
              end
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
              # Return a list of the comments for the specified message.
         
     | 
| 
      
 196 
     | 
    
         
            +
              def comments(post_id)
         
     | 
| 
      
 197 
     | 
    
         
            +
                records "comment", "/msg/comments/#{post_id}"
         
     | 
| 
      
 198 
     | 
    
         
            +
              end
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
              # Retrieve a specific comment
         
     | 
| 
      
 201 
     | 
    
         
            +
              def comment(id)
         
     | 
| 
      
 202 
     | 
    
         
            +
                record "/msg/comment/#{id}"
         
     | 
| 
      
 203 
     | 
    
         
            +
              end
         
     | 
| 
      
 204 
     | 
    
         
            +
             
     | 
| 
      
 205 
     | 
    
         
            +
              # Add a new comment to a message. +comment+ must be a hash describing the
         
     | 
| 
      
 206 
     | 
    
         
            +
              # comment. You can add attachments to the comment, too, by giving them in
         
     | 
| 
      
 207 
     | 
    
         
            +
              # an array. See the #post_message method for a description of how to do that.
         
     | 
| 
      
 208 
     | 
    
         
            +
              def create_comment(post_id, comment, attachments=[])
         
     | 
| 
      
 209 
     | 
    
         
            +
                prepare_attachments(attachments)
         
     | 
| 
      
 210 
     | 
    
         
            +
                record "/msg/create_comment", :comment => comment.merge(:post_id => post_id),
         
     | 
| 
      
 211 
     | 
    
         
            +
                  :attachments => attachments
         
     | 
| 
      
 212 
     | 
    
         
            +
              end
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
              # Update the given comment. Attachments follow the same format as #post_message.
         
     | 
| 
      
 215 
     | 
    
         
            +
              def update_comment(id, comment, attachments=[])
         
     | 
| 
      
 216 
     | 
    
         
            +
                prepare_attachments(attachments)
         
     | 
| 
      
 217 
     | 
    
         
            +
                record "/msg/update_comment", :comment_id => id,
         
     | 
| 
      
 218 
     | 
    
         
            +
                  :comment => comment, :attachments => attachments
         
     | 
| 
      
 219 
     | 
    
         
            +
              end
         
     | 
| 
      
 220 
     | 
    
         
            +
             
     | 
| 
      
 221 
     | 
    
         
            +
              # Deletes (and returns) the given comment.
         
     | 
| 
      
 222 
     | 
    
         
            +
              def delete_comment(id)
         
     | 
| 
      
 223 
     | 
    
         
            +
                record "/msg/delete_comment/#{id}"
         
     | 
| 
      
 224 
     | 
    
         
            +
              end
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
              # =========================================================================
         
     | 
| 
      
 227 
     | 
    
         
            +
              # TODO LISTS AND ITEMS
         
     | 
| 
      
 228 
     | 
    
         
            +
              # =========================================================================
         
     | 
| 
      
 229 
     | 
    
         
            +
             
     | 
| 
      
 230 
     | 
    
         
            +
              # Marks the given item completed.
         
     | 
| 
      
 231 
     | 
    
         
            +
              def complete_item(id)
         
     | 
| 
      
 232 
     | 
    
         
            +
                record "/todos/complete_item/#{id}"
         
     | 
| 
      
 233 
     | 
    
         
            +
              end
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
              # Marks the given item uncompleted.
         
     | 
| 
      
 236 
     | 
    
         
            +
              def uncomplete_item(id)
         
     | 
| 
      
 237 
     | 
    
         
            +
                record "/todos/uncomplete_item/#{id}"
         
     | 
| 
      
 238 
     | 
    
         
            +
              end
         
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
              # Creates a new to-do item.
         
     | 
| 
      
 241 
     | 
    
         
            +
              def create_item(list_id, content, responsible_party=nil, notify=true)
         
     | 
| 
      
 242 
     | 
    
         
            +
                record "/todos/create_item/#{list_id}",
         
     | 
| 
      
 243 
     | 
    
         
            +
                  :content => content, :responsible_party => responsible_party,
         
     | 
| 
      
 244 
     | 
    
         
            +
                  :notify => notify
         
     | 
| 
      
 245 
     | 
    
         
            +
              end
         
     | 
| 
      
 246 
     | 
    
         
            +
             
     | 
| 
      
 247 
     | 
    
         
            +
              # Creates a new list using the given hash of list metadata.
         
     | 
| 
      
 248 
     | 
    
         
            +
              def create_list(project_id, list)
         
     | 
| 
      
 249 
     | 
    
         
            +
                record "/projects/#{project_id}/todos/create_list", list
         
     | 
| 
      
 250 
     | 
    
         
            +
              end
         
     | 
| 
      
 251 
     | 
    
         
            +
             
     | 
| 
      
 252 
     | 
    
         
            +
              # Deletes the given item from it's parent list.
         
     | 
| 
      
 253 
     | 
    
         
            +
              def delete_item(id)
         
     | 
| 
      
 254 
     | 
    
         
            +
                record "/todos/delete_item/#{id}"
         
     | 
| 
      
 255 
     | 
    
         
            +
              end
         
     | 
| 
      
 256 
     | 
    
         
            +
             
     | 
| 
      
 257 
     | 
    
         
            +
              # Deletes the given list and all of its items.
         
     | 
| 
      
 258 
     | 
    
         
            +
              def delete_list(id)
         
     | 
| 
      
 259 
     | 
    
         
            +
                record "/todos/delete_list/#{id}"
         
     | 
| 
      
 260 
     | 
    
         
            +
              end
         
     | 
| 
      
 261 
     | 
    
         
            +
             
     | 
| 
      
 262 
     | 
    
         
            +
              # Retrieves the specified list, and all of its items.
         
     | 
| 
      
 263 
     | 
    
         
            +
              def get_list(id)
         
     | 
| 
      
 264 
     | 
    
         
            +
                record "/todos/list/#{id}"
         
     | 
| 
      
 265 
     | 
    
         
            +
              end
         
     | 
| 
      
 266 
     | 
    
         
            +
             
     | 
| 
      
 267 
     | 
    
         
            +
              # Return all lists for a project. If complete is true, only completed lists
         
     | 
| 
      
 268 
     | 
    
         
            +
              # are returned. If complete is false, only uncompleted lists are returned.
         
     | 
| 
      
 269 
     | 
    
         
            +
              def lists(project_id, complete=nil)
         
     | 
| 
      
 270 
     | 
    
         
            +
                records "todo-list", "/projects/#{project_id}/todos/lists", :complete => complete
         
     | 
| 
      
 271 
     | 
    
         
            +
              end
         
     | 
| 
      
 272 
     | 
    
         
            +
             
     | 
| 
      
 273 
     | 
    
         
            +
              # Repositions an item to be at the given position in its list
         
     | 
| 
      
 274 
     | 
    
         
            +
              def move_item(id, to)
         
     | 
| 
      
 275 
     | 
    
         
            +
                record "/todos/move_item/#{id}", :to => to
         
     | 
| 
      
 276 
     | 
    
         
            +
              end
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
      
 278 
     | 
    
         
            +
              # Repositions a list to be at the given position in its project
         
     | 
| 
      
 279 
     | 
    
         
            +
              def move_list(id, to)
         
     | 
| 
      
 280 
     | 
    
         
            +
                record "/todos/move_list/#{id}", :to => to
         
     | 
| 
      
 281 
     | 
    
         
            +
              end
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
      
 283 
     | 
    
         
            +
              # Updates the given item
         
     | 
| 
      
 284 
     | 
    
         
            +
              def update_item(id, content, responsible_party=nil, notify=true)
         
     | 
| 
      
 285 
     | 
    
         
            +
                record "/todos/update_item/#{id}",
         
     | 
| 
      
 286 
     | 
    
         
            +
                  :item => { :content => content }, :responsible_party => responsible_party,
         
     | 
| 
      
 287 
     | 
    
         
            +
                  :notify => notify
         
     | 
| 
      
 288 
     | 
    
         
            +
              end
         
     | 
| 
      
 289 
     | 
    
         
            +
             
     | 
| 
      
 290 
     | 
    
         
            +
              # Updates the given list's metadata
         
     | 
| 
      
 291 
     | 
    
         
            +
              def update_list(id, list)
         
     | 
| 
      
 292 
     | 
    
         
            +
                record "/todos/update_list/#{id}", :list => list
         
     | 
| 
      
 293 
     | 
    
         
            +
              end
         
     | 
| 
      
 294 
     | 
    
         
            +
             
     | 
| 
      
 295 
     | 
    
         
            +
              # =========================================================================
         
     | 
| 
      
 296 
     | 
    
         
            +
              # MILESTONES
         
     | 
| 
      
 297 
     | 
    
         
            +
              # =========================================================================
         
     | 
| 
      
 298 
     | 
    
         
            +
             
     | 
| 
      
 299 
     | 
    
         
            +
              # Complete the milestone with the given id
         
     | 
| 
      
 300 
     | 
    
         
            +
              def complete_milestone(id)
         
     | 
| 
      
 301 
     | 
    
         
            +
                record "/milestones/complete/#{id}"
         
     | 
| 
      
 302 
     | 
    
         
            +
              end
         
     | 
| 
      
 303 
     | 
    
         
            +
             
     | 
| 
      
 304 
     | 
    
         
            +
              # Create a new milestone for the given project. +data+ must be hash of the
         
     | 
| 
      
 305 
     | 
    
         
            +
              # values to set, including +title+, +deadline+, +responsible_party+, and
         
     | 
| 
      
 306 
     | 
    
         
            +
              # +notify+.
         
     | 
| 
      
 307 
     | 
    
         
            +
              def create_milestone(project_id, data)
         
     | 
| 
      
 308 
     | 
    
         
            +
                create_milestones(project_id, [data]).first
         
     | 
| 
      
 309 
     | 
    
         
            +
              end
         
     | 
| 
      
 310 
     | 
    
         
            +
             
     | 
| 
      
 311 
     | 
    
         
            +
              # As #create_milestone, but can create multiple milestones in a single
         
     | 
| 
      
 312 
     | 
    
         
            +
              # request. The +milestones+ parameter must be an array of milestone values as
         
     | 
| 
      
 313 
     | 
    
         
            +
              # descrbed in #create_milestone.
         
     | 
| 
      
 314 
     | 
    
         
            +
              def create_milestones(project_id, milestones)
         
     | 
| 
      
 315 
     | 
    
         
            +
                records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
         
     | 
| 
      
 316 
     | 
    
         
            +
              end
         
     | 
| 
      
 317 
     | 
    
         
            +
             
     | 
| 
      
 318 
     | 
    
         
            +
              # Destroys the milestone with the given id.
         
     | 
| 
      
 319 
     | 
    
         
            +
              def delete_milestone(id)
         
     | 
| 
      
 320 
     | 
    
         
            +
                record "/milestones/delete/#{id}"
         
     | 
| 
      
 321 
     | 
    
         
            +
              end
         
     | 
| 
      
 322 
     | 
    
         
            +
             
     | 
| 
      
 323 
     | 
    
         
            +
              # Returns a list of all milestones for the given project, optionally filtered
         
     | 
| 
      
 324 
     | 
    
         
            +
              # by whether they are completed, late, or upcoming.
         
     | 
| 
      
 325 
     | 
    
         
            +
              def milestones(project_id, find="all")
         
     | 
| 
      
 326 
     | 
    
         
            +
                records "milestone", "/projects/#{project_id}/milestones/list", :find => find
         
     | 
| 
      
 327 
     | 
    
         
            +
              end
         
     | 
| 
      
 328 
     | 
    
         
            +
             
     | 
| 
      
 329 
     | 
    
         
            +
              # Uncomplete the milestone with the given id
         
     | 
| 
      
 330 
     | 
    
         
            +
              def uncomplete_milestone(id)
         
     | 
| 
      
 331 
     | 
    
         
            +
                record "/milestones/uncomplete/#{id}"
         
     | 
| 
      
 332 
     | 
    
         
            +
              end
         
     | 
| 
      
 333 
     | 
    
         
            +
             
     | 
| 
      
 334 
     | 
    
         
            +
              # Updates an existing milestone.
         
     | 
| 
      
 335 
     | 
    
         
            +
              def update_milestone(id, data, move=false, move_off_weekends=false)
         
     | 
| 
      
 336 
     | 
    
         
            +
                record "/milestones/update/#{id}", :milestone => data,
         
     | 
| 
      
 337 
     | 
    
         
            +
                  :move_upcoming_milestones => move,
         
     | 
| 
      
 338 
     | 
    
         
            +
                  :move_upcoming_milestones_off_weekends => move_off_weekends
         
     | 
| 
      
 339 
     | 
    
         
            +
              end
         
     | 
| 
      
 340 
     | 
    
         
            +
             
     | 
| 
      
 341 
     | 
    
         
            +
              # Make a raw web-service request to Basecamp. This will return a Hash of
         
     | 
| 
      
 342 
     | 
    
         
            +
              # Arrays of the response, and may seem a little odd to the uninitiated.
         
     | 
| 
      
 343 
     | 
    
         
            +
              def request(path, parameters = {}, second_try = false)
         
     | 
| 
      
 344 
     | 
    
         
            +
                response = post(path, convert_body(parameters), "Content-Type" => content_type)
         
     | 
| 
      
 345 
     | 
    
         
            +
             
     | 
| 
      
 346 
     | 
    
         
            +
                if response.code.to_i / 100 == 2
         
     | 
| 
      
 347 
     | 
    
         
            +
                  result = XmlSimple.xml_in(response.body, 'keeproot' => true,
         
     | 
| 
      
 348 
     | 
    
         
            +
                    'contentkey' => '__content__', 'forcecontent' => true)
         
     | 
| 
      
 349 
     | 
    
         
            +
                  typecast_value(result)
         
     | 
| 
      
 350 
     | 
    
         
            +
                elsif response.code == "302" && !second_try
         
     | 
| 
      
 351 
     | 
    
         
            +
                  connect!(@url, !@use_ssl)
         
     | 
| 
      
 352 
     | 
    
         
            +
                  request(path, parameters, true)
         
     | 
| 
      
 353 
     | 
    
         
            +
                else
         
     | 
| 
      
 354 
     | 
    
         
            +
                  raise "#{response.message} (#{response.code})"
         
     | 
| 
      
 355 
     | 
    
         
            +
                end
         
     | 
| 
      
 356 
     | 
    
         
            +
              end
         
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
      
 358 
     | 
    
         
            +
              # A convenience method for wrapping the result of a query in a Record
         
     | 
| 
      
 359 
     | 
    
         
            +
              # object. This assumes that the result is a singleton, not a collection.
         
     | 
| 
      
 360 
     | 
    
         
            +
              def record(path, parameters={})
         
     | 
| 
      
 361 
     | 
    
         
            +
                result = request(path, parameters)
         
     | 
| 
      
 362 
     | 
    
         
            +
                (result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
         
     | 
| 
      
 363 
     | 
    
         
            +
              end
         
     | 
| 
      
 364 
     | 
    
         
            +
             
     | 
| 
      
 365 
     | 
    
         
            +
              # A convenience method for wrapping the result of a query in Record
         
     | 
| 
      
 366 
     | 
    
         
            +
              # objects. This assumes that the result is a collection--any singleton
         
     | 
| 
      
 367 
     | 
    
         
            +
              # result will be wrapped in an array.
         
     | 
| 
      
 368 
     | 
    
         
            +
              def records(node, path, parameters={})
         
     | 
| 
      
 369 
     | 
    
         
            +
                result = request(path, parameters).values.first or return []
         
     | 
| 
      
 370 
     | 
    
         
            +
                result = result[node] or return []
         
     | 
| 
      
 371 
     | 
    
         
            +
                result = [result] unless Array === result
         
     | 
| 
      
 372 
     | 
    
         
            +
                result.map { |row| Record.new(node, row) }
         
     | 
| 
      
 373 
     | 
    
         
            +
              end
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
              private
         
     | 
| 
      
 376 
     | 
    
         
            +
             
     | 
| 
      
 377 
     | 
    
         
            +
                def connect!(url, use_ssl)
         
     | 
| 
      
 378 
     | 
    
         
            +
                  @use_ssl = use_ssl
         
     | 
| 
      
 379 
     | 
    
         
            +
                  @url = url
         
     | 
| 
      
 380 
     | 
    
         
            +
                  @connection = Net::HTTP.new(url, use_ssl ? 443 : 80)
         
     | 
| 
      
 381 
     | 
    
         
            +
                  @connection.use_ssl = @use_ssl
         
     | 
| 
      
 382 
     | 
    
         
            +
                  @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
         
     | 
| 
      
 383 
     | 
    
         
            +
                end
         
     | 
| 
      
 384 
     | 
    
         
            +
             
     | 
| 
      
 385 
     | 
    
         
            +
                def convert_body(body)
         
     | 
| 
      
 386 
     | 
    
         
            +
                  body = use_xml ? body.to_xml : body.to_yaml
         
     | 
| 
      
 387 
     | 
    
         
            +
                end
         
     | 
| 
      
 388 
     | 
    
         
            +
             
     | 
| 
      
 389 
     | 
    
         
            +
                def content_type
         
     | 
| 
      
 390 
     | 
    
         
            +
                  use_xml ? "application/xml" : "application/x-yaml"
         
     | 
| 
      
 391 
     | 
    
         
            +
                end
         
     | 
| 
      
 392 
     | 
    
         
            +
             
     | 
| 
      
 393 
     | 
    
         
            +
                def post(path, body, header={})
         
     | 
| 
      
 394 
     | 
    
         
            +
                  request = Net::HTTP::Post.new(path, header.merge('Accept' => 'application/xml'))
         
     | 
| 
      
 395 
     | 
    
         
            +
                  request.basic_auth(@user_name, @password)
         
     | 
| 
      
 396 
     | 
    
         
            +
                  @connection.request(request, body)
         
     | 
| 
      
 397 
     | 
    
         
            +
                end
         
     | 
| 
      
 398 
     | 
    
         
            +
             
     | 
| 
      
 399 
     | 
    
         
            +
                def store_file(contents)
         
     | 
| 
      
 400 
     | 
    
         
            +
                  response = post("/upload", contents, 'Content-Type' => 'application/octet-stream',
         
     | 
| 
      
 401 
     | 
    
         
            +
                    'Accept' => 'application/xml')
         
     | 
| 
      
 402 
     | 
    
         
            +
             
     | 
| 
      
 403 
     | 
    
         
            +
                  if response.code == "200"
         
     | 
| 
      
 404 
     | 
    
         
            +
                    result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'forcearray' => false)
         
     | 
| 
      
 405 
     | 
    
         
            +
                    return result["upload"]["id"]
         
     | 
| 
      
 406 
     | 
    
         
            +
                  else
         
     | 
| 
      
 407 
     | 
    
         
            +
                    raise "Could not store file: #{response.message} (#{response.code})"
         
     | 
| 
      
 408 
     | 
    
         
            +
                  end
         
     | 
| 
      
 409 
     | 
    
         
            +
                end
         
     | 
| 
      
 410 
     | 
    
         
            +
             
     | 
| 
      
 411 
     | 
    
         
            +
                def typecast_value(value)
         
     | 
| 
      
 412 
     | 
    
         
            +
                  case value
         
     | 
| 
      
 413 
     | 
    
         
            +
                  when Hash
         
     | 
| 
      
 414 
     | 
    
         
            +
                    if value.has_key?("__content__")
         
     | 
| 
      
 415 
     | 
    
         
            +
                      content = translate_entities(value["__content__"]).strip
         
     | 
| 
      
 416 
     | 
    
         
            +
                      case value["type"]
         
     | 
| 
      
 417 
     | 
    
         
            +
                      when "integer"  then content.to_i
         
     | 
| 
      
 418 
     | 
    
         
            +
                      when "boolean"  then content == "true"
         
     | 
| 
      
 419 
     | 
    
         
            +
                      when "datetime" then Time.parse(content)
         
     | 
| 
      
 420 
     | 
    
         
            +
                      when "date"     then Date.parse(content)
         
     | 
| 
      
 421 
     | 
    
         
            +
                      else                 content
         
     | 
| 
      
 422 
     | 
    
         
            +
                      end
         
     | 
| 
      
 423 
     | 
    
         
            +
                    # a special case to work-around a bug in XmlSimple. When you have an empty
         
     | 
| 
      
 424 
     | 
    
         
            +
                    # tag that has an attribute, XmlSimple will not add the __content__ key
         
     | 
| 
      
 425 
     | 
    
         
            +
                    # to the returned hash. Thus, we check for the presense of the 'type'
         
     | 
| 
      
 426 
     | 
    
         
            +
                    # attribute to look for empty, typed tags, and simply return nil for
         
     | 
| 
      
 427 
     | 
    
         
            +
                    # their value.
         
     | 
| 
      
 428 
     | 
    
         
            +
                    elsif value.keys == %w(type)
         
     | 
| 
      
 429 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 430 
     | 
    
         
            +
                    elsif value["nil"] == "true"
         
     | 
| 
      
 431 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 432 
     | 
    
         
            +
                    # another special case, introduced by the latest rails, where an array
         
     | 
| 
      
 433 
     | 
    
         
            +
                    # type now exists. This is parsed by XmlSimple as a two-key hash, where
         
     | 
| 
      
 434 
     | 
    
         
            +
                    # one key is 'type' and the other is the actual array value.
         
     | 
| 
      
 435 
     | 
    
         
            +
                    elsif value.keys.length == 2 && value["type"] == "array"
         
     | 
| 
      
 436 
     | 
    
         
            +
                      value.delete("type")
         
     | 
| 
      
 437 
     | 
    
         
            +
                      typecast_value(value)
         
     | 
| 
      
 438 
     | 
    
         
            +
                    else
         
     | 
| 
      
 439 
     | 
    
         
            +
                      value.empty? ? nil : value.inject({}) do |h,(k,v)|
         
     | 
| 
      
 440 
     | 
    
         
            +
                        h[k] = typecast_value(v)
         
     | 
| 
      
 441 
     | 
    
         
            +
                        h
         
     | 
| 
      
 442 
     | 
    
         
            +
                      end
         
     | 
| 
      
 443 
     | 
    
         
            +
                    end
         
     | 
| 
      
 444 
     | 
    
         
            +
                  when Array
         
     | 
| 
      
 445 
     | 
    
         
            +
                    value.map! { |i| typecast_value(i) }
         
     | 
| 
      
 446 
     | 
    
         
            +
                    case value.length
         
     | 
| 
      
 447 
     | 
    
         
            +
                    when 0 then nil
         
     | 
| 
      
 448 
     | 
    
         
            +
                    when 1 then value.first
         
     | 
| 
      
 449 
     | 
    
         
            +
                    else value
         
     | 
| 
      
 450 
     | 
    
         
            +
                    end
         
     | 
| 
      
 451 
     | 
    
         
            +
                  else
         
     | 
| 
      
 452 
     | 
    
         
            +
                    raise "can't typecast #{value.inspect}"
         
     | 
| 
      
 453 
     | 
    
         
            +
                  end
         
     | 
| 
      
 454 
     | 
    
         
            +
                end
         
     | 
| 
      
 455 
     | 
    
         
            +
             
     | 
| 
      
 456 
     | 
    
         
            +
                def translate_entities(value)
         
     | 
| 
      
 457 
     | 
    
         
            +
                  value.gsub(/</, "<").
         
     | 
| 
      
 458 
     | 
    
         
            +
                        gsub(/>/, ">").
         
     | 
| 
      
 459 
     | 
    
         
            +
                        gsub(/"/, '"').
         
     | 
| 
      
 460 
     | 
    
         
            +
                        gsub(/'/, "'").
         
     | 
| 
      
 461 
     | 
    
         
            +
                        gsub(/&/, "&")
         
     | 
| 
      
 462 
     | 
    
         
            +
                end
         
     | 
| 
      
 463 
     | 
    
         
            +
             
     | 
| 
      
 464 
     | 
    
         
            +
                def prepare_attachments(list)
         
     | 
| 
      
 465 
     | 
    
         
            +
                  (list || []).each do |data|
         
     | 
| 
      
 466 
     | 
    
         
            +
                    upload = data[:file]
         
     | 
| 
      
 467 
     | 
    
         
            +
                    id = store_file(upload.content)
         
     | 
| 
      
 468 
     | 
    
         
            +
                    data[:file] = { :file => id,
         
     | 
| 
      
 469 
     | 
    
         
            +
                                    :content_type => "application/octet-stream",
         
     | 
| 
      
 470 
     | 
    
         
            +
                                    :original_filename => upload.filename }
         
     | 
| 
      
 471 
     | 
    
         
            +
                  end
         
     | 
| 
      
 472 
     | 
    
         
            +
                end
         
     | 
| 
      
 473 
     | 
    
         
            +
            end
         
     | 
| 
      
 474 
     | 
    
         
            +
             
     | 
| 
      
 475 
     | 
    
         
            +
            # A minor hack to let Xml-Simple serialize symbolic keys in hashes
         
     | 
| 
      
 476 
     | 
    
         
            +
            #class Symbol
         
     | 
| 
      
 477 
     | 
    
         
            +
            #  def [](*args)
         
     | 
| 
      
 478 
     | 
    
         
            +
            #    to_s[*args]
         
     | 
| 
      
 479 
     | 
    
         
            +
            #  end
         
     | 
| 
      
 480 
     | 
    
         
            +
            #end
         
     | 
| 
      
 481 
     | 
    
         
            +
            #class Hash
         
     | 
| 
      
 482 
     | 
    
         
            +
            #  def to_xml
         
     | 
| 
      
 483 
     | 
    
         
            +
            #    XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
         
     | 
| 
      
 484 
     | 
    
         
            +
            #  end
         
     | 
| 
      
 485 
     | 
    
         
            +
            #end
         
     |