rubyfocus 0.3.0
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.
- checksums.yaml +7 -0
 - data/.gitignore +4 -0
 - data/.rspec +1 -0
 - data/Manifest +36 -0
 - data/README.md +121 -0
 - data/lib/rubyfocus/document.rb +146 -0
 - data/lib/rubyfocus/errors.rb +2 -0
 - data/lib/rubyfocus/fetchers/fetcher.rb +83 -0
 - data/lib/rubyfocus/fetchers/local_fetcher.rb +66 -0
 - data/lib/rubyfocus/fetchers/oss_fetcher.rb +102 -0
 - data/lib/rubyfocus/includes/conditional_exec.rb +7 -0
 - data/lib/rubyfocus/includes/idref.rb +29 -0
 - data/lib/rubyfocus/includes/parser.rb +36 -0
 - data/lib/rubyfocus/includes/searchable.rb +82 -0
 - data/lib/rubyfocus/items/context.rb +15 -0
 - data/lib/rubyfocus/items/folder.rb +18 -0
 - data/lib/rubyfocus/items/item.rb +63 -0
 - data/lib/rubyfocus/items/named_item.rb +13 -0
 - data/lib/rubyfocus/items/project.rb +76 -0
 - data/lib/rubyfocus/items/ranked_item.rb +13 -0
 - data/lib/rubyfocus/items/setting.rb +17 -0
 - data/lib/rubyfocus/items/task.rb +133 -0
 - data/lib/rubyfocus/location.rb +17 -0
 - data/lib/rubyfocus/patch.rb +142 -0
 - data/lib/rubyfocus/review_period.rb +43 -0
 - data/lib/rubyfocus/xml_translator.rb +36 -0
 - data/lib/rubyfocus.rb +16 -0
 - data/rubyfocus.gemspec +22 -0
 - data/version.txt +1 -0
 - metadata +113 -0
 
| 
         @@ -0,0 +1,82 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Rubyfocus
         
     | 
| 
      
 2 
     | 
    
         
            +
            	# The Searchable module allows you to easily search arrays of items by
         
     | 
| 
      
 3 
     | 
    
         
            +
            	# effectively supplying find and select methods. Passing a hash to either
         
     | 
| 
      
 4 
     | 
    
         
            +
            	# of these methods allows you to search by given properties - for example:
         
     | 
| 
      
 5 
     | 
    
         
            +
            	# 
         
     | 
| 
      
 6 
     | 
    
         
            +
            	#   data_store.find(name: "Foobar")
         
     | 
| 
      
 7 
     | 
    
         
            +
            	#
         
     | 
| 
      
 8 
     | 
    
         
            +
            	# Is basically the same as:
         
     | 
| 
      
 9 
     | 
    
         
            +
            	#
         
     | 
| 
      
 10 
     | 
    
         
            +
            	# 	data_store.find{ |o| o.name == "Foobard" }
         
     | 
| 
      
 11 
     | 
    
         
            +
            	#
         
     | 
| 
      
 12 
     | 
    
         
            +
            	# If you pass +find+ or +select+ a String or Fixnum, it will look for objects
         
     | 
| 
      
 13 
     | 
    
         
            +
            	# whose +id+ equals this value.
         
     | 
| 
      
 14 
     | 
    
         
            +
            	#
         
     | 
| 
      
 15 
     | 
    
         
            +
            	# You should ensure that your class' +array+ method is overwritten to point
         
     | 
| 
      
 16 
     | 
    
         
            +
            	# to the appriopriate array.
         
     | 
| 
      
 17 
     | 
    
         
            +
            	module Searchable
         
     | 
| 
      
 18 
     | 
    
         
            +
            		# This method will return only the *first* object that matches the given
         
     | 
| 
      
 19 
     | 
    
         
            +
            		# criteria, or +nil+ if no object is found.
         
     | 
| 
      
 20 
     | 
    
         
            +
            		def find(arg=nil, &blck)
         
     | 
| 
      
 21 
     | 
    
         
            +
            			fs_block = find_select_block(arg) || blck
         
     | 
| 
      
 22 
     | 
    
         
            +
            			if fs_block
         
     | 
| 
      
 23 
     | 
    
         
            +
            				return array.find(&fs_block)
         
     | 
| 
      
 24 
     | 
    
         
            +
            			else
         
     | 
| 
      
 25 
     | 
    
         
            +
            				raise ArgumentError, "ItemArray#find called with #{arg.class} argument."
         
     | 
| 
      
 26 
     | 
    
         
            +
            			end
         
     | 
| 
      
 27 
     | 
    
         
            +
            		end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            		# This method will return an array of all objects that match the given
         
     | 
| 
      
 30 
     | 
    
         
            +
            		# criteria, or +[]+ if no object is found.
         
     | 
| 
      
 31 
     | 
    
         
            +
            		def select(arg=nil, &blck)
         
     | 
| 
      
 32 
     | 
    
         
            +
            			fs_block = find_select_block(arg) || blck
         
     | 
| 
      
 33 
     | 
    
         
            +
            			if fs_block
         
     | 
| 
      
 34 
     | 
    
         
            +
            				return array.select(&fs_block)
         
     | 
| 
      
 35 
     | 
    
         
            +
            			else
         
     | 
| 
      
 36 
     | 
    
         
            +
            				raise ArgumentError, "ItemArray#select called with #{arg.class} argument."
         
     | 
| 
      
 37 
     | 
    
         
            +
            			end
         
     | 
| 
      
 38 
     | 
    
         
            +
            		end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            		# This method should be overriden
         
     | 
| 
      
 41 
     | 
    
         
            +
            		def array
         
     | 
| 
      
 42 
     | 
    
         
            +
            			raise RuntimeError, "Method #{self.class}#array has not been implemented!"
         
     | 
| 
      
 43 
     | 
    
         
            +
            		end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            		private
         
     | 
| 
      
 46 
     | 
    
         
            +
            		# This method determines the correct block to use in find or select operations
         
     | 
| 
      
 47 
     | 
    
         
            +
            		def find_select_block(arg)
         
     | 
| 
      
 48 
     | 
    
         
            +
            			case arg
         
     | 
| 
      
 49 
     | 
    
         
            +
            			when Fixnum # ID
         
     | 
| 
      
 50 
     | 
    
         
            +
            				string_id = arg.to_s
         
     | 
| 
      
 51 
     | 
    
         
            +
            				Proc.new{ |item| item.id == string_id }
         
     | 
| 
      
 52 
     | 
    
         
            +
            			when String
         
     | 
| 
      
 53 
     | 
    
         
            +
            				Proc.new{ |item| item.id == arg }
         
     | 
