calendar-assistant 0.8.0 → 0.9.0

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
  SHA256:
3
- metadata.gz: 0d3530bae8b1e8aac4260f546dd2fb2c3673b09c57d1c98e5f9e91bf91bdf5ed
4
- data.tar.gz: e7c80eca297a7aa6ce37abfae434507368c54764c1af4d4175a346e7a4cbf9f4
3
+ metadata.gz: 71db4f83ef4cb314c49b308e9fbf752b958823bb320aa7db5d404665e23c3eb9
4
+ data.tar.gz: 8bd30697df26b5a3975e9a06add5de2c49be64344a3787a09576d3b081bbe121
5
5
  SHA512:
6
- metadata.gz: b7dcd5d6d01422c2ee042f47f0fba9fad1886fa3b2d2d2b65c411289007d5b20d3c262b888d325036c1f4bb79bf4c4d405e47c206f467adcf1a02de77be26afc
7
- data.tar.gz: 34a1c905b4c45e5562639001721eacc977a5c3511f8527b986b9de294249e590a0e8ec0cebe689d22d2ffb0a6066f4565532d729a22491240a25d6ee6c4425a6
6
+ metadata.gz: b1701eb120de9b9ebdb5065695762eeca199b043e26554386feac9eae62b9ad9cd2b2aa6abe63e7de90716c5e55b74a7fa4e80bcc0056ab29f98c4d4ff840d56
7
+ data.tar.gz: 0f5f584849f62313666931d943f0b070d0e34d3388f94df57d6ca2b7751db6c23f2ef9ed65b691980ddfda2f54a3ebbd255a356186aae25c0eab25c24abd57a8
data/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - see views on your calendar events for a date or time range
10
10
  - book (and re-book) one-on-ones and other meetings automatically
11
11
 
