mhc 1.2.4 → 1.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +14 -0
- data/README.org +37 -0
- data/bin/mhc +198 -2
- data/emacs/Cask +1 -1
- data/emacs/mhc-db.el +42 -7
- data/emacs/mhc-summary.el +3 -1
- data/emacs/mhc-vars.el +1 -1
- data/emacs/mhc.el +30 -6
- data/lib/mhc/calendar.rb +20 -0
- data/lib/mhc/config.rb +5 -1
- data/lib/mhc/converter.rb +74 -3
- data/lib/mhc/datastore.rb +9 -5
- data/lib/mhc/formatter/html.rb +32 -0
- data/lib/mhc/formatter.rb +3 -0
- data/lib/mhc/templates/full-calendar.html.erb +417 -0
- data/lib/mhc/version.rb +1 -1
- data/mhc.gemspec +2 -1
- data/samples/japanese-holidays.mhcc +6 -6
- metadata +22 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0b65ae5e1dd5f504de2eeb59f280eff7ef0422133520006e9814ab52510e9ac
|
4
|
+
data.tar.gz: '0168de3d55867bf8356a02cf9be81d3ecd199b01f64d5c2cdc5f6835a0605fdd'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4533cef977bafd8491f1f3bbb709049dfa17b7ca70f0286381aaba60ffb51a096d34b28d6cb7239e1a5f932729237dce1d931332c1830983357ec99196f75e5
|
7
|
+
data.tar.gz: 40dbdfcdcacf2ac4f274db2897bfa91f25a58ebb0d80c4b705d9134efef3e2fafa7e1ec51a743817e075e7a0f015b53262a6c3c1f915400ce5e0dd5dad31727d
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Style/RedundantReturn:
|
2
|
+
Enabled: false
|
3
|
+
Style/CommentedKeyword:
|
4
|
+
Enabled: false
|
5
|
+
Style/Documentation:
|
6
|
+
Enabled: false
|
7
|
+
Style/RedundantSelf:
|
8
|
+
Enabled: false
|
9
|
+
Style/StringLiterals:
|
10
|
+
Enabled: false
|
11
|
+
Metrics/MethodLength:
|
12
|
+
Enabled: false
|
13
|
+
Metrics/ClassLength:
|
14
|
+
Enabled: false
|
data/README.org
CHANGED
@@ -127,6 +127,7 @@
|
|
127
127
|
| =M= | Open pointed article to edit |
|
128
128
|
| =D= | Delete pointed article |
|
129
129
|
| =C= | Copy article temporally as a reusable template |
|
130
|
+
| =R= | Copy article like =C= using completing-read |
|
130
131
|
| =Y= | Same as =E= but use the template stored by =C= |
|
131
132
|
|
132
133
|
3) Draft Buffer
|
@@ -149,6 +150,7 @@
|
|
149
150
|
X-SC-Time: 18:00-21:00
|
150
151
|
X-SC-Category: Private Party
|
151
152
|
X-SC-Cond:
|
153
|
+
X-SC-Recurrence-Tag: HomeParty
|
152
154
|
X-SC-Duration:
|
153
155
|
X-SC-Record-Id: C34D89F5-27FA-4243-AC6C-168D8D243D9A
|
154
156
|
X-SC-Sequence: 0
|
@@ -241,6 +243,40 @@
|
|
241
243
|
of 2015-07-15 (Wed) and inclusion of 2015-08-01 (Sat).
|
242
244
|
#+END_EXAMPLE
|
243
245
|
|
246
|
+
** Headers for Grouping
|
247
|
+
*** X-SC-Category
|
248
|
+
=X-SC-Category:= is a space separated list of category
|
249
|
+
It is useful for selective display both in CLI:
|
250
|
+
: mhc scan today --category=Private
|
251
|
+
and Emacs UI:
|
252
|
+
: M-x mhc-set-default-category
|
253
|
+
: Default Category: Private && !Party
|
254
|
+
|
255
|
+
Also useful sync with Google Calendar. See [[https://github.com/yoshinari-nomura/mhc/blob/master/samples/DOT.mhc-config.yml][mhc-config.yml]] for
|
256
|
+
details.
|
257
|
+
|
258
|
+
*** X-SC-Recurrence-Tag
|
259
|
+
=X-SC-Recurrence-Tag:= is a tag for bundling multiple event articles as one recurring group.
|
260
|
+
|
261
|
+
MHC allows flexible description of repeating events using
|
262
|
+
=X-SC-Cond:=, =X-SC-Duration:=, or =X-SC-Day:=. However, we know
|
263
|
+
this is not sufficient.
|
264
|
+
|
265
|
+
For example, in my experience, some monthly meetings do not have
|
266
|
+
distinct recurring patterns such like "Third Wednesday of each
|
267
|
+
month." Instead, the next date is fixed by coordination during the meeting.
|
268
|
+
|
269
|
+
In such cases, it is difficult to mark these events as a series of related events.
|
270
|
+
That's why we need =X-SC-Recurrence-Tag:=
|
271
|
+
|
272
|
+
Using =X-SC-Recurrence-Tag:=, MHC suggests what event should be arranged.
|
273
|
+
: mhc stuck_recurrences
|
274
|
+
will tell you to make the next appointment.
|
275
|
+
|
276
|
+
For example, if you have "X-SC-Recurrence-Tag: Dentist" in your
|
277
|
+
articles of past dentist events, mhc will point out that you
|
278
|
+
forgot to make your next dentist appointment.
|
279
|
+
|
244
280
|
* INFORMATION FOR DEVELOPERS
|
245
281
|
** INSTALL for developers:
|
246
282
|
1) Install rbenv + ruby-build
|
@@ -326,6 +362,7 @@
|
|
326
362
|
(setq load-path
|
327
363
|
(cons "~/src/mhc/emacs" load-path))
|
328
364
|
(autoload 'mhc "mhc" "Message Harmonized Calendar system." t)
|
365
|
+
(autoload 'mhc-import "mhc" "Import a schedule." t)
|
329
366
|
|
330
367
|
;; M-x mhc
|
331
368
|
#+END_SRC
|
data/bin/mhc
CHANGED
@@ -72,9 +72,10 @@ class MhcCLI < Thor
|
|
72
72
|
register_option :repository, :desc => "Set MHC top directory", :banner => "DIRECTORY"
|
73
73
|
register_option :calendar, :desc => "Set source CALENDAR"
|
74
74
|
register_option :category, :desc => "Pick items only in CATEGORY"
|
75
|
-
register_option :format, :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json)
|
75
|
+
register_option :format, :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json html)
|
76
76
|
register_option :search, :desc => "Search items by complex expression"
|
77
77
|
register_option :dry_run, :desc => "Perform a trial run with no changes made", :type => :boolean
|
78
|
+
register_option :verbose, :desc => "Show verbose message", :type => :boolean
|
78
79
|
|
79
80
|
################################################################
|
80
81
|
# command name mappings
|
@@ -113,7 +114,9 @@ class MhcCLI < Thor
|
|
113
114
|
Mhc::Command::Cache.new(builder.datastore)
|
114
115
|
end
|
115
116
|
|
117
|
+
################################################################
|
116
118
|
# Command: todo
|
119
|
+
################################################################
|
117
120
|
desc "todo", "List Todo entries in MHC calendar"
|
118
121
|
|
119
122
|
named_option :repository
|
@@ -131,7 +134,8 @@ class MhcCLI < Thor
|
|
131
134
|
search_range = nil
|
132
135
|
end
|
133
136
|
next if task.in_category?("done") && !options[:show_all]
|
134
|
-
|
137
|
+
task_first = task.occurrences(range: search_range).first
|
138
|
+
todos << task_first if task_first
|
135
139
|
end
|
136
140
|
todos.each.sort{|a, b| a.dtstart <=> b.dtstart}.each do |t|
|
137
141
|
deadline = t.dtstart
|
@@ -168,6 +172,38 @@ class MhcCLI < Thor
|
|
168
172
|
Mhc::Command::Completions.new(help, global_options, command, config)
|
169
173
|
end
|
170
174
|
|
175
|
+
################################################################
|
176
|
+
# Command: compose
|
177
|
+
################################################################
|
178
|
+
desc "compose", "Create new draft and open it in your editor"
|
179
|
+
|
180
|
+
long_desc <<-LONGDESC
|
181
|
+
Create a new draft and open it in your editor.
|
182
|
+
To select your favorite editor,
|
183
|
+
set environment variable MHC_EDITOR or EDITOR.
|
184
|
+
LONGDESC
|
185
|
+
|
186
|
+
def compose
|
187
|
+
require 'securerandom'
|
188
|
+
ev = Mhc::Event.new
|
189
|
+
ev.record_id = SecureRandom.uuid.upcase
|
190
|
+
path = builder.datastore.create(ev, true)
|
191
|
+
puts "Create draft #{path}"
|
192
|
+
editor = ENV["MHC_EDITOR"] || ENV["EDITOR"] || "vi"
|
193
|
+
system(editor, path)
|
194
|
+
|
195
|
+
while add(path) != 0
|
196
|
+
if ask("Retry?") =~ /^[Yy]/
|
197
|
+
system(editor, path)
|
198
|
+
else
|
199
|
+
puts "Abort..."
|
200
|
+
exit(1)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
puts "Successfully registered."
|
204
|
+
exit(0)
|
205
|
+
end
|
206
|
+
|
171
207
|
################################################################
|
172
208
|
# Command: config
|
173
209
|
################################################################
|
@@ -179,6 +215,40 @@ class MhcCLI < Thor
|
|
179
215
|
puts Mhc::Converter::Emacs.new.to_emacs(config.get_value(name))
|
180
216
|
end
|
181
217
|
|
218
|
+
################################################################
|
219
|
+
# Command: add
|
220
|
+
################################################################
|
221
|
+
desc "add FILE", "Add event FILE to repository"
|
222
|
+
|
223
|
+
named_option :repository
|
224
|
+
|
225
|
+
def add(file)
|
226
|
+
full_path = File.expand_path(file)
|
227
|
+
|
228
|
+
unless File.exist?(full_path)
|
229
|
+
puts Mhc::Converter::Emacs.new.to_emacs("No such file #{file}.")
|
230
|
+
return 1
|
231
|
+
end
|
232
|
+
|
233
|
+
event_string = File.open(full_path).read
|
234
|
+
errors = Mhc::Event.validate(event_string)
|
235
|
+
|
236
|
+
if errors.empty?
|
237
|
+
ev = Mhc::Event.parse(event_string)
|
238
|
+
calendar.add(ev)
|
239
|
+
return 0
|
240
|
+
else
|
241
|
+
string = ""
|
242
|
+
errors.each do |err, key|
|
243
|
+
string += "#{err.to_s.capitalize}"
|
244
|
+
string += " in X-SC-#{key.capitalize}" if key
|
245
|
+
string += ".\n"
|
246
|
+
end
|
247
|
+
puts string
|
248
|
+
return 1
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
182
252
|
################################################################
|
183
253
|
# Command: init
|
184
254
|
################################################################
|
@@ -188,6 +258,55 @@ class MhcCLI < Thor
|
|
188
258
|
Mhc::Command::Init.new(top_dir, options[:config] || DEFAULT_CONFIG_PATH, ENV["MHC_TZID"])
|
189
259
|
end
|
190
260
|
|
261
|
+
################################################################
|
262
|
+
# Command: stuck_recurrences
|
263
|
+
################################################################
|
264
|
+
desc "stuck_recurrences", "List stuck recurrences in MHC calendar"
|
265
|
+
|
266
|
+
named_option :repository
|
267
|
+
named_option :verbose
|
268
|
+
named_option :format
|
269
|
+
|
270
|
+
# * stuck_recurrences
|
271
|
+
#
|
272
|
+
# For all "living recurrences", if no next event has been set in
|
273
|
+
# the "future", show the information on the last time.
|
274
|
+
#
|
275
|
+
# + "living recurrences" ::
|
276
|
+
# + There is a last-time evnet within a year and a half.
|
277
|
+
# + "future" :: ten years from now.
|
278
|
+
#
|
279
|
+
def stuck_recurrences(format = options[:format])
|
280
|
+
today = Mhc::PropertyValue::Date.today
|
281
|
+
|
282
|
+
# Seek -1.5y to +10y
|
283
|
+
search_range = (today - ((365 * 5).to_i)) ..
|
284
|
+
(today + ((365 * 10).to_i))
|
285
|
+
|
286
|
+
recurrences = {}
|
287
|
+
|
288
|
+
# Get all recurrences in search_range
|
289
|
+
calendar.occurrences(search_range).each do |oc|
|
290
|
+
rtag = oc.recurrence_tag.to_s
|
291
|
+
next if rtag.empty?
|
292
|
+
recurrences[rtag] ||= []
|
293
|
+
recurrences[rtag] << oc
|
294
|
+
end
|
295
|
+
|
296
|
+
formatter = Mhc::Formatter.build(formatter: format || :text, date_range: search_range)
|
297
|
+
|
298
|
+
recurrences.each do |rtag, recurrence|
|
299
|
+
if options[:verbose]
|
300
|
+
rec_put_debug(recurrence)
|
301
|
+
else
|
302
|
+
formatter << recurrence.last if rec_stuck?(recurrence)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
str = formatter.to_s
|
306
|
+
return str if format == :json || options[:verbose]
|
307
|
+
puts str
|
308
|
+
end # stuck_recurrences
|
309
|
+
|
191
310
|
################################################################
|
192
311
|
# Command: scan
|
193
312
|
################################################################
|
@@ -222,6 +341,9 @@ class MhcCLI < Thor
|
|
222
341
|
named_option :calendar, :category, :format, :repository, :search
|
223
342
|
|
224
343
|
def scan(range)
|
344
|
+
if options[:format].to_s == 'html'
|
345
|
+
$stuck_recurrences_json = stuck_recurrences(:json)
|
346
|
+
end
|
225
347
|
begin
|
226
348
|
Mhc::Command::Scan.new(calendar, range, **symbolize_keys(options))
|
227
349
|
rescue Mhc::PropertyValue::ParseError, Mhc::Formatter::NameError, Mhc::Query::ParseError => e
|
@@ -326,6 +448,80 @@ class MhcCLI < Thor
|
|
326
448
|
|
327
449
|
private
|
328
450
|
|
451
|
+
def rec_put_debug(recurrence)
|
452
|
+
ol = recurrence.last
|
453
|
+
|
454
|
+
today = Mhc::PropertyValue::Date.today
|
455
|
+
last_occurrence = recurrence.last
|
456
|
+
last_occurrence_date = last_occurrence.dtstart.to_date
|
457
|
+
# Ignore living recurrences
|
458
|
+
return false if last_occurrence_date > today
|
459
|
+
|
460
|
+
status = if rec_stuck?(recurrence) then " TODO" else "" end
|
461
|
+
print "*#{status} #{ol.recurrence_tag}#{status} "
|
462
|
+
|
463
|
+
print format("%s %s",
|
464
|
+
ol.dtstart.strftime("%Y-%m-%d %a"),
|
465
|
+
ol.subject)
|
466
|
+
|
467
|
+
print format(" (length: %d, max: %d, min: %d, avr: %d)\n",
|
468
|
+
recurrence.length,
|
469
|
+
*rec_stat(recurrence))
|
470
|
+
|
471
|
+
pr = recurrence.first
|
472
|
+
recurrence.each do |oc|
|
473
|
+
puts format(" %s %s (%d)",
|
474
|
+
oc.dtstart.strftime("%Y-%m-%d %a"),
|
475
|
+
oc.subject,
|
476
|
+
(oc.dtstart.to_date - pr.dtstart.to_date).to_i
|
477
|
+
)
|
478
|
+
pr = oc
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# Judge if a recurrence is stuck or not. recurrence is a set of occurrences.
|
483
|
+
def rec_stuck?(recurrence, today = Mhc::PropertyValue::Date.today)
|
484
|
+
last_occurrence = recurrence.last
|
485
|
+
last_occurrence_date = last_occurrence.dtstart.to_date
|
486
|
+
max_interval, _min_interval, avr_interval = rec_stat(recurrence)
|
487
|
+
|
488
|
+
# Not stuck if scheduled in the future dates
|
489
|
+
return false if last_occurrence_date > today
|
490
|
+
|
491
|
+
# Stuck if last event occurred within 90 days, but average
|
492
|
+
# interval is less than 60 days.
|
493
|
+
if (today - last_occurrence_date).to_i < 90 && avr_interval < 60
|
494
|
+
return true
|
495
|
+
end
|
496
|
+
|
497
|
+
# Not stuck if the number of days between the last event and today
|
498
|
+
# does not reach 80% of the maximum interval.
|
499
|
+
return false if (today - last_occurrence_date).to_i < max_interval * 4 / 5
|
500
|
+
|
501
|
+
# Not stuck if the number of days between the last event and today
|
502
|
+
# is more than 30 days or 1.5 years beyond the maximum interval.
|
503
|
+
# It would be no more recurring.
|
504
|
+
return false if (today - last_occurrence_date).to_i > [max_interval + 30, 365*1.5].min
|
505
|
+
|
506
|
+
# Otherwise, stuck
|
507
|
+
return true
|
508
|
+
end
|
509
|
+
|
510
|
+
# Return statistics on recurrences: max/min/average interval.
|
511
|
+
def rec_stat(recurrence)
|
512
|
+
# Calcurate all intervals of the recurrence
|
513
|
+
intervals = recurrence.each_cons(2).map{|ocs|
|
514
|
+
(ocs[1].dtstart.to_date - ocs[0].dtstart.to_date).to_i
|
515
|
+
}.reject{|interval| interval < 1}
|
516
|
+
|
517
|
+
# Assume annual event if the recurrence is one-shot.
|
518
|
+
intervals = [365] if intervals.empty?
|
519
|
+
|
520
|
+
return [intervals.max,
|
521
|
+
intervals.min,
|
522
|
+
intervals.reduce(0, :+) / intervals.length]
|
523
|
+
end
|
524
|
+
|
329
525
|
def exit_on_error(&block)
|
330
526
|
begin
|
331
527
|
yield if block_given?
|
data/emacs/Cask
CHANGED
data/emacs/mhc-db.el
CHANGED
@@ -34,6 +34,12 @@ If optional SEARCH is non-nil returned value is clipped by search string."
|
|
34
34
|
(if category (format " --category=%s" category) "")
|
35
35
|
(if search (format " --search='%s'" search) ""))))
|
36
36
|
|
37
|
+
(defun mhc-db-stuck-recurrences ()
|
38
|
+
"List stuck recurrences in MHC calendar."
|
39
|
+
(mhc-db-flatten
|
40
|
+
(mhc-process-send-command
|
41
|
+
(format "stuck_recurrences --format=emacs"))))
|
42
|
+
|
37
43
|
(defun mhc-db-scan-flat (begin-date end-date &optional nosort category search)
|
38
44
|
"Scan MHC database from BEGIN-DATE to END-DATE.
|
39
45
|
Unlike `mhc-db-scan`, returned value is not grouped by date.
|
@@ -42,19 +48,48 @@ For example:
|
|
42
48
|
If optional NOSORT is non-nil, returned value is not sort.
|
43
49
|
If optional CATEGORY is non-nil, returned value is clipped by category.
|
44
50
|
If optional SEARCH is non-nil returned value is clipped by search string."
|
45
|
-
(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
51
|
+
(mhc-db-flatten (mhc-db-scan begin-date end-date nosort category search)))
|
52
|
+
|
53
|
+
(defun mhc-db-flatten (dayinfo-list)
|
54
|
+
"Flatten DAYINFO-LIST scanned from `mhc-db-scan`.
|
55
|
+
Unlike `mhc-db-scan`, returned value is not grouped by date."
|
56
|
+
(apply 'append
|
57
|
+
(mapcar (lambda (dayinfo)
|
58
|
+
(let ((date (mhc-day-date dayinfo))
|
59
|
+
(schedules (mhc-day-schedules dayinfo)))
|
60
|
+
(mapcar (lambda (sch) (cons date sch)) schedules)))
|
61
|
+
dayinfo-list)))
|
52
62
|
|
53
63
|
(defun mhc-db-search (&rest query)
|
54
64
|
(let ((b (mhc-date-new 1970 1 1))
|
55
65
|
(e (mhc-date-yy+ (mhc-date-now) 10)))
|
56
66
|
(mhc-db-scan b e nil nil (mhc-db/query-to-search-string query))))
|
57
67
|
|
68
|
+
(defun mhc-db-past-occurrences (&optional days)
|
69
|
+
(let ((now (mhc-date-now)))
|
70
|
+
(nreverse
|
71
|
+
(mhc-db-scan-flat (mhc-date- now (or days 732)) now))))
|
72
|
+
|
73
|
+
(defun mhc-db-make-completion-list (occurrences)
|
74
|
+
(delq nil
|
75
|
+
(mapcar
|
76
|
+
(lambda (occurrence)
|
77
|
+
(let ((sch (cdr occurrence)))
|
78
|
+
(if (not (string= (mhc-record-name (mhc-schedule-record sch)) ""))
|
79
|
+
(cons (mhc-db-real-to-display occurrence) occurrence))))
|
80
|
+
occurrences)))
|
81
|
+
|
82
|
+
(defun mhc-db-real-to-display (occurrence)
|
83
|
+
(let* ((date (car occurrence))
|
84
|
+
(sch (cdr occurrence))
|
85
|
+
(location (mhc-schedule-location sch)))
|
86
|
+
(concat
|
87
|
+
(mhc-date-format date "%04d/%02d/%02d" yy mm dd)
|
88
|
+
(format " %11s " (mhc-schedule-time-as-string sch))
|
89
|
+
(mhc-schedule-subject sch)
|
90
|
+
(if (and location (not (string= location "")))
|
91
|
+
(format " [%s]" location)))))
|
92
|
+
|
58
93
|
(defun mhc-db/quote-string (string)
|
59
94
|
(format "\"%s\"" string))
|
60
95
|
|
data/emacs/mhc-summary.el
CHANGED
@@ -294,9 +294,10 @@ If optional argument FOR-DRAFT is non-nil, Hilight message as draft message."
|
|
294
294
|
(set-visited-file-name nil)
|
295
295
|
;; (rename-buffer (file-name-nondirectory file) 'unique)
|
296
296
|
;; (run-hooks 'mhc-calendar-view-file-hook)
|
297
|
+
(let ((buffer-read-only nil))
|
298
|
+
(mhc-message-mode))
|
297
299
|
(set-buffer-modified-p nil)
|
298
300
|
(setq buffer-read-only t)
|
299
|
-
(mhc-message-mode)
|
300
301
|
(mhc-message-set-file-name file)
|
301
302
|
)))
|
302
303
|
|
@@ -633,6 +634,7 @@ If BANNER is set, it is printed on the horizontal line."
|
|
633
634
|
(define-key mhc-summary-mode-map "E" 'mhc-edit)
|
634
635
|
(define-key mhc-summary-mode-map "M" 'mhc-modify)
|
635
636
|
(define-key mhc-summary-mode-map "C" 'mhc-reuse-copy)
|
637
|
+
(define-key mhc-summary-mode-map "R" 'mhc-reuse-past-event)
|
636
638
|
(define-key mhc-summary-mode-map "Y" 'mhc-reuse-create)
|
637
639
|
|
638
640
|
(define-key mhc-summary-mode-map "n" 'mhc-summary-display-next)
|
data/emacs/mhc-vars.el
CHANGED
data/emacs/mhc.el
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
;; Description: Message Harmonized Calendaring system.
|
4
4
|
;; Author: Yoshinari Nomura <nom@quickhack.net>
|
5
5
|
;; Created: 1994-07-04
|
6
|
-
;; Version: 1.2.
|
6
|
+
;; Version: 1.2.5
|
7
7
|
;; Keywords: calendar
|
8
8
|
;; URL: http://www.quickhack.net/mhc
|
9
9
|
;; Package-Requires: ((calfw "20150703"))
|
@@ -19,7 +19,8 @@
|
|
19
19
|
;;
|
20
20
|
;; (setq load-path
|
21
21
|
;; (cons "~/src/mhc/emacs" load-path))
|
22
|
-
;; (autoload 'mhc "mhc")
|
22
|
+
;; (autoload 'mhc "mhc" nil t)
|
23
|
+
;; (autoload 'mhc-import "mhc" nil t)
|
23
24
|
;;
|
24
25
|
;; and M-x mhc
|
25
26
|
|
@@ -325,10 +326,11 @@
|
|
325
326
|
ret))))
|
326
327
|
|
327
328
|
(defun mhc-expr-compile (string)
|
328
|
-
(
|
329
|
-
|
330
|
-
|
331
|
-
|
329
|
+
(let ((lexical-binding nil))
|
330
|
+
(byte-compile
|
331
|
+
`(lambda (schedule)
|
332
|
+
,(mhc-expr-parse string)
|
333
|
+
))))
|
332
334
|
|
333
335
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
334
336
|
;;
|
@@ -796,6 +798,7 @@ Returns t if the importation was succeeded."
|
|
796
798
|
:group 'mhc
|
797
799
|
:type 'boolean)
|
798
800
|
|
801
|
+
;;;###autoload
|
799
802
|
(defun mhc-import (&optional get-original)
|
800
803
|
"Import a schedule from the current article.
|
801
804
|
The default action of this command is to import a schedule from the
|
@@ -912,6 +915,27 @@ the default action of this command is changed to the latter."
|
|
912
915
|
(message "%s is copied." (mhc-record-subject-as-string record)))
|
913
916
|
(message "No file here."))))
|
914
917
|
|
918
|
+
(defun mhc-reuse-past-event ()
|
919
|
+
"Select past schedule and copy to template."
|
920
|
+
(interactive)
|
921
|
+
(let* ((completion-ignore-case t)
|
922
|
+
(read-file-name-completion-ignore-case t)
|
923
|
+
(read-buffer-completion-ignore-case t)
|
924
|
+
(case-fold-search t)
|
925
|
+
(collection
|
926
|
+
(mhc-db-make-completion-list
|
927
|
+
(mhc-db-past-occurrences)))
|
928
|
+
(completion-table
|
929
|
+
(lambda (string pred action)
|
930
|
+
(if (eq action 'metadata)
|
931
|
+
'(metadata (display-sort-function . identity)
|
932
|
+
(cycle-sort-function . identity))
|
933
|
+
(complete-with-action action collection string pred))))
|
934
|
+
(key (completing-read "Select occurrence: " completion-table nil t))
|
935
|
+
(sch (cdr (cdr (assoc key collection)))))
|
936
|
+
(mhc-reuse-copy
|
937
|
+
(mhc-record-name (mhc-schedule-record sch)))))
|
938
|
+
|
915
939
|
(defun mhc-modify ()
|
916
940
|
"Modify the current schedule."
|
917
941
|
(interactive)
|
data/lib/mhc/calendar.rb
CHANGED
@@ -23,6 +23,22 @@ module Mhc
|
|
23
23
|
@datastore.entries(category: "todo")
|
24
24
|
end
|
25
25
|
|
26
|
+
def recurrences(rec, &scope_block)
|
27
|
+
@datastore.entries(recurrence: rec)
|
28
|
+
end
|
29
|
+
|
30
|
+
def recurrence_tags
|
31
|
+
hash = {}
|
32
|
+
@datastore.entries.each do |ev|
|
33
|
+
tag = ev.recurrence_tag.to_s
|
34
|
+
next if tag == ""
|
35
|
+
next if hash[tag]
|
36
|
+
|
37
|
+
yield tag
|
38
|
+
hash[tag] = ev
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
26
42
|
def occurrences(date_range, &scope_block)
|
27
43
|
ocs = []
|
28
44
|
@datastore.entries(range: date_range).each do |event|
|
@@ -34,6 +50,10 @@ module Mhc
|
|
34
50
|
return ocs.sort
|
35
51
|
end
|
36
52
|
|
53
|
+
def add(event)
|
54
|
+
@datastore.update(event)
|
55
|
+
end
|
56
|
+
|
37
57
|
################################################################
|
38
58
|
## for sync manager
|
39
59
|
|
data/lib/mhc/config.rb
CHANGED
@@ -52,7 +52,11 @@ module Mhc
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def self.create_from_yaml_string(yaml_string, filename = nil)
|
55
|
-
hash = YAML.
|
55
|
+
hash = if YAML.respond_to?(:unsafe_load)
|
56
|
+
YAML.unsafe_load(yaml_string, filename: filename)
|
57
|
+
else
|
58
|
+
YAML.load(yaml_string, filename)
|
59
|
+
end || {}
|
56
60
|
return new(hash)
|
57
61
|
end
|
58
62
|
|
data/lib/mhc/converter.rb
CHANGED
@@ -131,15 +131,86 @@ module Mhc
|
|
131
131
|
end
|
132
132
|
end
|
133
133
|
|
134
|
+
# https://github.com/rubyredrick/ri_cal
|
135
|
+
def value_part(unit, diff) # :nodoc:
|
136
|
+
(diff == 0) ? nil : "#{diff}#{unit}"
|
137
|
+
end
|
138
|
+
|
139
|
+
# https://github.com/rubyredrick/ri_cal
|
140
|
+
def from_datetimes(start, finish, sign = '') # :nodoc:
|
141
|
+
if start > finish
|
142
|
+
from_datetimes(finish, start, '-')
|
143
|
+
else
|
144
|
+
diff = finish - start
|
145
|
+
days_diff = diff.to_i
|
146
|
+
hours = (diff - days_diff) * 24
|
147
|
+
hour_diff = hours.to_i
|
148
|
+
minutes = (hours - hour_diff) * 60
|
149
|
+
min_diff = minutes.to_i
|
150
|
+
seconds = (minutes - min_diff) * 60
|
151
|
+
sec_diff = seconds.to_i
|
152
|
+
|
153
|
+
day_part = value_part('D',days_diff)
|
154
|
+
hour_part = value_part('H', hour_diff)
|
155
|
+
min_part = value_part('M', min_diff)
|
156
|
+
sec_part = value_part('S', sec_diff)
|
157
|
+
t_part = (hour_diff.abs + min_diff.abs + sec_diff.abs) == 0 ? "" : "T"
|
158
|
+
"P#{day_part}#{t_part}#{hour_part}#{min_part}#{sec_part}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Convert ~X-SC-Days:~ to RFC5545 3.8.2 ~RDATE~
|
163
|
+
#
|
164
|
+
# RDATE allows three types of format: PERIOD, DATE, DATE-TIME.
|
165
|
+
# + DATE :: 20140301
|
166
|
+
# + DATE-TIME :: 20140301T123000
|
167
|
+
# + PERIOD :: 20140301T123000/20140301T143000 or 20140301T123000/PT2H
|
168
|
+
#
|
169
|
+
# NOTE: *Google calenadr does not accept PERIOD*
|
170
|
+
# That's why we cannot make X-SC-Day: have different duration-period for each occurrence.
|
171
|
+
#
|
134
172
|
def rdates(event)
|
135
173
|
return nil if event.dates.empty?
|
136
|
-
|
174
|
+
# Get all RDATE candidates from X-SC-Date
|
175
|
+
ocs = Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).map do |oc|
|
176
|
+
if oc.dtstart.respond_to?(:hour)
|
177
|
+
# duration = from_datetimes(oc.dtstart, oc.dtend)
|
178
|
+
|
179
|
+
# 20140301T123000/20140301T143000
|
180
|
+
dtstart = IcalendarImporter.tz_convert(oc.dtstart, dst_tzid: 'UTC').strftime("%Y%m%dT%H%M00Z")
|
181
|
+
dtend = IcalendarImporter.tz_convert(oc.dtend, dst_tzid: 'UTC').strftime("%Y%m%dT%H%M00Z")
|
182
|
+
|
183
|
+
dtstart + "/" + dtend
|
184
|
+
# dtstart + "/" + duration
|
185
|
+
else
|
186
|
+
oc.dtstart
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Check if all in X-SC-Date should be converted into RDATE,
|
191
|
+
# because the first occurrence of X-SC-Date might be dedicated
|
192
|
+
# to DTSTART in some particular case.
|
193
|
+
#
|
137
194
|
if event.recurring?
|
195
|
+
# Use all values of X-SC-Date as RDATE, because DTSTART
|
196
|
+
# should be calculated from recurrence-rule (X-SC-Cond and
|
197
|
+
# X-SC-Duration).
|
138
198
|
ocs
|
139
199
|
else
|
140
|
-
|
141
|
-
|
200
|
+
# This is Workaround for avoiding the serious bug in Android.
|
201
|
+
# Android badly ignores DTSTART-DTEND if RDATE is set.
|
202
|
+
# As a result, the first occurence is vanished.
|
203
|
+
# So, I set the first occurence twice; in RDATE and DTSTART.
|
204
|
+
return nil if ocs[1..-1].empty?
|
142
205
|
return ocs
|
206
|
+
|
207
|
+
# This is what I wanted to do:
|
208
|
+
# Remove the first occurence of X-SC-Date, because the first
|
209
|
+
# value should be dedicated to DTSTART.
|
210
|
+
#
|
211
|
+
# ocs = ocs[1..-1]
|
212
|
+
# return nil if ocs.empty?
|
213
|
+
# return ocs
|
143
214
|
end
|
144
215
|
end
|
145
216
|
|
data/lib/mhc/datastore.rb
CHANGED
@@ -25,7 +25,7 @@ module Mhc
|
|
25
25
|
Dir.foreach(".") do |ent|
|
26
26
|
parse_mhcc(ent).each {|ev|
|
27
27
|
next if category && !ev.in_category?(category)
|
28
|
-
next if recurrence && !ev.
|
28
|
+
next if recurrence && !(ev.recurrence_tag.to_s == recurrence)
|
29
29
|
yielder << ev
|
30
30
|
} if /\.mhcc$/ =~ ent
|
31
31
|
next unless /\.mhc$/ =~ ent
|
@@ -52,22 +52,25 @@ module Mhc
|
|
52
52
|
return Event.parse_file(path)
|
53
53
|
end
|
54
54
|
|
55
|
-
def create(event)
|
55
|
+
def create(event, draft = false)
|
56
56
|
if find_by_uid(event.uid)
|
57
57
|
raise "Already exist uid:#{uid} in #{@basedir}"
|
58
58
|
end
|
59
|
+
path = uid_to_path(event.uid, draft)
|
59
60
|
File.open(path, "w") do |f|
|
60
61
|
f.write(event.dump)
|
61
62
|
end
|
63
|
+
return path.to_s
|
62
64
|
end
|
63
65
|
|
64
|
-
def update(event)
|
65
|
-
unless path = uid_to_path(event.uid)
|
66
|
+
def update(event, draft = false)
|
67
|
+
unless path = uid_to_path(event.uid, draft)
|
66
68
|
raise "Not found uid:#{uid} in #{@basedir}"
|
67
69
|
end
|
68
70
|
File.open(path, "w") do |f|
|
69
71
|
f.write(event.dump)
|
70
72
|
end
|
73
|
+
return path.to_s
|
71
74
|
end
|
72
75
|
|
73
76
|
def delete(uid_or_event)
|
@@ -105,7 +108,8 @@ module Mhc
|
|
105
108
|
return Dir.glob(glob).first
|
106
109
|
end
|
107
110
|
|
108
|
-
def uid_to_path(uid)
|
111
|
+
def uid_to_path(uid, draft = false)
|
112
|
+
return @basedir + ('draft/' + uid + '.mhc') if draft
|
109
113
|
return @basedir + ('spool/' + uid + '.mhc')
|
110
114
|
end
|
111
115
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Mhc
|
2
|
+
class Formatter
|
3
|
+
class Html < Base
|
4
|
+
TEMPLATE_DIR = File.expand_path("../../templates", __FILE__)
|
5
|
+
|
6
|
+
def initialize(date_range:, options:)
|
7
|
+
super(date_range: date_range, options: options)
|
8
|
+
@json_formatter = Json.new(date_range: date_range, options: options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def <<(occurrence)
|
12
|
+
@json_formatter << occurrence
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def format_header(context)
|
18
|
+
require "erb"
|
19
|
+
template_path = File.expand_path("full-calendar.html.erb", TEMPLATE_DIR)
|
20
|
+
@template = ERB.new(File.open(template_path).read, nil, "-")
|
21
|
+
return ""
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_body(context)
|
25
|
+
env = Struct.new(:json_event_array, :tzid)
|
26
|
+
mhc = env.new(@json_formatter.to_s, Mhc.default_tzid) # used in ERB template
|
27
|
+
@template.result(binding)
|
28
|
+
end
|
29
|
+
|
30
|
+
end # class Html
|
31
|
+
end # class Formatter
|
32
|
+
end # module Mhc
|
data/lib/mhc/formatter.rb
CHANGED
@@ -21,6 +21,8 @@ module Mhc
|
|
21
21
|
Howm.new(date_range: date_range, options:options)
|
22
22
|
when :json
|
23
23
|
Json.new(date_range: date_range, options:options)
|
24
|
+
when :html
|
25
|
+
Html.new(date_range: date_range, options:options)
|
24
26
|
else
|
25
27
|
raise Formatter::NameError.new("Unknown format: #{formatter} (#{formatter.class})")
|
26
28
|
end
|
@@ -37,6 +39,7 @@ module Mhc
|
|
37
39
|
autoload :OrgTable, "#{dir}/org_table.rb"
|
38
40
|
autoload :SymbolicExpression, "#{dir}/symbolic_expression.rb"
|
39
41
|
autoload :Text, "#{dir}/text.rb"
|
42
|
+
autoload :Html, "#{dir}/html.rb"
|
40
43
|
|
41
44
|
end # class Formatter
|
42
45
|
end # module Mhc
|
@@ -0,0 +1,417 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
7
|
+
|
8
|
+
<title>Calendar</title>
|
9
|
+
|
10
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.9.0/fullcalendar.min.css" />
|
11
|
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
|
12
|
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css" />
|
13
|
+
|
14
|
+
<style media="screen" type="text/css">
|
15
|
+
.container {
|
16
|
+
// max-width: 100%;
|
17
|
+
width: auto;
|
18
|
+
}
|
19
|
+
|
20
|
+
.ui-draggable {
|
21
|
+
cursor: grab;
|
22
|
+
}
|
23
|
+
|
24
|
+
a {
|
25
|
+
cursor: pointer;
|
26
|
+
}
|
27
|
+
|
28
|
+
.fc-sun {
|
29
|
+
color:#gray;
|
30
|
+
background-color: #fff0f0;
|
31
|
+
}
|
32
|
+
|
33
|
+
.fc-sat {
|
34
|
+
color:#gray;
|
35
|
+
background-color: #f0f0ff;
|
36
|
+
}
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
#calendar .mhc-allday {
|
41
|
+
background-color: lightblue;
|
42
|
+
border-color: lightblue;
|
43
|
+
color: black;
|
44
|
+
}
|
45
|
+
|
46
|
+
#calendar .mhc-time-range {
|
47
|
+
background-color: #f7f7ff;
|
48
|
+
border-color: #f7f7ff;
|
49
|
+
color: black;
|
50
|
+
}
|
51
|
+
|
52
|
+
#calendar .mhc-time-range:hover {
|
53
|
+
background-color: aliceblue;
|
54
|
+
border-color: lavender;
|
55
|
+
color: black;
|
56
|
+
}
|
57
|
+
|
58
|
+
#calendar .mhc-category-holiday {
|
59
|
+
background-color: orangered;
|
60
|
+
border-color: orangered;
|
61
|
+
color: white;
|
62
|
+
}
|
63
|
+
|
64
|
+
#calendar .mhc-category-birthday {
|
65
|
+
background-color: #ffffe8;
|
66
|
+
border-color: #ffffe8;
|
67
|
+
color: black;
|
68
|
+
}
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
#side-calendar .mhc-category-birthday {
|
74
|
+
display: none;
|
75
|
+
}
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
#stuck-events .fc-list-heading {
|
81
|
+
display: none;
|
82
|
+
}
|
83
|
+
|
84
|
+
</style>
|
85
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.js"></script>
|
86
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.js"></script>
|
87
|
+
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.js"></script>
|
88
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment.js"></script>
|
89
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.9.0/fullcalendar.js"></script>
|
90
|
+
|
91
|
+
</head>
|
92
|
+
<body>
|
93
|
+
<!--
|
94
|
+
<div class="navmenu navmenu-default navmenu-fixed-left offcanvas">
|
95
|
+
<a class="navmenu-brand" href="#">brand</a>
|
96
|
+
<ul class="nav navmenu-nav">
|
97
|
+
<li><a href="#">Menu text 1</a></li>
|
98
|
+
<li><a href="#">Menu text 2</a></li>
|
99
|
+
<li><a href="#">Menu text 3</a></li>
|
100
|
+
</ul>
|
101
|
+
</div>
|
102
|
+
<div class="navbar navbar-default navbar-fixed-top">
|
103
|
+
<button type="button" class="navbar-toggle" data-toggle="offcanvas" data-target=".navmenu" data-canvas="body">
|
104
|
+
<span class="icon-bar"></span>
|
105
|
+
<span class="icon-bar"></span>
|
106
|
+
<span class="icon-bar"></span>
|
107
|
+
</button>
|
108
|
+
</div>
|
109
|
+
-->
|
110
|
+
|
111
|
+
<div class="container">
|
112
|
+
<div class="row">
|
113
|
+
|
114
|
+
<div class="col-xs-3">
|
115
|
+
<ul class="nav nav-tabs">
|
116
|
+
<li class="active"><a data-toggle="tab" href="#side-calendar">Past Events</a></li>
|
117
|
+
<li><a data-toggle="tab" href="#stuck-events">Stuck Events</a></li>
|
118
|
+
</ul>
|
119
|
+
<div class="tab-content">
|
120
|
+
<div id="side-calendar" class="tab-pane fade in active"></div>
|
121
|
+
<div id="stuck-events" class="tab-pane fade"></div>
|
122
|
+
</div>
|
123
|
+
</div><!-- col-xs-3 -->
|
124
|
+
|
125
|
+
<div class="col-xs-9" id="calendar"></div> <!-- col-xs-9 -->
|
126
|
+
</div><!-- row -->
|
127
|
+
</div><!-- /.container -->
|
128
|
+
|
129
|
+
<script>
|
130
|
+
function create_uuid() {
|
131
|
+
var d = +new Date();
|
132
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
|
133
|
+
.replace(/[xy]/g, function(c) {
|
134
|
+
var r = (d + Math.random() * 16) % 16 | 0;
|
135
|
+
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16).toUpperCase();
|
136
|
+
});
|
137
|
+
}
|
138
|
+
|
139
|
+
function max_height_for(element) {
|
140
|
+
var top = $(element).position().top;
|
141
|
+
return $(window).height() - top - 7;
|
142
|
+
}
|
143
|
+
|
144
|
+
function adjust_height(element) {
|
145
|
+
var height = max_height_for(element);
|
146
|
+
$(element).fullCalendar('option', 'height', height);
|
147
|
+
}
|
148
|
+
|
149
|
+
var json_event_array = [<%= mhc.json_event_array %>];
|
150
|
+
|
151
|
+
$(document).ready(function() {
|
152
|
+
|
153
|
+
$('#side-calendar').fullCalendar({
|
154
|
+
defaultView: 'listMonth',
|
155
|
+
header: {
|
156
|
+
left: '',
|
157
|
+
center: 'title',
|
158
|
+
right: ''
|
159
|
+
},
|
160
|
+
height: max_height_for('#side-calendar'),
|
161
|
+
editable: true,
|
162
|
+
eventResourceEditable: true,
|
163
|
+
eventLimit: true,
|
164
|
+
selectable: true,
|
165
|
+
droppable: true,
|
166
|
+
timezone: '<%= mhc.tzid %>',
|
167
|
+
timeFormat: "HH:mm",
|
168
|
+
displayEventEnd: false,
|
169
|
+
allDayText: "",
|
170
|
+
month: 'HH:mm',
|
171
|
+
week: 'HH:mm',
|
172
|
+
day: 'HH:mm',
|
173
|
+
// eventSources: [<%= $stuck_recurrences_json %>],
|
174
|
+
eventSources: json_event_array,
|
175
|
+
XeventDragStart: function(calEvent, jsEvent, ui, view) {
|
176
|
+
alert('Drag Event: ' + calEvent.title);
|
177
|
+
alert('Event ID: ' + calEvent.id);
|
178
|
+
alert('Coordinates: ' + jsEvent.pageX + ',' + jsEvent.pageY);
|
179
|
+
alert('View: ' + view.name);
|
180
|
+
// change the border color just for fun
|
181
|
+
$(this).css('border-color', 'red');
|
182
|
+
},
|
183
|
+
viewRender: function(view, element) {
|
184
|
+
$('tr.fc-list-item').each(function() {
|
185
|
+
var duration;
|
186
|
+
var seg = $(this).data('fc-seg');
|
187
|
+
var myevent = seg.footprint.getEventLegacy();
|
188
|
+
if (myevent.end) {
|
189
|
+
var minutes = moment.duration(myevent.end.diff(myevent.start)).asMinutes();
|
190
|
+
var days = Math.floor(minutes/(24*60));
|
191
|
+
minutes -= days * 24 * 60;
|
192
|
+
var hours = Math.floor(minutes/60);
|
193
|
+
minutes -= hours * 60;
|
194
|
+
duration = days + "." + hours + ":" + minutes;
|
195
|
+
} else {
|
196
|
+
duration = "0.00:00";
|
197
|
+
}
|
198
|
+
if (myevent.title == "母の日") {
|
199
|
+
// alert('My Event: ' + myevent.title);
|
200
|
+
// alert('Duration: ' + duration);
|
201
|
+
}
|
202
|
+
var uuid = create_uuid();
|
203
|
+
// alert('Seg: ' + seg);
|
204
|
+
$(this).data('event', {
|
205
|
+
title: myevent.title,
|
206
|
+
allDay: myevent.allDay,
|
207
|
+
start: myevent.start.format("HH:mm"),
|
208
|
+
duration: duration,
|
209
|
+
stick: true,
|
210
|
+
className: myevent.className
|
211
|
+
});
|
212
|
+
});
|
213
|
+
|
214
|
+
$('tr.fc-list-item').draggable({
|
215
|
+
appendTo: "#calendar",
|
216
|
+
cursorAt: {top: 5, left: 5},
|
217
|
+
iframeFix: true,
|
218
|
+
refreshPositions: true,
|
219
|
+
scroll: false,
|
220
|
+
cursor: "move",
|
221
|
+
revert: true, // will cause the event to go back to its
|
222
|
+
revertDuration: 0, // original position after the drag
|
223
|
+
helper: "clone",
|
224
|
+
zIndex: 100,
|
225
|
+
opacity: 0.35,
|
226
|
+
|
227
|
+
start: function(event, ui) {
|
228
|
+
}
|
229
|
+
});
|
230
|
+
if(view.name == 'listMonth') {
|
231
|
+
adjust_height('#side-calendar');
|
232
|
+
}
|
233
|
+
},
|
234
|
+
windowResize: function(view) {
|
235
|
+
if(view.name == 'listMonth') {
|
236
|
+
adjust_height('#side-calendar');
|
237
|
+
}
|
238
|
+
}
|
239
|
+
});
|
240
|
+
|
241
|
+
$('#stuck-events').fullCalendar({
|
242
|
+
defaultView: 'listall',
|
243
|
+
Xheader: {
|
244
|
+
left: '',
|
245
|
+
center: '',
|
246
|
+
right: ''
|
247
|
+
},
|
248
|
+
header: false,
|
249
|
+
views: {
|
250
|
+
listall: {
|
251
|
+
type: 'list',
|
252
|
+
duration: { days: 3650},
|
253
|
+
dayCount: 30
|
254
|
+
}
|
255
|
+
},
|
256
|
+
height: max_height_for('#side-calendar'), // XXX
|
257
|
+
editable: true,
|
258
|
+
eventResourceEditable: true,
|
259
|
+
eventLimit: true,
|
260
|
+
selectable: true,
|
261
|
+
droppable: true,
|
262
|
+
timezone: '<%= mhc.tzid %>',
|
263
|
+
timeFormat: " ",
|
264
|
+
allDayText: "",
|
265
|
+
month: 'HH:mm',
|
266
|
+
week: 'HH:mm',
|
267
|
+
day: 'HH:mm',
|
268
|
+
eventSources: [<%= $stuck_recurrences_json %>],
|
269
|
+
XeventDragStart: function(calEvent, jsEvent, ui, view) {
|
270
|
+
alert('Drag Event: ' + calEvent.title);
|
271
|
+
alert('Event ID: ' + calEvent.id);
|
272
|
+
alert('Coordinates: ' + jsEvent.pageX + ',' + jsEvent.pageY);
|
273
|
+
alert('View: ' + view.name);
|
274
|
+
// change the border color just for fun
|
275
|
+
$(this).css('border-color', 'red');
|
276
|
+
},
|
277
|
+
viewRender: function(view, element) {
|
278
|
+
$('tr.fc-list-item').each(function() {
|
279
|
+
var duration;
|
280
|
+
var seg = $(this).data('fc-seg');
|
281
|
+
var myevent = seg.footprint.getEventLegacy();
|
282
|
+
if (myevent.end) {
|
283
|
+
var minutes = moment.duration(myevent.end.diff(myevent.start)).asMinutes();
|
284
|
+
var days = Math.floor(minutes/(24*60));
|
285
|
+
minutes -= days * 24 * 60;
|
286
|
+
var hours = Math.floor(minutes/60);
|
287
|
+
minutes -= hours * 60;
|
288
|
+
duration = days + "." + hours + ":" + minutes;
|
289
|
+
} else {
|
290
|
+
duration = "0.00:00";
|
291
|
+
}
|
292
|
+
if (myevent.title == "母の日") {
|
293
|
+
// alert('My Event: ' + myevent.title);
|
294
|
+
// alert('Duration: ' + duration);
|
295
|
+
}
|
296
|
+
var uuid = create_uuid();
|
297
|
+
// alert('Seg: ' + seg);
|
298
|
+
$(this).data('event', {
|
299
|
+
title: myevent.title,
|
300
|
+
allDay: myevent.allDay,
|
301
|
+
start: myevent.start.format("HH:mm"),
|
302
|
+
duration: duration,
|
303
|
+
stick: true,
|
304
|
+
className: myevent.className
|
305
|
+
});
|
306
|
+
});
|
307
|
+
|
308
|
+
$('tr.fc-list-item').draggable({
|
309
|
+
appendTo: "#calendar",
|
310
|
+
cursorAt: {top: 5, left: 5},
|
311
|
+
iframeFix: true,
|
312
|
+
refreshPositions: true,
|
313
|
+
scroll: false,
|
314
|
+
cursor: "move",
|
315
|
+
revert: true, // will cause the event to go back to its
|
316
|
+
revertDuration: 0, // original position after the drag
|
317
|
+
helper: "clone",
|
318
|
+
zIndex: 100,
|
319
|
+
opacity: 0.35,
|
320
|
+
|
321
|
+
start: function(event, ui) {
|
322
|
+
}
|
323
|
+
});
|
324
|
+
if(view.name == 'listMonth') {
|
325
|
+
adjust_height('#stuck-events');
|
326
|
+
}
|
327
|
+
},
|
328
|
+
windowResize: function(view) {
|
329
|
+
if(view.name == 'listMonth') {
|
330
|
+
adjust_height('#stuck-events');
|
331
|
+
}
|
332
|
+
}
|
333
|
+
});
|
334
|
+
|
335
|
+
$('#calendar').fullCalendar({
|
336
|
+
header: {
|
337
|
+
left: 'prev,next today',
|
338
|
+
center: 'title',
|
339
|
+
right: 'month,agendaWeek,agendaDay,listMonth'
|
340
|
+
},
|
341
|
+
firstDay: 1,
|
342
|
+
height: max_height_for('#calendar'),
|
343
|
+
editable: true,
|
344
|
+
eventResourceEditable: true,
|
345
|
+
eventLimit: true,
|
346
|
+
selectable: true,
|
347
|
+
droppable: true,
|
348
|
+
|
349
|
+
// drop: Event ではない draggable からドロップされたとき.
|
350
|
+
// draggable が event として認識できないようなときでも発火する
|
351
|
+
drop: function(date) {
|
352
|
+
// alert("Dropped on " + date.format());
|
353
|
+
},
|
354
|
+
|
355
|
+
// eventReceive: Event ではない draggable からドロップされたとき
|
356
|
+
// draggable が event として認識可能な property を持っている場合だけ発火する
|
357
|
+
eventReceive: function(event) {
|
358
|
+
event.id = create_uuid(); // XXX! これがうまくいかない
|
359
|
+
$('#calendar').fullCalendar('updateEvent', event);
|
360
|
+
// alert("Drop event from ourter world! ID: " + event.id);
|
361
|
+
},
|
362
|
+
|
363
|
+
// eventDrop: 既に登録されている event を変更したとき
|
364
|
+
eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
|
365
|
+
// alert("eventDrop:" + event.title);
|
366
|
+
// alert("eventDrop:" + event.id);
|
367
|
+
},
|
368
|
+
|
369
|
+
// return (this.eventInstance || this.eventDef).toLegacy()
|
370
|
+
eventClick: function(calEvent, jsEvent, view) {
|
371
|
+
var yesno = prompt('Do you delete ' + calEvent.title + '?');
|
372
|
+
// alert('Event ID: ' + calEvent.id);
|
373
|
+
if (yesno == "yes"){
|
374
|
+
$('#calendar').fullCalendar('removeEvents', [calEvent.id]);
|
375
|
+
}
|
376
|
+
// alert('This Event: ' + $(this).getEventLegacy().title);
|
377
|
+
//alert('Coordinates: ' + jsEvent.pageX + ',' + jsEvent.pageY);
|
378
|
+
//alert('View: ' + view.name);
|
379
|
+
// change the border color just for fun
|
380
|
+
// $(this).css('border-color', 'red');
|
381
|
+
},
|
382
|
+
|
383
|
+
timezone: '<%= mhc.tzid %>',
|
384
|
+
timeFormat: "HH:mm",
|
385
|
+
allDayText: "",
|
386
|
+
month: 'HH:mm',
|
387
|
+
week: 'HH:mm',
|
388
|
+
day: 'HH:mm',
|
389
|
+
eventSources: json_event_array,
|
390
|
+
// eventSources: [<%= mhc.json_event_array %>],
|
391
|
+
viewRender: function(view, element) {
|
392
|
+
if(view.name == 'month') {
|
393
|
+
adjust_height('#calendar');
|
394
|
+
adjust_height('#stuck-events');
|
395
|
+
}
|
396
|
+
var currentDate = $('#calendar').fullCalendar('getDate');
|
397
|
+
// alert("The current date of the calendar is " + currentDate.format());
|
398
|
+
$('#side-calendar').fullCalendar('gotoDate', currentDate.subtract(1, 'y'));
|
399
|
+
$('#stuck-events').fullCalendar('gotoDate', currentDate.subtract(5, 'y'));
|
400
|
+
},
|
401
|
+
windowResize: function(view) {
|
402
|
+
if(view.name == 'month') {
|
403
|
+
adjust_height('#calendar');
|
404
|
+
adjust_height('#stuck-events');
|
405
|
+
}
|
406
|
+
}
|
407
|
+
}) // $('#calendar')
|
408
|
+
|
409
|
+
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
410
|
+
var target = $(e.target).attr("href") // activated tab
|
411
|
+
adjust_height(target);
|
412
|
+
});
|
413
|
+
|
414
|
+
});
|
415
|
+
</script>
|
416
|
+
</body>
|
417
|
+
</html>
|
data/lib/mhc/version.rb
CHANGED
data/mhc.gemspec
CHANGED
@@ -23,7 +23,8 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
24
24
|
spec.require_paths = ["lib"]
|
25
25
|
|
26
|
-
spec.add_runtime_dependency "thor", ">=
|
26
|
+
spec.add_runtime_dependency "thor", ">= 1.2.0"
|
27
|
+
spec.add_runtime_dependency "rexml", ">= 3.2.4"
|
27
28
|
spec.add_runtime_dependency "ri_cal", ">= 0.8.8"
|
28
29
|
spec.add_runtime_dependency "tzinfo", ">= 1.2.2"
|
29
30
|
spec.add_runtime_dependency "tzinfo-data", ">= 1.2015.4"
|
@@ -31,14 +31,14 @@ X-SC-Record-Id: AC1B378C-BAFE-4B6E-AF9C-78C2377E8AA4
|
|
31
31
|
X-SC-Subject: 天皇誕生日
|
32
32
|
X-SC-Category: Holiday Japanese
|
33
33
|
X-SC-Cond: 29 Apr
|
34
|
-
X-SC-Duration:
|
34
|
+
X-SC-Duration: 19490429-19880429
|
35
35
|
X-SC-Record-Id: B78EAAEE-9963-4573-9EC7-0879F8940AEE
|
36
36
|
|
37
37
|
X-SC-Subject: みどりの日
|
38
38
|
X-SC-Category: Holiday Japanese
|
39
39
|
X-SC-Cond: 29 Apr
|
40
40
|
X-SC-Duration: 19890429-20060429
|
41
|
-
X-SC-Record-Id:
|
41
|
+
X-SC-Record-Id: 8C05BF80-006C-4FA7-8CF1-5F1C18CB8641
|
42
42
|
|
43
43
|
X-SC-Subject: 昭和の日
|
44
44
|
X-SC-Category: Holiday Japanese
|
@@ -186,12 +186,12 @@ X-SC-Day: 19730430 19730924 19740506 19740916 19741104 19751124
|
|
186
186
|
20180430 20180924 20181224 20190506 20190812 20191104 20200224 20200506
|
187
187
|
20210809
|
188
188
|
20230102 20240212 20240506 20240812 20240923 20241104 20250224 20250506
|
189
|
-
20251124 20260506 20270322 20290212 20290430 20290924
|
189
|
+
20251124 20260506 20270322 20290212 20290430 20290924
|
190
190
|
20300506 20300812 20301104 20310224 20310506 20311124 20330321 20340102
|
191
|
-
20350212 20350430 20350924
|
192
|
-
20400102 20400430
|
191
|
+
20350212 20350430 20350924 20360506 20361124 20370506
|
192
|
+
20400102 20400430 20410506 20410812 20411104 20420224 20420506
|
193
193
|
20421124 20430506 20440321 20450102 20460212 20460430 20460924
|
194
|
-
|
194
|
+
20470506 20470812 20471104 20480224 20480506 20500321
|
195
195
|
X-SC-Category: Holiday Japanese
|
196
196
|
X-SC-Record-Id: E2D696DF-EB49-44D7-B06C-812B10BD99A2
|
197
197
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mhc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yoshinari Nomura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -16,14 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 1.2.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 1.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rexml
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.2.4
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.2.4
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: ri_cal
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,6 +118,7 @@ extra_rdoc_files: []
|
|
104
118
|
files:
|
105
119
|
- ".gitignore"
|
106
120
|
- ".rspec"
|
121
|
+
- ".rubocop.yml"
|
107
122
|
- ".travis.yml"
|
108
123
|
- COPYRIGHT
|
109
124
|
- Gemfile
|
@@ -187,6 +202,7 @@ files:
|
|
187
202
|
- lib/mhc/formatter/base.rb
|
188
203
|
- lib/mhc/formatter/emacs.rb
|
189
204
|
- lib/mhc/formatter/howm.rb
|
205
|
+
- lib/mhc/formatter/html.rb
|
190
206
|
- lib/mhc/formatter/icalendar.rb
|
191
207
|
- lib/mhc/formatter/json.rb
|
192
208
|
- lib/mhc/formatter/mail.rb
|
@@ -215,6 +231,7 @@ files:
|
|
215
231
|
- lib/mhc/sync/strategy.rb
|
216
232
|
- lib/mhc/sync/syncinfo.rb
|
217
233
|
- lib/mhc/templates/config.yml.erb
|
234
|
+
- lib/mhc/templates/full-calendar.html.erb
|
218
235
|
- lib/mhc/version.rb
|
219
236
|
- lib/mhc/webdav.rb
|
220
237
|
- mhc.gemspec
|
@@ -255,7 +272,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
255
272
|
- !ruby/object:Gem::Version
|
256
273
|
version: '0'
|
257
274
|
requirements: []
|
258
|
-
rubygems_version: 3.
|
275
|
+
rubygems_version: 3.2.22
|
259
276
|
signing_key:
|
260
277
|
specification_version: 4
|
261
278
|
summary: Message Harmonized Calendaring
|