mhc 1.2.4 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|