| 
      
 54 
     | 
    
         
            +
            			when Hash
         
     | 
| 
      
 55 
     | 
    
         
            +
            				Proc.new{ |item| arg.all?{ |k,v| item.send(k) == v } }
         
     | 
| 
      
 56 
     | 
    
         
            +
            			else
         
     | 
| 
      
 57 
     | 
    
         
            +
            				nil
         
     | 
| 
      
 58 
     | 
    
         
            +
            			end
         
     | 
| 
      
 59 
     | 
    
         
            +
            		end
         
     | 
| 
      
 60 
     | 
    
         
            +
            	end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
            	# A sample class, SearchableArray, is basically a wrapper around a standard array
         
     | 
| 
      
 64 
     | 
    
         
            +
            	class SearchableArray
         
     | 
| 
      
 65 
     | 
    
         
            +
            		include Searchable
         
     | 
| 
      
 66 
     | 
    
         
            +
            		attr_reader :array
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
            		# Takes the same arguments as the array it wraps
         
     | 
| 
      
 69 
     | 
    
         
            +
            		def initialize(*args, &blck)
         
     | 
| 
      
 70 
     | 
    
         
            +
            		  @array = Array.new(*args, &blck)
         
     | 
| 
      
 71 
     | 
    
         
            +
            		end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
            		# In all other respects, it's an array - handily governed by this method
         
     | 
| 
      
 74 
     | 
    
         
            +
            		def method_missing meth, *args, &blck
         
     | 
| 
      
 75 
     | 
    
         
            +
            			if @array.respond_to?(meth)
         
     | 
| 
      
 76 
     | 
    
         
            +
            				@array.send(meth, *args, &blck)
         
     | 
| 
      
 77 
     | 
    
         
            +
            			else
         
     | 
| 
      
 78 
     | 
    
         
            +
            				super(meth, *args, &blck)
         
     | 
| 
      
 79 
     | 
    
         
            +
            			end
         
     | 
| 
      
 80 
     | 
    
         
            +
            		end
         
     | 
| 
      
 81 
     | 
    
         
            +
            	end
         
     | 
| 
      
 82 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,15 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Rubyfocus::Context < Rubyfocus::RankedItem
         
     | 
| 
      
 2 
     | 
    
         
            +
            	include Rubyfocus::Parser
         
     | 
| 
      
 3 
     | 
    
         
            +
            	def self.matches_node?(node)
         
     | 
| 
      
 4 
     | 
    
         
            +
            		return (node.name == "context")
         
     | 
| 
      
 5 
     | 
    
         
            +
            	end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            	idref :container
         
     | 
| 
      
 8 
     | 
    
         
            +
            	attr_accessor :location
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            	def apply_xml(n)
         
     | 
| 
      
 11 
     | 
    
         
            +
            		super(n)
         
     | 
| 
      
 12 
     | 
    
         
            +
            		conditional_set(:container_id,n.at_xpath("xmlns:context")) { |e| e["idref"] }
         
     | 
| 
      
 13 
     | 
    
         
            +
            		conditional_set(:location, 		n.at_xpath("xmlns:location")){ |e| Rubyfocus::Location.new(e) }
         
     | 
| 
      
 14 
     | 
    
         
            +
            	end
         
     | 
| 
      
 15 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,18 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Rubyfocus::Folder < Rubyfocus::RankedItem
         
     | 
| 
      
 2 
     | 
    
         
            +
            	include Rubyfocus::Parser
         
     | 
| 
      
 3 
     | 
    
         
            +
            	def self.matches_node?(node)
         
     | 
| 
      
 4 
     | 
    
         
            +
            		return (node.name == "folder")
         
     | 
| 
      
 5 
     | 
    
         
            +
            	end
         
     | 
| 
      
 6 
     | 
    
         
            +
            	
         
     | 
| 
      
 7 
     | 
    
         
            +
            	idref :container
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            	def apply_xml(n)
         
     | 
| 
      
 10 
     | 
    
         
            +
            		super(n)
         
     | 
| 
      
 11 
     | 
    
         
            +
            		conditional_set(:container_id, n.at_xpath("xmlns:folder")){ |e| e["idref"] }
         
     | 
| 
      
 12 
     | 
    
         
            +
            	end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            	private
         
     | 
| 
      
 15 
     | 
    
         
            +
            	def inspect_properties
         
     | 
| 
      
 16 
     | 
    
         
            +
            		super + %w(container_id)
         
     | 
| 
      
 17 
     | 
    
         
            +
            	end
         
     | 
| 
      
 18 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,63 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # The Rubyfocus Item represents an item found in an Omnifocus XML file, and thus also
         
     | 
| 
      
 2 
     | 
    
         
            +
            # any Omnifocus entity.
         
     | 
| 
      
 3 
     | 
    
         
            +
            #
         
     | 
| 
      
 4 
     | 
    
         
            +
            # The Rubyfocus Item has a parent "document" as well as a series of properties determined
         
     | 
| 
      
 5 
     | 
    
         
            +
            # by the XML file proper. You can pass an XML document at creation or leave it blank. You
         
     | 
| 
      
 6 
     | 
    
         
            +
            # can always apply XML later using Item#apply_xml or Item#<<
         
     | 
| 
      
 7 
     | 
    
         
            +
            #
         
     | 
| 
      
 8 
     | 
    
         
            +
            # By separating these two methods, we can also patch the object with another XML node, e.g.
         
     | 
| 
      
 9 
     | 
    
         
            +
            # through an update. This is important!
         
     | 
| 
      
 10 
     | 
    
         
            +
            class Rubyfocus::Item
         
     | 
| 
      
 11 
     | 
    
         
            +
            	include Rubyfocus::IDRef
         
     | 
| 
      
 12 
     | 
    
         
            +
            	include Rubyfocus::ConditionalExec
         
     | 
| 
      
 13 
     | 
    
         
            +
            	attr_accessor :id, :added, :modified, :document
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            	def initialize(document=nil, n=nil)
         
     | 
| 
      
 16 
     | 
    
         
            +
            		self.document = document
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            		case n
         
     | 
| 
      
 19 
     | 
    
         
            +
            		when Nokogiri::XML::Element
         
     | 
| 
      
 20 
     | 
    
         
            +
            			apply_xml(n)
         
     | 
| 
      
 21 
     | 
    
         
            +
            		when Hash
         
     | 
| 
      
 22 
     | 
    
         
            +
            			n.each do |k,v|
         
     | 
| 
      
 23 
     | 
    
         
            +
            				setter = "#{k}="
         
     | 
| 
      
 24 
     | 
    
         
            +
            				send(setter,v) if respond_to?(setter)
         
     | 
