Class: Rubyfocus::Document

Inherits:
Object
  • Object
show all
Includes:
Searchable
Defined in:
lib/rubyfocus/document.rb

Overview

The Document is how rubyfocus stores an OmniFocus document, both locally and otherwise. A Document contains a number of arrays of contexts, settings, folders, projects, and tasks, and is also able to keep track of what patch it’s up to, for updating.

You can initialize a document through Document.new(doc), where doc is either an XML string, or a Nokogiri XML document (or nil). Alternatively, you can initialize through Document.from_file(file), which reads the file and parses it as XML

You add XML to the document by running Document::apply_xml(doc), which takes all children of the root XML node, tries to turn each child into a relevant object, and adds it to the document. This is done using the private ivar_for method, as well as add_element(e), which you can use to add individual objects.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Searchable

#find, #select

Constructor Details

#initialize(doc = nil) ⇒ Document

Initalise with one of:

  • a Nokogiri document

  • a string

  • a fetcher subclass



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/rubyfocus/document.rb', line 28

def initialize(doc=nil)
	%w(contexts settings projects folders tasks).each{ |s| instance_variable_set("@#{s}", Rubyfocus::SearchableArray.new) }

	if doc
		if doc.is_a?(String)
			apply_xml(Nokogiri::XML(doc))
		elsif doc.is_a?(Nokogiri::XML)
			apply_xml(doc)
		elsif doc.kind_of?(Rubyfocus::Fetcher)
			self.fetcher = doc
			base = Nokogiri::XML(doc.base)
			self.apply_xml(base)
			self.patch_id = doc.base_id
		end
	end
end

Instance Attribute Details

#contextsObject (readonly)

A number of arrays into which elements may fit



15
16
17
# File 'lib/rubyfocus/document.rb', line 15

def contexts
  @contexts
end

#fetcherObject

This is the fetcher object, used to fetch new data



22
23
24
# File 'lib/rubyfocus/document.rb', line 22

def fetcher
  @fetcher
end

#foldersObject (readonly)

A number of arrays into which elements may fit



15
16
17
# File 'lib/rubyfocus/document.rb', line 15

def folders
  @folders
end

#patch_idObject

This is the identifier of the current patch level. This also determines which patches can be applied to the current document.



19
20
21
# File 'lib/rubyfocus/document.rb', line 19

def patch_id
  @patch_id
end

#projectsObject (readonly)

A number of arrays into which elements may fit



15
16
17
# File 'lib/rubyfocus/document.rb', line 15

def projects
  @projects
end

#settingsObject (readonly)

A number of arrays into which elements may fit



15
16
17
# File 'lib/rubyfocus/document.rb', line 15

def settings
  @settings
end

#tasksObject (readonly)

A number of arrays into which elements may fit



15
16
17
# File 'lib/rubyfocus/document.rb', line 15

def tasks
  @tasks
end

Class Method Details

.from_localObject

Initialize from the local repo



51
52
53
# File 'lib/rubyfocus/document.rb', line 51

def self.from_local
	new(Rubyfocus::LocalFetcher.new)
end

.from_url(url) ⇒ Object

Initialize with a URL, for remote fetching. Not implemented yet TODO implement

Raises:

  • (RuntimeError)


57
58
59
60
# File 'lib/rubyfocus/document.rb', line 57

def self.from_url(url)
	raise RuntimeError, "Rubyfocus::Document.from_url not yet implemented."
	# new(Rubyfocus::RemoteFetcher.new(url))
end

.from_xml(file) ⇒ Object

…or from file! If you provide it with an XML file, it’ll load up without a fetcher.



46
47
48
# File 'lib/rubyfocus/document.rb', line 46

def self.from_xml(file)
	new(File.read(file))
end

.load_from_file(file_location) ⇒ Object

Load from a a hash



63
64
65
66
67
# File 'lib/rubyfocus/document.rb', line 63

def self.load_from_file(file_location)
	d = YAML::load_file(file_location)
	d.fetcher.reset
	d
end

Instance Method Details

#[](search_id) ⇒ Object


Find elements from id



209
210
211
# File 'lib/rubyfocus/document.rb', line 209

def [] search_id
	self.elements.find{ |elem| elem.id == search_id }
end

#add_element(e, overwrite: false) ⇒ Object

Add an element. Element should be a Project, Task, Context, Folder, or Setting. If overwrite set to false and ID already occurs in the document, throw an error. If ID is nil, throw an error.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/rubyfocus/document.rb', line 103

