icalendar 1.5.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7f4b5d7252174baceb4b7e37a64286aaa38523ff
4
- data.tar.gz: bfa01a3b7d32a7216f82a63f3b216ddfc2bc8745
3
+ metadata.gz: 44c08a1f244a35efd4affc4705028dfe9798f707
4
+ data.tar.gz: f67a4772936897ed6fed5efd4d3d6d8f6c564ac9
5
5
  SHA512:
6
- metadata.gz: a79833843dcf6a77bc4d4135e25b1bdaa436db93c4195ef76a40e3e8f00fc01dbf28c24871bac057bf362eea201b066b655a2fe16215da5a32576221d3393658
7
- data.tar.gz: a2a2d04e8784be3f057cc9f23adcf45ec365282d035cb8655701fe6d08c531381dc7c0fceb8fb42948f8226042d20b1f7cee56a51e3d0ac46dcf1500cd387cfa
6
+ metadata.gz: 336fc24e516f61e887be407e55c5670475c965770a13e42be3b97fc1843cb02f5a21f40cba4aa76887f868c51637c0d9510548748f4dce34d8ff0d62579d0529
7
+ data.tar.gz: aeb52da8e30cea70b4faaa2b931b827e19952837f85c2403959859702fa2b80213a0ef4d05d0c7d45ac0ab99bc90b7fa6f96e36e5bc7d1c163d1f6ac4dca06ee
data/History.txt CHANGED
@@ -1,3 +1,7 @@
1
+ === 1.5.1 2014-02-27
2
+ * Check for dtend existence before setting timezone - Jonas Grau
3
+ * Clean up and refactor components - Kasper Timm Hansen
4
+
1
5
  === 1.5.0 2013-12-06
2
6
  * Support for custom x- properties - Jake Craige
3
7
 
data/README.md CHANGED
@@ -6,6 +6,24 @@ iCalendar -- Internet calendaring, Ruby style
6
6
 
7
7
  <http://github.com/icalendar/icalendar>
8
8
 