| 
      
 25 
     | 
    
         
            +
            			end
         
     | 
| 
      
 26 
     | 
    
         
            +
            		end
         
     | 
| 
      
 27 
     | 
    
         
            +
            	end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            	def apply_xml(n)
         
     | 
| 
      
 30 
     | 
    
         
            +
            		self.id ||= n["id"] # This should not change once set!
         
     | 
| 
      
 31 
     | 
    
         
            +
            		conditional_set(:added, 		n.at_xpath("xmlns:added"))		{ |e| Time.parse(e) }
         
     | 
| 
      
 32 
     | 
    
         
            +
            		conditional_set(:modified, 	n.at_xpath("xmlns:modified"))	{ |e| Time.parse(e) }
         
     | 
| 
      
 33 
     | 
    
         
            +
            	end
         
     | 
| 
      
 34 
     | 
    
         
            +
            	alias_method :<<, :apply_xml
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            	#---------------------------------------
         
     | 
| 
      
 37 
     | 
    
         
            +
            	# Inspection method
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            	def inspect
         
     | 
| 
      
 40 
     | 
    
         
            +
            		msgs = inspect_properties.select{ |sym| self.respond_to?(sym) && !self.send(sym).nil? }
         
     | 
| 
      
 41 
     | 
    
         
            +
            		"#<#{self.class} " + msgs.map{ |e| %|#{e}=#{self.send(e).inspect}| }.join(" ") + ">"
         
     | 
| 
      
 42 
     | 
    
         
            +
            	end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
            	def to_serial
         
     | 
| 
      
 45 
     | 
    
         
            +
            		inspect_properties.each_with_object({}){ |s,hsh| hsh[s] = self.send(s) }
         
     | 
| 
      
 46 
     | 
    
         
            +
            	end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            	#---------------------------------------
         
     | 
| 
      
 49 
     | 
    
         
            +
            	# Document set/get methods
         
     | 
| 
      
 50 
     | 
    
         
            +
            	def document= d
         
     | 
| 
      
 51 
     | 
    
         
            +
            		@document.remove_element(self) if @document
         
     | 
| 
      
 52 
     | 
    
         
            +
            		@document = d
         
     | 
| 
      
 53 
     | 
    
         
            +
            		@document.add_element(self) if @document
         
     | 
| 
      
 54 
     | 
    
         
            +
            	end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
            	#-------------------------------------------------------------------------------
         
     | 
| 
      
 57 
     | 
    
         
            +
            	# Private inspect methods
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
            	private
         
     | 
| 
      
 60 
     | 
    
         
            +
            	def inspect_properties
         
     | 
| 
      
 61 
     | 
    
         
            +
            		%w(id added modified)
         
     | 
| 
      
 62 
     | 
    
         
            +
            	end
         
     | 
| 
      
 63 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,76 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # The project represents an OmniFocus project object.
         
     | 
| 
      
 2 
     | 
    
         
            +
            class Rubyfocus::Project < Rubyfocus::Task
         
     | 
| 
      
 3 
     | 
    
         
            +
            	# It's parseable
         
     | 
| 
      
 4 
     | 
    
         
            +
            	include Rubyfocus::Parser
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            	#-------------------------------------------------------------------------------
         
     | 
| 
      
 7 
     | 
    
         
            +
            	# Parsing stuff
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            	# Projects are <task>s with an interior <project> node
         
     | 
| 
      
 10 
     | 
    
         
            +
            	def self.matches_node?(node)
         
     | 
| 
      
 11 
     | 
    
         
            +
            		return (node.name == "task" && (node/"project").size > 0)
         
     | 
| 
      
 12 
     | 
    
         
            +
            	end
         
     | 
| 
      
 13 
     | 
    
         
            +
            	
         
     | 
| 
      
 14 
     | 
    
         
            +
            	# Singleton: contains one-off tasks
         
     | 
| 
      
 15 
     | 
    
         
            +
            	attr_accessor :singleton
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            	# How often to we review this project?
         
     | 
| 
      
 18 
     | 
    
         
            +
            	attr_accessor :review_interval
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            	# When did we last review the project?
         
     | 
| 
      
 21 
     | 
    
         
            +
            	attr_accessor :last_review
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            	# What's the status of the project? Valid options: :active, :inactive, :done. Default
         
     | 
| 
      
 24 
     | 
    
         
            +
            	# is :active
         
     | 
| 
      
 25 
     | 
    
         
            +
            	attr_accessor :status
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            	# Initialize the project with a document and optional node to base it off of.
         
     | 
| 
      
 28 
     | 
    
         
            +
            	# Also sets default values
         
     | 
| 
      
 29 
     | 
    
         
            +
            	def initialize(document=nil, n=nil)
         
     | 
| 
      
 30 
     | 
    
         
            +
            		@singleton = false
         
     | 
| 
      
 31 
     | 
    
         
            +
            		@order = :sequential
         
     | 
| 
      
 32 
     | 
    
         
            +
            		@status = :active
         
     | 
| 
      
 33 
     | 
    
         
            +
            		super(document, n)
         
     | 
| 
      
 34 
     | 
    
         
            +
            	end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            	# Apply XML node to project
         
     | 
| 
      
 37 
     | 
    
         
            +
            	def apply_xml(n)
         
     | 
| 
      
 38 
     | 
    
         
            +
            		super(n)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            		#First, set project
         
     | 
| 
      
 41 
     | 
    
         
            +
            		p = n.at_xpath("xmlns:project")
         
     | 
| 
      
 42 
     | 
    
         
            +
            		conditional_set(:singleton, 			p.at_xpath("xmlns:singleton"))					{ |e| e.inner_html == "true" }
         
     | 
| 
      
 43 
     | 
    
         
            +
            		conditional_set(:review_interval, p.at_xpath("xmlns:review-interval") ) 	{ |e| Rubyfocus::ReviewPeriod.from_string(e.inner_html) }
         
     | 
| 
      
 44 
     | 
    
         
            +
            		conditional_set(:last_review, 		p.at_xpath("xmlns:last-review"))				{ |e| Time.parse e.inner_html }
         
     | 
| 
      
 45 
     | 
    
         
            +
            		conditional_set(:status,					p.at_xpath("xmlns:status"))							{ |e| e.inner_html.to_sym }							
         
     | 
| 
      
 46 
     | 
    
         
            +
            	end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            	alias_method :singleton?, :singleton
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            	private
         
     | 
| 
      
 51 
     | 
    
         
            +
            	def inspect_properties
         
     | 
| 
      
 52 
     | 
    
         
            +
            		super + %w(singleton review_interval last_review)
         
     | 
| 
      
 53 
     | 
    
         
            +
            	end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
            	public
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            	#-------------------------------------------------------------------------------
         
     | 
| 
      
 58 
     | 
    
         
            +
            	# Convenience methods
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            	# Status methods
         
     | 
| 
      
 61 
     | 
    
         
            +
            	def active?; status == :active; end
         
     | 
| 
      
 62 
     | 
    
         
            +
            	def on_hold?; status == :inactive; end
         
     | 
| 
      
 63 
     | 
    
         
            +
            	def completed?; status == :done; end
         
     | 
| 
      
 64 
     | 
    
         
            +
            	def dropped?; status == :dropped; end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
            	#---------------------------------------
         
     | 
| 
      
 67 
     | 
    
         
            +
            	# Convert to a task
         
     | 
| 
      
 68 
     | 
    
         
            +
            	def to_task
         
     | 
| 
      
 69 
     | 
    
         
            +
            		t = Rubyfocus::Task.new(self.document)
         
     | 
| 
      
 70 
     | 
    
         
            +
            		instance_variables.each do |ivar|
         
     | 
| 
      
 71 
     | 
    
         
            +
            			setter = ivar.to_s.gsub(/^@/,"") + "="
         
     | 
| 
      
 72 
     | 
    
         
            +
            			t.send(setter, self.instance_variable_get(ivar))	if t.respond_to?(setter)
         
     | 
| 
      
 73 
     | 
    
         
            +
            		end
         
     | 
| 
      
 74 
     | 
    
         
            +
            		t
         
     | 
| 
      
 75 
     | 
    
         
            +
            	end
         
     | 
| 
      
 76 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,13 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Rubyfocus::RankedItem < Rubyfocus::NamedItem
         
     | 
| 
      
 2 
     | 
    
         
            +
            	attr_accessor :rank
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            	def apply_xml(n)
         
     | 
| 
      
 5 
     | 
    
         
            +
            		super(n)
         
     | 
| 
      
 6 
     | 
    
         
            +
            		conditional_set(:rank, n.at_xpath("xmlns:rank")){ |e| e.inner_html.to_i }
         
     | 
| 
      
 7 
     | 
    
         
            +
            	end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            	private
         
     | 
| 
      
 10 
     | 
    
         
            +
            	def inspect_properties
         
     | 
| 
      
 11 
     | 
    
         
            +
            		super + %w(rank)
         
     | 
| 
      
 12 
     | 
    
         
            +
            	end
         
     | 
| 
      
 13 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,17 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Rubyfocus::Setting < Rubyfocus::Item
         
     | 
| 
      
 2 
     | 
    
         
            +
            	include Rubyfocus::Parser
         
     | 
| 
      
 3 
     | 
    
         
            +
            	def self.matches_node?(node)
         
     | 
| 
      
 4 
     | 
    
         
            +
            		return (node.name == "xmlns:plist")
         
     | 
| 
      
 5 
     | 
    
         
            +
            	end
         
     | 
| 
      
 6 
     | 
    
         
            +
            	
         
     | 
| 
      
 7 
     | 
    
         
            +
            	attr_accessor :value
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            	def apply_xml(n)
         
     | 
| 
      
 10 
     | 
    
         
            +
            		super(n)
         
     | 
| 
      
 11 
     | 
    
         
            +
            		conditional_set(:value, n.at_xpath("xmlns:plist").children.first){ |e| Rubyfocus::XMLTranslator.parse(e) }
         
     | 
| 
      
 12 
     | 
    
         
            +
            	end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            	def inspect
         
     | 
| 
      
 15 
     | 
    
         
            +
            		form_inspector(:id, :name, :value, :added, :modified)
         
     | 
| 
      
 16 
     | 
    
         
            +
            	end
         
     | 
| 
      
 17 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,133 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Rubyfocus::Task < Rubyfocus::RankedItem
         
     | 
| 
      
 2 
     | 
    
         
            +
            	include Rubyfocus::Parser
         
     | 
| 
      
 3 
     | 
    
         
            +
            	def self.matches_node?(node)
         
     | 
| 
      
 4 
     | 
    
         
            +
            		return (node.name == "task")
         
     | 
| 
      
 5 
     | 
    
         
            +
            	end
         
     | 
| 
      
 6 
     | 
    
         
            +
            	
         
     | 
| 
      
 7 
     | 
    
         
            +
            	# Inherits from RankedItem:
         
     | 
| 
      
 8 
     | 
    
         
            +
            	# * rank
         
     | 
| 
      
 9 
     | 
    
         
            +
            	# Inherits from NamedItem:
         
     | 
| 
      
 10 
     | 
    
         
            +
            	# * name
         
     | 
| 
      
 11 
     | 
    
         
            +
            	# Inherits from Item:
         
     | 
| 
      
 12 
     | 
    
         
            +
            	# * id
         
     | 
| 
      
 13 
     | 
    
         
            +
            	# * added
         
     | 
| 
      
 14 
     | 
    
         
            +
            	# * modified
         
     | 
| 
      
 15 
     | 
    
         
            +
            	# * document
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            	attr_accessor :note, :flagged, :order, :start, :due, :completed
         
     | 
| 
      
 18 
     | 
    
         
            +
            	idref :container, :context
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            	def initialize(document, n=nil)
         
     | 
| 
      
 21 
     | 
    
         
            +
            		@order = :sequential
         
     | 
| 
      
 22 
     | 
    
         
            +
            		@flagged = false
         
     | 
| 
      
 23 
     | 
    
         
            +
            	  super(document,n)
         
     | 
| 
      
 24 
     | 
    
         
            +
            	end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            	def apply_xml(n)
         
     | 
| 
      
 27 
     | 
    
         
            +
            		super(n)
         
     | 
| 
      
 28 
     | 
    
         
            +
            		conditional_set(:container_id, n.at_xpath("xmlns:task") || n.at_xpath("xmlns:project/xmlns:folder")){ |e| e["idref"] }
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            		conditional_set(:context_id, 	n.at_xpath("xmlns:context"))	{ |e| e["idref"] }
         
     | 
| 
      
 31 
     | 
    
         
            +
            		conditional_set(:note, 				n.at_xpath("xmlns:note"))			{ |e| e.inner_html.strip }
         
     | 
| 
      
 32 
     | 
    
         
            +
            		conditional_set(:order, 			n.at_xpath("xmlns:order"))		{ |e| e.inner_html.to_sym }
         
     | 
| 
      
 33 
     | 
    
         
            +
            		conditional_set(:flagged,			n.at_xpath("xmlns:flagged"))	{ |e| e.inner_html == "true" }
         
     | 
| 
      
 34 
     | 
    
         
            +
            		conditional_set(:start, 			n.at_xpath("xmlns:start"))		{ |e| Time.parse(e.inner_html) }
         
     | 
| 
      
 35 
     | 
    
         
            +
            		conditional_set(:due, 		 		n.at_xpath("xmlns:due"))			{ |e| Time.parse(e.inner_html) }
         
     | 
| 
      
 36 
     | 
    
         
            +
            		conditional_set(:completed,		n.at_xpath("xmlns:completed")){ |e| Time.parse(e.inner_html) }
         
     | 
| 
      
 37 
     | 
    
         
            +
            	end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            	# Convenience methods
         
     | 
| 
      
 40 
     | 
    
         
            +
            	def completed?; !self.completed.nil?; end
         
     | 
| 
      
 41 
     | 
    
         
            +
            	alias_method :flagged?, :flagged
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            	# Collect all child tasks. If child tasks have their own subtasks, will instead fetch those.
         
     | 
| 
      
 44 
     | 
    
         
            +
            	def tasks
         
     | 
| 
      
 45 
     | 
    
         
            +
            		@tasks ||= if self.id.nil?
         
     | 
| 
      
 46 
     | 
    
         
            +
            			[]
         
     | 
| 
      
 47 
     | 
    
         
            +
            		else
         
     | 
| 
      
 48 
     | 
    
         
            +
            			t_arr = document.tasks.select(container_id: self.id)
         
     | 
| 
      
 49 
     | 
    
         
            +
            			i = 0
         
     | 
| 
      
 50 
     | 
    
         
            +
            			while i < t_arr.size
         
     | 
| 
      
 51 
     | 
    
         
            +
            				task = t_arr[i]
         
     | 
| 
      
 52 
     | 
    
         
            +
            				if task.has_subtasks?
         
     | 
| 
      
 53 
     | 
    
         
            +
            					t_arr += t_arr.delete_at(i).tasks
         
     | 
| 
      
 54 
     | 
    
         
            +
            				else
         
     | 
| 
      
 55 
     | 
    
         
            +
            					i += 1
         
     | 
| 
      
 56 
     | 
    
         
            +
            				end
         
     | 
| 
      
 57 
     | 
    
         
            +
            			end
         
     | 
| 
      
 58 
     | 
    
         
            +
            			t_arr
         
     | 
| 
      
 59 
     | 
    
         
            +
            		end
         
     | 
| 
      
 60 
     | 
    
         
            +
            	end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            	# Collect only immediate tasks: I don't care about this subtasks malarky
         
     | 
| 
      
 63 
     | 
    
         
            +
            	def immediate_tasks
         
     | 
| 
      
 64 
     | 
    
         
            +
            		document.tasks.select(container_id: self.id)
         
     | 
| 
      
 65 
     | 
    
         
            +
            	end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
            	# The first non-completed task, determined by order
         
     | 
| 
      
 68 
     | 
    
         
            +
            	def next_available_task
         
     | 
| 
      
 69 
     | 
    
         
            +
            		nat_candidate = immediate_tasks.select{ |t| !t.completed? }.sort_by(&:rank).first
         
     | 
| 
      
 70 
     | 
    
         
            +
            		if nat_candidate.has_subtasks?
         
     | 
| 
      
 71 
     | 
    
         
            +
            			nat_candidate.next_available_task
         
     | 
| 
      
 72 
     | 
    
         
            +
            		else
         
     | 
| 
      
 73 
     | 
    
         
            +
            			nat_candidate
         
     | 
| 
      
 74 
     | 
    
         
            +
            		end
         
     | 
| 
      
 75 
     | 
    
         
            +
            	end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
            	# A list of all tasks that you can take action on. Actionable tasks
         
     | 
| 
      
 78 
     | 
    
         
            +
            	# are tasks that are:
         
     | 
| 
      
 79 
     | 
    
         
            +
            	# * not completed
         
     | 
| 
      
 80 
     | 
    
         
            +
            	# * not blocked (as part of a sequential project or task group)
         
     | 
| 
      
 81 
     | 
    
         
            +
            	# * not due to start in the future
         
     | 
| 
      
 82 
     | 
    
         
            +
            	def actionable_tasks
         
     | 
| 
      
 83 
     | 
    
         
            +
            		@actionable_tasks ||= next_tasks.select{ |t| !t.deferred? }
         
     | 
| 
      
 84 
     | 
    
         
            +
            	end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            	# A list of all tasks that are not blocked.
         
     | 
| 
      
 87 
     | 
    
         
            +
            	def next_tasks
         
     | 
| 
      
 88 
     | 
    
         
            +
            		@next_tasks ||= incomplete_tasks.select{ |t| !t.blocked? }
         
     | 
| 
      
 89 
     | 
    
         
            +
            	end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
            	# A list of all tasks that aren't complete
         
     | 
| 
      
 92 
     | 
    
         
            +
            	def incomplete_tasks
         
     | 
| 
      
 93 
     | 
    
         
            +
            		@incomplete_tasks ||= tasks.select{ |t| !t.completed? }
         
     | 
| 
      
 94 
     | 
    
         
            +
            	end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
            	# Are there any tasks on this project which aren't completed?
         
     | 
| 
      
 97 
     | 
    
         
            +
            	def tasks_remain?
         
     | 
| 
      
 98 
     | 
    
         
            +
            		tasks.any?{ |t| t.completed.nil? }
         
     | 
| 
      
 99 
     | 
    
         
            +
            	end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
            	# Does this task have any subtasks?
         
     | 
| 
      
 102 
     | 
    
         
            +
            	def has_subtasks?
         
     | 
| 
      
 103 
     | 
    
         
            +
            		tasks.size > 0
         
     | 
| 
      
 104 
     | 
    
         
            +
            	end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
            	# Can we only start this task at some point in the future?
         
     | 
| 
      
 107 
     | 
    
         
            +
            	def deferred?
         
     | 
| 
      
 108 
     | 
    
         
            +
            		start && start > Time.now
         
     | 
| 
      
 109 
     | 
    
         
            +
            	end
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
            	# Can we attack this task, or does its container stop that happening?
         
     | 
| 
      
 112 
     | 
    
         
            +
            	def blocked?
         
     | 
| 
      
 113 
     | 
    
         
            +
            		container && (container.order == :sequential) && (container.next_available_task != self)
         
     | 
| 
      
 114 
     | 
    
         
            +
            	end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
            	#---------------------------------------
         
     | 
| 
      
 117 
     | 
    
         
            +
            	# Conversion methods
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
            	# Convert the task to a project
         
     | 
| 
      
 120 
     | 
    
         
            +
            	def to_project
         
     | 
| 
      
 121 
     | 
    
         
            +
            		p = Rubyfocus::Project.new(self.document)
         
     | 
| 
      
 122 
     | 
    
         
            +
            		instance_variables.each do |ivar|
         
     | 
| 
      
 123 
     | 
    
         
            +
            			setter = ivar.to_s.gsub(/^@/,"") + "="
         
     | 
| 
      
 124 
     | 
    
         
            +
            			p.send(setter, self.instance_variable_get(ivar))	if p.respond_to?(setter)
         
     | 
| 
      
 125 
     | 
    
         
            +
            		end
         
     | 
| 
      
 126 
     | 
    
         
            +
            		p
         
     | 
| 
      
 127 
     | 
    
         
            +
            	end
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
            	private
         
     | 
| 
      
 130 
     | 
    
         
            +
            	def inspect_properties
         
     | 
| 
      
 131 
     | 
    
         
            +
            		super + %w(note container_id context_id order flagged start due completed)
         
     | 
| 
      
 132 
     | 
    
         
            +
            	end
         
     | 
| 
      
 133 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,17 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # A location file. Really a collection of properties and initializer.
         
     | 
| 
      
 2 
     | 
    
         
            +
            class Rubyfocus::Location
         
     | 
| 
      
 3 
     | 
    
         
            +
            	attr_accessor :name, :latitude, :longitude, :radius, :notification_flags
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            	def initialize(n)
         
     | 
| 
      
 6 
     | 
    
         
            +
            		@notification_flags = 0
         
     | 
| 
      
 7 
     | 
    
         
            +
            		self.name = n["name"]
         
     | 
| 
      
 8 
     | 
    
         
            +
            		self.latitude = n["latitude"].to_f
         
     | 
| 
      
 9 
     | 
    
         
            +
            		self.longitude = n["longitude"].to_f
         
     | 
| 
      
 10 
     | 
    
         
            +
            		self.radius = n["radius"].to_i
         
     | 
| 
      
 11 
     | 
    
         
            +
            		self.notification_flags = n["notificationFlags"].to_i
         
     | 
| 
      
 12 
     | 
    
         
            +
            	end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            	def inspect
         
     | 
| 
      
 15 
     | 
    
         
            +
            		%|#<Rubyfocus::Location latitude="#{self.latitude}" longitude="#{self.longitude}">|
         
     | 
| 
      
 16 
     | 
    
         
            +
            	end
         
     | 
| 
      
 17 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,142 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # The patch class represents a text-file patch, storing update, delete, and creation operations.
         
     | 
| 
      
 2 
     | 
    
         
            +
            # It should also be able to apply itself to an existing document.
         
     | 
| 
      
 3 
     | 
    
         
            +
            class Rubyfocus::Patch
         
     | 
| 
      
 4 
     | 
    
         
            +
            	# The fetcher this patch belongs to. We mainly use this to work out how to fetch content for the patch proper
         
     | 
| 
      
 5 
     | 
    
         
            +
            	attr_accessor :fetcher
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            	# Operations to be performed on a document
         
     | 
| 
      
 8 
     | 
    
         
            +
            	attr_accessor :update, :delete, :create
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            	# The file the patch loads from
         
     | 
| 
      
 11 
     | 
    
         
            +
            	attr_accessor :file
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            	# These record the transformation in terms of patch ID values.
         
     | 
| 
      
 14 
     | 
    
         
            +
            	attr_accessor :from_ids, :to_id
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            	# The time the file was submitted
         
     | 
| 
      
 17 
     | 
    
         
            +
            	attr_accessor :time
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            	# By default we initialize patches from a file. To initialize from a string, use the .from_string method.
         
     | 
| 
      
 20 
     | 
    
         
            +
            	# This class will lazily load data from the file proper
         
     | 
| 
      
 21 
     | 
    
         
            +
            	def initialize(fetcher=nil, file=nil)
         
     | 
| 
      
 22 
     | 
    
         
            +
            		@fetcher = fetcher
         
     | 
| 
      
 23 
     | 
    
         
            +
            		@file = file
         
     | 
| 
      
 24 
     | 
    
         
            +
            		@update = []
         
     | 
| 
      
 25 
     | 
    
         
            +
            		@create = []
         
     | 
| 
      
 26 
     | 
    
         
            +
            		@delete = []
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            		if file 
         
     | 
| 
      
 29 
     | 
    
         
            +
            			if File.basename(file) =~ /^(\d+)=(.*)\./
         
     | 
| 
      
 30 
     | 
    
         
            +
            				time_string = $1
         
     | 
| 
      
 31 
     | 
    
         
            +
            				self.time = if (time_string == "00000000000000")
         
     | 
| 
      
 32 
     | 
    
         
            +
            					Time.at(0)
         
     | 
| 
      
 33 
     | 
    
         
            +
            				else
         
     | 
| 
      
 34 
     | 
    
         
            +
            					Time.parse(time_string)
         
     | 
| 
      
 35 
     | 
    
         
            +
            				end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            				ids 					= $2.split("+")
         
     | 
| 
      
 38 
     | 
    
         
            +
            				self.to_id 		= ids.pop
         
     | 
| 
      
 39 
     | 
    
         
            +
            				self.from_ids = ids
         
     | 
| 
      
 40 
     | 
    
         
            +
            			else
         
     | 
| 
      
 41 
     | 
    
         
            +
            				raise ArgumentError, "Constructed patch from a malformed patch file: #{file}."
         
     | 
| 
      
 42 
     | 
    
         
            +
            			end
         
     | 
| 
      
 43 
     | 
    
         
            +
            		end
         
     | 
| 
      
 44 
     | 
    
         
            +
            	end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
            	# Load from a string.
         
     | 
| 
      
 47 
     | 
    
         
            +
            	def self.from_string(fetcher, str)
         
     | 
| 
      
 48 
     | 
    
         
            +
            		n = new(fetcher)
         
     | 
| 
      
 49 
     | 
    
         
            +
            		n.load_data(str)
         
     | 
| 
      
 50 
     | 
    
         
            +
            		n
         
     | 
| 
      
 51 
     | 
    
         
            +
            	end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            	# Loads data from the file. Optional argument +str+ if you want to supply your own data,
         
     | 
| 
      
 54 
     | 
    
         
            +
            	# otherwise will load file data
         
     | 
| 
      
 55 
     | 
    
         
            +
            	def load_data(str=nil)
         
     | 
| 
      
 56 
     | 
    
         
            +
            		return if @data_loaded
         
     | 
| 
      
 57 
     | 
    
         
            +
            		@data_loaded = true
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
            		str ||= fetcher.patch(self.file)
         
     | 
| 
      
 60 
     | 
    
         
            +
            	  doc = Nokogiri::XML(str)
         
     | 
| 
      
 61 
     | 
    
         
            +
            	  doc.root.children.select{ |n| !n.text?}.each do |child|
         
     | 
| 
      
 62 
     | 
    
         
            +
            	  	case child["op"]
         
     | 
| 
      
 63 
     | 
    
         
            +
            	  	when "update"
         
     | 
| 
      
 64 
     | 
    
         
            +
            	  		@update << child
         
     | 
| 
      
 65 
     | 
    
         
            +
            	  	when "delete"
         
     | 
| 
      
 66 
     | 
    
         
            +
            	  		@delete << child
         
     | 
| 
      
 67 
     | 
    
         
            +
            	  	when "reference" # Ignore!
         
     | 
| 
      
 68 
     | 
    
         
            +
            	  	when nil
         
     | 
| 
      
 69 
     | 
    
         
            +
            	  		@create << child
         
     | 
| 
      
 70 
     | 
    
         
            +
            	  	else
         
     | 
| 
      
 71 
     | 
    
         
            +
            	  		raise RuntimeError, "Rubyfocus::Patch encountered unknown operation type #{child["op"]}."
         
     | 
| 
      
 72 
     | 
    
         
            +
            	  	end
         
     | 
| 
      
 73 
     | 
    
         
            +
            	  end
         
     | 
| 
      
 74 
     | 
    
         
            +
            	end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
            	# Update, delete and create methods
         
     | 
| 
      
 77 
     | 
    
         
            +
            	def update; load_data; @update; end
         
     | 
| 
      
 78 
     | 
    
         
            +
            	def delete; load_data; @delete; end
         
     | 
| 
      
 79 
     | 
    
         
            +
            	def create; load_data; @create; end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
            	# Can we apply this patch to a given document?
         
     | 
| 
      
 82 
     | 
    
         
            +
            	def can_patch?(document)
         
     | 
| 
      
 83 
     | 
    
         
            +
            		self.from_ids.include?(document.patch_id)
         
     | 
| 
      
 84 
     | 
    
         
            +
            	end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            	# Apply this patch to a document. Check to make sure ids match
         
     | 
| 
      
 87 
     | 
    
         
            +
            	def apply_to(document)
         
     | 
| 
      
 88 
     | 
    
         
            +
            		if can_patch?(document)
         
     | 
| 
      
 89 
     | 
    
         
            +
            			apply_to!(document)
         
     | 
| 
      
 90 
     | 
    
         
            +
            		else
         
     | 
| 
      
 91 
     | 
    
         
            +
            			raise RuntimeError, "Patch ID mismatch (patch from_ids: [#{self.from_ids.join(", ")}], document.patch_id: #{document.patch_id}"
         
     | 
| 
      
 92 
     | 
    
         
            +
            		end
         
     | 
| 
      
 93 
     | 
    
         
            +
            	end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
            	# Apply this patch to a document. Who needs error checking amirite?
         
     | 
| 
      
 96 
     | 
    
         
            +
            	def apply_to!(document)
         
     | 
| 
      
 97 
     | 
    
         
            +
            		# Updates modify elements
         
     | 
| 
      
 98 
     | 
    
         
            +
            		self.update.each do |node|
         
     | 
| 
      
 99 
     | 
    
         
            +
            			elem = document[node["id"]]
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
            			# Tasks can become projects and v.v.: check this.
         
     | 
| 
      
 102 
     | 
    
         
            +
            			if [Rubyfocus::Task, Rubyfocus::Project].include?(elem.class)
         
     | 
| 
      
 103 
     | 
    
         
            +
            				should_be_project = (node.at_xpath("xmlns:project") != nil)
         
     | 
| 
      
 104 
     | 
    
         
            +
            				if (elem.class == Rubyfocus::Project) && !should_be_project
         
     | 
| 
      
 105 
     | 
    
         
            +
            					elem.document = nil # Remove this from current document
         
     | 
| 
      
 106 
     | 
    
         
            +
            					elem = elem.to_task # Convert to task
         
     | 
| 
      
 107 
     | 
    
         
            +
            					elem.document = document # Insert again!
         
     | 
| 
      
 108 
     | 
    
         
            +
            				elsif (elem.class == Rubyfocus::Task) && should_be_project
         
     | 
| 
      
 109 
     | 
    
         
            +
            					elem.document = nil # Remove this from current document
         
     | 
| 
      
 110 
     | 
    
         
            +
            					elem = elem.to_project # Convert to task
         
     | 
| 
      
 111 
     | 
    
         
            +
            					elem.document = document # Insert again!
         
     | 
| 
      
 112 
     | 
    
         
            +
            				end
         
     | 
| 
      
 113 
     | 
    
         
            +
            			end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
            			elem.apply_xml(node) if elem
         
     | 
| 
      
 116 
     | 
    
         
            +
            		end
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
            		# Deletes remove elements
         
     | 
| 
      
 119 
     | 
    
         
            +
            		self.delete.each do |node|
         
     | 
| 
      
 120 
     | 
    
         
            +
            			document.remove_element(node["id"])
         
     | 
| 
      
 121 
     | 
    
         
            +
            		end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
            		# Creates make new elements
         
     | 
| 
      
 124 
     | 
    
         
            +
            		self.create.each do |node|
         
     | 
| 
      
 125 
     | 
    
         
            +
            			if Rubyfocus::Parser.parse(document, node).nil?
         
     | 
| 
      
 126 
     | 
    
         
            +
            				raise RuntimeError, "Encountered unparsable XML during patch reading: #{node}."
         
     | 
| 
      
 127 
     | 
    
         
            +
            			end
         
     | 
| 
      
 128 
     | 
    
         
            +
            		end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            		# Modify current patch_id to show new value
         
     | 
| 
      
 131 
     | 
    
         
            +
            		document.patch_id = self.to_id
         
     | 
| 
      
 132 
     | 
    
         
            +
            	end
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
            	# String representation
         
     | 
| 
      
 135 
     | 
    
         
            +
            	def to_s
         
     | 
| 
      
 136 
     | 
    
         
            +
            		if from_ids.size == 1
         
     | 
| 
      
 137 
     | 
    
         
            +
            			"(#{from_ids.first} -> #{to_id})"
         
     | 
| 
      
 138 
     | 
    
         
            +
            		else
         
     | 
| 
      
 139 
     | 
    
         
            +
            			"([#{from_ids.join(", ")}] -> #{to_id})"
         
     | 
| 
      
 140 
     | 
    
         
            +
            		end
         
     | 
| 
      
 141 
     | 
    
         
            +
            	end
         
     | 
| 
      
 142 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,43 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # The ReviewPeriod represents a review period used with Projects in OmniFocus
         
     | 
| 
      
 2 
     | 
    
         
            +
            # The ReviewPeriod is made up of three sections:
         
     | 
| 
      
 3 
     | 
    
         
            +
            # * The precursor symbol (~ or @, use unknown)
         
     | 
| 
      
 4 
     | 
    
         
            +
            # * A numerical "size"
         
     | 
| 
      
 5 
     | 
    
         
            +
            # * A unit ([d]ays, [w]eeks, [m]onths or [y]ears)
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            class Rubyfocus::ReviewPeriod
         
     | 
| 
      
 8 
     | 
    
         
            +
            	attr_accessor :size, :unit
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            	ALLOWED_UNITS = %i(days weeks months years)
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            	def self.from_string(str)
         
     | 
| 
      
 13 
     | 
    
         
            +
            		if str =~ /^[@~]?(\d+)([a-z])$/
         
     | 
| 
      
 14 
     | 
    
         
            +
            			size = $1.to_i
         
     | 
| 
      
 15 
     | 
    
         
            +
            			unit = {"d" => :days, "w" => :weeks, "m" => :months, "y" => :years}[$2]
         
     | 
| 
      
 16 
     | 
    
         
            +
            			new(size: size, unit: unit)
         
     | 
| 
      
 17 
     | 
    
         
            +
            		else
         
     | 
| 
      
 18 
     | 
    
         
            +
            			raise ArgumentError, "Unrecognised review period format: \"#{str}\"."
         
     | 
| 
      
 19 
     | 
    
         
            +
            		end
         
     | 
| 
      
 20 
     | 
    
         
            +
            	end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            	def initialize(size:0, unit: :months)
         
     | 
| 
      
 23 
     | 
    
         
            +
            		self.size = size
         
     | 
| 
      
 24 
     | 
    
         
            +
            		self.unit = unit
         
     | 
| 
      
 25 
     | 
    
         
            +
            	end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            	def unit= value
         
     | 
| 
      
 28 
     | 
    
         
            +
            		raise ArgumentError, "Tried to set ReviewPeriod.unit to invalid value \"#{value}\"." unless ALLOWED_UNITS.include?(value)
         
     | 
| 
      
 29 
     | 
    
         
            +
            		@unit = value
         
     | 
| 
      
 30 
     | 
    
         
            +
            		@short_unit = nil
         
     | 
| 
      
 31 
     | 
    
         
            +
            	end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            	def short_unit
         
     | 
| 
      
 34 
     | 
    
         
            +
            		@short_unit ||= @unit.to_s[0]
         
     | 
| 
      
 35 
     | 
    
         
            +
            	end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            	def to_s
         
     | 
| 
      
 38 
     | 
    
         
            +
            		"#{size}#{short_unit}"
         
     | 
| 
      
 39 
     | 
    
         
            +
            	end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            	alias_method :inspect, :to_s
         
     | 
| 
      
 42 
     | 
    
         
            +
            	alias_method :to_serial, :to_s
         
     | 
| 
      
 43 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,36 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Rubyfocus::XMLTranslator
         
     | 
| 
      
 2 
     | 
    
         
            +
            	class << self
         
     | 
| 
      
 3 
     | 
    
         
            +
            		VALID_NODE_NAMES = %w(string true false integer array)
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            		# Actual parsing method
         
     | 
| 
      
 6 
     | 
    
         
            +
            		def parse(node)
         
     | 
| 
      
 7 
     | 
    
         
            +
            			method_name = node.name
         
     | 
| 
      
 8 
     | 
    
         
            +
            			if VALID_NODE_NAMES.include?(method_name)
         
     | 
| 
      
 9 
     | 
    
         
            +
            				self.send(method_name, node)
         
     | 
| 
      
 10 
     | 
    
         
            +
            			else
         
     | 
| 
      
 11 
     | 
    
         
            +
            				raise RuntimeError, "Does not recognise node type: #{method_name}."
         
     | 
| 
      
 12 
     | 
    
         
            +
            			end
         
     | 
| 
      
 13 
     | 
    
         
            +
            		end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            		# Individual parsing methods
         
     | 
| 
      
 16 
     | 
    
         
            +
            		def string(node)
         
     | 
| 
      
 17 
     | 
    
         
            +
            			node.inner_html
         
     | 
| 
      
 18 
     | 
    
         
            +
            		end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            		def true(node)
         
     | 
| 
      
 21 
     | 
    
         
            +
            			true
         
     | 
| 
      
 22 
     | 
    
         
            +
            		end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            		def false(node)
         
     | 
| 
      
 25 
     | 
    
         
            +
            			false
         
     | 
| 
      
 26 
     | 
    
         
            +
            		end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            		def integer(node)
         
     | 
| 
      
 29 
     | 
    
         
            +
            			node.inner_html.to_i
         
     | 
| 
      
 30 
     | 
    
         
            +
            		end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            		def array(node)
         
     | 
| 
      
 33 
     | 
    
         
            +
            			node.children.map{ |child| parse(child) }
         
     | 
| 
      
 34 
     | 
    
         
            +
            		end
         
     | 
| 
      
 35 
     | 
    
         
            +
            	end
         
     | 
| 
      
 36 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/rubyfocus.rb
    ADDED
    
    | 
         @@ -0,0 +1,16 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "time"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "nokogiri"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "zip"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "yaml"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "httparty"
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Rubyfocus; end
         
     | 
| 
      
 8 
     | 
    
         
            +
            # Require library files
         
     | 
| 
      
 9 
     | 
    
         
            +
            Dir[File.join(__dir__, "rubyfocus/includes/*")].each{ |f| require f }
         
     | 
| 
      
 10 
     | 
    
         
            +
            %w(fetcher local_fetcher oss_fetcher).each{ |f| require File.join(__dir__, "rubyfocus/fetchers", f) }
         
     | 
| 
      
 11 
     | 
    
         
            +
            Dir[File.join(__dir__, "rubyfocus/*.rb")].each{ |f| require f }
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            # Need to load items in a specific order
         
     | 
| 
      
 14 
     | 
    
         
            +
            %w(item named_item ranked_item task project context folder setting).each do |f|
         
     | 
| 
      
 15 
     | 
    
         
            +
            	require File.join(__dir__, "rubyfocus/items", f)
         
     | 
| 
      
 16 
     | 
    
         
            +
            end
         
     |