12
+ [![Gem Version](https://badge.fury.io/rb/calendar-assistant.svg)](https://badge.fury.io/rb/calendar-assistant)
12
13
  [![Concourse CI](https://ci.nokogiri.org/api/v1/teams/calendar-assistants/pipelines/calendar-assistant/jobs/rake-spec/badge)](https://ci.nokogiri.org/teams/calendar-assistants/pipelines/calendar-assistant)
13
14
  [![Maintainability](https://api.codeclimate.com/v1/badges/3525792e1feeccfd8875/maintainability)](https://codeclimate.com/github/flavorjones/calendar-assistant/maintainability)
14
15
  [![Test Coverage](https://api.codeclimate.com/v1/badges/3525792e1feeccfd8875/test_coverage)](https://codeclimate.com/github/flavorjones/calendar-assistant/test_coverage)
@@ -119,10 +120,9 @@ Some commands, like `location-set`, will refer to you by nickname if you configu
119
120
 
120
121
  Set `nickname` to a string that would uniquely and briefly identify you to others, like "Mike D" or "JK".
121
122
 
122
-
123
123
  #### Location Emoji
124
124
 
125
- There is a `[settings]` key called `location-icons` that may be set to an array denoting all emoji that indicate a location event. The first icon in the array will be used as the default. The default emoji included are `["🗺 ", "🌎"]` with the map icon first
125
+ There is a `[settings]` key called `location-icon` that may be set to an emoji denoting a location event. By default CalendarAssistant will use `"🌎"`, but you can change this.
126
126
 
127
127
 
128
128
  #### Command-Specific Preferences
@@ -131,10 +131,12 @@ If there are user preferences you'd like to set for just a single command (e.g.,
131
131
 
132
132
  ```toml
133
133
  [settings]
134
- visibility = default
134
+ visibility = "default"
135
+ nickname = "uniquely-me"
135
136
 
136
137
  [settings.location_set]
137
- visibility = public
138
+ visibility = "public"
139
+ calendars = ["teamcalendar@group.calendar.google.com","teamcalendar2@group.calendar.google.com"]
138
140
  ```
139
141
 
140
142
 
@@ -162,7 +164,8 @@ Description:
162
164
  API, and saving the credentials necessary to access the API on behalf of users.
163
165
 
164
166
  If you already have downloaded client credentials, you don't need to run this command. Instead,
165
- rename the downloaded JSON file to `/home/user/.calendar-assistant.client`
167
+ rename the downloaded JSON file to
168
+ `/home/user/.calendar-assistant.client`
166
169
  </pre>
167
170
 
168
171
 
@@ -264,18 +267,18 @@ Usage:
264
267
  calendar-assistant availability [DATE | DATERANGE | TIMERANGE]
265
268
 
266
269
  Options:
267
- -l, [--meeting-length=LENGTH] # [default 30m] find chunks of available time at least as long as LENGTH (which is a ChronicDuration string like '30m' or '2h')
268
- -s, [--start-of-day=TIME] # [default 9am] find chunks of available time after TIME (which is a BusinessTime string like '9am' or '14:30')
269
- -e, [--end-of-day=TIME] # [default 6pm] find chunks of available time before TIME (which is a BusinessTime string like '9am' or '14:30')
270
- -a, [--attendees=ATTENDEE1[,ATTENDEE2[,...]]] # [default 'me'] people (email IDs) to whom this command will be applied
271
- -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
272
- -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
273
- -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
274
- -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
275
- -h, -?, [--help], [--no-help]
276
- [--debug], [--no-debug] # how dare you suggest there are bugs
277
- -f, [--formatting], [--no-formatting] # Enable Text Formatting
278
- # Default: true
270
+ -l, [--meeting-length=LENGTH] # [default 30m] find chunks of available time at least as long as LENGTH (which is a ChronicDuration string like '30m' or '2h')
271
+ -s, [--start-of-day=TIME] # [default 9am] find chunks of available time after TIME (which is a BusinessTime string like '9am' or '14:30')
272
+ -e, [--end-of-day=TIME] # [default 6pm] find chunks of available time before TIME (which is a BusinessTime string like '9am' or '14:30')
273
+ -a, --attendees, [--calendars=CALENDAR1[,CALENDAR2[,...]]] # [default 'me'] people (email IDs) to whom this command will be applied
274
+ -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
275
+ -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
276
+ -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
277
+ -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
278
+ -h, -?, [--help], [--no-help]
279
+ [--debug], [--no-debug] # how dare you suggest there are bugs
280
+ -f, [--formatting], [--no-formatting] # Enable Text Formatting
281
+ # Default: true
279
282
 
280
283
  Show your availability for a date or range of dates (default 'today')
281
284
  </pre>
@@ -367,15 +370,17 @@ Usage:
367
370
  calendar-assistant location-set LOCATION [DATE | DATERANGE]
368
371
 
369
372
  Options:
370
- [--visibility=VISIBILITY] # [default is 'default'] Set the visbility of the event. Values are 'public', 'private', 'default'.
371
- -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
372
- -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
373
- -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
374
- -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
375
- -h, -?, [--help], [--no-help]
376
- [--debug], [--no-debug] # how dare you suggest there are bugs
377
- -f, [--formatting], [--no-formatting] # Enable Text Formatting
378
- # Default: true
373
+ [--force] # will manage location across multiple calendars whether a nickname is set or not
374
+ [--visibility=VISIBILITY] # [default is 'default'] Set the visibility of the event. Values are 'public', 'private', 'default'.
375
+ -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
376
+ -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
377
+ -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
378
+ -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
379
+ -a, --attendees, [--calendars=CALENDAR1[,CALENDAR2[,...]]] # [default 'me'] people (email IDs) to whom this command will be applied
380
+ -h, -?, [--help], [--no-help]
381
+ [--debug], [--no-debug] # how dare you suggest there are bugs
382
+ -f, [--formatting], [--no-formatting] # Enable Text Formatting
383
+ # Default: true
379
384
 
380
385
  Set your location to LOCATION for a date or range of dates (default 'today')
381
386
  </pre>
@@ -444,16 +449,16 @@ Usage:
444
449
  calendar-assistant show [DATE | DATERANGE | TIMERANGE]
445
450
 
446
451
  Options:
447
- -c, [--commitments], [--no-commitments] # only show events that you've accepted with another person
448
- -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
449
- -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
450
- -a, [--attendees=ATTENDEE1[,ATTENDEE2[,...]]] # [default 'me'] people (email IDs) to whom this command will be applied
451
- -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
452
- -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
453
- -h, -?, [--help], [--no-help]
454
- [--debug], [--no-debug] # how dare you suggest there are bugs
455
- -f, [--formatting], [--no-formatting] # Enable Text Formatting
456
- # Default: true
452
+ -c, [--commitments], [--no-commitments] # only show events that you've accepted with another person
453
+ -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
454
+ -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
455
+ -a, --attendees, [--calendars=CALENDAR1[,CALENDAR2[,...]]] # [default 'me'] people (email IDs) to whom this command will be applied
456
+ -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
457
+ -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
458
+ -h, -?, [--help], [--no-help]
459
+ [--debug], [--no-debug] # how dare you suggest there are bugs
460
+ -f, [--formatting], [--no-formatting] # Enable Text Formatting
461
+ # Default: true
457
462
 
458
463
  Show your events for a date or range of dates (default 'today')
459
464
  </pre>
@@ -515,15 +520,15 @@ Usage:
515
520
  calendar-assistant lint [DATE | DATERANGE | TIMERANGE]
516
521
 
517
522
  Options:
518
- -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
519
- -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
520
- -a, [--attendees=ATTENDEE1[,ATTENDEE2[,...]]] # [default 'me'] people (email IDs) to whom this command will be applied
521
- -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
522
- -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
523
- -h, -?, [--help], [--no-help]
524
- [--debug], [--no-debug] # how dare you suggest there are bugs
525
- -f, [--formatting], [--no-formatting] # Enable Text Formatting
526
- # Default: true
523
+ -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
524
+ -l, [--local-store=FILENAME] # Load events from a local file instead of Google Calendar
525
+ -a, --attendees, [--calendars=CALENDAR1[,CALENDAR2[,...]]] # [default 'me'] people (email IDs) to whom this command will be applied
526
+ -b, [--must-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be true (see README)
527
+ -n, [--must-not-be=PROPERTY1[,PROPERTY2[,...]]] # Event properties that must be false (see README)
528
+ -h, -?, [--help], [--no-help]
529
+ [--debug], [--no-debug] # how dare you suggest there are bugs
530
+ -f, [--formatting], [--no-formatting] # Enable Text Formatting
531
+ # Default: true
527
532
 
528
533
  Lint your events for a date or range of dates (default 'today')
529
534
  </pre>
@@ -571,7 +576,6 @@ The output is TOML, which is suitable for dumping into `~/.calendar-assistant` a
571
576
  end-of-day = "6pm"
572
577
  location-icon = "🌎"
573
578
  meeting-length = "30m"
574
- profile = "work"
575
579
  start-of-day = "9am"
576
580
  </pre>
577
581
 
@@ -13,7 +13,7 @@ autoload :BusinessTime, "business_time"
13
13
  autoload :Chronic, "chronic"
14
14
  autoload :ChronicDuration, "chronic_duration"
15
15
  autoload :Google, "calendar_assistant/extensions/google_apis_extensions"
16
- autoload :Launchy, "launchy"
16
+ autoload :Launchy, "calendar_assistant/extensions/launchy_extensions"
17
17
  autoload :TOML, "toml"
18
18
  autoload :Thor, "thor"
19
19
  require "calendar_assistant/extensions/rainbow_extensions" # Rainbow() doesn't trigger autoload
@@ -25,17 +25,22 @@ require "active_support/time" # Time doesn't trigger autoload
25
25
  require "calendar_assistant/calendar_assistant"
26
26
 
27
27
  class CalendarAssistant
28
- autoload :VERSION, "calendar_assistant/version"
29
- autoload :Config, "calendar_assistant/config"
30
- autoload :StringHelpers, "calendar_assistant/string_helpers"
31
- autoload :DateHelpers, "calendar_assistant/date_helpers"
32
- autoload :Event, "calendar_assistant/event"
33
- autoload :EventRepository, "calendar_assistant/event_repository"
34
- autoload :EventRepositoryFactory, "calendar_assistant/event_repository_factory"
35
- autoload :EventSet, "calendar_assistant/event_set"
36
- autoload :Scheduler, "calendar_assistant/scheduler"
37
- autoload :LocalService, "calendar_assistant/local_service"
38
- autoload :PredicateCollection, "calendar_assistant/predicate_collection"
28
+ autoload :VERSION, "calendar_assistant/version"
29
+ autoload :Config, "calendar_assistant/config"
30
+ autoload :StringHelpers, "calendar_assistant/string_helpers"
31
+ autoload :DateHelpers, "calendar_assistant/date_helpers"
32
+ autoload :HasDuration, "calendar_assistant/has_duration"
33
+ autoload :AvailableBlock, "calendar_assistant/available_block"
34
+ autoload :Event, "calendar_assistant/event"
35
+ autoload :EventRepository, "calendar_assistant/event_repository"
36
+ autoload :EventRepositoryFactory, "calendar_assistant/event_repository_factory"
37
+ autoload :EventSet, "calendar_assistant/event_set"
38
+ autoload :Scheduler, "calendar_assistant/scheduler"
39
+ autoload :LocalService, "calendar_assistant/local_service"
40
+ autoload :LocationEventRepository, "calendar_assistant/location_event_repository"
41
+ autoload :LintEventRepository, "calendar_assistant/lint_event_repository"
42
+ autoload :PredicateCollection, "calendar_assistant/predicate_collection"
43
+ autoload :LocationConfigValidator, "calendar_assistant/location_config_validator"
39
44
  end
40
45
 
41
46
  require "calendar_assistant/cli"
@@ -0,0 +1,12 @@
1
+ class CalendarAssistant
2
+ class AvailableBlock
3
+ include HasDuration
4
+
5
+ attr_reader :start, :end
6
+
7
+ def initialize(**params)
8
+ @start = HasDuration.cast_datetime(params[:start]) if params[:start]
9
+ @end = HasDuration.cast_datetime(params[:end]) if params[:end]
10
+ end
11
+ end
12
+ end
@@ -36,7 +36,7 @@ class CalendarAssistant
36
36
 
37
37
  @calendar = service.get_calendar Config::DEFAULT_CALENDAR_ID
38
38
  @event_repository_factory = event_repository_factory
39
- @event_repositories = {} # calendar_id → event_repository
39
+ @event_repositories = {} # type, calendar_id → event_repository
40
40
  @event_predicates = PredicateCollection.build(config.must_be, config.must_not_be)
41
41
  end
42
42
 
@@ -56,16 +56,15 @@ class CalendarAssistant
56
56
  end
57
57
 
58
58
  def lint_events time_range
59
- calendar_ids = config.attendees
59
+ calendar_ids = config.calendar_ids
60
60
  if calendar_ids.length > 1
61
61
  raise BaseException, "CalendarAssistant#lint_events only supports one person (for now)"
62
62
  end
63
-
64
- event_repository(calendar_ids.first).find(time_range, predicates: @event_predicates.merge({needs_action?: true}))
63
+ event_repository(calendar_ids.first, type: :lint).find(time_range, predicates: @event_predicates)
65
64
  end
66
65
 
67
66
  def find_events time_range
68
- calendar_ids = config.attendees
67
+ calendar_ids = config.calendar_ids
69
68
  if calendar_ids.length > 1
70
69
  raise BaseException, "CalendarAssistant#find_events only supports one person (for now)"
71
70
  end
@@ -73,7 +72,7 @@ class CalendarAssistant
73
72
  end
74
73
 
75
74
  def availability time_range
76
- calendar_ids = config.attendees
75
+ calendar_ids = config.calendar_ids
77
76
  ers = calendar_ids.map do |calendar_id|
78
77
  event_repository calendar_id
79
78
  end
@@ -81,47 +80,30 @@ class CalendarAssistant
81
80
  end
82
81
 
83
82
  def find_location_events time_range
84
- event_set = event_repository.find(time_range, predicates: @event_predicates)
85
- event_set.new event_set.events.select { |e| e.location_event? }
83
+ event_repository(type: :location).find(time_range, predicates: @event_predicates)
86
84
  end
87
85
 
88
- def create_location_event time_range, location
89
- # find pre-existing events that overlap
90
- existing_event_set = find_location_events time_range
91
-
92
- # augment event end date appropriately
93
- range = CalendarAssistant.date_range_cast time_range
94
-
95
- deleted_events = []
96
- modified_events = []
97
-
98
- event = event_repository.create(
99
- transparency: CalendarAssistant::Event::Transparency::TRANSPARENT,
100
- start: range.first, end: range.last,
101
- summary: "#{Event.location_event_prefix(@config)}#{location}"
102
- )
103
-
104
- existing_event_set.events.each do |existing_event|
105
- if existing_event.start_date >= event.start_date && existing_event.end_date <= event.end_date
106
- event_repository.delete existing_event
107
- deleted_events << existing_event
108
- elsif existing_event.start_date <= event.end_date && existing_event.end_date > event.end_date
109
- event_repository.update existing_event, start: range.last
110
- modified_events << existing_event
111
- elsif existing_event.start_date < event.start_date && existing_event.end_date >= event.start_date
112
- event_repository.update existing_event, end: range.first
113
- modified_events << existing_event
114
- end
86
+ def create_location_events time_range, location
87
+ LocationConfigValidator.valid?(config)
88
+
89
+ event_set = EventSet::Hash.new(event_repository,{})
90
+
91
+ unique_calendar_ids.each do |calendar_id|
92
+ event_set[calendar_id] = event_repository(calendar_id, type: :location).create(time_range, location, predicates: @event_predicates)
115
93
  end
116
94
 
117
- response = {created: [event]}
118
- response[:deleted] = deleted_events unless deleted_events.empty?
119
- response[:modified] = modified_events unless modified_events.empty?
95
+ event_set
96
+ end
120
97
 
121
- existing_event_set.new response
98
+ def event_repository calendar_id=Config::DEFAULT_CALENDAR_ID, type: :base
99
+ @event_repositories[type] ||= {}
100
+ @event_repositories[type][calendar_id] ||=
101
+ @event_repository_factory.new_event_repository(@service, calendar_id, config: config, type: type)
122
102
  end
123
103
 
124
- def event_repository calendar_id=Config::DEFAULT_CALENDAR_ID
125
- @event_repositories[calendar_id] ||= @event_repository_factory.new_event_repository(@service, calendar_id, config: config)
104
+ private
105
+
106
+ def unique_calendar_ids
107
+ @unique_calendar_ids ||= Array(config.calendar_ids) | [Config::DEFAULT_CALENDAR_ID]
126
108
  end
127
109
  end
@@ -27,12 +27,12 @@ class CalendarAssistant
27
27
  aliases: [ "-n" ]
28
28
  end
29
29
 
30
- def self.has_attendees
31
- option CalendarAssistant::Config::Keys::Options::ATTENDEES,
30
+ def self.has_multiple_calendars
31
+ option CalendarAssistant::Config::Keys::Options::CALENDARS,
32
32
  type: :string,
33
- banner: "ATTENDEE1[,ATTENDEE2[,...]]",
33
+ banner: "CALENDAR1[,CALENDAR2[,...]]",
34
34
  desc: "[default 'me'] people (email IDs) to whom this command will be applied",
35
- aliases: ["-a"]
35
+ aliases: ["-a", "--attendees"]
36
36
  end
37
37
 
38
38
  default_config = CalendarAssistant::CLI::Config.new options: options # used in option descriptions
@@ -139,7 +139,7 @@ class CalendarAssistant
139
139
  desc "lint [DATE | DATERANGE | TIMERANGE]",
140
140
  "Lint your events for a date or range of dates (default 'today')"
141
141
  will_create_a_service
142
- has_attendees
142
+ has_multiple_calendars
143
143
 
144
144
  has_events
145
145
  def lint datespec = "today"
@@ -156,7 +156,7 @@ class CalendarAssistant
156
156
  desc: "only show events that you've accepted with another person",
157
157
  aliases: ["-c"]
158
158
  will_create_a_service
159
- has_attendees
159
+ has_multiple_calendars
160
160
 
161
161
  has_events
162
162
  def show datespec = "today"
@@ -207,17 +207,21 @@ class CalendarAssistant
207
207
 
208
208
  desc "location-set LOCATION [DATE | DATERANGE]",
209
209
  "Set your location to LOCATION for a date or range of dates (default 'today')"
210
+ option CalendarAssistant::Config::Keys::Options::FORCE,
211
+ type: :boolean,
212
+ desc: "will manage location across multiple calendars whether a nickname is set or not"
210
213
  option CalendarAssistant::Config::Keys::Settings::VISIBILITY,
211
214
  type: :string,
212
215
  banner: "VISIBILITY",
213
- desc: "[default is 'default'] Set the visbility of the event. Values are 'public', 'private', 'default'."
216
+ desc: "[default is 'default'] Set the visibility of the event. Values are 'public', 'private', 'default'."
214
217
  will_create_a_service
215
218
  has_events
219
+ has_multiple_calendars
216
220
  def location_set location = nil, datespec = "today"
217
221
  return help! if location.nil?
218
222
 
219
223
  calendar_assistant(datespec) do |ca, date, out|
220
- event_set = ca.create_location_event date, location
224
+ event_set = ca.create_location_events date, location
221
225
  out.print_events ca, event_set
222
226
  end
223
227
  end
@@ -243,7 +247,7 @@ class CalendarAssistant
243
247
  desc: sprintf("[default %s] find chunks of available time before TIME (which is a BusinessTime string like '9am' or '14:30')",
244
248
  default_config.setting(CalendarAssistant::Config::Keys::Settings::END_OF_DAY)),
245
249
  aliases: ["-e"]
246
- has_attendees
250
+ has_multiple_calendars
247
251
  will_create_a_service
248
252
  has_events
249
253
  def availability datespec = "today"
@@ -1,6 +1,9 @@
1
+ # coding: utf-8
1
2
  class CalendarAssistant
2
3
  module CLI
3
4
  class Printer
5
+ class LaunchUrlException < CalendarAssistant::BaseException ; end
6
+
4
7
 
5
8
  attr_reader :io
6
9
 
@@ -9,7 +12,11 @@ class CalendarAssistant
9
12
  end
10
13
 
11
14
  def launch url
12
- Launchy.open url
15
+ begin
16
+ Launchy.open(url)
17
+ rescue Exception => e
18
+ raise LaunchUrlException.new(e)
19
+ end
13
20
  end
14
21
 
15
22
  def puts *args
@@ -32,13 +39,13 @@ class CalendarAssistant
32
39
  end
33
40
  end
34
41
 
35
- def print_events ca, event_set, omit_title: false, presenter_class: CLI::EventSetPresenter
42
+ def print_events ca, event_set, presenter_class: CLI::EventSetPresenter
36
43
  puts presenter_class.new(event_set, config: ca.config).to_s
37
44
  puts
38
45
  end
39
46
 
40
47
  def print_available_blocks ca, event_set, omit_title: false
41
- ers = ca.config.attendees.map {|calendar_id| ca.event_repository calendar_id}
48
+ ers = ca.config.calendar_ids.map {|calendar_id| ca.event_repository calendar_id}
42
49
  time_zones = ers.map {|er| er.calendar.time_zone}.uniq
43
50
 
44
51
  unless omit_title
@@ -33,13 +33,14 @@ class CalendarAssistant
33
33
  module Options
34
34
  COMMITMENTS = "commitments" # bool
35
35
  JOIN = "join" # bool
36
- ATTENDEES = "attendees" # array of calendar ids (comma-delimited)
36
+ CALENDARS = "calendars" # array of calendar ids (comma-delimited)
37
37
  LOCAL_STORE = "local-store" # filename
38
38
  DEBUG = "debug" # bool
39
39
  FORMATTING = "formatting" # Rainbow
40
40
  MUST_BE = "must-be" # array of event predicates (comma-delimited)
41
41
  MUST_NOT_BE = "must-not-be" # array of event predicates (comma-delimited)
42
42
  CONTEXT = "context" # symbol referring to command context
43
+ FORCE = "force" # bool
43
44
  end
44
45
  end
45
46
 
@@ -50,7 +51,7 @@ class CalendarAssistant
50
51
  Keys::Settings::MEETING_LENGTH => "30m", # ChronicDuration
51
52
  Keys::Settings::START_OF_DAY => "9am", # BusinessTime
52
53
  Keys::Settings::END_OF_DAY => "6pm", # BusinessTime
53
- Keys::Options::ATTENDEES => [DEFAULT_CALENDAR_ID], # array of calendar ids
54
+ Keys::Options::CALENDARS => [DEFAULT_CALENDAR_ID], # array of calendar ids
54
55
  Keys::Options::FORMATTING => true, # Rainbow
55
56
  }
56
57
 
@@ -145,10 +146,10 @@ class CalendarAssistant
145
146
  end
146
147
 
147
148
  #
148
- # helper method for Keys::Options::ATTENDEES
149
+ # helper method for Keys::Options::CALENDARS
149
150
  #
150
- def attendees
151
- split_if_array(Keys::Options::ATTENDEES)
151
+ def calendar_ids
152
+ split_if_array(Keys::Options::CALENDARS)
152
153
  end
153
154
 
154
155
  def must_be
@@ -190,32 +191,24 @@ class CalendarAssistant
190
191
  end
191
192
 
192
193
  def self.find_in_hash hash, keypath
193
- current_val = hash
194
- keypath = keypath.split(".") unless keypath.is_a?(Array)
195
-
196
- keypath.each do |key|
197
- if current_val.has_key?(key)
198
- current_val = current_val[key]
199
- else
200
- current_val = nil
201
- break
202
- end
194
+ split_keypath(keypath).inject(hash) do |current_val, key|
195
+ break unless current_val.has_key?(key)
196
+ current_val[key]
203
197
  end
204
-
205
- current_val
206
198
  end
207
199
 
208
200
  def self.set_in_hash hash, keypath, new_value
209
- current_hash = hash
210
- keypath = keypath.split(".") unless keypath.is_a?(Array)
211
- *path_parts, key = *keypath
201
+ *path_parts, key = *split_keypath(keypath)
212
202
 
213
- path_parts.each do |path_part|
214
- current_hash[path_part] ||= {}
215
- current_hash = current_hash[path_part]
203
+ current_hash = path_parts.inject(hash) do |current_val, path|
204
+ current_val[path] ||= {}
216
205
  end
217
206
 
218
207
  current_hash[key] = new_value
219
208
  end
209
+
210
+ def self.split_keypath(keypath)
211
+ keypath.is_a?(Array) ? keypath : keypath.split(".")
212
+ end
220
213
  end
221
214
  end
@@ -1,6 +1,7 @@
1
1
  class CalendarAssistant
2
2
  class Event < SimpleDelegator
3
- #
3
+ include HasDuration
4
+
4
5
  # constants describing enumerated attribute values
5
6
  # see https://developers.google.com/calendar/v3/reference/events
6
7
  #
@@ -69,9 +70,6 @@ class CalendarAssistant
69
70
  #
70
71
  # class methods
71
72
  #
72
- def self.duration_in_seconds start_time, end_time
73
- (end_time.to_datetime - start_time.to_datetime).days.to_i
74
- end
75
73
 
76
74
  def self.location_event_prefix config
77
75
  icon = config[CalendarAssistant::Config::Keys::Settings::LOCATION_ICON]
@@ -98,30 +96,6 @@ class CalendarAssistant
98
96
  !! summary.try(:starts_with?, Event.location_event_prefix(@config))
99
97
  end
100
98
 
101
- def all_day?
102
- start.try(:date) || self.end.try(:date)
103
- end
104
-
105
- def past?
106
- if all_day?
107
- Date.today >= end_date
108
- else
109
- Time.now >= end_time
110
- end
111
- end
112
-
113
- def current?
114
- ! (past? || future?)
115
- end
116
-
117
- def future?
118
- if all_day?
119
- start_date > Date.today
120
- else
121
- start_time > Time.now
122
- end
123
- end
124
-
125
99
  def accepted?
126
100
  response_status == CalendarAssistant::Event::Response::ACCEPTED
127
101
  end
@@ -191,56 +165,6 @@ class CalendarAssistant
191
165
  gcsog.nil? ? true : !!gcsog
192
166
  end
193
167
 
194
- def start_time
195
- if all_day?
196
- start_date.beginning_of_day
197
- else
198
- start.date_time
199
- end
200
- end
201
-
202
- def start_date
203
- if all_day?
204
- start.to_date
205
- else
206
- start_time.to_date
207
- end
208
- end
209
-
210
- def end_time
211
- if all_day?
212
- end_date.beginning_of_day
213
- else
214
- self.end.date_time
215
- end
216
- end
217
-
218
- def end_date
219
- if all_day?
220
- self.end.to_date
221
- else
222
- end_time.to_date
223
- end
224
- end
225
-
226
- def duration
227
- if all_day?
228
- days = (end_date - start_date).to_i
229
- return "#{days}d"
230
- end
231
-
232
- p = ActiveSupport::Duration.build(duration_in_seconds).parts
233
- s = []
234
- s << "#{p[:hours]}h" if p.has_key?(:hours)
235
- s << "#{p[:minutes]}m" if p.has_key?(:minutes)
236
- s.join(" ")
237
- end
238
-
239
-
240
- def duration_in_seconds
241
- Event.duration_in_seconds start_time, end_time
242
- end
243
-
244
168
  def other_human_attendees
245
169
  return nil if attendees.nil?
246
170
  attendees.select { |a| ! a.resource && ! a.self }
@@ -278,10 +202,5 @@ class CalendarAssistant
278
202
  nil
279
203
  end
280
204
  end
281
-
282
- def contains? time
283
- start_time <= time && time < end_time
284
- end
285
-
286
205
  end
287
- end
206
+ end
@@ -1,5 +1,8 @@
1
1
  class CalendarAssistant
2
2
  class EventRepository
3
+ class CalendarNotFoundException < CalendarAssistant::BaseException;
4
+ end
5
+
3
6
  attr_reader :calendar, :calendar_id, :config
4
7
 
5
8
  def initialize(service, calendar_id, config: CalendarAssistant::Config.new)
@@ -8,7 +11,7 @@ class CalendarAssistant
8
11
  @calendar_id = calendar_id
9
12
  @calendar = @service.get_calendar @calendar_id
10
13
  rescue Google::Apis::ClientError => e
11
- raise BaseException, "Calendar for #{@calendar_id} not found" if e.status_code == 404
14
+ raise CalendarNotFoundException, "Calendar for #{@calendar_id} not found" if e.status_code == 404
12
15
  raise
13
16
  end
14
17
 
@@ -46,6 +49,7 @@ class CalendarAssistant
46
49
 
47
50
  def delete event
48
51
  @service.delete_event @calendar_id, event.id
52
+ event
49
53
  end
50
54
 
51
55
  def update(event, attributes)
@@ -54,15 +58,6 @@ class CalendarAssistant
54
58
  CalendarAssistant::Event.new(updated_event, config: config)
55
59
  end
56
60
 
57
- def available_block start_time, end_time
58
- e = Google::Apis::CalendarV3::Event.new(
59
- start: Google::Apis::CalendarV3::EventDateTime.new(date_time: start_time.in_time_zone(calendar.time_zone).to_datetime),
60
- end: Google::Apis::CalendarV3::EventDateTime.new(date_time: end_time.in_time_zone(calendar.time_zone).to_datetime),
61
- summary: "available"
62
- )
63
- CalendarAssistant::Event.new e, config: config
64
- end
65
-
66
61
  private
67
62
 
68
63
  def filter_by_predicates(events, predicates)
@@ -1,7 +1,16 @@
1
1
  class CalendarAssistant
2
2
  class EventRepositoryFactory
3
- def self.new_event_repository service, calendar_id, config: CalendarAssistant::Config.new
4
- EventRepository.new service, calendar_id, config: config
3
+ def self.new_event_repository service, calendar_id, config: CalendarAssistant::Config.new, type: :base
4
+ klass = case type
5
+ when :location
6
+ LocationEventRepository
7
+ when :lint
8
+ LintEventRepository
9
+ else
10
+ EventRepository
11
+ end
12
+
13
+ klass.new service, calendar_id, config: config
5
14
  end
6
15
  end
7
16
  end
@@ -8,6 +8,9 @@ class CalendarAssistant
8
8
  #
9
9
  class EventSet
10
10
  def self.new event_repository, events=nil
11
+ if events.is_a?(EventSet::Hash)
12
+ return EventSet::Hash.new event_repository, events.try(:events)
13
+ end
11
14
  if events.is_a?(::Hash)
12
15
  return EventSet::Hash.new event_repository, events
13
16
  end
@@ -55,6 +58,14 @@ class CalendarAssistant
55
58
  end
56
59
  end
57
60
 
61
+ def [] key
62
+ events[key] ||= []
63
+ end
64
+
65
+ def []= key, value
66
+ events[key] = value
67
+ end
68
+
58
69
  def available_blocks length: 1
59
70
  event_repository.in_tz do
60
71
  dates = events.keys.sort
@@ -76,15 +87,15 @@ class CalendarAssistant
76
87
  next if Time.before_business_hours?(e.end_time.to_time)
77
88
  next if Time.after_business_hours?(e.start_time.to_time)
78
89
 
79
- if Event.duration_in_seconds(start_time, e.start_time) >= length
80
- avail_time[date] << event_repository.available_block(start_time, e.start_time)
90
+ if HasDuration.duration_in_seconds(start_time, e.start_time) >= length
91
+ avail_time[date] << AvailableBlock.new(start: start_time, end: e.start_time)
81
92
  end
82
93
  start_time = [e.end_time, start_time].max
83
94
  break if ! start_time.during_business_hours?
84
95
  end
85
96
 
86
- if Event.duration_in_seconds(start_time, end_time) >= length
87
- avail_time[date] << event_repository.available_block(start_time, end_time)
97
+ if HasDuration.duration_in_seconds(start_time, end_time) >= length
98
+ avail_time[date] << AvailableBlock.new(start: start_time, end: end_time)
88
99
  end
89
100
 
90
101
  avail_time
@@ -106,8 +117,8 @@ class CalendarAssistant
106
117
  event_b.contains?(event_a.end_time-1)
107
118
  start_time = [event_a.start_time, event_b.start_time].max
108
119
  end_time = [event_a.end_time, event_b.end_time ].min
109
- if Event.duration_in_seconds(start_time, end_time) >= length
110
- set.events[date] << event_repository.available_block(start_time, end_time)
120
+ if HasDuration.duration_in_seconds(start_time, end_time) >= length
121
+ set.events[date] << AvailableBlock.new(start: start_time, end: end_time)
111
122
  end
112
123
  end
113
124
  end
@@ -0,0 +1,44 @@
1
+ require "launchy"
2
+
3
+ #
4
+ # extend Launchy to handle zoom web URLs via the zoom commandline
5
+ # executable.
6
+ #
7
+ # note this doesn't handle "personal links" like
8
+ #
9
+ # "https://robin.zoom.us/my/usernamehere"
10
+ #
11
+ # which depends on an http 302 redirect from the zoom site
12
+ #
13
+ class CalendarAssistant
14
+ class ZoomLaunchy < Launchy::Application::Browser
15
+ ZOOM_URI_REGEXP = %r(https?://\w+.zoom.us/j/(\d+))
16
+
17
+ def self.handles? uri
18
+ return true if ZOOM_URI_REGEXP.match(uri)
19
+ end
20
+
21
+ def darwin_app_list
22
+ [find_executable("open")]
23
+ end
24
+
25
+ def nix_app_list
26
+ [find_executable("xdg-open")]
27
+ end
28
+
29
+ def open uri, options={}
30
+ command = host_os_family.app_list(self).compact.first
31
+ if command.nil?
32
+ super uri, options
33
+ else
34
+ confno = ZOOM_URI_REGEXP.match(uri)[1]
35
+ url = "zoommtg://zoom.us/join?confno=#{confno}"
36
+ run command, [url]
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # we need to be first so we get right of first refusal on `https?` URLs
43
+ Launchy::Application.children.delete(CalendarAssistant::ZoomLaunchy)
44
+ Launchy::Application.children.prepend(CalendarAssistant::ZoomLaunchy)
@@ -0,0 +1,102 @@
1
+ class CalendarAssistant
2
+ module HasDuration
3
+ def self.duration_in_seconds start_time, end_time
4
+ (end_time.to_datetime - start_time.to_datetime).days.to_i
5
+
6
+ end
7
+
8
+ def self.cast_datetime(datetime, time_zone = Time.zone.name)
9
+ return datetime if datetime.is_a?(Google::Apis::CalendarV3::EventDateTime)
10
+ Google::Apis::CalendarV3::EventDateTime.new(date_time: datetime.in_time_zone(time_zone).to_datetime)
11
+ end
12
+
13
+ def all_day?
14
+ start.try(:date) || self.end.try(:date)
15
+ end
16
+
17
+ def past?
18
+ if all_day?
19
+ Date.today >= end_date
20
+ else
21
+ Time.now >= end_time
22
+ end
23
+ end
24
+
25
+ def current?
26
+ !(past? || future?)
27
+ end
28
+
29
+ def future?
30
+ if all_day?
31
+ start_date > Date.today
32
+ else
33
+ start_time > Time.now
34
+ end
35
+ end
36
+
37
+ def cover?(event)
38
+ event.start_date >= start_date && event.end_date <= end_date
39
+ end
40
+
41
+ def overlaps_start_of?(event)
42
+ event.start_date <= end_date && event.end_date > end_date
43
+ end
44
+
45
+ def overlaps_end_of?(event)
46
+ event.start_date < start_date && event.end_date >= start_date
47
+ end
48
+
49
+ def start_time
50
+ if all_day?
51
+ start_date.beginning_of_day
52
+ else
53
+ start.date_time
54
+ end
55
+ end
56
+
57
+ def start_date
58
+ if all_day?
59
+ start.to_date
60
+ else
61
+ start_time.to_date
62
+ end
63
+ end
64
+
65
+ def end_time
66
+ if all_day?
67
+ end_date.beginning_of_day
68
+ else
69
+ self.end.date_time
70
+ end
71
+ end
72
+
73
+ def end_date
74
+ if all_day?
75
+ self.end.to_date
76
+ else
77
+ end_time.to_date
78
+ end
79
+ end
80
+
81
+ def duration
82
+ if all_day?
83
+ days = (end_date - start_date).to_i
84
+ return "#{days}d"
85
+ end
86
+
87
+ p = ActiveSupport::Duration.build(duration_in_seconds).parts
88
+ s = []
89
+ s << "#{p[:hours]}h" if p.has_key?(:hours)
90
+ s << "#{p[:minutes]}m" if p.has_key?(:minutes)
91
+ s.join(" ")
92
+ end
93
+
94
+ def duration_in_seconds
95
+ HasDuration.duration_in_seconds start_time, end_time
96
+ end
97
+
98
+ def contains? time
99
+ start_time <= time && time < end_time
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,7 @@
1
+ class CalendarAssistant
2
+ class LintEventRepository < EventRepository
3
+ def find time, predicates: {}
4
+ super(time, predicates: predicates.merge({needs_action?: true}))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # coding: utf-8
2
+ class CalendarAssistant
3
+ class LocationConfigValidator
4
+ class LocationConfigValidationException < CalendarAssistant::BaseException;
5
+ end
6
+
7
+ def self.valid?(config)
8
+ return if (config.calendar_ids - [ Config::DEFAULT_CALENDAR_ID ]).empty?
9
+ return if !!config[CalendarAssistant::Config::Keys::Settings::NICKNAME]
10
+ return if !!config[CalendarAssistant::Config::Keys::Options::FORCE]
11
+
12
+ raise LocationConfigValidationException, "Managing location across multiple calendars when a nickname is not set is not recommended, use --force to override"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ class CalendarAssistant
2
+ class LocationEventRepository < EventRepository
3
+ def find time, predicates: {}
4
+ event_set = super time, predicates: predicates
5
+ event_set.new event_set.events.select { |e| e.location_event? }
6
+ end
7
+
8
+ def create time, location, predicates: {}
9
+ # find pre-existing events that overlap
10
+ existing_event_set = find time, predicates: predicates
11
+
12
+ # augment event end date appropriately
13
+ range = CalendarAssistant.date_range_cast time
14
+
15
+
16
+ event = super(
17
+ transparency: CalendarAssistant::Event::Transparency::TRANSPARENT,
18
+ start: range.first, end: range.last,
19
+ summary: "#{Event.location_event_prefix(@config)}#{location}"
20
+ )
21
+
22
+ modify_location_events(event, existing_event_set)
23
+ end
24
+
25
+ private
26
+
27
+ def modify_location_events(event, existing_event_set)
28
+ response = existing_event_set.new({created: [event]})
29
+
30
+ existing_event_set.events.each do |existing_event|
31
+ if event.cover?(existing_event)
32
+ response[:deleted] << delete(existing_event)
33
+ elsif event.overlaps_start_of?(existing_event)
34
+ response[:modified] << update(existing_event, start: event.end_date)
35
+ elsif event.overlaps_end_of?(existing_event)
36
+ response[:modified] << update(existing_event, end: event.start_date)
37
+ end
38
+ end
39
+
40
+ response
41
+ end
42
+ end
43
+ end
@@ -1,3 +1,3 @@
1
1
  class CalendarAssistant
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calendar-assistant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-01-28 00:00:00.000000000 Z
12
+ date: 2019-02-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -149,14 +149,14 @@ dependencies:
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '0.14'
152
+ version: 0.14.8
153
153
  type: :development
154
154
  prerelease: false
155
155
  version_requirements: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: '0.14'
159
+ version: 0.14.8
160
160
  - !ruby/object:Gem::Dependency
161
161
  name: bundler
162
162
  requirement: !ruby/object:Gem::Requirement
@@ -285,6 +285,7 @@ files:
285
285
  - Rakefile
286
286
  - bin/calendar-assistant
287
287
  - lib/calendar_assistant.rb
288
+ - lib/calendar_assistant/available_block.rb
288
289
  - lib/calendar_assistant/calendar_assistant.rb
289
290
  - lib/calendar_assistant/cli.rb
290
291
  - lib/calendar_assistant/cli/authorizer.rb
@@ -305,8 +306,13 @@ files:
305
306
  - lib/calendar_assistant/event_repository_factory.rb
306
307
  - lib/calendar_assistant/event_set.rb
307
308
  - lib/calendar_assistant/extensions/google_apis_extensions.rb
309
+ - lib/calendar_assistant/extensions/launchy_extensions.rb
308
310
  - lib/calendar_assistant/extensions/rainbow_extensions.rb
311
+ - lib/calendar_assistant/has_duration.rb
312
+ - lib/calendar_assistant/lint_event_repository.rb
309
313
  - lib/calendar_assistant/local_service.rb
314
+ - lib/calendar_assistant/location_config_validator.rb
315
+ - lib/calendar_assistant/location_event_repository.rb
310
316
  - lib/calendar_assistant/predicate_collection.rb
311
317
  - lib/calendar_assistant/scheduler.rb
312
318
  - lib/calendar_assistant/string_helpers.rb