def add_element(e, overwrite:false)
	# Error check
	raise(Rubyfocus::DocumentElementException, "Adding element to document, but it has no ID.") if e.id.nil?
	raise(Rubyfocus::DocumentElementException, "Adding element to document, but element with this ID already exists.") if !overwrite && has_id?(e.id)

	# Otherwise, full steam ahead
	e.document = self

	if (dupe_element = self[e.id]) && overwrite
		remove_element(dupe_element)
	end

	# Add to the correct array
	dest = ivar_for(e)
	if dest
		dest << e
	else
		raise ArgumentError, "You passed a #{e.class} to Document#add_element - I don't know what to do with this."
	end
end

#apply_xml(doc) ⇒ Object


Apply XML!



82
83
84
85
86
# File 'lib/rubyfocus/document.rb', line 82

def apply_xml(doc)
	doc.root.children.select{ |e| !e.text? }.each do |node|
		elem = Rubyfocus::Parser.parse(self, node)
	end
end

#elementsObject Also known as: array


Searchable stuff



199
200
201
# File 'lib/rubyfocus/document.rb', line 199

def elements
	@tasks + @projects + @contexts + @folders + @settings
end

#has_id?(id) ⇒ Boolean

Check if the document has an element of a given ID

Returns:

  • (Boolean)


214
215
216
# File 'lib/rubyfocus/document.rb', line 214

def has_id?(id)
	self.elements.any?{ |e| e.id == id }
end

#overwrite_element(node) ⇒ Object

Update an element in-place by creating a new element, deleting the old, and adding the new. This method is chiefly used for patching OF documents using V1 patches. Properties not explicitly mentioned in the patch are reverted to their default values. This method also takes into account:

  • new nodes (i.e. silently creates if required)

  • tasks upgraded to projects (if task has a <project> element)

  • projects downgraded to tasks (if project has no <project> element)

Note that unlike add_element, this takes pure XML



190
191
192
193
194
195
# File 'lib/rubyfocus/document.rb', line 190

def overwrite_element(node)
	element = self[node["id"]]
	self.remove_element(element) if element

	Rubyfocus::Parser.parse(self, node)
end

#remove_element(e) ⇒ Object

Remove an element from the document.



125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/rubyfocus/document.rb', line 125

def remove_element(e)
	e = self[e] if e.is_a?(String)
	return if e.nil?

	e.document = nil

	dest = ivar_for(e)
	if dest
		dest.delete(e)
	else
		raise ArgumentError, "You passed a #{e.class} to Document#remove_element - I don't know what to do with this."	
	end
end

#save(file) ⇒ Object


YAML export



221
222
223
# File 'lib/rubyfocus/document.rb', line 221

def save(file)
	File.open(file, "w"){ |io| io.puts YAML::dump(self) }
end

#updateObject


Use the linked fetcher to update the document



71
72
73
74
75
76
77
78
# File 'lib/rubyfocus/document.rb', line 71

def update
	if fetcher
		raise RuntimeError, "Rubyfocus cannot currently read encrypted databases." if fetcher.encrypted?
		fetcher.update_full(self)
	else
		raise RuntimeError, "Tried to update a document with no fetcher."
	end
end

#update_element(node) ⇒ Object

Update an element in-place by applying xml. This method also takes into account:

  • new nodes (i.e. silently creates if required)

  • tasks upgraded to projects (if task has a non-empty <project> element)

  • projects downgraded to tasks (if project has an empty <project> element)

Note that unlike add_element, this takes pure XML



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/rubyfocus/document.rb', line 144

def update_element(node)
	element = self[node["id"]]

	# Does element already exist?
	if element
		# Quick check: is it a task being upgraded to a project?
		# Upgrade criteria: non-empty project tag
		if(
			element.class == Rubyfocus::Task &&
			(node / "project *").size > 0
		)

			# Upgrade
			new_node = element.to_project
			new_node.apply_xml(node)
			add_element(new_node, overwrite:true)
		# or is the project being downgraded to a task?
		# Downgrade criteria: presence of an empty project tag
		elsif(
			element.class == Rubyfocus::Project &&
			(node / "project").size > 0 &&
			(node / "project *").size == 0
			)

			# Downgrade
			new_node = element.to_task
			new_node.apply_xml(node)
			add_element(new_node, overwrite:true)
		else
			# Update in-place
			element.apply_xml(node)
		end
	else
		# Create a new node and add it
		Rubyfocus::Parser.parse(self,node)
	end
end