9
+ 2.x Status
10
+ ---
11
+
12
+ iCalendar 2.0 is under active development, and can be followed in the
13
+ [2.0beta branch](https://github.com/icalendar/icalendar/tree/2.0beta).
14
+
15
+ iCalendar 1.x (currently the master branch) will still survive for a
16
+ while, but will only be accepting bug fixes from this point forward
17
+ unless someone else wants to take over more active maintainership of
18
+ the 1.x series.
19
+
20
+ ### 2.0 Goals ###
21
+
22
+ * Implements [RFC 5545](http://tools.ietf.org/html/rfc5545)
23
+ * More obvious access to parameters and values
24
+ * Cleaner & easier timezone support
25
+
26
+
9
27
  DESCRIPTION
10
28
  ---
11
29
 
@@ -92,7 +110,7 @@ ALARMS
92
110
 
93
111
  ### Within an event ###
94
112
 
95
- cal.event.do
113
+ cal.event do
96
114
  # ...other event properties
97
115
  alarm do
98
116
  action "EMAIL"
@@ -186,6 +204,31 @@ TIMEZONES
186
204
  # END:STANDARD
187
205
  # END:VTIMEZONE
188
206
 
207
+ iCalendar has some basic support for creating VTIMEZONE blocks from timezone information pulled from `tzinfo`. You must require `tzinfo` support manually to take advantage, and iCalendar only supports `tzinfo` with versions `~> 0.3`
208
+
209
+ #### Example ####
210
+
211
+ require 'tzinfo'
212
+ require 'icalendar/tzinfo'
213
+
214
+ cal = Calendar.new
215
+
216
+ event_start = DateTime.new 2008, 12, 29, 8, 0, 0
217
+ event_end = DateTime.new 2008, 12, 29, 11, 0, 0
218
+
219
+ tzid = "America/Chicago"
220
+ tz = TZInfo::Timezone.get tzid
221
+ timezone = tz.ical_timezone event_start
222
+ cal.add timezone
223
+
224
+ cal.event do
225
+ dtstart event_start.tap { |d| d.ical_params = {'TZID' => tzid} }
226
+ dtend event_end.tap { |d| d.ical_params = {'TZID' => tzid} }
227
+ summary "Meeting with the man."
228
+ description "Have a long lunch meeting and decide nothing..."
229
+ end
230
+
231
+
189
232
  Unicode
190
233
  ---
191
234
 
data/Rakefile CHANGED
@@ -8,3 +8,11 @@ Rake::TestTask.new do |t|
8
8
  end
9
9
 
10
10
  task default: [:test, :build]
11
+
12
+ task :console do
13
+ require 'irb'
14
+ require 'irb/completion'
15
+ require 'icalendar'
16
+ ARGV.clear
17
+ IRB.start
18
+ end
@@ -11,7 +11,7 @@ require 'logger'
11
11
 
12
12
  module Icalendar #:nodoc:
13
13
 
14
- VERSION = '1.5.0'
14
+ VERSION = '1.5.1'
15
15
 
16
16
  # A simple error class to differentiate iCalendar library exceptions
17
17
  # from ruby language exceptions or others.
@@ -25,88 +25,60 @@ module Icalendar
25
25
  self.version = "2.0" # Version of the specification
26
26
  end
27
27
 
28
- def print_component
29
- "BEGIN:#{@name.upcase}\r\n" +
30
- "VERSION:#{version}\r\n" +
31
-
32
- # Then the properties
33
- print_properties(@properties.select { |k,v| k != 'version' }) +
34
-
35
- # sub components
36
- yield +
28
+ def print_headers
29
+ "VERSION:#{version}\r\n"
30
+ end
37
31
 
38
- # End of this component
39
- "END:#{@name.upcase}\r\n"
32
+ def properties_to_print
33
+ @properties.select { |k,v| k != 'version' }
40
34
  end
41
35
 
42
36
  def event(&block)
43
- e = Event.new
44
- # Note: I'm not sure this is the best way to pass this down, but it works
45
- e.tzid = self.timezones[0].tzid if self.timezones.length > 0
46
-
47
- self.add_component e
48
-
49
- if block
50
- e.instance_eval(&block)
51
- if e.tzid
52
- e.dtstart.ical_params = { "TZID" => e.tzid }
53
- e.dtend.ical_params = { "TZID" => e.tzid }
37
+ calendar_tzid = timezone_id
38
+ build_component Event.new do
39
+ # Note: I'm not sure this is the best way to pass this down, but it works
40
+ self.tzid = calendar_tzid
41
+
42
+ if block
43
+ instance_eval(&block)
44
+ if tzid
45
+ dtstart.ical_params = { "TZID" => e.tzid }
46
+ dtend.ical_params = { "TZID" => e.tzid } unless dtend.nil?
47
+ end
54
48
  end
55
49
  end
56
-
57
- e
58
50
  end
59
51
 
60
52
  def find_event(uid)
61
- self.events.find {|e| e.uid == uid}
53
+ events.find {|e| e.uid == uid}
62
54
  end
63
55
 
64
56
  def todo(&block)
65
- e = Todo.new
66
- self.add_component e
67
-
68
- e.instance_eval(&block) if block
69
-
70
- e
57
+ build_component Todo.new, &block
71
58
  end
72
59
 
73
60
  def find_todo(uid)
74
- self.todos.find {|t| t.uid == uid}
61
+ todos.find {|t| t.uid == uid}
75
62
  end
76
63
 
77
64
  def journal(&block)
78
- e = Journal.new
79
- self.add_component e
80
-
81
- e.instance_eval(&block) if block
82
-
83
- e
65
+ build_component Journal.new, &block
84
66
  end
85
67
 
86
68
  def find_journal(uid)
87
- self.journals.find {|j| j.uid == uid}
69
+ journals.find {|j| j.uid == uid}
88
70
  end
89
71
 
90
72
  def freebusy(&block)
91
- e = Freebusy.new
92
- self.add_component e
93
-
94
- e.instance_eval(&block) if block
95
-
96
- e
73
+ build_component Freebusy.new, &block
97
74
  end
98
75
 
99
76
  def find_freebusy(uid)
100
- self.freebusys.find {|f| f.uid == uid}
77
+ freebusys.find {|f| f.uid == uid}
101
78
  end
102
79
 
103
80
  def timezone(&block)
104
- e = Timezone.new
105
- self.add_component e
106
-
107
- e.instance_eval(&block) if block
108
-
109
- e
81
+ build_component Timezone.new, &block
110
82
  end
111
83
 
112
84
  # The "PUBLISH" method in a "VEVENT" calendar component is an
@@ -122,6 +94,17 @@ module Icalendar
122
94
  self.ip_method = "PUBLISH"
123
95
  end
124
96
 
97
+ private
98
+
99
+ def build_component(component, &block)
100
+ add_component component
101
+ component.instance_eval(&block) if block
102
+ component
103
+ end
104
+
105
+ def timezone_id
106
+ timezones[0].tzid if timezones.length > 0
107
+ end
125
108
  end # class Calendar
126
109
 
127
110
  end # module Icalendar
@@ -49,7 +49,7 @@ module Icalendar
49
49
 
50
50
  def initialize(name)
51
51
  @name = name
52
- @components = Hash.new([])
52
+ @components = Hash.new { |h, k| h[k] = [] }
53
53
  @properties = {}
54
54
 
55
55
  @@logger.info("New #{@name[1,@name.size].capitalize}...")
@@ -57,13 +57,7 @@ module Icalendar
57
57
 
58
58
  # Add a sub-component to the current component object.
59
59
  def add_component(component)
60
- key = (component.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
61
-
62
- unless @components.has_key? key
63
- @components[key] = []
64
- end
65
-
66
- @components[key] << component
60
+ @components[component.key_name] << component
67
61
  end
68
62
 
69
63
  # Add a component to the calendar.
@@ -79,11 +73,7 @@ module Icalendar
79
73
  alias add_journal add_component
80
74
 
81
75
  def remove_component(component)
82
- key = (component.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
83
-
84
- if @components.has_key? key
85
- @components[key].delete(component)
86
- end
76
+ @components[component.key_name].delete(component)
87
77
  end
88
78
 
89
79
  # Remove a component from the calendar.
@@ -105,95 +95,82 @@ module Icalendar
105
95
 
106
96
  # Output in the icalendar format
107
97
  def to_ical
108
- print_component do
109
- s = ""
110
- @components.each do |key, comps|
111
- comps.each { |component| s << component.to_ical }
112
- end
113
- s
98
+ printer do
99
+ [print_headers,
100
+ print_properties,
101
+ print_subcomponents].join
114
102
  end
115
103
  end
116
104
 
117
105
  # Print this icalendar component
118
106
  def print_component
119
- # Begin a new component
120
- "BEGIN:#{@name.upcase}\r\n" +
121
-
122
- # Then the properties
123
- print_properties +
124
-
125
- # sub components
126
- yield +
107
+ to_ical
108
+ end
127
109
 
128
- # End of this component
129
- "END:#{@name.upcase}\r\n"
110
+ def print_subcomponents
111
+ @components.values.map do |component_parts|
112
+ Array(component_parts).map &:to_ical
113
+ end.join
130
114
  end
131
115
 
132
- def print_properties(properties = @properties)
133
- s = ""
116
+ def printer
117
+ ["BEGIN:#{@name.upcase}\r\n",
118
+ yield,
119
+ "END:#{@name.upcase}\r\n"].join
120
+ end
134
121
 
135
- properties.sort.each do |key,val|
136
- # Take out underscore for property names that conflicted
137
- # with built-in words.
138
- if key =~ /ip_.*/
139
- key = key[3..-1]
122
+ def print_properties(properties = properties_to_print)
123
+ excludes = %w(geo rrule categories exdate)
124
+ properties.sort.map do |key, val|
125
+ property = fix_conflict_with_built_in(key)
126
+ prelude = property.gsub(/_/, '-').upcase
127
+ params = print_parameters(val)
128
+
129
+ value = ":#{val.to_ical}"
130
+ multiline = multiline_property?(property)
131
+ if multiline || (!multiline && !excludes.include?(property))
132
+ value = escape_chars(value)
140
133
  end
141
134
 
142
- # Property name
143
- if !multiline_property?(key)
144
- prelude = "#{key.gsub(/_/, '-').upcase}#{print_parameters val}"
145
-
146
- # Property value
147
- value = ":#{val.to_ical}"
148
- value = escape_chars(value) unless %w[geo rrule categories exdate].include?(key)
149
- add_sliced_text(s, prelude + value)
150
- else
151
- prelude = "#{key.gsub(/_/, '-').upcase}"
152
- val.each do |v|
153
- params = print_parameters(v)
154
- value = ":#{v.to_ical}"
155
- value = escape_chars(value)
156
- add_sliced_text(s, prelude + params + value)
157
- end
158
- end
159
- end
160
- s
135
+ chunk_lines(prelude + params + value)
136
+ end.join
137
+ end
138
+
139
+ # Take out underscore for property names that conflicted
140
+ # with built-in words.
141
+ def fix_conflict_with_built_in(key)
142
+ key.sub(/\Aip_/, '')
161
143
  end
162
144
 
163
145
  def escape_chars(value)
164
- v = value.gsub("\\", "\\\\").gsub("\r\n", "\n").gsub("\r", "\n").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
165
- return v
166
- # return value
146
+ value.gsub("\\", "\\\\").gsub("\r\n", "\n").gsub("\r", "\n").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
167
147
  end
168
148
 
169
- def add_sliced_text(add_to,escaped)
170
- escaped = escaped.split('') # split is unicdoe-aware when `$KCODE = 'u'`
171
- add_to << escaped.slice!(0,MAX_LINE_LENGTH).join << "\r\n " while escaped.length != 0 # shift(MAX_LINE_LENGTH) does not work with ruby 1.8.6
172
- add_to.gsub!(/ *$/, '')
149
+ def chunk_lines(str, length = MAX_LINE_LENGTH, separator = "\r\n ")
150
+ chunks = str.scan(/.{1,#{length}}/)
151
+ lines = chunks.join(separator) << separator
152
+ lines.gsub(/ *$/, '')
173
153
  end
174
154
 
175
155
  # Print the parameters for a specific property.
176
156
  def print_parameters(value)
177
- s = ""
178
- return s unless value.respond_to?(:ical_params) && !value.ical_params.nil?
179
-
180
- value.ical_params.each do |key, val|
181
- s << ";#{key}"
182
- val = [ val ] unless val.is_a?(Array)
183
-
184
- # Possible parameter values
185
- unless val.empty?
186
- s << "="
187
- s << val.map do |pval|
188
- if pval.respond_to? :to_ical
189
- param = pval.to_ical
190
- param = %|"#{param}"| unless param =~ %r{\A#{Parser::QSTR}\z|\A#{Parser::PTEXT}\z}
191
- param
192
- end
193
- end.compact.join(',')
194
- end
195
- end
196
- s
157
+ return "" unless value.respond_to?(:ical_params)
158
+
159
+ Array(value.ical_params).map do |key, val|
160
+ val = Array(val)
161
+ next if val.empty?
162
+
163
+ escaped = val.map { |v| Parser.escape(v.to_ical) }.join(',')
164
+ ";#{key}=" << escaped
165
+ end.join
166
+ end
167
+
168
+ def properties_to_print
169
+ @properties # subclasses can exclude properties
170
+ end
171
+
172
+ def print_headers
173
+ "" # subclasses can specify headers
197
174
  end
198
175
 
199
176
  # TODO: Look into the x-property, x-param stuff...
@@ -214,6 +191,10 @@ module Icalendar
214
191
  # Make it protected so we can monitor usage...
215
192
  protected
216
193
 
194
+ def key_name
195
+ (self.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
196
+ end
197
+
217
198
  def self.ical_component(*syms)
218
199
  hash_accessor :@components, *syms
219
200
  end
@@ -108,7 +108,7 @@ module Icalendar
108
108
 
109
109
  # Now doing some basic initialization
110
110
  sequence 0
111
- timestamp DateTime.now
111
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
112
112
  uid new_uid
113
113
  end
114
114
 
@@ -31,7 +31,7 @@ module Icalendar
31
31
  def initialize()
32
32
  super("VFREEBUSY")
33
33
 
34
- timestamp DateTime.now
34
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
35
35
  uid new_uid
36
36
  end
37
37
  end
@@ -52,7 +52,7 @@ module Icalendar
52
52
  super("VJOURNAL")
53
53
 
54
54
  sequence 0
55
- timestamp DateTime.now
55
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
56
56
  uid new_uid
57
57
  end
58
58
 
@@ -45,19 +45,6 @@ module Icalendar
45
45
  @components[key] = component
46
46
  end
47
47
 
48
- # Also need a custom to_ical because typically it iterates over an array
49
- # of components.
50
- def to_ical
51
- print_component do
52
- s = ""
53
- @components.each_value do |comp|
54
- s << comp.to_ical
55
- end
56
- s
57
- end
58
- end
59
-
60
-
61
48
  def initialize(name = "VTIMEZONE")
62
49
  super(name)
63
50
  end
@@ -56,7 +56,7 @@ module Icalendar
56
56
  super("VTODO")
57
57
 
58
58
  sequence 0
59
- timestamp DateTime.now
59
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
60
60
  uid new_uid
61
61
  end
62
62
 
@@ -65,6 +65,19 @@ module Icalendar
65
65
  @@logger.debug("New Calendar Parser: #{@file.inspect}")
66
66
  end
67
67
 
68
+ def self.escape(value)
69
+ if value =~ %r{\A#{Parser::QSTR}\z|\A#{Parser::PTEXT}\z}
70
+ value
71
+ else
72
+ stripped = value.gsub '"', "'"
73
+ if stripped =~ /\A#{Parser::PTEXT}\z/
74
+ stripped
75
+ else
76
+ %|"#{stripped}"|
77
+ end
78
+ end
79
+ end
80
+
68
81
  # Define next line for an IO object.
69
82
  # Works for strings now with StringIO
70
83
  def next_line
@@ -45,6 +45,7 @@ class TestCalendar < Test::Unit::TestCase
45
45
 
46
46
  def test_create_multiple_event_calendar
47
47
  # Create a fresh calendar
48
+ Timecop.freeze DateTime.new(2013, 12, 26, 5, 0, 0, '+0000')
48
49
  cal = Calendar.new
49
50
  [1,2,3].each do |t|
50
51
  cal.event do
@@ -57,7 +58,49 @@ class TestCalendar < Test::Unit::TestCase
57
58
  self.summary = "test #{t} todo"
58
59
  end
59
60
  end
60
- cal.to_ical
61
+ expected_no_uid = <<-EXPECTED.gsub("\n", "\r\n")
62
+ BEGIN:VCALENDAR
63
+ VERSION:2.0
64
+ CALSCALE:GREGORIAN
65
+ PRODID:iCalendar-Ruby
66
+ BEGIN:VEVENT
67
+ DTEND:19970901T190000Z
68
+ DTSTAMP:20131226T050000Z
69
+ SEQUENCE:0
70
+ SUMMARY:This is summary 1
71
+ END:VEVENT
72
+ BEGIN:VEVENT
73
+ DTEND:19970902T190000Z
74
+ DTSTAMP:20131226T050000Z
75
+ SEQUENCE:0
76
+ SUMMARY:This is summary 2
77
+ END:VEVENT
78
+ BEGIN:VEVENT
79
+ DTEND:19970903T190000Z
80
+ DTSTAMP:20131226T050000Z
81
+ SEQUENCE:0
82
+ SUMMARY:This is summary 3
83
+ END:VEVENT
84
+ BEGIN:VTODO
85
+ DTSTAMP:20131226T050000Z
86
+ SEQUENCE:0
87
+ SUMMARY:test 1 todo
88
+ END:VTODO
89
+ BEGIN:VTODO
90
+ DTSTAMP:20131226T050000Z
91
+ SEQUENCE:0
92
+ SUMMARY:test 2 todo
93
+ END:VTODO
94
+ BEGIN:VTODO
95
+ DTSTAMP:20131226T050000Z
96
+ SEQUENCE:0
97
+ SUMMARY:test 3 todo
98
+ END:VTODO
99
+ END:VCALENDAR
100
+ EXPECTED
101
+ actual_no_uid = cal.to_ical.gsub /^UID:.*\r\n(?: .*\r\n)*/, ''
102
+ Timecop.return
103
+ assert_equal expected_no_uid, actual_no_uid
61
104
  end
62
105
 
63
106
  def test_find
@@ -40,9 +40,11 @@ class TestParameter < Test::Unit::TestCase
40
40
 
41
41
  def test_unquoted_property_parameters
42
42
  params = {'ALTREP' => ['"http://my.language.net"'],
43
- 'LANGUAGE' => ['SPANISH:CATILLAN']}
43
+ 'LANGUAGE' => ['SPANISH:CATILLAN'],
44
+ 'CN' => ['Joe "The Man" Tester']}
44
45
  expected_params = {'ALTREP' => ['"http://my.language.net"'],
45
- 'LANGUAGE' => ['"SPANISH:CATILLAN"']}
46
+ 'LANGUAGE' => ['"SPANISH:CATILLAN"'],
47
+ 'CN' => ["Joe 'The Man' Tester"]}
46
48
  @event.summary('This is a test summary.', params)
47
49
 
48
50
  assert_equal params, @event.summary.ical_params
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-07 00:00:00.000000000 Z
11
+ date: 2014-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake