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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ba866c7a553641c90dd8804d86c198a7cd720c3e
|
4
|
+
data.tar.gz: 58c47389f885a131eb300e18dd57aa5aed6eb197
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba9aac7051986e1aa922a7077d388aae5202866a2342c3b64554debddf133da49cf3fc3042caec5e506a9e886aed41f66c90e53c4c9a5c5bd15d13cadaa8f403
|
7
|
+
data.tar.gz: 870cc1ccc452e2deecf1418a49500a3f966dbc88521161c96647f7ff7f19002720e504d4ad90ae8e2ce365864e385d63ae4cad9fc7b39ba45a66ce26c2fb2c36
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-c
|
data/Manifest
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# INCLUDE bin/**/*
|
2
|
+
# INCLUDE lib/**/*
|
3
|
+
# INCLUDE *
|
4
|
+
# EXCLUDE **/.DS_Store
|
5
|
+
# EXCLUDE .rarity
|
6
|
+
# EXCLUDE stage_ofocus.rb
|
7
|
+
# EXCLUDE spec/files/ofocus_staging
|
8
|
+
|
9
|
+
.gitignore
|
10
|
+
.rspec
|
11
|
+
lib/rubyfocus.rb
|
12
|
+
lib/rubyfocus/document.rb
|
13
|
+
lib/rubyfocus/errors.rb
|
14
|
+
lib/rubyfocus/fetchers/fetcher.rb
|
15
|
+
lib/rubyfocus/fetchers/local_fetcher.rb
|
16
|
+
lib/rubyfocus/fetchers/oss_fetcher.rb
|
17
|
+
lib/rubyfocus/includes/conditional_exec.rb
|
18
|
+
lib/rubyfocus/includes/idref.rb
|
19
|
+
lib/rubyfocus/includes/parser.rb
|
20
|
+
lib/rubyfocus/includes/searchable.rb
|
21
|
+
lib/rubyfocus/items/context.rb
|
22
|
+
lib/rubyfocus/items/folder.rb
|
23
|
+
lib/rubyfocus/items/item.rb
|
24
|
+
lib/rubyfocus/items/named_item.rb
|
25
|
+
lib/rubyfocus/items/project.rb
|
26
|
+
lib/rubyfocus/items/ranked_item.rb
|
27
|
+
lib/rubyfocus/items/setting.rb
|
28
|
+
lib/rubyfocus/items/task.rb
|
29
|
+
lib/rubyfocus/location.rb
|
30
|
+
lib/rubyfocus/patch.rb
|
31
|
+
lib/rubyfocus/review_period.rb
|
32
|
+
lib/rubyfocus/xml_translator.rb
|
33
|
+
Manifest
|
34
|
+
README.md
|
35
|
+
rubyfocus.gemspec
|
36
|
+
version.txt
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
Rubyfocus is a one-way (read-only) ruby bridge to OmniFocus. Analyse, store, inspect, or play with your projects and tasks in OmniFocus from the comfort and flexibility of ruby!
|
2
|
+
|
3
|
+
# Installation
|
4
|
+
|
5
|
+
Rubyfocus is a ruby gem. It's not currently hosted on rubygems, though, so you'll have to download and install locally.
|
6
|
+
|
7
|
+
## Via git
|
8
|
+
|
9
|
+
First download rubyfocus to your computer:
|
10
|
+
|
11
|
+
```
|
12
|
+
git clone https://github.com/jyruzicka/rubyfocus.git
|
13
|
+
```
|
14
|
+
|
15
|
+
Now build and install it!
|
16
|
+
|
17
|
+
```
|
18
|
+
gem build rubyfocus.gemspec
|
19
|
+
gem install rubyfocus-0.1.0.gem
|
20
|
+
```
|
21
|
+
|
22
|
+
# Usage
|
23
|
+
|
24
|
+
## Getting set up
|
25
|
+
|
26
|
+
To create a new database from your local OmniFocus install:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require "rubyfocus"
|
30
|
+
|
31
|
+
f = Rubyfocus::LocalFetcher.new # This class lets you access a local OmniFocus install
|
32
|
+
d = Rubyfocus::Document.new(f) # This is how we create a document linked up to a local fetcher
|
33
|
+
|
34
|
+
d.update # This is how we get the document to update using its built-in fetcher
|
35
|
+
|
36
|
+
d.save("ofocus.yml") # Save the whole database to yaml!
|
37
|
+
```
|
38
|
+
|
39
|
+
To open it up again, it's even easier:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require "rubyfocus"
|
43
|
+
|
44
|
+
d = Rubyfocus::Document.load_from_file("ofocus.yml") # Your document will remember everything
|
45
|
+
d.update # Updates it against the local cache, in case you made changes
|
46
|
+
```
|
47
|
+
|
48
|
+
## Grabbing data
|
49
|
+
|
50
|
+
How do you access all that lovely data? Easy!
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
d.projects
|
54
|
+
d.tasks
|
55
|
+
d.contexts
|
56
|
+
d.folders
|
57
|
+
```
|
58
|
+
|
59
|
+
And if you want to select a certain project's tasks:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
d.projects.first.tasks
|
63
|
+
```
|
64
|
+
|
65
|
+
What if you want to select only certain projects? Sure, you can use standard `Array#find` or `Array#select` methods, but you can also make use of a hash of values:
|
66
|
+
|
67
|
+
```
|
68
|
+
d.projects.select(name: "Sample project")
|
69
|
+
```
|
70
|
+
|
71
|
+
Once you have your objects, you can query them for more information:
|
72
|
+
|
73
|
+
```
|
74
|
+
t = d.tasks.first
|
75
|
+
t.name # => "Sample task"
|
76
|
+
t.project.name # => "Sample project"
|
77
|
+
t.deferred? # => true/false
|
78
|
+
t.start # => Time or nil
|
79
|
+
```
|
80
|
+
|
81
|
+
## SyncServer-ing
|
82
|
+
|
83
|
+
To access an instance of the [Omni Sync Server](http://sync.omnigroup.com), use an `OSSFetcher` object:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
f = Rubyfocus::OSSFetcher.new(my_username, my_password)
|
87
|
+
d = Rubyfocus::Document.new(f)
|
88
|
+
d.update
|
89
|
+
```
|
90
|
+
|
91
|
+
If you use the Omni Sync Server, you'll definitely want to save your data locally and just update what's necessary - it takes a while to download and apply all those files.
|
92
|
+
|
93
|
+
# Behind the scenes
|
94
|
+
|
95
|
+
OmniFocus stores its data as a "base" XML file plus a series of "patches". These are all stored as zip files inside an ".ofocus" package. This means that you can have several different devices, all storing the OmniFocus task database at different states, and each one can easily update its database by comparing the various patches against its own database and downloading/applying only what's needed.
|
96
|
+
|
97
|
+
Rubyfocus makes use of this by fetching and reading OmniFocus' local store on your machine. As long as your local OmniFocus is up to date, `rubyfocus` will be able to fetch the database in its latest state.
|
98
|
+
|
99
|
+
# Further work
|
100
|
+
|
101
|
+
`rubyfocus` is a work in progress. In the near future I hope to release a more comprehensive document detailing exactly which details of OmniFocus' projects and tasks are available to the user, and what you can do with them.
|
102
|
+
|
103
|
+
Other goals include:
|
104
|
+
|
105
|
+
* Updating via the [Omni Sync Server](https://manage.sync.omnigroup.com/) rather than relying on local files.
|
106
|
+
* A couple of example projects using rubyfocus
|
107
|
+
|
108
|
+
# History
|
109
|
+
|
110
|
+
## 0.3.0 // 2015-10-17
|
111
|
+
|
112
|
+
* [New] Now supports remote syncing with the Omni Sync Server!
|
113
|
+
|
114
|
+
## 0.2.0 // 2015-10-11
|
115
|
+
|
116
|
+
* [Bugfix] Will now turn tasks into projects and projects into tasks if the user has done this in OmniFocus.
|
117
|
+
* [Bugfix] Rubyfocus::Patch now does patch application, rather than delegating to the Fetcher.
|
118
|
+
|
119
|
+
## 0.1.0 // 2015-10-10
|
120
|
+
|
121
|
+
* Hello, world!
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# The Document is how rubyfocus stores an OmniFocus document, both locally and otherwise.
|
2
|
+
# A Document contains a number of arrays of contexts, settings, folders, projects, and tasks,
|
3
|
+
# and is also able to keep track of what patch it's up to, for updating.
|
4
|
+
#
|
5
|
+
# You can initialize a document through +Document.new(doc)+, where +doc+ is either an XML string, or
|
6
|
+
# a Nokogiri XML document (or +nil+). Alternatively, you can initialize through +Document.from_file(file)+,
|
7
|
+
# which reads the file and parses it as XML
|
8
|
+
#
|
9
|
+
# You add XML to the document by running +Documet::apply_xml(doc)+, which takes all children of the root
|
10
|
+
# XML node, tries to turn each child into a relevant object, and adds it to the document. This is done using
|
11
|
+
# the private +ivar_for+ method, as well as +add_element(e)+, which you can use to add individual objects.
|
12
|
+
class Rubyfocus::Document
|
13
|
+
include Rubyfocus::Searchable
|
14
|
+
# A number of arrays into which elements may fit
|
15
|
+
attr_reader :contexts, :settings, :folders, :projects, :tasks
|
16
|
+
|
17
|
+
# This is the identifier of the current patch level. This also determines
|
18
|
+
# which patches can be applied to the current document.
|
19
|
+
attr_accessor :patch_id
|
20
|
+
|
21
|
+
# This is the fetcher object, used to fetch new data
|
22
|
+
attr_accessor :fetcher
|
23
|
+
|
24
|
+
# Initalise with one of:
|
25
|
+
# * a Nokogiri document
|
26
|
+
# * a string
|
27
|
+
# * a fetcher subclass
|
28
|
+
def initialize(doc=nil)
|
29
|
+
%w(contexts settings projects folders tasks).each{ |s| instance_variable_set("@#{s}", Rubyfocus::SearchableArray.new) }
|
30
|
+
|
31
|
+
if doc
|
32
|
+
if doc.is_a?(String)
|
33
|
+
apply_xml(Nokogiri::XML(doc))
|
34
|
+
elsif doc.is_a?(Nokogiri::XML)
|
35
|
+
apply_xml(doc)
|
36
|
+
elsif doc.kind_of?(Rubyfocus::Fetcher)
|
37
|
+
self.fetcher = doc
|
38
|
+
base = Nokogiri::XML(doc.base)
|
39
|
+
self.apply_xml(base)
|
40
|
+
self.patch_id = doc.base_id
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#...or from file! If you provide it with an XML file, it'll load up without a fetcher.
|
46
|
+
def self.from_xml(file)
|
47
|
+
new(File.read(file))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Initialize from the local repo
|
51
|
+
def self.from_local
|
52
|
+
new(Rubyfocus::LocalFetcher.new)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Initialize with a URL, for remote fetching.
|
56
|
+
# Not implemented yet
|
57
|
+
def self.from_url(url)
|
58
|
+
raise RuntimeError, "Rubyfocus::Document.from_url not yet implemented."
|
59
|
+
# new(Rubyfocus::RemoteFetcher.new(url))
|
60
|
+
end
|
61
|
+
|
62
|
+
# Load from a a hash
|
63
|
+
def self.load_from_file(file_location)
|
64
|
+
d = YAML::load_file(file_location)
|
65
|
+
d.fetcher.reset
|
66
|
+
d
|
67
|
+
end
|
68
|
+
|
69
|
+
#---------------------------------------
|
70
|
+
# Use the linked fetcher to update the document
|
71
|
+
def update
|
72
|
+
if fetcher
|
73
|
+
fetcher.update_full(self)
|
74
|
+
else
|
75
|
+
raise RuntimeError, "Tried to update a document with no fetcher."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#-------------------------------------------------------------------------------
|
80
|
+
# Apply XML!
|
81
|
+
def apply_xml(doc)
|
82
|
+
doc.root.children.select{ |e| !e.text? }.each do |node|
|
83
|
+
elem = Rubyfocus::Parser.parse(self, node)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Given an object, work out the correct instance variable for it to go into
|
88
|
+
def ivar_for(obj)
|
89
|
+
{
|
90
|
+
Rubyfocus::Project => @projects,
|
91
|
+
Rubyfocus::Context => @contexts,
|
92
|
+
Rubyfocus::Task => @tasks,
|
93
|
+
Rubyfocus::Folder => @folders,
|
94
|
+
Rubyfocus::Setting => @settings
|
95
|
+
}[obj.class]
|
96
|
+
end
|
97
|
+
private :ivar_for
|
98
|
+
|
99
|
+
# Add an element. Element should be a Project, Task, Context, Folder, or Setting
|
100
|
+
# We assume whoever does this will set document appropriately on the element
|
101
|
+
def add_element(e)
|
102
|
+
dest = ivar_for(e)
|
103
|
+
if dest
|
104
|
+
dest << e
|
105
|
+
else
|
106
|
+
raise ArgumentError, "You passed a #{e.class} to Document#add_element - I don't know what to do with this."
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Remove an element from the document.
|
111
|
+
# We assume whoever does this is smart enough to also set the element's #document value
|
112
|
+
# to nil
|
113
|
+
def remove_element(e)
|
114
|
+
e = self[e] if e.is_a?(String)
|
115
|
+
return if e.nil?
|
116
|
+
|
117
|
+
dest = ivar_for(e)
|
118
|
+
if dest
|
119
|
+
dest.delete(e)
|
120
|
+
else
|
121
|
+
raise ArgumentError, "You passed a #{e.class} to Document#remove_element - I don't know what to do with this."
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
#-------------------------------------------------------------------------------
|
126
|
+
# Searchable stuff
|
127
|
+
def elements
|
128
|
+
@tasks + @projects + @contexts + @folders + @settings
|
129
|
+
end
|
130
|
+
|
131
|
+
# For Searchable include
|
132
|
+
alias_method :array, :elements
|
133
|
+
|
134
|
+
#-------------------------------------------------------------------------------
|
135
|
+
# Find elements from id
|
136
|
+
def [] search_id
|
137
|
+
self.elements.find{ |elem| elem.id == search_id }
|
138
|
+
end
|
139
|
+
|
140
|
+
#---------------------------------------
|
141
|
+
# YAML export
|
142
|
+
|
143
|
+
def save(file)
|
144
|
+
File.open(file, "w"){ |io| io.puts YAML::dump(self) }
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# The Fetcher class is a class that fetches data from a place - generally either a file location or a web address.
|
2
|
+
# The Fetcher is initialized with (by default) no options, although subclasses of Fetcher may change this.
|
3
|
+
#
|
4
|
+
# Each Fetcher has two important methods:
|
5
|
+
#
|
6
|
+
# * Fetcher#base returns the text of the main xml file, upon which all patch are applied.
|
7
|
+
# * Fetcher#patch returns the text of each update file as an array.
|
8
|
+
#
|
9
|
+
# You may also use the Fetcher#each_patch to iterate through all updates.
|
10
|
+
#
|
11
|
+
# To patch a document, simply call +Fetcher#update_full()+, passing it the document to be patched.
|
12
|
+
# +Document#update+ will automatically send the fetcher the document.
|
13
|
+
|
14
|
+
class Rubyfocus::Fetcher
|
15
|
+
|
16
|
+
# This method is called when loading a fetcher from YAML
|
17
|
+
def init_with(coder)
|
18
|
+
raise RuntimeError, "Method Fetcher#init_with called for abstract class Fetcher"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the content of the base file
|
22
|
+
def base
|
23
|
+
raise RuntimeError, "Method Fetcher#base called for abstract class Fetcher."
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the id of the base
|
27
|
+
def base_id
|
28
|
+
raise RuntimeError, "Method Fetcher#base_id called for abstract class Fetcher."
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns an array of patch paths - either files or urls
|
32
|
+
def patches
|
33
|
+
raise RuntimeError, "Method Fetcher#patches called for abstract class Fetcher."
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the contents of a given patch
|
37
|
+
def patch(filename)
|
38
|
+
raise RuntimeError, "Method Fetcher#patch called for abstract class Fetcher."
|
39
|
+
end
|
40
|
+
|
41
|
+
#---------------------------------------
|
42
|
+
# Patching methods
|
43
|
+
|
44
|
+
# Update the document as far as we can
|
45
|
+
def update_full(document)
|
46
|
+
update_once(document) while can_patch?(document)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Update the document one step. Raises an error if the document cannot be updated.
|
50
|
+
def update_once(document)
|
51
|
+
np = self.next_patch(document)
|
52
|
+
if np
|
53
|
+
np.apply_to(document)
|
54
|
+
else
|
55
|
+
raise RuntimeError, "Patcher#update_once called, but I can't find a patch to apply!"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Can we patch this document? You should call this before update_onceing
|
60
|
+
def can_patch?(document)
|
61
|
+
!next_patch(document).nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Collect the next patch to the document. If more than one patch can be applied,
|
65
|
+
# apply the latest one.
|
66
|
+
def next_patch(document)
|
67
|
+
all_possible_patches = self.patches.select{ |patch| patch.can_patch?(document) }
|
68
|
+
return all_possible_patches.sort_by(&:time).last
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
#---------------------------------------
|
73
|
+
# Serialisation info
|
74
|
+
def encode_with(coder)
|
75
|
+
raise RuntimeError, "Fetcher#encode_with called on abstract class."
|
76
|
+
end
|
77
|
+
|
78
|
+
# Remove all cached information
|
79
|
+
def reset
|
80
|
+
@patches = nil
|
81
|
+
@base = nil
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class Rubyfocus::LocalFetcher < Rubyfocus::Fetcher
|
2
|
+
# This is where the files are usually stored
|
3
|
+
LOCATION = File.join(ENV["HOME"], "/Library/Containers/com.omnigroup.OmniFocus2/Data/Library/Application Support/OmniFocus/OmniFocus.ofocus")
|
4
|
+
|
5
|
+
#---------------------------------------
|
6
|
+
# Parent method overrides
|
7
|
+
|
8
|
+
# Init from yaml
|
9
|
+
def init_with(coder)
|
10
|
+
if coder["location"]
|
11
|
+
@location = coder["location"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Fetches the contents of the base file
|
16
|
+
def base
|
17
|
+
@base ||= begin
|
18
|
+
zip_file = Dir[File.join(self.location,"*.zip")].first
|
19
|
+
if zip_file
|
20
|
+
Zip::File.open(zip_file){ |z| z.get_entry("contents.xml").get_input_stream.read }
|
21
|
+
else
|
22
|
+
raise RuntimeError, "Rubyfocs::LocalFetcher looking for zip files at #{self.location}: none found."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Fetches the ID Of the base file
|
28
|
+
def base_id
|
29
|
+
base_file = File.basename(Dir[File.join(self.location,"*.zip")].first)
|
30
|
+
if base_file =~ /^\d+\=.*\+(.*)\.zip$/
|
31
|
+
$1
|
32
|
+
else
|
33
|
+
raise RuntimeError, "Malformed patch file #{base_file}."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Fetches a list of every patch file
|
38
|
+
def patches
|
39
|
+
@patches ||= Dir[File.join(self.location, "*.zip")][1..-1].map{ |f| Rubyfocus::Patch.new(self, File.basename(f)) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Fetches the contents of a given patch file
|
43
|
+
def patch(file)
|
44
|
+
filename = File.join(self.location, file)
|
45
|
+
if File.exists?(filename)
|
46
|
+
Zip::File.open(filename){ |z| z.get_entry("contents.xml").get_input_stream.read }
|
47
|
+
else
|
48
|
+
raise ArgumentError, "Trying to fetch patch #{file}, but file does not exist."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Save to disk
|
53
|
+
def encode_with(coder)
|
54
|
+
coder.map = {"location" => @location}
|
55
|
+
end
|
56
|
+
|
57
|
+
#---------------------------------------
|
58
|
+
# Location file setters and getters
|
59
|
+
def location
|
60
|
+
@location || LOCATION
|
61
|
+
end
|
62
|
+
|
63
|
+
def location= l
|
64
|
+
@location = l
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# This fetcher fetches data from the OmniSyncServer.
|
2
|
+
# NOTE: This does *not* register a client with the OmniSyncServer. As such, if you don't
|
3
|
+
# sync with the database regularly, you will likely lose track of the head and have to re-sync.
|
4
|
+
class Rubyfocus::OSSFetcher < Rubyfocus::Fetcher
|
5
|
+
# Used to log in to the Omni Sync Server. Also determines the URL of your file
|
6
|
+
attr_accessor :username
|
7
|
+
attr_accessor :password
|
8
|
+
|
9
|
+
# The engine used to fetch data. Defaults to HTTParty
|
10
|
+
attr_accessor :fetcher
|
11
|
+
|
12
|
+
#---------------------------------------
|
13
|
+
# Parent method overrides
|
14
|
+
|
15
|
+
# Initialise with username and password
|
16
|
+
def initialize(u,p)
|
17
|
+
@username = u
|
18
|
+
@password = p
|
19
|
+
@fetcher = HTTParty
|
20
|
+
end
|
21
|
+
|
22
|
+
# Init from yaml
|
23
|
+
def init_with(coder)
|
24
|
+
@username = coder["username"]
|
25
|
+
@password = coder["password"]
|
26
|
+
@fetcher = HTTParty
|
27
|
+
end
|
28
|
+
|
29
|
+
# Fetches the contents of the base file
|
30
|
+
def base
|
31
|
+
@base ||= if self.patches.size > 0
|
32
|
+
fetch_file(self.patches.first.file)
|
33
|
+
else
|
34
|
+
raise Rubyfocus::OSSFetcherError, "Looking for zip files at #{url}: none found."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Fetches the ID Of the base file
|
39
|
+
def base_id
|
40
|
+
if self.patches.size > 0
|
41
|
+
base_file = self.patches.first
|
42
|
+
if base_file.file =~ /^\d+\=.*\+(.*)\.zip$/
|
43
|
+
$1
|
44
|
+
else
|
45
|
+
raise Rubyfocus::OSSFetcherError, "Malformed patch file #{base_file}."
|
46
|
+
end
|
47
|
+
else
|
48
|
+
raise Rubyfocus::OSSFetcherError, "Looking for zip files at #{url}: none found."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Fetches a list of every patch file
|
53
|
+
def patches
|
54
|
+
@patches ||= begin
|
55
|
+
response = self.fetcher.get(url, digest_auth: auth).body
|
56
|
+
# Text is in first table, let's assume
|
57
|
+
table = response[/<table>(.*?)<\/table>/m,1]
|
58
|
+
if table
|
59
|
+
links = table.scan(/<a href="([^"]+)"/).flatten.select{ |f| f.end_with?(".zip") }
|
60
|
+
links.map{ |u| Rubyfocus::Patch.new(self,u) }
|
61
|
+
else
|
62
|
+
[]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Fetches the contents of a given patch file
|
68
|
+
def patch(file)
|
69
|
+
fetch_file(file)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Save to disk
|
73
|
+
def encode_with(coder)
|
74
|
+
coder.map = {
|
75
|
+
"username" => @username,
|
76
|
+
"password" => @password
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
#---------------------------------------
|
81
|
+
# Private aux methods
|
82
|
+
private
|
83
|
+
def auth
|
84
|
+
{username: @username, password: @password}
|
85
|
+
end
|
86
|
+
|
87
|
+
def url
|
88
|
+
"https://sync.omnigroup.com/#{@username}/OmniFocus.ofocus"
|
89
|
+
end
|
90
|
+
|
91
|
+
def fetch_file(f)
|
92
|
+
f = File.join(url,f)
|
93
|
+
data = self.fetcher.get(f, digest_auth: auth).body
|
94
|
+
io = StringIO.new(data)
|
95
|
+
Zip::InputStream.open(io) do |io|
|
96
|
+
while (entry = io.get_next_entry)
|
97
|
+
return io.read if entry.name == "contents.xml"
|
98
|
+
end
|
99
|
+
raise Rubyfocus::OSSFetcherError, "Malformed OmniFocus zip file #{zipfile}."
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Rubyfocus
|
2
|
+
module IDRef
|
3
|
+
module ClassMethods
|
4
|
+
def idref *names
|
5
|
+
names.each do |name|
|
6
|
+
name_id = "#{name}_id".to_sym
|
7
|
+
attr_accessor name_id
|
8
|
+
define_method(name) do
|
9
|
+
return document && document.find(send(name_id))
|
10
|
+
end
|
11
|
+
|
12
|
+
define_method("#{name}=") do |o|
|
13
|
+
if o.nil?
|
14
|
+
self.send("#{name}_id=", nil)
|
15
|
+
elsif o.respond_to?(:id)
|
16
|
+
self.send("#{name}_id=", o.id)
|
17
|
+
else
|
18
|
+
raise ArgumentError, "#{self.class}##{name}= called with argument #{o}, which does not respond to :id."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.included(mod)
|
26
|
+
mod.extend ClassMethods
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rubyfocus
|
2
|
+
module Parser
|
3
|
+
@subclasses = []
|
4
|
+
|
5
|
+
def self.parse(document, node)
|
6
|
+
matching_classes = @subclasses.select{ |klass| klass.matches_node?(node) }
|
7
|
+
|
8
|
+
# More than one matches? Take the most specific
|
9
|
+
if matching_classes.size > 1
|
10
|
+
classes_with_subclasses = matching_classes.select{ |c| matching_classes.any?{ |sc| sc < c } }
|
11
|
+
matching_classes = matching_classes - classes_with_subclasses
|
12
|
+
end
|
13
|
+
|
14
|
+
case matching_classes.size
|
15
|
+
when 0
|
16
|
+
nil
|
17
|
+
when 1
|
18
|
+
return matching_classes.first.new(document, node)
|
19
|
+
else
|
20
|
+
raise RuntimeError, "Node #{node.inspect} matches more than one Rubyfocus::Item subclass."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(mod)
|
25
|
+
@subclasses << mod
|
26
|
+
mod.extend ClassMethods
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
# Filler method
|
31
|
+
def matches_node?(node)
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|